luolongfei 4 lat temu
rodzic
commit
b1dffed96d
100 zmienionych plików z 8142 dodań i 0 usunięć
  1. 8 0
      .dockerignore
  2. 62 0
      .env.example
  3. 3 0
      .github/FUNDING.yml
  4. 33 0
      .github/ISSUE_TEMPLATE/bug_report.md
  5. 20 0
      .github/ISSUE_TEMPLATE/feature_request.md
  6. 3 0
      .gitignore
  7. 21 0
      Dockerfile
  8. 545 0
      README.md
  9. 196 0
      README_EN.md
  10. 369 0
      app/Console/FreeNom.php
  11. 0 0
      app/Exceptions/.gitkeep
  12. 29 0
      app/Exceptions/LlfException.php
  13. 248 0
      app/helpers.php
  14. 37 0
      composer.json
  15. 844 0
      composer.lock
  16. 42 0
      config.php
  17. 58 0
      docker-entrypoint.sh
  18. 89 0
      libs/Argv.php
  19. 74 0
      libs/Config.php
  20. 72 0
      libs/Env.php
  21. 74 0
      libs/Lang.php
  22. 134 0
      libs/Log.php
  23. 143 0
      libs/Mail.php
  24. 57 0
      libs/PhpColor.php
  25. 128 0
      libs/TelegramBot.php
  26. 2 0
      logs/.gitignore
  27. 25 0
      resources/lang/zh.php
  28. 326 0
      resources/mail/LlfException.html
  29. 366 0
      resources/mail/default.html
  30. BIN
      resources/mail/images/163/163mail01.png
  31. BIN
      resources/mail/images/163/163mail02.png
  32. BIN
      resources/mail/images/163/163mail03.png
  33. BIN
      resources/mail/images/Snipaste_2018-08-13_15-58-52.png
  34. BIN
      resources/mail/images/github_actions/ga01.png
  35. BIN
      resources/mail/images/github_actions/ga02.png
  36. BIN
      resources/mail/images/github_actions/ga03.png
  37. BIN
      resources/mail/images/github_actions/ga04.png
  38. BIN
      resources/mail/images/github_actions/ga05.png
  39. BIN
      resources/mail/images/github_actions/ga06.png
  40. BIN
      resources/mail/images/github_actions/ga07.png
  41. BIN
      resources/mail/images/github_actions/ga08.png
  42. BIN
      resources/mail/images/gmail/gmail01.png
  43. BIN
      resources/mail/images/gmail/gmail01_en.png
  44. BIN
      resources/mail/images/gmail/gmail02.png
  45. BIN
      resources/mail/images/mmsgletter_2_bg.png
  46. BIN
      resources/mail/images/mmsgletter_2_bg_topline.png
  47. BIN
      resources/mail/images/mmsgletter_2_btn.png
  48. BIN
      resources/mail/images/mmsgletter_chat_left.gif
  49. BIN
      resources/mail/images/mmsgletter_chat_right.gif
  50. BIN
      resources/mail/images/pay.png
  51. BIN
      resources/mail/images/qq/qq01.png
  52. BIN
      resources/mail/images/qq/qq02.png
  53. BIN
      resources/mail/images/qq/qq03.png
  54. BIN
      resources/mail/images/qq/qq04.png
  55. BIN
      resources/mail/images/ting.jpg
  56. 366 0
      resources/mail/notice.html
  57. 0 0
      resources/screenshot/.gitkeep
  58. BIN
      resources/screenshot/lie.jpg
  59. BIN
      resources/screenshot/lizhi.jpg
  60. BIN
      resources/screenshot/scf.png
  61. 74 0
      run
  62. 7 0
      vendor/autoload.php
  63. 1 0
      vendor/bramus/ansi-php/.gitignore
  64. 12 0
      vendor/bramus/ansi-php/.travis.yml
  65. 19 0
      vendor/bramus/ansi-php/LICENSE
  66. 26 0
      vendor/bramus/ansi-php/composer.json
  67. 1199 0
      vendor/bramus/ansi-php/composer.lock
  68. 14 0
      vendor/bramus/ansi-php/phpunit.xml.dist
  69. 337 0
      vendor/bramus/ansi-php/readme.md
  70. 111 0
      vendor/bramus/ansi-php/src/Ansi.php
  71. 13 0
      vendor/bramus/ansi-php/src/ControlFunctions/Backspace.php
  72. 71 0
      vendor/bramus/ansi-php/src/ControlFunctions/Base.php
  73. 13 0
      vendor/bramus/ansi-php/src/ControlFunctions/Bell.php
  74. 13 0
      vendor/bramus/ansi-php/src/ControlFunctions/CarriageReturn.php
  75. 361 0
      vendor/bramus/ansi-php/src/ControlFunctions/Enums/C0.php
  76. 13 0
      vendor/bramus/ansi-php/src/ControlFunctions/Escape.php
  77. 13 0
      vendor/bramus/ansi-php/src/ControlFunctions/LineFeed.php
  78. 13 0
      vendor/bramus/ansi-php/src/ControlFunctions/Tab.php
  79. 105 0
      vendor/bramus/ansi-php/src/ControlSequences/Base.php
  80. 30 0
      vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/Base.php
  81. 31 0
      vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/ED.php
  82. 31 0
      vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/EL.php
  83. 26 0
      vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/Enums/ED.php
  84. 26 0
      vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/Enums/EL.php
  85. 49 0
      vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/Enums/FinalByte.php
  86. 385 0
      vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/Enums/SGR.php
  87. 37 0
      vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/SGR.php
  88. 34 0
      vendor/bramus/ansi-php/src/ControlSequences/Traits/HasFinalByte.php
  89. 52 0
      vendor/bramus/ansi-php/src/ControlSequences/Traits/HasIntermediateBytes.php
  90. 52 0
      vendor/bramus/ansi-php/src/ControlSequences/Traits/HasParameterBytes.php
  91. 74 0
      vendor/bramus/ansi-php/src/Traits/ControlFunctions.php
  92. 54 0
      vendor/bramus/ansi-php/src/Traits/EscapeSequences/ED.php
  93. 57 0
      vendor/bramus/ansi-php/src/Traits/EscapeSequences/EL.php
  94. 136 0
      vendor/bramus/ansi-php/src/Traits/EscapeSequences/SGR.php
  95. 61 0
      vendor/bramus/ansi-php/src/Writers/BufferWriter.php
  96. 19 0
      vendor/bramus/ansi-php/src/Writers/FlushableInterface.php
  97. 60 0
      vendor/bramus/ansi-php/src/Writers/ProxyWriter.php
  98. 78 0
      vendor/bramus/ansi-php/src/Writers/StreamWriter.php
  99. 16 0
      vendor/bramus/ansi-php/src/Writers/WriterInterface.php
  100. 45 0
      vendor/bramus/ansi-php/tests/AnsiTest.php

+ 8 - 0
.dockerignore

@@ -0,0 +1,8 @@
+.git
+.idea
+.env
+logs
+README.md
+README_EN.md
+LICENSE
+.github

+ 62 - 0
.env.example

@@ -0,0 +1,62 @@
+#####################################################################
+# 注意事项
+#
+# - 环境变量的格式为“键=值”,顶格写,注意等号两边不能有空格,值可以用单引号或者双引号引起来,不引也行(下面的特殊情况必须引起来)
+# - 因为环境变量中“#”代表注释,若密码中存在“#”字符的,一定要使用单引号将整个密码引起来,否则解析会在“#”字符前截止,如果密码中存在单双引号的,
+#    需要在单双引号前加“\”转义
+# - 配置多账户不可省略单引号,且多个账户和密码的格式必须是“<账户1>@<密码1>|<账户2>@<密码2>|<账户3>@<密码3>”,不要有空格,就算有程序也会给你干掉
+#    e.g. MULTIPLE_ACCOUNTS='<账户1>@<密码1>|<账户2>@<密码2>|<账户3>@<密码3>'
+#    注意不要省略“<>”符号,否则无法正确匹配
+# - 若你只有单个账户,只配置FREENOM_USERNAME和FREENOM_PASSWORD就够了
+# - 单账户和多账户的配置会被合并在一起读取并去重
+#####################################################################
+
+######################  账户配置 Account config  #########################
+# Freenom账户 Freenom Account
[email protected]
+
+# Freenom密码 Freenom password
+FREENOM_PASSWORD=''
+
+# 多账户支持 Support for multiple accounts
+MULTIPLE_ACCOUNTS=''
+######################  end 账户配置  #########################
+
+######################  通知邮件配置 Email config  #########################
+# 机器人邮箱账户 Email of robot
[email protected]
+
+# 机器人邮箱密码(Gmail填密码,QQ邮箱或163邮箱填授权码) Password of the robot email
+MAIL_PASSWORD=''
+
+# 用于接收通知的邮箱 Email address used to receive notifications
+TO=''
+
+# 是否启用邮件推送功能 true:启用 false:不启用 Whether to enable email push features true: enabled false: not enabled
+MAIL_ENABLE=true
+######################  end 通知邮件配置  #########################
+
+######################  Telegram bot  #########################
+# 可选配置,通过 Telegram bot 发送通知消息 This is an optional configuration to send notification messages via Telegram bot
+
+# 你的chat_id,通过发送“/start”给@userinfobot可以获取自己的id Your chat_id, you can get your own id by sending "/start" to @userinfobot
+TELEGRAM_CHAT_ID=''
+
+# 你的Telegram bot的token Token for your Telegram bot
+TELEGRAM_BOT_TOKEN=''
+
+# 是否启用 Telegram Bot 功能 true:启用 false:不启用 Whether to enable Telegram Bot features true: enabled false: not enabled
+TELEGRAM_BOT_ENABLE=false
+######################  end Telegram bot  #########################
+
+# 通知频率 0:仅当有续期操作的时候 1:每次执行 Notification frequency 0: Only when there is a renewal operation 1: Each execution
+NOTICE_FREQ=1
+
+# 在 Github Actions 上运行 Run on github actions
+ON_GITHUB_ACTIONS=false
+
+# 是否验证服务器证书 Whether to verify server certificate
+VERIFY_SSL=false
+
+# 是否开启 Debug 模式 Whether to enable debug mode
+DEBUG=false

+ 3 - 0
.github/FUNDING.yml

@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+custom: ["https://www.paypal.me/mybsdc", "https://s2.ax1x.com/2020/01/31/1394at.png"]

+ 33 - 0
.github/ISSUE_TEMPLATE/bug_report.md

@@ -0,0 +1,33 @@
+---
+name: Bug report
+about: 根据此模板格式提问更可能得到帮助,没有具体描述的报告将被忽略。Create a report to help us improve.
+title: bug_report
+labels: ''
+assignees: ''
+
+---
+
+**描述问题**
+```
+A clear and concise description of what the bug is.
+```
+
+**重现步骤**
+```
+步骤一
+```
+
+**截图&日志**
+
+**你做了哪些尝试**
+```
+描述你发现问题后做了哪些尝试,方便我快速排除某些问题,提高沟通效率。
+```
+
+**环境信息**
+ - centos7 x64
+ - php7.2
+ - freenom续期脚本版本 v0.2.2
+
+**额外的备注**
+- Add any other context about the problem here.

+ 20 - 0
.github/ISSUE_TEMPLATE/feature_request.md

@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: feature_request
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+.env
+app/tmp/
+app/num_limit/

+ 21 - 0
Dockerfile

@@ -0,0 +1,21 @@
+FROM php:7.4.19-alpine3.13
+
+LABEL author="mybsdc <[email protected]>" \
+    maintainer="luolongfei <[email protected]>"
+
+ENV TZ Asia/Shanghai
+
+WORKDIR /app
+
+COPY . ./
+
+RUN set -eux \
+    && apk update \
+    && apk add --no-cache tzdata bash
+
+VOLUME ["/conf", "/app/logs"]
+
+COPY docker-entrypoint.sh /usr/local/bin/
+ENTRYPOINT ["docker-entrypoint.sh"]
+
+CMD ["crond", "-f"]

+ 545 - 0
README.md

@@ -0,0 +1,545 @@
+<div align="center">
+<h1>Freenom:freenom域名自动续期</h1>
+
+[![Build Status](https://img.shields.io/badge/build-passed-brightgreen?style=for-the-badge)](https://scrutinizer-ci.com/g/luolongfei/freenom/build-status/master)
+[![Php Version](https://img.shields.io/badge/php-%3E=7.2-brightgreen.svg?style=for-the-badge)](https://secure.php.net/)
+[![Scrutinizer Code Quality](https://img.shields.io/badge/scrutinizer-9.31-brightgreen?style=for-the-badge)](https://scrutinizer-ci.com/g/luolongfei/freenom/?branch=master)
+[![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](https://github.com/luolongfei/freenom/blob/master/LICENSE)
+
+Documentation: [English version](https://github.com/luolongfei/freenom/blob/master/README_EN.md) | 中文版
+</div>
+
+[📃  前言](#--前言)
+
+[🍭  效果](#--效果)
+
+[🎁  事前准备](#--事前准备)
+
+[📪  配置发信邮箱](#--配置发信邮箱)
+
+<hr>
+
+*(下面三种部署方式,选择其中一种即可)*
+
+[⛵  通过 Docker 方式部署](#--方式一通过-docker-部署最简单的方式)(最简单的方式,推荐)
+
+[🕹  通过腾讯云函数(SCF)部署](#--方式二通过腾讯云函数scf部署)
+
+[🚧  直接拉取源码部署](#--方式三直接拉取源码部署)
+
+<hr>
+
+[📋  捐赠名单 Donate List](#--捐赠名单-donate-list)
+
+[❤  捐赠 Donate](#--捐赠-donate)
+
+[🚁  我正在用的 VPS](#--我正在用的-VPS)
+
+[🍺  信仰](#--信仰)
+
+[🌚  作者](#--作者)
+
+[📰  更新日志](#--更新日志) (每次新版本发布,可以参考此日志决定是否更新)
+
+[🎉  鸣谢](#--鸣谢)
+
+[🥝  开源协议](#--开源协议)
+
+<h5>注意:GitHub 官方不允许使用 GitHub Action 做签到或者续期类应用,否则会封禁项目甚至封号,故为项目能长期维护下去,应 GitHub 官方要求,
+本项目已经移除 Action 方式的应用,望周知。已经 fork 使用的,可以尽快将项目转移到自己的 VPS 上,推荐通过 Docker 部署,
+或者直接搬运到腾讯云函数部署。本项目依然长期维护。</h5>
+
+### 📃  前言
+众所周知,Freenom是地球上唯一一个提供免费顶级域名的商家,不过需要每年续期,每次续期最多一年。由于我申请了一堆域名,而且不是同一时段申请的,
+所以每次续期都觉得折腾,于是就写了这个自动续期的脚本。
+
+### 🍭  效果
+![邮件示例](https://s2.ax1x.com/2020/01/31/139Rrd.png "邮件内容")
+
+无论是续期成败或者脚本执行出错,都会收到的程序发出的邮件。如果是续期成败相关的邮件,邮件会包括未续期域名的到期天数等内容。
+邮件参考了微信发送的注销公众号的邮件样式。
+
+### 🎁  事前准备
+- 发信邮箱:为了方便理解又称机器人邮箱,用于发送通知邮件。目前支持`Gmail`、`QQ邮箱`以及`163邮箱`,程序会自动判断发信邮箱类型并使用合适的配置。
+- 收信邮箱:用于接收机器人发出的通知邮件。推荐使用`QQ邮箱`,`QQ邮箱`唯一的好处只是收到邮件会在`QQ`弹出消息。
+- VPS:随便一台服务器都行,系统推荐`Centos7`,另外PHP版本需在`php7.2`及以上。如果你没有 VPS,可以考虑一下 [🚁  我正在用的 VPS](#--我正在用的-VPS) ,最便宜的机器`9.9美元`一年。
+- 没有了
+
+### 📪  配置发信邮箱
+下面分别介绍`Gmail`、`QQ邮箱`以及`163邮箱`的设置,你只用看自己需要的部分。注意,`QQ邮箱`与`163邮箱`均使用账户加授权码的方式登录,
+`谷歌邮箱`使用账户加密码的方式登录,请知悉。另外还想吐槽一下,国产邮箱你得花一毛钱给邮箱提供方发一条短信才能拿到授权码。
+
+*(点击即可展开或收起)*
+
+<details>
+    <summary>设置Gmail</summary>
+<br>
+  
+1、在`设置>转发和POP/IMAP`中,勾选
+- 对所有邮件启用 POP 
+- 启用 IMAP
+
+![gmail配置01](https://s2.ax1x.com/2020/01/31/13tKsg.png "gmail配置01")
+
+然后保存更改。
+
+2、允许不够安全的应用
+
+登录谷歌邮箱后,访问 [谷歌权限设置界面](https://myaccount.google.com/u/0/lesssecureapps?pli=1&pageId=none) ,启用允许不够安全的应用。
+
+![gmail配置02](https://s2.ax1x.com/2020/01/31/1392KH.png "gmail配置02")
+
+另外,若遇到提示
+> 不允许访问账户
+
+登录谷歌邮箱后,去 [gmail的这个界面](https://accounts.google.com/b/0/DisplayUnlockCaptcha) 点击允许。这种情况较为少见。
+
+***
+
+</details>
+
+<details>
+    <summary>设置QQ邮箱</summary>
+<br>
+
+在`设置>账户>POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务`下,开启`POP3/SMTP服务`
+
+![qq邮箱配置01](https://s2.ax1x.com/2020/01/31/13cIKA.png "qq邮箱配置01")
+
+此时坑爹的QQ邮箱会要求你用手机发送一条短信给腾讯,发送完了点一下`我已发送`
+
+![qq邮箱配置02](https://s2.ax1x.com/2020/01/31/13c4vd.png "qq邮箱配置02")
+
+然后你就能看到你的邮箱授权码了,使用邮箱账户加授权码即可登录,记下授权码
+
+![qq邮箱配置03](https://s2.ax1x.com/2020/01/31/13cTbt.png "qq邮箱配置03")
+
+![qq邮箱配置04](https://s2.ax1x.com/2020/01/31/13coDI.png "qq邮箱配置04")
+
+***
+
+</details>
+
+<details>
+    <summary>设置163邮箱</summary>
+<br>
+
+在`设置>POP3/SMTP/IMAP`下,开启`POP3/SMTP服务`和`IMAP/SMTP服务`并保存
+
+![163邮箱配置01](https://s2.ax1x.com/2020/01/31/13WKZn.png "163邮箱配置01")
+
+![163邮箱配置02](https://s2.ax1x.com/2020/01/31/13WQI0.png "163邮箱配置02")
+
+现在点击侧边栏的`客户端授权密码`,并获取授权码,你看到画面可能和我不一样,因为我已经获取了授权码,所以只有`重置授权码`按钮,这里自己根据网站提示申请获取授权码,网易和腾讯一样恶心,需要你用手机给它发一条短信才能拿到授权码
+
+![163邮箱配置03](https://s2.ax1x.com/2020/01/31/13WMaq.png "163邮箱配置03")
+
+163 邮箱送信后,接收方如果没收到可以在垃圾邮件里面找一下。
+
+***
+
+</details>
+
+#### Telegram bot
+
+上面介绍了三种邮箱的设置方法,如果你不想使用邮件推送,也可以使用 Telegram bot,灵活配置。在`.env`文件中,
+将`TELEGRAM_BOT_ENABLE`的值改为`true`,即可启用 Telegram bot,同样的,将`MAIL_ENABLE`的值改为`false`即可关闭邮件推送方式。
+Telegram bot 有两个配置项,一个是`chatID`(对应`.env`文件中的`TELEGRAM_CHAT_ID`),
+通过使用你的 Telegram 账户发送`/start`给`@userinfobot`即可以获取自己的id,
+另一个是`token`(对应`.env`文件中的`TELEGRAM_BOT_TOKEN`),你的 Telegram bot 令牌,你会创建 Telegram bot 就知道怎么获取,
+不多赘述。如何创建一个 Telegram bot 请参考:[官方文档](https://core.telegram.org/bots#6-botfather)
+
+***
+
+*与通知相关的设置到此就完成了,下面开始讲本项目的三种使用方式,一种是通过 Docker,另一种是通过腾讯云函数,再一种是直接拉取源码部署,推荐使用 Docker 方式,无需纠结环境。*
+
+### ⛵  方式一:通过 Docker 部署(最简单的方式)
+
+<hr>
+
+Docker 仓库地址为: [https://hub.docker.com/r/luolongfei/freenom](https://hub.docker.com/r/luolongfei/freenom) ,同样欢迎 star 。
+此镜像支持的架构为`linux/amd64`,`linux/arm64`,`linux/ppc64le`,`linux/s390x`,`linux/386`,`linux/arm/v7`,`linux/arm/v6`,
+理论上支持`群晖`、`威联通`、`树莓派`以及各种类型的`VPS`。
+
+#### 1、安装 Docker
+
+##### 1.1 以 root 用户登录,执行一键脚本安装 Docker
+
+升级源并安装软件(下面两行命令二选一,根据你自己的系统)
+
+Debian / Ubuntu
+```shell
+apt-get update && apt-get install -y wget vim
+```
+CentOS
+```shell
+yum update && yum install -y wget vim
+```
+
+执行此命令等候自动安装 Docker
+```shell
+wget -qO- get.docker.com | bash
+```
+
+说明:请使用 KVM 架构的 VPS,OpenVZ 架构的 VPS 不支持安装 Docker,另外 CentOS 8 不支持用此脚本来安装 Docker。
+更多关于 Docker 安装的内容参考 [Docker 官方安装指南](https://docs.docker.com/engine/install/) 。
+
+##### 1.2 对 Docker 的一些命令操作
+
+查看 Docker 安装版本等信息
+```shell
+docker version
+```
+
+启动 Docker 服务
+```shell
+systemctl start docker
+```
+
+查看 Docker 运行状态
+```shell
+systemctl status docker
+```
+
+将 Docker 服务加入开机自启动
+```shell
+systemctl enable docker
+```
+
+#### 2、通过 Docker 部署域名续期脚本
+
+##### 2.1 用 Docker 创建并启动容器
+
+命令如下
+```shell
+docker run -d --name freenom --restart always -v $(pwd):/conf -v $(pwd)/logs:/app/logs luolongfei/freenom
+```
+或者,如果你想自定义脚本执行时间,则命令如下
+```shell
+docker run -d --name freenom --restart always -v $(pwd):/conf -v $(pwd)/logs:/app/logs -e RUN_AT="11:24" luolongfei/freenom
+```
+ 上面这条命令只比上上条命令多了个` -e RUN_AT="11:24"`,其中`11:24`表示在北京时间每天的 11:24 执行续期任务,你可以自定义这个时间。
+ 这里的`RUN_AT`参数同时也支持 CRON 命令里的时间形式,比如,` -e RUN_AT="9 11 * * *"`,表示每天北京时间 11:09 执行续期任务,
+ 如果你不想每天执行任务,只想隔几天执行,只用修改`RUN_AT`的值即可。
+ 
+ **注意:不推荐自定义脚本执行时间。因为你可能跟很多人定义的是同一个时间点,这样可能导致所有人都是同一时间向 Freenom 的服务器发起请求,
+ 使得 Freenom 无法稳定提供服务。而如果你不自定义时间,程序会自动指定北京时间 06 ~ 23 点全时段随机的一个时间点作为执行时间,
+ 每次重启容器都会自动重新指定。**
+
+<details>
+    <summary>点我查看上方 Docker 命令的参数解释</summary>
+<br>
+
+| 命令 | 含义 |
+| :--- | :--- |
+| docker run | 开始运行一个容器 |
+| -d 参数 | 容器以后台运行并输出容器 ID |
+| --name 参数 | 给容器分配一个识别符,方便将来的启动,停止,删除等操作 |
+| --restart 参数 | 配置容器启动类型,always 即为 docker 服务重新启动时自动启动本容器 |
+| -v 参数 | 挂载卷(volume),冒号后面是容器的路径,冒号前面是宿主机的路径(只支持绝对路径),`$(pwd)`表示当前目录 |
+| -e 参数 | 指定容器中的环境变量 |
+| luolongfei/freenom | 这是从 docker hub 下载回来的镜像完整路径名 |
+
+</details>
+
+至此,你的自动续期容器就跑起来了,执行`ls -a`后你就可以看到在你的当前目录下,有一个`.env`文件和一个`logs`目录,`logs`目录里面存放的是程序日志,
+而`.env`则是配置文件,现在直接执行`vim .env`将`.env`文件里的所有配置项改为你自己的并保存即可。然后重启容器,如果配置正确的话,便很快可以收到相关邮件。
+
+<details>
+    <summary>点我查看 .env 文件中部分配置项的含义</summary>
+<br>
+
+| 变量名 | 含义 | 默认值 | 是否必须 | 备注 |
+| :---: | :---: | :---: | :---: | :---: |
+| FREENOM_USERNAME | Freenom 账户 | - | 是 | 只支持邮箱账户,如果你是使用第三方社交账户登录的用户,请在 Freenom 管理页面绑定邮箱,绑定后即可使用邮箱账户登录 |
+| FREENOM_PASSWORD | Freenom 密码 | - | 是 | 某些特殊字符可能需要转义,详见`.env`文件内注释 |
+| MULTIPLE_ACCOUNTS | 多账户支持 | - | 否 | 多个账户和密码的格式必须是“`<账户1>@<密码1>\|<账户2>@<密码2>\|<账户3>@<密码3>`”,注意不要省略“<>”符号,否则无法正确匹配。如果设置了多账户,上面的`FREENOM_USERNAME`和`FREENOM_PASSWORD`可不设置 |
+| MAIL_USERNAME | 机器人邮箱账户 | - | 是 | 支持`Gmail`、`QQ邮箱`以及`163邮箱`,尽可能使用`163邮箱`或者`QQ邮箱`而非`Gmail`。因为谷歌的安全机制,每次在新设备登录 `Gmail` 都会先被限制,需要手动解除限制才行。具体的配置方法参考「 [配置发信邮箱](#--配置发信邮箱) 」 |
+| MAIL_PASSWORD | 机器人邮箱密码 | - | 是 | `Gmail`填密码,`QQ邮箱`或`163邮箱`填授权码 |
+| TO | 接收通知的邮箱 | - | 是 | 你自己最常用的邮箱,推荐使用`QQ邮箱`,用来接收机器人邮箱发出的域名相关邮件 |
+| MAIL_ENABLE | 是否启用邮件推送功能 | true | 否 | `true`:启用<br>`false`:不启用<br>默认启用,如果设为`false`,不启用邮件推送功能,则上面的`MAIL_USERNAME`、`MAIL_PASSWORD`、`TO`变量变为非必须,可不设置 |
+| TELEGRAM_CHAT_ID | 你的`chat_id` | - | 否 | 通过发送`/start`给`@userinfobot`可以获取自己的`id` |
+| TELEGRAM_BOT_TOKEN | 你的`Telegram bot`的`token` | - | 否 ||
+| TELEGRAM_BOT_ENABLE | 是否启用`Telegram Bot`推送功能 | false | 否 | `true`:启用<br>`false`:不启用<br>默认不启用,如果设为`true`,则必须设置上面的`TELEGRAM_CHAT_ID`和`TELEGRAM_BOT_TOKEN`变量 |
+| NOTICE_FREQ | 通知频率 | 1 | 否 | `0`:仅当有续期操作的时候<br>`1`:每次执行 |
+
+*更多配置项含义,请参考`.env`文件中的注释。*
+
+</details>
+
+> 如何验证你的配置是否正确呢?
+>
+
+修改并保存`.env`文件后,执行`docker restart freenom`重启容器,等待 5 秒钟左右,然后执行`docker logs freenom`查看输出内容,
+观察输出内容中有`执行成功`字样,则表示配置无误。如果你还来不及配置送信邮箱等内容,可先停用邮件功能。
+
+##### 2.2 后期容器处理常用命令
+
+查看容器在线状态及大小
+```shell
+docker ps -as
+```
+
+查看容器的运行输出日志
+```shell
+docker logs freenom
+```
+
+重新启动容器
+```shell
+docker restart freenom
+```
+
+停止容器的运行
+```shell
+docker stop freenom
+```
+
+移除容器
+```shell
+docker rm $name
+```
+
+查看 docker 容器占用 CPU,内存等信息
+```shell
+docker stats --no-stream
+```
+
+*有关容器部署的内容结束。*
+
+### 🕹  方式二:通过腾讯云函数(SCF)部署
+
+<hr>
+
+#### 1、下载 SCF 版本的压缩包
+
+此版本为特别版,支持通过腾讯云函数部署,与主分支版本不兼容,版本号为`v0.3_scf`,下载地址:
+[https://github.com/luolongfei/freenom/archive/refs/tags/v0.3_scf.zip](https://github.com/luolongfei/freenom/archive/refs/tags/v0.3_scf.zip)
+
+下载后解压到你能找到的任意目录,你将得到一个文件夹,后期将通过文件夹的形式上传到腾讯云函数。
+
+#### 2、创建腾讯云函数
+
+直接访问腾讯云函数控制台创建云函数: [https://console.cloud.tencent.com/scf/list-create](https://console.cloud.tencent.com/scf/list-create) ,
+按照下图所示的说明进行创建。如果无法看清图片,可访问: [https://github.com/luolongfei/freenom/blob/master/resources/screenshot/scf.png](https://github.com/luolongfei/freenom/blob/master/resources/screenshot/scf.png) 
+或者 [https://z3.ax1x.com/2021/06/01/2nKCF0.png](https://z3.ax1x.com/2021/06/01/2nKCF0.png) 查看原图。 
+
+[![scf01](https://z3.ax1x.com/2021/06/01/2nKCF0.png)](https://imgtu.com/i/2nKCF0)
+
+按照上图所示部署完成后,可以点击云函数的名称进入云函数管理画面,管理画面往下翻可看到`部署`与`测试`按钮,点击`测试`,稍等几秒钟,即可看到输出日志,
+根据输出日志判断配置以及部署是否正确。
+
+[![scf02](https://z3.ax1x.com/2021/06/01/2nGZ3q.png)](https://imgtu.com/i/2nGZ3q)
+
+*有关腾讯云函数部署的内容结束。*
+
+### 🚧  方式三:直接拉取源码部署
+
+<hr>
+
+所有操作均在Centos7系统下进行,其它Linux发行版大同小异
+#### 1、获取源码
+创建文件夹
+```shell script
+mkdir -p /data/wwwroot/freenom && cd /data/wwwroot/freenom
+```
+clone 本仓库源码
+```shell script
+git clone https://github.com/luolongfei/freenom.git ./
+```
+
+#### 2、修改配置
+复制配置文件模板
+```shell script
+cp .env.example .env
+```
+编辑配置文件
+```shell script
+vim .env
+```
+```shell script
+# 注意事项
+# .env 文件里每个项目都有详细的说明,这里不再赘述,简言之,你需要把里面所有项都改成你自己的。需要注意的是多账户配置的格式:
+# e.g. MULTIPLE_ACCOUNTS='<账户1>@<密码1>|<账户2>@<密码2>|<账户3>@<密码3>'
+# (注意不要省略“<>”符号,否则无法正确匹配)
+# 当然,若你只有单个账户,只配置 FREENOM_USERNAME 和 FREENOM_PASSWORD 就够了,单账户和多账户的配置会被合并在一起读取并去重。
+
+# 编辑完成后,按“Esc”回到命令模式,输入“:wq”回车即保存并退出,不会用 vim 编辑器的可以谷歌一下:)
+```
+
+#### 3、添加计划任务
+
+##### 3.1 安装 crontabs 以及 cronie
+```shell script
+yum -y install cronie crontabs
+```
+验证 crond 是否安装及启动
+```shell script
+yum list cronie && systemctl status crond
+```
+验证crontab是否安装
+```shell script
+yum list crontabs $$ which crontab && crontab -l
+```
+
+##### 3.2 打开任务表单,并编辑
+```shell script
+crontab -e
+```
+```shell script
+# 任务内容如下
+# 此任务的含义是在每天早上 9点 执行 /data/wwwroot/freenom/ 路径下的 run 文件,最佳实践是将这个时间修改为一个非整点的时间,防止与很多人在同一时间进行续期操作导致 freenom 无法稳定提供服务
+# 注意:某些情况下,crontab 可能找不到你的 php 路径,下面的命令执行后会在 freenom_crontab.log 文件输出错误信息,你应该指定 php 路径:把下面的 php 替换为 /usr/local/php/bin/php (根据实际情况,执行 whereis php 即可看到 php 执行文件的真实路径)
+00 09 * * * cd /data/wwwroot/freenom/ && php run > freenom_crontab.log 2>&1
+```
+
+##### 3.3 重启crond守护进程(每次编辑任务表单后都需此步,以使任务生效)
+```shell script
+systemctl restart crond
+```
+若要检查`计划任务`是否正常,你可以将上面的任务执行时间设置在几分钟后,然后等到任务执行完成,
+检查`/data/wwwroot/freenom/`目录下的`freenom_crontab.log`文件内容,是否有报错信息。常见的错误信息如下:
+- /bin/sh: php: command not found
+- /bin/sh: /usr/local/php: Is a directory
+
+*(点击即可展开或收起)*
+<details>
+    <summary>解决方案</summary>
+<br>
+
+>
+> 执行
+> ```shell script
+> whereis php
+> ```
+> ```shell script
+> # 上面的命令可确定 php 执行文件的位置,一般输出为“php: /usr/local/php /usr/local/php/bin/php”,选长的那个即:/usr/local/php/bin/php
+> ```
+> 
+> 现在我们知道 php 执行文件的路径是`/usr/local/php/bin/php`(根据你自己系统的实际情况,可能不同),然后修改表单任务里的命令,把
+> 
+> `00 09 * * * cd /data/wwwroot/freenom/ && php run > freenom_crontab.log 2>&1`
+> 
+> 改为
+> 
+> `00 09 * * * cd /data/wwwroot/freenom/ && /usr/local/php/bin/php run > freenom_crontab.log 2>&1`
+> 
+> 更多参考:[点这里](https://stackoverflow.com/questions/7397469/why-is-crontab-not-executing-my-php-script)
+>
+
+</details>
+
+当然,如果你的`计划任务`能正确找到`php路径`,没有错误,那你什么也不用做。
+
+*至此,所有的配置都已经完成,下面我们验证一下整个流程是否走通。*
+
+##### 3.4 验证
+你可以先将`.env`中的`NOTICE_FREQ`的值改为1(即每次执行都推送通知),然后执行
+```shell script
+cd /data/wwwroot/freenom/ && php run
+```
+不出意外的话,你将收到一封关于域名情况的邮件。
+
+***
+
+遇到任何问题或 Bug 欢迎提 [issue](https://github.com/luolongfei/freenom/issues) (请按模板格式提`issue`,以便我快速复现你的问题,否则问题会被忽略),
+如果`Freenom`改变算法导致此项目失效,请提 [issue](https://github.com/luolongfei/freenom/issues) 告知,我会及时修复,本项目长期维护。
+欢迎`star`~
+
+### 📋  捐赠名单 Donate List
+非常感谢「 [这些用户](https://github.com/luolongfei/freenom/wiki/Donate-List) 」对本项目的捐赠支持!
+
+### ❤  捐赠 Donate
+如果你觉得本项目真的有帮助到你并且想回馈作者,感谢你的捐赠。
+#### PayPal: [https://www.paypal.me/mybsdc](https://www.paypal.me/mybsdc)
+> Every time you spend money, you're casting a vote for the kind of world you want. -- Anna Lappe
+
+![pay](https://s2.ax1x.com/2020/01/31/1394at.png "Donate")
+
+![每一次你花的钱都是在为你想要的世界投票。](https://s2.ax1x.com/2020/01/31/13P8cF.jpg)
+
+**你的 star 或者`小额打赏`是我长期维护此项目的动力所在,由衷感谢每一位支持者,“每一次你花的钱都是在为你想要的世界投票”。
+另外,将本项目推荐给更多的人,也是一种支持的方式,用的人越多更新的动力越足。**
+
+### 🚁  我正在用的 VPS
+
+走我的 Aff 地址购买,我可以获得一点返利,也算是一种支持方式,介意者复制官网地址访问即可。
+
+| 名称 | 购买地址 | 备注 |
+| :---: | :--- | :--- |
+| 搬瓦工 | [https://bwh81.net/aff.php?aff=24499&pid=104](https://bwh81.net/aff.php?aff=24499&pid=104) (日本软银 VPS 限量版,`65美元`一年,优惠码:`BWH3HYATVBJW`) <br> [https://bwh81.net/aff.php?aff=24499&pid=94](https://bwh81.net/aff.php?aff=24499&pid=94) (CN2 GIA LIMITED EDITION,`DC 6`机房,`46.8美元`一年) <br> [https://bwh81.net/aff.php?aff=24499&pid=71](https://bwh81.net/aff.php?aff=24499&pid=71) (CN2 GIA 丐版,`DC 9`机房,`37.79美元`一年) | 稳定大厂,它们家`限量版 GIA`很香。目前是我的主力机型。 |
+| Vultr | [https://www.vultr.com/?ref=7429134](https://www.vultr.com/?ref=7429134) (最低`2.5美元`每月) <br> [https://www.vultr.com/?ref=8399703-6G](https://www.vultr.com/?ref=8399703-6G) (新用户可得`100美元`体验金) | 随开随停,很方便。 |
+| PacificRack | [https://pacificrack.com/portal/aff.php?aff=1150&pid=11](https://pacificrack.com/portal/aff.php?aff=1150&pid=11) (`9.9美元`一年) | 最便宜的机型`9.9美元`一年,QuadraNet 机房,我用了两年了目前感觉很稳。 |
+
+### 🍺  信仰
+
+![南京市民李先生](https://s2.ax1x.com/2020/02/04/1Bm3Ps.jpg "南京市民李先生")
+> 
+> 认真是我们参与这个社会的方式,认真是我们改变这个社会的方式。  ——李志
+
+### 🌚  作者
+- 主程序以及框架:[@luolongfei](https://github.com/luolongfei)
+- 英文版文档:[@肖阿姨](#)
+
+### 📰  更新日志
+
+此处省略了很多较为久远的记录,以前的日志只记录了比较大的变更,以后的日志会尽可能详尽一些。
+
+#### [Unreleased]
+
+##### Added
+
+- 支持通过 Server酱 以及 企业微信 推送通知
+
+##### Changed
+
+- 多个账户的续期结果通知合并为同一条消息
+- 整合各种送信方式,优化相关逻辑
+- 支持交互式安装,免去手动修改配置的繁琐操作
+
+#### [v0.3](https://github.com/luolongfei/freenom/releases/tag/v0.3) - 2021-05-27
+
+##### Added
+
+- 追加 Docker 版本,支持通过 Docker 方式部署,简化部署流程
+
+#### [v0.2.5](https://github.com/luolongfei/freenom/releases/tag/v0.2.5) - 2020-06-23
+
+##### Added
+
+- 支持在 Github Actions 上执行(应 GitHub 官方要求,已移除此功能)
+
+#### [v0.2.2](https://github.com/luolongfei/freenom/releases/tag/v0.2.2) - 2020-02-06
+
+##### Added
+
+- 新增通过 Telegram bot 送信
+- 各种送信方式支持单独开关
+
+#### [v0.2](https://github.com/luolongfei/freenom/releases/tag/v0.2) - 2020-02-01
+
+##### Added
+
+- 支持多个 Freenom 账户进行域名续期
+
+##### Changed
+
+- 进行了彻底的重构,框架化
+- 优化邮箱模块,支持自动选择合适的邮箱配置
+
+
+*(版本在 v0.1 到 v0.2 期间代码有过很多次变更,之前没有发布版本,故此处不再赘述相关变更日志)*
+
+#### [v0.1](https://github.com/luolongfei/freenom/releases/tag/v0.1) - 2018-8-13
+
+##### Added
+
+- 初版,开源,基础的续期功能
+
+### 🎉  鸣谢
+- [PHPMailer](https://github.com/PHPMailer/PHPMailer/) (邮件发送功能依赖此库)
+- [guzzle](https://github.com/guzzle/guzzle) (Curl库)
+- [秋水逸冰](https://teddysun.com/569.html) (本项目 Docker 相关文档有参考秋水逸冰的文章)
+
+### 🥝  开源协议
+[MIT](https://opensource.org/licenses/mit-license.php)

+ 196 - 0
README_EN.md

@@ -0,0 +1,196 @@
+<div align="center">
+<h1>Freenom: freenom domain name renews automatically</h1>
+
+[![Build Status](https://img.shields.io/badge/build-passed-brightgreen?style=for-the-badge)](https://scrutinizer-ci.com/g/luolongfei/freenom/build-status/master)
+[![Php Version](https://img.shields.io/badge/php-%3E=7.2-brightgreen.svg?style=for-the-badge)](https://secure.php.net/)
+[![Scrutinizer Code Quality](https://img.shields.io/badge/scrutinizer-9.31-brightgreen?style=for-the-badge)](https://scrutinizer-ci.com/g/luolongfei/freenom/?branch=master)
+[![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](https://github.com/luolongfei/freenom/blob/master/LICENSE)
+
+Documentation: English version | [中文版](https://github.com/luolongfei/freenom)
+</div>
+
+[📃  Why write this script](#--Why-write-this-script)
+
+[🍭  Demo](#--Demo)
+
+[🎁  Preparation](#--Preparation)
+
+[📪  Setting up Gmail](#--Setting-up-Gmail)
+
+[🤶  Telegram bot](#--Telegram-bot)
+
+[🚧  Configuration script](#--Configuration-script)
+
+[🎈  Add scheduled task](#--Add-scheduled-task)
+
+[☕  Verification](#--Verification)
+
+[❤  Donate](#--Donate)
+
+[🌚  Author](#--Author)
+
+[🎉  Acknowledgements](#--Acknowledgements)
+
+[🥝  Open source agreement](#--Open-source-agreement)
+
+
+### 📃  Why write this script
+As we all know, Freenom is the only merchant on the planet that provides free top-level domain names, but it needs to be renewed every year for up to one year at a time. Since I applied for a bunch of domain names, and not at the same time,
+So I felt frustrated every time I renewed, so I wrote this automatic renewal script.
+
+### 🍭  Demo
+![Email example](https://s2.ax1x.com/2020/01/31/139Rrd.png "Email content")
+
+Regardless of the success or failure of the renewal or the execution of the script, you will receive emails from the program. In the case of a renewal success or failure email, the email will include the number of days that the domain name has not been renewed.
+
+### 🎁  Preparation
+- Email of robot: Used to send notification emails.
+- Your email: Used to receive notification emails sent by robots.
+- VPS: Any server can be used. The system recommends `Centos7`, and the PHP version must be` php7.1` or above.
+- No more
+
+### 📪  Setting up Gmail
+***
+1.In `Settings > Forwarding and POP/IMAP`, tick
+- Enable POP for all messages
+- Enable IMAP
+
+![gmail Configuration 01](https://s2.ax1x.com/2020/02/01/1GDsMR.png "gmail Configuration 01")
+
+Then save your changes.
+
+2.Allow less secure applications
+
+After logging into Google Mail, visit [this page](https://myaccount.google.com/u/0/lesssecureapps?pli=1&pageId=none) and enable the application that is not secure enough.
+
+Also, if prompted
+> Do not allow access to account
+
+After logging in to Google Mail, go to [this page](https://accounts.google.com/b/0/DisplayUnlockCaptcha) and click Allow. This situation is relatively rare.
+
+### 🤶  Telegram bot
+***
+If you don't want to use email push, you can also use Telegram bot. In the `.env` file,
+Change the value of `TELEGRAM_BOT_ENABLE` to `true` to enable the Telegram bot.
+Similarly, change the value of `MAIL_ENABLE` to `false` to disable the mail push method.
+Telegram bot has two configuration items, one is `chatID` (corresponding to `TELEGRAM_CHAT_ID` in `.env` file),
+You can get your own id by sending `/start` to `@userinfobot` using your Telegram account,
+The other is `token` (corresponding to `TELEGRAM_BOT_TOKEN` in the `.env` file), 
+your Telegram bot token, how to create a Telegram bot and how to get the token please refer to: 
+[Official Document](https://core.telegram.org/bots#6-botfather)
+
+*This completes the settings related to notifications, followed by the configuration related to this program* :)
+
+### 🚧  Configuration script
+All operations are performed under Centos7 system, other Linux distributions are similar
+#### Get the source code
+```bash
+$ mkdir -p /data/wwwroot/freenom
+$ cd /data/wwwroot/freenom
+
+# clone the repository source
+$ git clone https://github.com/luolongfei/freenom.git ./
+```
+
+#### Configuration process
+```bash
+# Copy configuration file template
+$ cp .env.example .env
+
+# Edit configuration file
+$ vim .env
+
+# .env Each item in the file has a detailed description, which will not be repeated here. In short, you need to change all the items in it to your own. Note the format of the multi-account configuration:
+# e.g. MULTIPLE_ACCOUNTS='<account1>@<password1>|<account2>@<password2>|<account3>@<password3>'
+# Of course, if you only have a single account, you only need to configure FREEENOM_USERNAME and FREEENOM_PASSWORD. The configurations of single account and multiple accounts will be read together and duplicated.
+
+# After editing, press "Esc" to return to the command mode, enter ":wq" and press Enter to save and exit. If you don't use vim editor, you can ask Uncle Google. :)
+```
+
+### 🎈  Add scheduled task
+#### Install crontabs and cronie
+```bash
+$ yum -y install cronie crontabs
+
+# Verify if crond is installed and started
+$ yum list cronie && systemctl status crond
+
+# Verify that crontab is installed
+$ yum list crontabs $$ which crontab && crontab -l
+```
+
+#### Open the task form and edit
+```bash
+$ crontab -e
+
+# Task content is as follows
+# The meaning of this task is to execute the run file under /data/wwwroot/freenom/ at 9 AM every day
+# Note: In some cases, crontab may not find your php path. The following command will output an error message in the freenom_crontab.log file. You should specify the php path: replace the following php with /usr/local/php/bin/php (based on the actual situation)
+00 09 * * * cd /data/wwwroot/freenom/ && php run > freenom_crontab.log 2>&1
+```
+
+#### Restart the crond daemon (This step is required each time you edit the task form for the task to take effect)
+```bash
+$ systemctl restart crond
+```
+To check if the `Task` is normal, you can set the execution time of the above task to a few minutes, and then wait until the task execution is completed,
+check the contents of the `freenom_crontab.log` file in the `/data/wwwroot/freenom/` directory for errors. Common error messages are as follows:
+- /bin/sh: php: command not found
+- /bin/sh: /usr/local/php: Is a directory
+
+*(Click to expand or collapse)*
+<details>
+    <summary>solution</summary>
+<br>
+
+>
+> execute
+> ```bash
+> $ whereis php
+> # Determine the location of php, the general output is "php: /usr/local/php /usr/local/php/bin/php", we choose: /usr/local/php/bin/php
+> ```
+> Now we know that php's path is `/usr/local/php/bin/php` (may be different according to the actual situation of your own system), 
+> and then modify the commands in the form task, change
+> 
+> `00 09 * * * cd /data/wwwroot/freenom/ && php run > freenom_crontab.log 2>&1`
+> 
+> to
+> 
+> `00 09 * * * cd /data/wwwroot/freenom/ && /usr/local/php/bin/php run > freenom_crontab.log 2>&1`
+> 
+> More information: [click here](https://stackoverflow.com/questions/7397469/why-is-crontab-not-executing-my-php-script)
+> 
+
+</details>
+
+Of course, if your `crontab` can correctly find the `php path` without error, you don't need to do anything.
+
+*So far, all the configurations have been completed, let's verify if the whole process works* :)
+
+### ☕  Verification
+You can first change the value of `NOTICE_FREQ` in `.env` to 1 (Push notification every time the script is executed), and then execute
+```bash
+$ cd /data/wwwroot/freenom/ && php run
+```
+If nothing else, you will receive an email about the domain name.
+
+If you encounter any problems or bugs, please mention [issues](https://github.com/luolongfei/freenom/issues). If freenom changes the algorithm and causes this project to fail,
+Please mention [issues](https://github.com/luolongfei/freenom/issues) to inform me that I will fix it in time and maintain this project for a long time. Welcome star ~
+
+### ❤  Donate
+
+#### PayPal: [https://www.paypal.me/mybsdc](https://www.paypal.me/mybsdc)
+> Every time you spend money, you're casting a vote for the kind of world you want .-- Anna Lappe
+
+![Every time you spend your money, you are voting for the world you want. ](https://s2.ax1x.com/2020/01/31/13P8cF.jpg)
+
+### 🌚  Author
+- Main program and framework: [@luolongfei](https://github.com/luolongfei)
+- English document: [@肖阿姨](#)
+
+### 🎉  Acknowledgements
+- [PHPMailer](https://github.com/PHPMailer/PHPMailer/) (Mail sending function depends on this library)
+- [guzzle](https://github.com/guzzle/guzzle) (Curl library)
+
+### 🥝  Open source agreement
+[MIT](https://opensource.org/licenses/mit-license.php)

+ 369 - 0
app/Console/FreeNom.php

@@ -0,0 +1,369 @@
+<?php
+/**
+ * FreeNom域名自动续期
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2020/1/19
+ * @time 17:29
+ * @link https://github.com/luolongfei/freenom
+ */
+
+namespace Luolongfei\App\Console;
+
+use Luolongfei\App\Exceptions\LlfException;
+use GuzzleHttp\Client;
+use GuzzleHttp\Cookie\CookieJar;
+use Luolongfei\Lib\Log;
+use Luolongfei\Lib\Mail;
+use Luolongfei\Lib\TelegramBot;
+
+class FreeNom
+{
+    const VERSION = 'v0.3';
+
+    const TIMEOUT = 34.52;
+
+    // FreeNom登录地址
+    const LOGIN_URL = 'https://my.freenom.com/dologin.php';
+
+    // 域名状态地址
+    const DOMAIN_STATUS_URL = 'https://my.freenom.com/domains.php?a=renewals';
+
+    // 域名续期地址
+    const RENEW_DOMAIN_URL = 'https://my.freenom.com/domains.php?submitrenewals=true';
+
+    // 匹配token的正则
+    const TOKEN_REGEX = '/name="token"\svalue="(?P<token>[^"]+)"/i';
+
+    // 匹配域名信息的正则
+    const DOMAIN_INFO_REGEX = '/<tr><td>(?P<domain>[^<]+)<\/td><td>[^<]+<\/td><td>[^<]+<span class="[^"]+">(?P<days>\d+)[^&]+&domain=(?P<id>\d+)"/i';
+
+    // 匹配登录状态的正则
+    const LOGIN_STATUS_REGEX = '/<li.*?Logout.*?<\/li>/i';
+
+    /**
+     * @var FreeNom
+     */
+    protected static $instance;
+
+    /**
+     * @var Client
+     */
+    protected $client;
+
+    /**
+     * @var CookieJar | bool
+     */
+    protected $jar = true;
+
+    /**
+     * @var string freenom账户
+     */
+    protected $username;
+
+    /**
+     * @var string freenom密码
+     */
+    protected $password;
+
+    public function __construct()
+    {
+        $this->client = new Client([
+            'headers' => [
+                'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
+                'Accept-Encoding' => 'gzip, deflate, br',
+                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
+            ],
+            'timeout' => self::TIMEOUT,
+            CURLOPT_FOLLOWLOCATION => true,
+            CURLOPT_AUTOREFERER => true,
+            'verify' => config('verifySSL'),
+            'debug' => config('debug')
+        ]);
+
+        system_log(sprintf('当前程序版本 %s', self::VERSION));
+    }
+
+    /**
+     * @return FreeNom
+     */
+    public static function instance()
+    {
+        if (!self::$instance instanceof self) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 登录
+     */
+    protected function login()
+    {
+        $this->client->post(self::LOGIN_URL, [
+            'headers' => [
+                'Content-Type' => 'application/x-www-form-urlencoded',
+                'Referer' => 'https://my.freenom.com/clientarea.php'
+            ],
+            'form_params' => [
+                'username' => $this->username,
+                'password' => $this->password
+            ],
+            'cookies' => $this->jar
+        ]);
+    }
+
+    /**
+     * 续期
+     *
+     * @throws \Exception
+     * @throws LlfException
+     */
+    public function renewDomains()
+    {
+        // 所有请求共用一个CookieJar实例
+        $this->jar = new CookieJar();
+
+        $this->login();
+        $authCookie = $this->jar->getCookieByName('WHMCSZH5eHTGhfvzP')->getValue();
+        if (empty($authCookie)) {
+            throw new LlfException(34520002);
+        }
+
+        // 检查域名状态
+        $response = $this->client->get(self::DOMAIN_STATUS_URL, [
+            'headers' => [
+                'Referer' => 'https://my.freenom.com/clientarea.php'
+            ],
+            'cookies' => $this->jar
+        ]);
+        $body = (string)$response->getBody();
+
+        if (!preg_match(self::LOGIN_STATUS_REGEX, $body)) {
+            throw new LlfException(34520009);
+        }
+
+        // 域名数据
+        if (!preg_match_all(self::DOMAIN_INFO_REGEX, $body, $domains, PREG_SET_ORDER)) {
+            throw new LlfException(34520003);
+        }
+
+        // 页面token
+        if (!preg_match(self::TOKEN_REGEX, $body, $matches)) {
+            throw new LlfException(34520004);
+        }
+        $token = $matches['token'];
+
+        // 续期
+        $result = '';
+        $renewed = $renewedTG = ''; // 续期成功的域名
+        $notRenewed = $notRenewedTG = ''; // 记录续期出错的域名,用于邮件通知内容
+        $domainInfo = $domainInfoTG = ''; // 域名状态信息,用于邮件通知内容
+        foreach ($domains as $d) {
+            $domain = $d['domain'];
+            $days = intval($d['days']);
+            $id = $d['id'];
+
+            // 免费域名只允许在到期前14天内续期
+            if ($days <= 14) {
+                try {
+                    $response = $this->client->post(self::RENEW_DOMAIN_URL, [
+                        'headers' => [
+                            'Referer' => sprintf('https://my.freenom.com/domains.php?a=renewdomain&domain=%s', $id),
+                            'Content-Type' => 'application/x-www-form-urlencoded'
+                        ],
+                        'form_params' => [
+                            'token' => $token,
+                            'renewalid' => $id,
+                            sprintf('renewalperiod[%s]', $id) => '12M', // 续期一年
+                            'paymentmethod' => 'credit', // 支付方式:信用卡
+                        ],
+                        'cookies' => $this->jar
+                    ]);
+                } catch (\Exception $e) {
+                    system_log(sprintf('%s:续期请求出错:%s', $this->username, $e->getMessage()));
+                    continue;
+                }
+
+                $body = (string)$response->getBody();
+                sleep(1);
+
+                if (stripos($body, 'Order Confirmation') === false) { // 续期失败
+                    $result .= sprintf("%s续期失败\n", $domain);
+                    $notRenewed .= sprintf('<a href="http://%s" rel="noopener" target="_blank">%s</a>', $domain, $domain);
+                    $notRenewedTG .= sprintf('[%s](http://%s)  ', $domain, $domain);
+                } else {
+                    $result .= sprintf("%s续期成功\n", $domain);
+                    $renewed .= sprintf('<a href="http://%s" rel="noopener" target="_blank">%s</a>', $domain, $domain);
+                    $renewedTG .= sprintf('[%s](http://%s)  ', $domain, $domain);
+                    continue;
+                }
+            }
+
+            $domainInfo .= sprintf('<a href="http://%s" rel="noopener" target="_blank">%s</a>还有<span style="font-weight: bold; font-size: 16px;">%d</span>天到期,', $domain, $domain, $days);
+            $domainInfoTG .= sprintf('[%s](http://%s)还有*%d*天到期,', $domain, $domain, $days);
+        }
+        $domainInfoTG .= "更多信息可以参考[Freenom官网](https://my.freenom.com/domains.php?a=renewals)哦~\n\n(如果你不想每次执行都收到推送,请将 .env 中 NOTICE_FREQ 的值设为0,使程序只在有续期操作时才推送)";
+
+        if ($notRenewed || $renewed) {
+            Mail::send(
+                '主人,我刚刚帮你续期域名啦~',
+                [
+                    $this->username,
+                    $renewed ? sprintf('续期成功:%s<br>', $renewed) : '',
+                    $notRenewed ? sprintf('续期出错:%s<br>', $notRenewed) : '',
+                    $domainInfo ?: '哦豁,没看到其它域名。'
+                ]
+            );
+            TelegramBot::send(sprintf(
+                "主人,我刚刚帮你续期域名啦~\n\n%s%s\n另外,%s",
+                $renewedTG ? sprintf("续期成功:%s\n", $renewedTG) : '',
+                $notRenewedTG ? sprintf("续期失败:%s\n", $notRenewedTG) : '',
+                $domainInfoTG
+            ));
+            system_log(sprintf("%s:续期结果如下:\n%s", $this->username, $result));
+        } else {
+            if (config('noticeFreq') == 1) {
+                Mail::send(
+                    '报告,今天没有域名需要续期',
+                    [
+                        $this->username,
+                        $domainInfo
+                    ],
+                    '',
+                    'notice'
+                );
+                TelegramBot::send("报告,今天没有域名需要续期,所有域名情况如下:\n\n" . $domainInfoTG);
+            } else {
+                system_log('当前通知频率为「仅当有续期操作时」,故本次不会推送通知');
+            }
+            system_log(sprintf('%s:<green>执行成功,今次没有需要续期的域名</green>', $this->username));
+        }
+    }
+
+    /**
+     * 二维数组去重
+     *
+     * @param array $array 原始数组
+     * @param array $keys 可指定对应的键联合
+     *
+     * @return bool
+     */
+    public function arrayUnique(array &$array, array $keys = [])
+    {
+        if (!isset($array[0]) || !is_array($array[0])) {
+            return false;
+        }
+
+        if (empty($keys)) {
+            $keys = array_keys($array[0]);
+        }
+
+        $tmp = [];
+        foreach ($array as $k => $items) {
+            $combinedKey = '';
+            foreach ($keys as $key) {
+                $combinedKey .= $items[$key];
+            }
+
+            if (isset($tmp[$combinedKey])) {
+                unset($array[$k]);
+            } else {
+                $tmp[$combinedKey] = $k;
+            }
+        }
+        unset($tmp);
+
+        return true;
+    }
+
+    /**
+     * 获取freenom账户信息
+     *
+     * @return array
+     * @throws LlfException
+     */
+    protected function getAccounts()
+    {
+        $accounts = [];
+        $multipleAccounts = preg_replace('/\s/', '', env('MULTIPLE_ACCOUNTS'));
+        if (preg_match_all('/<(?P<u>.*?)>@<(?P<p>.*?)>/i', $multipleAccounts, $matches, PREG_SET_ORDER)) {
+            foreach ($matches as $m) {
+                $accounts[] = [
+                    'username' => $m['u'],
+                    'password' => $m['p']
+                ];
+            }
+        }
+
+        $username = env('FREENOM_USERNAME');
+        $password = env('FREENOM_PASSWORD');
+        if ($username && $password) {
+            $accounts[] = [
+                'username' => $username,
+                'password' => $password
+            ];
+        }
+
+        if (empty($accounts)) {
+            throw new LlfException(34520001);
+        }
+
+        // 去重
+        $this->arrayUnique($accounts);
+
+        return $accounts;
+    }
+
+    /**
+     * 发送异常报告
+     *
+     * @param \Exception $e
+     *
+     * @throws \Exception
+     */
+    private function sendExceptionReport($e)
+    {
+        Mail::send(
+            '主人,' . $e->getMessage(),
+            [
+                $this->username,
+                sprintf('具体是在%s文件的第%d行,抛出了一个异常。异常的内容是%s,快去看看吧。', $e->getFile(), $e->getLine(), $e->getMessage()),
+            ],
+            '',
+            'LlfException'
+        );
+
+        TelegramBot::send(sprintf(
+            '主人,出错了。具体是在%s文件的第%d行,抛出了一个异常。异常的内容是%s,快去看看吧。(账户:%s)',
+            $e->getFile(),
+            $e->getLine(),
+            $e->getMessage(),
+            $this->username
+        ), '', false);
+    }
+
+    /**
+     * @throws LlfException
+     * @throws \Exception
+     */
+    public function handle()
+    {
+        $accounts = $this->getAccounts();
+        foreach ($accounts as $account) {
+            try {
+                $this->username = $account['username'];
+                $this->password = $account['password'];
+
+                $this->renewDomains();
+            } catch (LlfException $e) {
+                system_log(sprintf('出错:<red>%s</red>', $e->getMessage()));
+                $this->sendExceptionReport($e);
+            } catch (\Exception $e) {
+                system_log(sprintf('出错:<red>%s</red>', $e->getMessage()), $e->getTrace());
+                $this->sendExceptionReport($e);
+            }
+        }
+    }
+}

+ 0 - 0
app/Exceptions/.gitkeep


+ 29 - 0
app/Exceptions/LlfException.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * 业务逻辑异常时抛出
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2018/8/10
+ * @time 14:48
+ */
+
+namespace Luolongfei\App\Exceptions;
+
+class LlfException extends \Exception
+{
+    public function __construct($code, $additional = null, \Exception $previous = null)
+    {
+        $message = lang('exception_msg.' . $code) ?: '';
+
+        if ($additional !== null) {
+            if (is_array($additional)) {
+                array_unshift($additional, $message);
+                $message = call_user_func_array('sprintf', $additional);
+            } else if (is_string($additional)) {
+                $message = sprintf($message, $additional);
+            }
+        }
+
+        parent::__construct($message . "(Error code: {$code})", $code, $previous);
+    }
+}

+ 248 - 0
app/helpers.php

@@ -0,0 +1,248 @@
+<?php
+/**
+ * 助手函数
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2019/3/3
+ * @time 16:34
+ */
+
+use Luolongfei\App\Exceptions\LlfException;
+use Luolongfei\Lib\Argv;
+use Luolongfei\Lib\Config;
+use Luolongfei\Lib\Log;
+use Luolongfei\Lib\Env;
+use Luolongfei\Lib\Lang;
+use Luolongfei\Lib\PhpColor;
+
+if (!function_exists('config')) {
+    /**
+     * 获取配置
+     *
+     * @param string $key 键,支持点式访问
+     * @param string $default 默认值
+     *
+     * @return array|mixed
+     */
+    function config($key = '', $default = null)
+    {
+        return Config::instance()->get($key, $default);
+    }
+}
+
+if (!function_exists('lang')) {
+    /**
+     * 读取语言包
+     *
+     * @param string $key 键,支持点式访问
+     *
+     * @return array|mixed|null
+     */
+    function lang($key = '')
+    {
+        return Lang::instance()->get($key);
+    }
+}
+
+if (!function_exists('system_log')) {
+    /**
+     * 写日志
+     *
+     * @param $content
+     * @param array $response
+     * @param string $fileName
+     * @description 受支持的着色标签
+     * 'reset', 'bold', 'dark', 'italic', 'underline', 'blink', 'reverse', 'concealed', 'default', 'black', 'red',
+     * 'green', 'yellow', 'blue', 'magenta', 'cyan', 'light_gray', 'dark_gray', 'light_red', 'light_green',
+     * 'light_yellow', 'light_blue', 'light_magenta', 'light_cyan', 'white', 'bg_default', 'bg_black', 'bg_red',
+     * 'bg_green', 'bg_yellow', 'bg_blue', 'bg_magenta', 'bg_cyan', 'bg_light_gray', 'bg_dark_gray', 'bg_light_red',
+     * 'bg_light_green','bg_light_yellow', 'bg_light_blue', 'bg_light_magenta', 'bg_light_cyan', 'bg_white'
+     */
+    function system_log($content, array $response = [], $fileName = '')
+    {
+        try {
+            $path = sprintf('%s/logs/%s/', ROOT_PATH, date('Y-m'));
+            $file = $path . ($fileName ?: date('d')) . '.log';
+
+            if (!is_dir($path)) {
+                mkdir($path, 0777, true);
+                chmod($path, 0777);
+            }
+
+            $handle = fopen($file, 'a'); // 追加而非覆盖
+
+            if (!filesize($file)) {
+                chmod($file, 0666);
+            }
+
+            $msg = sprintf(
+                "[%s] %s %s\n",
+                date('Y-m-d H:i:s'),
+                is_string($content) ? $content : json_encode($content),
+                $response ? json_encode($response, JSON_UNESCAPED_UNICODE) : '');
+
+            // 在 Github Actions 上运行,过滤敏感信息
+            if (env('ON_GITHUB_ACTIONS')) {
+                $msg = preg_replace_callback('/(?P<secret>[\w-.]{1,4}?)(?=@[\w-.]+)/i', function ($m) {
+                    return str_ireplace($m['secret'], str_repeat('*', strlen($m['secret'])), $m['secret']);
+                }, $msg);
+            }
+
+            // 尝试为消息着色
+            $c = PhpColor::instance()->getColorInstance();
+            echo $c($msg)->colorize();
+
+            // 干掉着色标签
+            $msg = strip_tags($msg); // 不完整或者破损标签将导致更多的数据被删除
+
+            fwrite($handle, $msg);
+            fclose($handle);
+
+            flush();
+        } catch (\Exception $e) {
+            // do nothing
+        }
+    }
+}
+
+if (!function_exists('is_locked')) {
+    /**
+     * 检查任务是否已被锁定
+     *
+     * @param string $taskName
+     * @param bool $always 是否被永久锁定
+     *
+     * @return bool
+     * @throws Exception
+     */
+    function is_locked($taskName = '', $always = false)
+    {
+        try {
+            $lock = sprintf(
+                '%s/num_limit/%s/%s.lock',
+                APP_PATH,
+                $always ? 'always' : date('Y-m-d'),
+                $taskName
+            );
+
+            return file_exists($lock);
+        } catch (\Exception $e) {
+            system_log(sprintf('检查任务%s是否锁定时出错,错误原因:%s', $taskName, $e->getMessage()));
+        }
+
+        return false;
+    }
+}
+
+if (!function_exists('lock_task')) {
+    /**
+     * 锁定任务
+     *
+     * 防止重复执行
+     *
+     * @param string $taskName
+     * @param bool $always 是否永久锁定
+     *
+     * @return bool
+     */
+    function lock_task($taskName = '', $always = false)
+    {
+        try {
+            $lock = sprintf(
+                '%s/num_limit/%s/%s.lock',
+                APP_PATH,
+                $always ? 'always' : date('Y-m-d'),
+                $taskName
+            );
+
+            $path = dirname($lock);
+            if (!is_dir($path)) {
+                mkdir($path, 0777, true);
+                chmod($path, 0777);
+            }
+
+            if (file_exists($lock)) {
+                return true;
+            }
+
+            $handle = fopen($lock, 'a'); // 追加而非覆盖
+
+            if (!filesize($lock)) {
+                chmod($lock, 0666);
+            }
+
+            fwrite($handle, sprintf(
+                    "Locked at %s.\n",
+                    date('Y-m-d H:i:s')
+                )
+            );
+
+            fclose($handle);
+
+            Log::info(sprintf('%s已被锁定,此任务%s已不会再执行,请知悉', $taskName, $always ? '' : '今天内'));
+        } catch (\Exception $e) {
+            system_log(sprintf('创建锁定任务文件%s时出错,错误原因:%s', $lock, $e->getMessage()));
+
+            return false;
+        }
+
+        return true;
+    }
+}
+
+if (!function_exists('env')) {
+    /**
+     * 获取环境变量值
+     *
+     * @param string $key
+     * @param string $default 默认值
+     *
+     * @return array | bool | false | null | string
+     */
+    function env($key = '', $default = null)
+    {
+        return Env::instance()->get($key, $default);
+    }
+}
+
+if (!function_exists('get_argv')) {
+    /**
+     * 获取命令行传参
+     *
+     * @param string $name
+     * @param string $default 默认值
+     *
+     * @return mixed|string
+     */
+    function get_argv(string $name, string $default = '')
+    {
+        return Argv::instance()->get($name, $default);
+    }
+}
+
+if (!function_exists('system_check')) {
+    /**
+     * 检查环境是否满足要求
+     *
+     * @throws LlfException
+     */
+    function system_check()
+    {
+        if (!function_exists('putenv')) {
+            throw new LlfException(34520005);
+        }
+
+        if (version_compare(PHP_VERSION, '7.0.0') < 0) {
+            throw new LlfException(34520006);
+        }
+
+        $envFile = ROOT_PATH . '/.env';
+        if (!file_exists($envFile)) {
+            throw new LlfException(copy(ROOT_PATH . '/.env.example', $envFile) ? 34520007 : 34520008);
+        }
+
+        if (!extension_loaded('curl')) {
+            throw new LlfException(34520010);
+        }
+    }
+}

+ 37 - 0
composer.json

@@ -0,0 +1,37 @@
+{
+  "name": "luolongfei/freenom",
+  "description": "freenom域名自动续期。",
+  "type": "project",
+  "require": {
+    "php": ">=7.1.0",
+    "ext-curl": "*",
+    "ext-openssl": "*",
+    "guzzlehttp/guzzle": "^6.3",
+    "monolog/monolog": "^1.24",
+    "bramus/monolog-colored-line-formatter": "~2.0",
+    "phpmailer/phpmailer": "^6.0",
+    "vlucas/phpdotenv": "^3.3",
+    "predis/predis": "^1.1",
+    "ext-json": "*",
+    "ext-bcmath": "*",
+    "ext-pdo": "*",
+    "kevinlebrun/colors.php": "^1.0"
+  },
+  "license": "MIT",
+  "authors": [
+    {
+      "name": "luolongfei",
+      "email": "[email protected]",
+      "homepage": "https://github.com/luolongfei/freenom"
+    }
+  ],
+  "autoload": {
+    "psr-4": {
+      "Luolongfei\\Lib\\": "libs",
+      "Luolongfei\\App\\": "app"
+    },
+    "files": [
+      "app/helpers.php"
+    ]
+  }
+}

+ 844 - 0
composer.lock

@@ -0,0 +1,844 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "1490a7a3df10963c3b2891841cfe0591",
+    "packages": [
+        {
+            "name": "bramus/ansi-php",
+            "version": "3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/bramus/ansi-php.git",
+                "reference": "fb0be33f36053af7454d462e3ddc0a2ac0b2f311"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/bramus/ansi-php/zipball/fb0be33f36053af7454d462e3ddc0a2ac0b2f311",
+                "reference": "fb0be33f36053af7454d462e3ddc0a2ac0b2f311",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Bramus\\Ansi\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bramus Van Damme",
+                    "email": "[email protected]",
+                    "homepage": "https://www.bram.us/"
+                }
+            ],
+            "description": "ANSI Control Functions and ANSI Control Sequences (Colors, Erasing, etc.) for PHP CLI Apps",
+            "time": "2019-12-03T09:04:38+00:00"
+        },
+        {
+            "name": "bramus/monolog-colored-line-formatter",
+            "version": "2.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/bramus/monolog-colored-line-formatter.git",
+                "reference": "6bff15eee00afe2690642535b0f1541f10852c2b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/bramus/monolog-colored-line-formatter/zipball/6bff15eee00afe2690642535b0f1541f10852c2b",
+                "reference": "6bff15eee00afe2690642535b0f1541f10852c2b",
+                "shasum": ""
+            },
+            "require": {
+                "bramus/ansi-php": "~3.0",
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "monolog/monolog": "~1.0",
+                "phpunit/phpunit": "~4.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Bramus\\Monolog\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bramus Van Damme",
+                    "email": "[email protected]",
+                    "homepage": "https://www.bram.us/"
+                }
+            ],
+            "description": "Colored Line Formatter for Monolog",
+            "time": "2015-01-07T22:12:35+00:00"
+        },
+        {
+            "name": "guzzlehttp/guzzle",
+            "version": "6.5.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle.git",
+                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82",
+                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "guzzlehttp/promises": "^1.0",
+                "guzzlehttp/psr7": "^1.6.1",
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "ext-curl": "*",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+                "psr/log": "^1.1"
+            },
+            "suggest": {
+                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+                "psr/log": "Required for using the Log middleware"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "6.5-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "[email protected]",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Guzzle is a PHP HTTP client library",
+            "homepage": "http://guzzlephp.org/",
+            "keywords": [
+                "client",
+                "curl",
+                "framework",
+                "http",
+                "http client",
+                "rest",
+                "web service"
+            ],
+            "time": "2019-12-23T11:57:10+00:00"
+        },
+        {
+            "name": "guzzlehttp/promises",
+            "version": "v1.3.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/promises.git",
+                "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+                "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.4-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Promise\\": "src/"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "[email protected]",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Guzzle promises library",
+            "keywords": [
+                "promise"
+            ],
+            "time": "2016-12-20T10:07:11+00:00"
+        },
+        {
+            "name": "guzzlehttp/psr7",
+            "version": "1.6.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/psr7.git",
+                "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
+                "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0",
+                "psr/http-message": "~1.0",
+                "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
+            },
+            "provide": {
+                "psr/http-message-implementation": "1.0"
+            },
+            "require-dev": {
+                "ext-zlib": "*",
+                "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
+            },
+            "suggest": {
+                "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.6-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Psr7\\": "src/"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "[email protected]",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "PSR-7 message implementation that also provides common utility methods",
+            "keywords": [
+                "http",
+                "message",
+                "psr-7",
+                "request",
+                "response",
+                "stream",
+                "uri",
+                "url"
+            ],
+            "time": "2019-07-01T23:21:34+00:00"
+        },
+        {
+            "name": "kevinlebrun/colors.php",
+            "version": "1.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/kevinlebrun/colors.php.git",
+                "reference": "cdda5eee41314b87cd5a8bb91b1ffc7c0210e673"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/kevinlebrun/colors.php/zipball/cdda5eee41314b87cd5a8bb91b1ffc7c0210e673",
+                "reference": "cdda5eee41314b87cd5a8bb91b1ffc7c0210e673",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "3.7.*",
+                "satooshi/php-coveralls": "1.0.*",
+                "squizlabs/php_codesniffer": "1.*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Colors": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Kevin Le Brun",
+                    "email": "[email protected]",
+                    "homepage": "http://kevinlebrun.fr",
+                    "role": "developer"
+                }
+            ],
+            "description": "Colors for PHP CLI scripts",
+            "homepage": "https://github.com/kevinlebrun/colors.php",
+            "keywords": [
+                "cli",
+                "color",
+                "colors",
+                "console",
+                "shell"
+            ],
+            "time": "2018-05-30T08:34:23+00:00"
+        },
+        {
+            "name": "monolog/monolog",
+            "version": "1.25.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Seldaek/monolog.git",
+                "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fa82921994db851a8becaf3787a9e73c5976b6f1",
+                "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0",
+                "psr/log": "~1.0"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0.0"
+            },
+            "require-dev": {
+                "aws/aws-sdk-php": "^2.4.9 || ^3.0",
+                "doctrine/couchdb": "~1.0@dev",
+                "graylog2/gelf-php": "~1.0",
+                "jakub-onderka/php-parallel-lint": "0.9",
+                "php-amqplib/php-amqplib": "~2.4",
+                "php-console/php-console": "^3.1.3",
+                "phpunit/phpunit": "~4.5",
+                "phpunit/phpunit-mock-objects": "2.3.0",
+                "ruflin/elastica": ">=0.90 <3.0",
+                "sentry/sentry": "^0.13",
+                "swiftmailer/swiftmailer": "^5.3|^6.0"
+            },
+            "suggest": {
+                "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+                "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+                "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+                "ext-mongo": "Allow sending log messages to a MongoDB server",
+                "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+                "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+                "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+                "php-console/php-console": "Allow sending log messages to Google Chrome",
+                "rollbar/rollbar": "Allow sending log messages to Rollbar",
+                "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
+                "sentry/sentry": "Allow sending log messages to a Sentry server"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Monolog\\": "src/Monolog"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "[email protected]",
+                    "homepage": "http://seld.be"
+                }
+            ],
+            "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+            "homepage": "http://github.com/Seldaek/monolog",
+            "keywords": [
+                "log",
+                "logging",
+                "psr-3"
+            ],
+            "time": "2019-12-20T14:15:16+00:00"
+        },
+        {
+            "name": "phpmailer/phpmailer",
+            "version": "v6.1.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPMailer/PHPMailer.git",
+                "reference": "c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3",
+                "reference": "c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-filter": "*",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "doctrine/annotations": "^1.2",
+                "friendsofphp/php-cs-fixer": "^2.2",
+                "phpunit/phpunit": "^4.8 || ^5.7"
+            },
+            "suggest": {
+                "ext-mbstring": "Needed to send email in multibyte encoding charset",
+                "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+                "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+                "psr/log": "For optional PSR-3 debug logging",
+                "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication",
+                "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PHPMailer\\PHPMailer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-only"
+            ],
+            "authors": [
+                {
+                    "name": "Marcus Bointon",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Jim Jagielski",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Andy Prevost",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Brent R. Matzelle"
+                }
+            ],
+            "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+            "time": "2020-05-27T12:24:03+00:00"
+        },
+        {
+            "name": "phpoption/phpoption",
+            "version": "1.7.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/schmittjoh/php-option.git",
+                "reference": "77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959",
+                "reference": "77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.5.9 || ^7.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.3",
+                "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.7-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PhpOption\\": "src/PhpOption/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Johannes M. Schmitt",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Graham Campbell",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Option Type for PHP",
+            "keywords": [
+                "language",
+                "option",
+                "php",
+                "type"
+            ],
+            "time": "2019-12-15T19:35:24+00:00"
+        },
+        {
+            "name": "predis/predis",
+            "version": "v1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nrk/predis.git",
+                "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1",
+                "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.9"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.8"
+            },
+            "suggest": {
+                "ext-curl": "Allows access to Webdis when paired with phpiredis",
+                "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Predis\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Daniele Alessandri",
+                    "email": "[email protected]",
+                    "homepage": "http://clorophilla.net"
+                }
+            ],
+            "description": "Flexible and feature-complete Redis client for PHP and HHVM",
+            "homepage": "http://github.com/nrk/predis",
+            "keywords": [
+                "nosql",
+                "predis",
+                "redis"
+            ],
+            "time": "2016-06-16T16:22:20+00:00"
+        },
+        {
+            "name": "psr/http-message",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
+            "keywords": [
+                "http",
+                "http-message",
+                "psr",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "time": "2016-08-06T14:39:51+00:00"
+        },
+        {
+            "name": "psr/log",
+            "version": "1.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801",
+                "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "Psr/Log/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "time": "2019-11-01T11:05:21+00:00"
+        },
+        {
+            "name": "ralouphie/getallheaders",
+            "version": "3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ralouphie/getallheaders.git",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.1",
+                "phpunit/phpunit": "^5 || ^6.5"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/getallheaders.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ralph Khattar",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "A polyfill for getallheaders.",
+            "time": "2019-03-08T08:55:37+00:00"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.13.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
+                "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "ext-ctype": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.13-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Gert de Pagter",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "time": "2019-11-27T13:56:44+00:00"
+        },
+        {
+            "name": "vlucas/phpdotenv",
+            "version": "v3.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/vlucas/phpdotenv.git",
+                "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1bdf24f065975594f6a117f0f1f6cabf1333b156",
+                "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.4 || ^7.0",
+                "phpoption/phpoption": "^1.5",
+                "symfony/polyfill-ctype": "^1.9"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.6-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Dotenv\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "[email protected]",
+                    "homepage": "https://gjcampbell.co.uk/"
+                },
+                {
+                    "name": "Vance Lucas",
+                    "email": "[email protected]",
+                    "homepage": "https://vancelucas.com/"
+                }
+            ],
+            "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
+            "keywords": [
+                "dotenv",
+                "env",
+                "environment"
+            ],
+            "time": "2019-09-10T21:37:39+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "php": ">=7.1.0",
+        "ext-curl": "*",
+        "ext-openssl": "*",
+        "ext-json": "*",
+        "ext-bcmath": "*",
+        "ext-pdo": "*"
+    },
+    "platform-dev": []
+}

+ 42 - 0
config.php

@@ -0,0 +1,42 @@
+<?php
+/**
+ * 配置
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2019/3/2
+ * @time 11:39
+ */
+
+return [
+    /**
+     * 邮箱配置
+     */
+    'mail' => [
+        /**
+         * 目前机器人邮箱账户支持谷歌邮箱、QQ邮箱以及163邮箱,程序会自动判断填入的邮箱类型并使用合适的配置。注意,QQ邮箱与163邮箱均使用
+         * 账户加授权码的方式登录,谷歌邮箱使用账户加密码的方式登录,请知悉。
+         */
+        'to' => env('TO'), // 用于接收通知的邮箱
+        'toName' => '主人', // 收件人名字
+        'username' => env('MAIL_USERNAME'), // 机器人邮箱账户
+        'password' => env('MAIL_PASSWORD'), // 机器人邮箱密码或授权码
+        'enable' => env('MAIL_ENABLE'), // 是否启用,默认启用
+
+        // 'replyTo' => '[email protected]', // 接收回复的邮箱
+        // 'replyToName' => '作者', // 接收回复的人名
+    ],
+
+    /**
+     * Telegram Bot
+     */
+    'telegram' => [
+        'chatID' => env('TELEGRAM_CHAT_ID'), // 你的chat_id,通过发送“/start”给@userinfobot可以获取自己的id
+        'token' => env('TELEGRAM_BOT_TOKEN'), // Telegram Bot 的 token
+        'enable' => env('TELEGRAM_BOT_ENABLE') // 是否启用,默认不启用
+    ],
+
+    'locale' => 'zh', // 指定语言包,位于resources/lang/目录下
+    'noticeFreq' => env('NOTICE_FREQ'), // 通知频率 0:仅当有续期操作的时候 1:每次执行
+    'verifySSL' => env('VERIFY_SSL'), // 请求时验证 SSL 证书行为,默认不验证,防止服务器证书过期或证书颁布者信息不全导致无法发出请求
+    'debug' => env('DEBUG'),
+];

+ 58 - 0
docker-entrypoint.sh

@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+
+#===================================================================#
+#   Author: mybsdc <[email protected]>                               #
+#   Intro: https://github.com/luolongfei/freenom                    #
+#===================================================================#
+
+set -e
+
+# 自定义颜色变量
+red='\033[0;31m'
+green='\033[0;32m'
+yellow='\033[0;33m'
+plain='\033[0m'
+
+# 生成配置文件
+if [ ! -f /conf/.env ]; then
+    cp /app/.env.example /conf/.env
+    echo -e "[${green}Info${plain}] 已生成 .env 文件,请将 .env 文件中的配置项改为你自己的,然后重启容器"
+fi
+if [ ! -f /app/.env ]; then
+    ln -s /conf/.env /app/.env
+fi
+
+# PHP 命令
+PHP_COMMAND='php /app/run > /app/logs/freenom_cron.log 2>&1'
+
+# 指定脚本执行时间
+if [ -z "${RUN_AT}" ]; then
+    minute=$( shuf -i 0-59 -n 1 )
+    hour=$( shuf -i 6-23 -n 1 )
+    CRON_COMMAND="${minute} ${hour} * * * ${PHP_COMMAND}"
+    echo -e "[${green}Info${plain}] 已自动指定执行时间,续期任务将在北京时间每天 「${hour}:${minute}」 执行"
+    echo -e "[${green}Info${plain}] 在没有手动指定 RUN_AT 环境变量的情况下,每次重启容器,程序都会重新在 06 ~ 23 点全时段中自动随机指定一个执行时间,目的是防止很多人在同一个时间点执行任务导致 Freenom 无法稳定提供服务"
+else
+    if [[ "${RUN_AT}" =~ ^([01][0-9]|2[0-3]|[0-9]):([0-5][0-9]|[0-9])$ ]]; then
+        minute=$( echo ${RUN_AT} | egrep -o '([0-5][0-9]|[0-9])$' )
+        hour=$( echo ${RUN_AT} | egrep -o '^([01][0-9]|2[0-3]|[0-9])' )
+        CRON_COMMAND="${minute} ${hour} * * * ${PHP_COMMAND}"
+        echo -e "[${green}Info${plain}] 你已指定执行时间,续期任务将在北京时间每天 「${hour}:${minute}」 执行"
+    elif [[ "${RUN_AT}" =~ ^([0-9\/*-]+( |$)){5}$ ]]; then
+        CRON_COMMAND="${RUN_AT} ${PHP_COMMAND}"
+    else
+        echo -e "[${red}Error${plain}] RUN_AT 的值无效"
+        echo -e "${yellow}请输入一个有效的时间指令,其值可以为时分格式,如:11:24,也可以为 CRON 命令中的时间格式,如:'24 11 * * *',甚至可以不输入,让程序自动生成,推荐采用自动生成的方式,不建议手动指定此环境变量"
+        exit 1
+    fi
+fi
+
+# 添加计划任务
+sed -i '/freenom_cron/'d /etc/crontabs/root
+echo -e "${CRON_COMMAND}" >> /etc/crontabs/root
+
+echo -e "[${green}Info${plain}] CRON_COMMAND: ${CRON_COMMAND}"
+
+php run
+
+exec "$@"

+ 89 - 0
libs/Argv.php

@@ -0,0 +1,89 @@
+<?php
+/**
+ * 命令行参数
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2020/1/3
+ * @time 16:32
+ */
+
+namespace Luolongfei\Lib;
+
+class Argv
+{
+    /**
+     * @var Argv
+     */
+    protected static $instance;
+
+    /**
+     * @var array 所有命令行传参
+     */
+    public $allArgvs = [];
+
+    public function __construct()
+    {
+        if ($this->get('help') || $this->get('h')) {
+            $desc = <<<FLL
+Description
+Params:
+-c: <string> 指定要实例化的类名。默认调用FreeNom类
+-m: <string> 指定要调用的方法名(不支持静态方法)。默认调用handle方法
+-h: 显示说明
+
+Example: 
+$ php run -c=FreeNom -m=handle
+
+FLL;
+            echo $desc;
+            exit(0);
+        }
+    }
+
+    /**
+     * @return Argv
+     */
+    public static function instance()
+    {
+        if (!self::$instance instanceof self) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 获取命令行参数
+     *
+     * @param string $name
+     * @param string $default
+     *
+     * @return mixed|string
+     */
+    public function get(string $name, string $default = '')
+    {
+        if (!$this->allArgvs) {
+            $this->setAllArgvs();
+        }
+
+        return $this->allArgvs[$name] ?? $default;
+    }
+
+    /**
+     * 设置命令行所有参数
+     *
+     * @return array
+     */
+    public function setAllArgvs()
+    {
+        global $argv;
+
+        foreach ($argv as $a) { // Windows默认命令行无法正确传入使用引号括住的带空格参数,换个命令行终端就好,Linux不受影响
+            if (preg_match('/^-{1,2}(?P<name>\w+)(?:=([\'"]|)(?P<val>[^\n\t\v\f\r\'"]+)\2)?$/i', $a, $m)) {
+                $this->allArgvs[$m['name']] = $m['val'] ?? true;
+            }
+        }
+
+        return $this->allArgvs;
+    }
+}

+ 74 - 0
libs/Config.php

@@ -0,0 +1,74 @@
+<?php
+/**
+ * 配置
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2019/3/3
+ * @time 16:41
+ */
+
+namespace Luolongfei\Lib;
+
+class Config
+{
+    /**
+     * @var Config
+     */
+    protected static $instance;
+
+    /**
+     * @var array 配置
+     */
+    protected $allConfig;
+
+    public function __construct()
+    {
+        $this->allConfig = require ROOT_PATH . '/config.php';
+    }
+
+    /**
+     * 获取配置
+     *
+     * @param string $key
+     * @param string $default 默认值
+     *
+     * @return array|mixed|null
+     */
+    public function get($key = '', $default = null)
+    {
+        $allConfig = $this->allConfig;
+
+        if (strlen($key)) {
+            if (strpos($key, '.')) {
+                $keys = explode('.', $key);
+                $val = $allConfig;
+                foreach ($keys as $k) {
+                    if (!isset($val[$k])) {
+                        return $default; // 任一下标不存在就返回默认值
+                    }
+
+                    $val = $val[$k];
+                }
+
+                return $val;
+            } else {
+                if (isset($allConfig[$key])) {
+                    return $allConfig[$key];
+                }
+
+                return $default;
+            }
+        }
+
+        return $allConfig;
+    }
+
+    public static function instance()
+    {
+        if (!self::$instance instanceof self) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+}

+ 72 - 0
libs/Env.php

@@ -0,0 +1,72 @@
+<?php
+/**
+ * 环境变量
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2019/6/2
+ * @time 17:28
+ */
+
+namespace Luolongfei\Lib;
+
+use Dotenv\Dotenv;
+
+class Env
+{
+    /**
+     * @var Env
+     */
+    protected static $instance;
+
+    /**
+     * @var array 环境变量值
+     */
+    protected $allValues;
+
+    public function __construct($fileName)
+    {
+        $this->allValues = Dotenv::create(ROOT_PATH, $fileName)->load();
+    }
+
+    public static function instance($fileName = '.env')
+    {
+        if (!self::$instance instanceof self) {
+            self::$instance = new self($fileName);
+        }
+
+        return self::$instance;
+    }
+
+    public function get($key = '', $default = null)
+    {
+        if (!strlen($key)) { // 不传key则返回所有环境变量
+            return $this->allValues;
+        }
+
+        $value = getenv($key);
+        if ($value === false) {
+            return $default;
+        }
+
+        switch (strtolower($value)) {
+            case 'true':
+            case '(true)':
+                return true;
+            case 'false':
+            case '(false)':
+                return false;
+            case 'empty':
+            case '(empty)':
+                return '';
+            case 'null':
+            case '(null)':
+                return null;
+        }
+
+        if (($valueLength = strlen($value)) > 1 && $value[0] === '"' && $value[$valueLength - 1] === '"') { // 去除双引号
+            return substr($value, 1, -1);
+        }
+
+        return $value;
+    }
+}

+ 74 - 0
libs/Lang.php

@@ -0,0 +1,74 @@
+<?php
+/**
+ * 语言包加载
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2020/1/16
+ * @time 16:30
+ */
+
+namespace Luolongfei\Lib;
+
+class Lang
+{
+    /**
+     * @var Lang
+     */
+    protected static $instance;
+
+    /**
+     * @var array
+     */
+    public $lang;
+
+    public function __construct()
+    {
+        $this->lang = require sprintf('%s/lang/%s.php', RESOURCES_PATH, config('locale'));
+    }
+
+    /**
+     * @param string $key
+     *
+     * @return array|mixed|null
+     */
+    public function get($key = '')
+    {
+        $lang = $this->lang;
+
+        if (strlen($key)) {
+            if (strpos($key, '.')) {
+                $keys = explode('.', $key);
+                $val = $lang;
+                foreach ($keys as $k) {
+                    if (!isset($val[$k])) {
+                        return null; // 任一下标不存在就返回null
+                    }
+
+                    $val = $val[$k];
+                }
+
+                return $val;
+            } else {
+                if (isset($lang[$key])) {
+                    return $lang[$key];
+                }
+
+                return null;
+            }
+        }
+
+        return $lang;
+    }
+
+    /**
+     * @return Lang
+     */
+    public static function instance()
+    {
+        if (!self::$instance instanceof self) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+}

+ 134 - 0
libs/Log.php

@@ -0,0 +1,134 @@
+<?php
+/**
+ * 日志
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2019/3/3
+ * @time 12:01
+ */
+
+namespace Luolongfei\Lib;
+
+use Monolog\Logger;
+use Monolog\Handler\StreamHandler;
+use Bramus\Monolog\Formatter\ColoredLineFormatter;
+
+class Log
+{
+    /**
+     * @var Logger
+     */
+    protected static $loggerInstance;
+
+    /**
+     * 由于php不能在类外使用已实例化的对象来访问静态属性,但可以在类外访问类里的静态方法,故定义此方法实现类外访问静态属性
+     *
+     * 注意,info等方法不写日志,error方法才写日志到指定目录
+     *
+     * @return Logger
+     * @throws \Exception
+     */
+    public static function logger()
+    {
+        if (!self::$loggerInstance instanceof Logger) {
+            $handler = new StreamHandler(
+                config('debug') ? 'php://stdout' : sprintf('%s/logs/%s.log', ROOT_PATH, date('Y-m/d')),
+                config('debug') ? Logger::DEBUG : Logger::INFO
+            );
+            if (config('debug')) {
+                $handler->setFormatter(new ColoredLineFormatter(null, "[%datetime%] %channel%.%level_name%: %message%\n"));
+            }
+
+            $logger = new Logger('pusher');
+            $logger->pushHandler($handler);
+
+            self::$loggerInstance = $logger;
+        }
+
+        return self::$loggerInstance;
+    }
+
+    /**
+     * @param $message
+     * @param array $context
+     *
+     * @return bool
+     * @throws \Exception
+     */
+    public static function debug($message, array $context = [])
+    {
+        return self::logger()->addDebug($message, $context);
+    }
+
+    /**
+     * @param $message
+     * @param array $context
+     *
+     * @return bool
+     * @throws \Exception
+     */
+    public static function info($message, array $context = [])
+    {
+        return self::logger()->addInfo($message, $context);
+    }
+
+    /**
+     * @param $message
+     * @param array $context
+     *
+     * @return bool
+     * @throws \Exception
+     */
+    public static function notice($message, array $context = [])
+    {
+        return self::logger()->addNotice($message, $context);
+    }
+
+    /**
+     * @param $message
+     * @param array $context
+     *
+     * @return bool
+     * @throws \Exception
+     */
+    public static function warning($message, array $context = [])
+    {
+        return self::logger()->addWarning($message, $context);
+    }
+
+    /**
+     * @param $message
+     * @param array $context
+     *
+     * @return bool
+     * @throws \Exception
+     */
+    public static function error($message, array $context = [])
+    {
+        return self::logger()->addError($message, $context);
+    }
+
+    /**
+     * @param $message
+     * @param array $context
+     *
+     * @return bool
+     * @throws \Exception
+     */
+    public static function alert($message, array $context = [])
+    {
+        return self::logger()->addAlert($message, $context);
+    }
+
+    /**
+     * @param $message
+     * @param array $context
+     *
+     * @return bool
+     * @throws \Exception
+     */
+    public static function emergency($message, array $context = [])
+    {
+        return self::logger()->addEmergency($message, $context);
+    }
+}

+ 143 - 0
libs/Mail.php

@@ -0,0 +1,143 @@
+<?php
+/**
+ * 邮件
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2019/5/12
+ * @time 16:38
+ */
+
+namespace Luolongfei\Lib;
+
+use Luolongfei\App\Exceptions\LlfException;
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\Exception as MailException;
+
+class Mail
+{
+    /**
+     * @var PHPMailer
+     */
+    protected static $mail;
+
+    /**
+     * @return PHPMailer
+     * @throws MailException
+     * @throws \Exception
+     */
+    public static function mail()
+    {
+        if (!self::$mail instanceof PHPMailer) {
+            self::$mail = new PHPMailer(true);
+
+            // 邮件服务配置
+            $username = config('mail.username');
+            $password = config('mail.password');
+            if (stripos($username, '@gmail.com') !== false) {
+                $host = 'smtp.gmail.com';
+                $secure = 'tls';
+                $port = 587;
+            } else if (stripos($username, '@qq.com') !== false) {
+                $host = 'smtp.qq.com';
+                $secure = 'tls';
+                $port = 587;
+            } else if (stripos($username, '@163.com') !== false) {
+                $host = 'smtp.163.com';
+                $secure = 'ssl';
+                $port = 465;
+            } else if (stripos($username, '@vip.163.com') !== false) {
+                $host = 'smtp.vip.163.com';
+                $secure = 'ssl';
+                $port = 465;
+            } else if (stripos($username, '@outlook.com') !== false) {
+                $host = 'smtp.office365.com';
+                $secure = 'starttls';
+                $port = 587;
+            } else {
+                throw new \Exception('不受支持的邮箱。目前仅支持谷歌邮箱、QQ邮箱以及163邮箱,推荐使用谷歌邮箱。');
+            }
+
+            self::$mail->SMTPDebug = config('debug') ? 2 : 0; // Debug 0:关闭 1:客户端信息 2:客户端和服务端信息
+            self::$mail->isSMTP(); // 告诉PHPMailer使用SMTP
+            self::$mail->Host = $host; // SMTP服务器
+            self::$mail->SMTPAuth = true; // 启用SMTP身份验证
+            self::$mail->Username = $username; // 账号
+            self::$mail->Password = $password; // 密码或授权码
+            self::$mail->SMTPSecure = $secure; // 将加密系统设置为使用 - ssl(不建议使用)或tls
+            self::$mail->Port = $port; // 设置SMTP端口号 - tsl使用587端口,ssl使用465端口
+            self::$mail->CharSet = 'UTF-8'; // 防止中文邮件乱码
+            self::$mail->setLanguage('zh_cn', VENDOR_PATH . '/phpmailer/phpmailer/language/'); // 设置语言
+            self::$mail->setFrom($username, 'im robot'); // 发件人
+        }
+
+        return self::$mail;
+    }
+
+    /**
+     * 发送邮件
+     *
+     * @param string $subject 标题
+     * @param string | array $content 正文
+     * @param string $to 收件人,选传
+     * @param string $template 模板,选传
+     *
+     * @return bool
+     * @throws \Exception
+     */
+    public static function send($subject, $content, $to = '', $template = '')
+    {
+        if (config('mail.enable') === false) {
+            system_log('由于没有启用邮件功能,故本次不通过邮件送信。');
+
+            return false;
+        }
+
+        $to = $to ?: config('mail.to');
+        if (!$to) {
+            throw new LlfException(env('ON_GITHUB_ACTIONS') ? 34520011 : 34520012);
+        }
+
+        self::mail()->addAddress($to, config('mail.toName', '主人')); // 添加收件人,参数2选填
+        self::mail()->addReplyTo(config('mail.replyTo', '[email protected]'), config('mail.replyToName', '作者')); // 备用回复地址,收到的回复的邮件将被发到此地址
+
+        /**
+         * 抄送和密送都是添加收件人,抄送方式下,被抄送者知道除被密送者外的所有的收件人,密送方式下,
+         * 被密送者知道所有的被抄送者,但不知道其它的被密送者。
+         * 抄送好比@,密送好比私信。
+         */
+//        self::mail()->addCC('[email protected]'); // 抄送
+//        self::mail()->addBCC('[email protected]'); // 密送
+
+        // 添加附件,参数2选填
+//        self::mail()->addAttachment('README.md', '说明.txt');
+
+        // 内容
+        self::mail()->Subject = $subject; // 标题
+
+        /**
+         * 正文
+         * 使用html文件内容作为正文,其中的图片将被base64编码,另确保html样式为内联形式,且某些样式可能需要!important方能正常显示,
+         * msgHTML方法的第二个参数指定html内容中图片的路径,在转换时会拼接html中图片的相对路径得到完整的路径,最右侧无需“/”,PHPMailer
+         * 源码里有加。css中的背景图片不会被转换,这是PHPMailer已知问题,建议外链。
+         * 此处也可替换为:
+         * self::mail()->isHTML(true); // 设为html格式
+         * self::mail()->Body = '正文'; // 支持html
+         * self::mail()->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'; // 纯文本消息正文。不支持html预览的邮件客户端将显示此预览消息,其它情况将显示正常的body
+         */
+        $template = file_get_contents(RESOURCES_PATH . '/mail/' . ($template ?: 'default') . '.html');
+        if (is_array($content)) {
+            array_unshift($content, $template);
+            $message = call_user_func_array('sprintf', $content);
+        } else if (is_string($content)) {
+            $message = $content;
+        } else {
+            throw new MailException('邮件内容格式错误,仅支持传入数组或字符串。');
+        }
+
+        self::mail()->msgHTML($message, APP_PATH . '/mail');
+
+        if (!self::mail()->send()) throw new MailException(self::mail()->ErrorInfo);
+
+        return true;
+    }
+}

+ 57 - 0
libs/PhpColor.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * PHP命令行颜色
+ *
+ * 目前多层标签嵌套还存在问题,当多层嵌套时,内嵌标签后面的文字样式会丢失,可暂时使用<reset>标签恢复,待原作者修正
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2019/11/29
+ * @time 10:52
+ */
+
+namespace Luolongfei\Lib;
+
+use Colors\Color;
+
+class PhpColor
+{
+    /**
+     * @var PhpColor
+     */
+    protected static $instance;
+
+    /**
+     * @var Color
+     */
+    protected $colorInstance;
+
+    public function __construct()
+    {
+        $this->colorInstance = new Color();
+
+        // Create my own style
+        $this->colorInstance->setUserStyles([
+//                '自定义标签' => 'red',
+        ]);
+    }
+
+    /**
+     * @return PhpColor
+     */
+    public static function instance()
+    {
+        if (!self::$instance instanceof self) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * @return Color
+     */
+    public function getColorInstance()
+    {
+        return $this->colorInstance;
+    }
+}

+ 128 - 0
libs/TelegramBot.php

@@ -0,0 +1,128 @@
+<?php
+/**
+ * Telegram Bot
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2020/2/3
+ * @time 15:23
+ */
+
+namespace Luolongfei\Lib;
+
+use GuzzleHttp\Client;
+
+class TelegramBot
+{
+    const TIMEOUT = 34.52;
+
+    /**
+     * @var TelegramBot
+     */
+    protected static $instance;
+
+    /**
+     * @var string chat_id
+     */
+    protected $chatID;
+
+    /**
+     * @var string TelegramBot token
+     */
+    protected $token;
+
+    /**
+     * @var Client
+     */
+    protected $client;
+
+    public function __construct()
+    {
+        $this->chatID = config('telegram.chatID');
+        $this->token = config('telegram.token');
+
+        $this->client = new Client([
+            'headers' => [
+                'Content-Type' => 'application/x-www-form-urlencoded'
+            ],
+            'cookies' => false,
+            'timeout' => self::TIMEOUT,
+            'verify' => config('verifySSL'),
+//            'http_errors' => false,
+            'debug' => config('debug')
+        ]);
+    }
+
+    protected static function instance()
+    {
+        if (!self::$instance instanceof self) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 发送消息
+     *
+     * @param string $content 支持markdown语法,但记得对非标记部分进行转义
+     * @param string $chatID 可单独指定chat_id参数
+     * @param bool $isMarkdown 默认内容为Markdown格式,传否则为Html格式
+     * @desc 注意对markdown标记占用的字符进行转义,否则无法正确发送,根据官方说明,以下字符如果不想被 Telegram Bot 识别为markdown标记,
+     * 应转义后传入,官方说明如下:
+     * In all other places characters '_‘, ’*‘, ’[‘, ’]‘, ’(‘, ’)‘, ’~‘, ’`‘, ’>‘, ’#‘, ’+‘, ’-‘, ’=‘, ’|‘,
+     * ’{‘, ’}‘, ’.‘, ’!‘ must be escaped with the preceding character ’\'.
+     * 如果你不转义,且恰好又不是正确的markdown语法,那 Telegram Bot 就只有报错了您勒
+     *
+     * 官方markdown语法示例:
+     * *bold \*text*
+     * _italic \*text_
+     * __underline__
+     * ~strikethrough~
+     * *bold _italic bold ~italic bold strikethrough~ __underline italic bold___ bold*
+     * [inline URL](http://www.example.com/)
+     * [inline mention of a user](tg://user?id=123456789)
+     * `inline fixed-width code`
+     * ```
+     * pre-formatted fixed-width code block
+     * ```
+     * ```python
+     * pre-formatted fixed-width code block written in the Python programming language
+     * ```
+     * 需要注意的是,普通markdown语法中加粗字体使用的是“**正文**”的形式,但是 Telegram Bot 中是“*加粗我呀*”的形式,更多相关信息请
+     * 参考官网:https://core.telegram.org/bots/api#sendmessage
+     * 另外我干掉了“_”、“~”、“-”、“.”和“>”关键字,分别对应斜体、删除线、无序列表、有序列表和引用符号,这几个我可能用不上:)
+     *
+     * @return bool
+     */
+    public static function send(string $content, $chatID = '', $isMarkdown = true)
+    {
+        if (config('telegram.enable') === false) {
+            system_log('由于没有启用 Telegram Bot 功能,故本次不通过 Telegram Bot 送信。');
+
+            return false;
+        }
+
+        if ($isMarkdown) {
+            // 这几个我可能用不上的markdown关键字我就直接干掉了
+            $content = preg_replace('/([.>~_-])/i', '\\\\$1', $content);
+        }
+
+        $telegramBot = self::instance();
+
+        $response = $telegramBot->client->post(
+            sprintf('https://api.telegram.org/bot%s/sendMessage', $telegramBot->token),
+            [
+                'form_params' => [
+                    'chat_id' => $chatID ? $chatID : $telegramBot->chatID,
+                    'text' => $content,
+                    'parse_mode' => $isMarkdown ? 'MarkdownV2' : 'HTML',
+                    'disable_web_page_preview' => true,
+                    'disable_notification' => false
+                ],
+            ]
+        );
+        $rp = json_decode((string)$response->getBody(), true);
+
+        return $rp['ok'] ?? false;
+    }
+}

+ 2 - 0
logs/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 25 - 0
resources/lang/zh.php

@@ -0,0 +1,25 @@
+<?php
+/**
+ * 语言包
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2018/8/10
+ * @time 14:39
+ */
+
+return [
+    'exception_msg' => [
+        '34520001' => '检测到你尚未配置 freenom 账户信息,请修改 .env 文件中与账户相关的项,否则程序无法正常运作',
+        '34520002' => '登录 freenom 出错,未取得正确的 cookie 值,请检查账户和密码是否正确',
+        '34520003' => '域名数据匹配失败,可能是你暂时没有域名或者页面改版导致正则失效,请及时联系作者',
+        '34520004' => 'token 匹配失败,可能是页面改版导致正则失效,请及时联系作者',
+        '34520005' => 'putenv() 函数被禁用,无法写入环境变量导致程序无法正常运作,解决方案参考:https://github.com/luolongfei/freenom/issues/22',
+        '34520006' => sprintf('不支持 php7 以下的版本,当前版本为%s,请升级到 php7 以上', PHP_VERSION),
+        '34520007' => sprintf('已自动在%s目录下生成 .env 配置文件,请将配置文件中的各项内容修改为你自己的', ROOT_PATH),
+        '34520008' => sprintf('请将%s目录下的 .env.example 文件复制为 .env 文件,并将 .env 文件中的各项内容修改为你自己的', ROOT_PATH),
+        '34520009' => '登录 freenom 失败,请检查账户和密码是否正确',
+        '34520010' => '缺少 curl 模块,无法发送请求,请检查你的 php 环境并在编译时带上 curl 模块',
+        '34520011' => '你尚未配置收信邮箱,可能无法收到通知邮件。请在你 Fork 的本仓库下的 Settings > Secrets 画面追加一个名为 TO 的 secret 变量,变量对应的值为你最常用的邮箱地址,用于接收机器人邮箱发出的域名相关邮件',
+        '34520012' => '你尚未配置收信邮箱,可能无法收到通知邮件。请将 .env 文件中的 TO 对应的值改为你最常用的邮箱地址,用于接收机器人邮箱发出的域名相关邮件',
+    ],
+];

+ 326 - 0
resources/mail/LlfException.html

@@ -0,0 +1,326 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <title>邮件通知</title>
+    <style>
+        .mmsgLetter {
+            width: 580px;
+            margin: 0 auto;
+            padding: 10px;
+            color: #333;
+            background: #fff;
+            border: 0px solid #aaa;
+            border-radius: 5px;
+            -webkit-box-shadow: 3px 3px 10px #999 !important;
+            -moz-box-shadow: 3px 3px 10px #999 !important;
+            box-shadow: 3px 3px 10px #999 !important;
+            font-family: Verdana, sans-serif;
+        }
+
+        .mmsgLetter a:link,
+        .mmsgLetter a:visited {
+            color: #407700;
+        }
+
+        .mmsgLetterContent {
+            text-align: left;
+            padding: 30px;
+            font-size: 14px;
+            line-height: 1.5;
+            /*background: url('images/ting.jpg') no-repeat top right;*/
+        }
+
+        .mmsgLetterContent h3 {
+            color: #000;
+            font-size: 20px;
+            font-weight: bold;
+            margin: 20px 0 20px;
+            border-top: 2px solid #eee;
+            padding: 20px 0 0 0;
+            font-family: "微软雅黑", "黑体", "Lucida Grande", Verdana, sans-serif;
+        }
+
+        .mmsgLetterContent p {
+            margin: 20px 0;
+            padding: 0;
+        }
+
+        .mmsgLetterContent .salutation {
+            font-weight: bold;
+        }
+
+        .mmsgLetterContent .mmsgMoreInfo {
+        }
+
+        .mmsgLetterContent a.mmsgButton {
+            display: block;
+            float: left;
+            height: 40px;
+            text-decoration: none;
+            text-align: center;
+            cursor: pointer;
+        }
+
+        .mmsgLetterContent a.mmsgButton span {
+            display: block;
+            float: left;
+            padding: 0 25px;
+            height: 40px;
+            line-height: 36px;
+            font-size: 14px;
+            font-weight: bold;
+            color: #fff;
+            text-shadow: 1px 0 0 #235e00;
+        }
+
+        .mmsgLetterContent a.mmsgButton:link,
+        .mmsgLetterContent a.mmsgButton:visited {
+            background: #338702 url('images/mmsgletter_2_btn.png') no-repeat right -40px;
+        }
+
+        .mmsgLetterContent a.mmsgButton:link span,
+        .mmsgLetterContent a.mmsgButton:visited span {
+            background: url('images/mmsgletter_2_btn.png') no-repeat 0 0;
+        }
+
+        .mmsgLetterContent a.mmsgButton:hover,
+        .mmsgLetterContent a.mmsgButton:active {
+            background: #338702 url('images/mmsgletter_2_btn.png') no-repeat right -120px;
+        }
+
+        .mmsgLetterContent a.mmsgButton:hover span,
+        .mmsgLetterContent a.mmsgButton:active span {
+            background: url('images/mmsgletter_2_btn.png') no-repeat 0 -80px;
+        }
+
+        .mmsgLetterInscribe {
+            padding: 40px 0 0;
+        }
+
+        .mmsgLetterInscribe .mmsgAvatar {
+            float: left;
+        }
+
+        .mmsgLetterInscribe .mmsgName {
+            margin: 0 0 10px;
+        }
+
+        .mmsgLetterInscribe .mmsgSender {
+            margin: 0 0 0 54px;
+        }
+
+        .mmsgLetterInscribe .mmsgInfo {
+            font-size: 12px;
+            margin: 0;
+            line-height: 1.2;
+        }
+
+        .mmsgLetterHeader {
+            height: 23px;
+            /*background: url('images/mmsgletter_2_bg_topline.png') repeat-x 0 0;*/
+            background: 7px 0 repeat-x #FFF;
+            background-image: -webkit-repeating-linear-gradient(135deg, #4882CE, #4882CE 20px, #FFF 20px, #FFF 35px, #EB1B2E 35px, #EB1B2E 55px, #FFF 55px, #FFF 70px);
+            background-image: repeating-linear-gradient(-45deg, #4882CE, #4882CE 20px, #FFF 20px, #FFF 35px, #EB1B2E 35px, #EB1B2E 55px, #FFF 55px, #FFF 70px);
+            background-size: 110px 10px;
+        }
+
+        .mmsgLetterFooter {
+            margin: 16px;
+            text-align: center;
+            font-size: 12px;
+            color: #888;
+            text-shadow: 1px 0px 0px #eee;
+        }
+
+        .mmsgLetterClr {
+            clear: both;
+            overflow: hidden;
+            height: 1px;
+        }
+
+        .mmsgLetterUser {
+            padding: 10px 0;
+        }
+
+        .mmsgLetterUserItem {
+            padding: 0 0 20px 0;
+        }
+
+        .mmsgLetterUserAvatar {
+            height: 40px;
+            border: 1px solid #ccc;
+            padding: 2px;
+            display: block;
+            float: left;
+        }
+
+        .mmsgLetterUserAvatar img {
+            width: 40px;
+            height: 40px;
+        }
+
+        .mmsgLetterInfo {
+            margin-left: 48px;
+        }
+
+        .mmsgLetterName {
+            display: block;
+            color: #5fa207;
+            font-weight: bold;
+            margin-left: 10px;
+        }
+
+        .mmsgLetterDesc {
+            font-size: 12px;
+            float: left;
+            height: 43px;
+            background: url('images/mmsgletter_chat_right.gif') no-repeat right top;
+        }
+
+        .mmsgLetterDesc div {
+            white-space: nowrap;
+            float: left;
+            height: 43px;
+            padding: 0 20px;
+            line-height: 40px;
+            background: url('images/mmsgletter_chat_left.gif') no-repeat left top;
+        }
+
+        .mmsgLetterUser {
+        }
+
+        .mmsgLetterAvatar {
+            float: left;
+        }
+
+        .mmsgLetterInfo {
+            margin: 0 0 0 60px;
+        }
+
+        .mmsgLetterNickName {
+            font-size: 14px;
+            font-weight: bold;
+        }
+
+        .mmsgLetterUin {
+            font-size: 12px;
+            color: #666;
+        }
+
+        .mmsgLetterUser {
+            padding: 10px 0;
+        }
+
+        .mmsgLetterUserItem {
+            padding: 0 0 20px 0;
+        }
+
+        .mmsgLetterUserAvatar {
+            height: 40px;
+            border: 1px solid #ccc;
+            padding: 2px;
+            display: block;
+            float: left;
+        }
+
+        .mmsgLetterUserAvatar img {
+            width: 40px;
+            height: 40px;
+        }
+
+        .mmsgLetterInfo {
+            margin-left: 48px;
+        }
+
+        .mmsgLetterName {
+            display: block;
+            color: #5fa207;
+            font-weight: bold;
+            margin-left: 10px;
+            padding-top: 10px;
+        }
+
+        .mmsgLetterDesc {
+            font-size: 12px;
+            float: left;
+            height: 43px;
+            background: url('images/mmsgletter_chat_right.gif') no-repeat right top;
+        }
+
+        .mmsgLetterDesc div {
+            white-space: nowrap;
+            float: left;
+            height: 43px;
+            padding: 0 20px;
+            line-height: 40px;
+            background: url('images/mmsgletter_chat_left.gif') no-repeat left top;
+        }
+
+        .qmbox style,
+        .qmbox script,
+        .qmbox head,
+        .qmbox link,
+        .qmbox meta {
+            display: none !important;
+        }
+
+        #mailContentContainer .txt {
+            height: auto;
+        }
+
+        a {
+            color: #407700 !important;
+        }
+
+        .renewed {
+            color: #407700!important;
+        }
+
+        .notRenewed {
+            color: #eb1b2e!important;
+        }
+
+        .renewed a,
+        .notRenewed a {
+            color: inherit!important;
+        }
+    </style>
+</head>
+<body>
+<div id="mailContentContainer" class="qmbox qm_con_body_content qqmail_webmail_only" style="">
+    <div style="background-color:#d0d0d0;background-image:url('images/mmsgletter_2_bg.png');text-align:center;padding:40px;">
+        <div class="mmsgLetter">
+            <div class="mmsgLetterHeader"
+                 style="height:23px;">
+            </div>
+            <div class="mmsgLetterContent" style="text-align:left;padding:30px;font-size:14px;line-height:1.5;/*background:url('images/ting.jpg') no-repeat top right;background-size: 224px 224px;*/">
+                <div>
+                    <p>主人
+                        <br>呜哇哇哇~程序执行出错啦~
+                        <br>
+                        <br>账户:<a href="#" rel="noopener">%s</a>
+                        <br>异常原因:
+                        <br>%s
+                    </p>
+                </div>
+                <div class="mmsgLetterInscribe" style="padding:40px 0 0;">
+                    <img class="mmsgAvatar" src="https://q2.qlogo.cn/headimg_dl?dst_uin=593198779&spec=100"
+                         style="float:left;width: 40px;height: 40px;">
+                    <div class="mmsgSender" style="margin:0 0 0 54px;">
+                        <p class="mmsgName" style="margin:0 0 10px;">Im Robot</p>
+                        <p class="mmsgInfo" style="font-size:12px;margin:0;line-height:1.2;">
+                            邮件推送机器人<br>
+                            <a href="mailto:[email protected]" rel="noopener" target="_blank">[email protected]</a>
+                        </p>
+                    </div>
+                </div>
+            </div>
+            <div class="mmsgLetterFooter"
+                 style="margin:16px;text-align:center;font-size:12px;color:#888;text-shadow:1px 0px 0px #eee;">
+            </div>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 366 - 0
resources/mail/default.html

@@ -0,0 +1,366 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <title>邮件通知</title>
+    <style>
+        .mmsgLetter {
+            width: 580px;
+            margin: 0 auto;
+            padding: 10px;
+            color: #333;
+            background: #fff;
+            border: 0px solid #aaa;
+            border-radius: 5px;
+            -webkit-box-shadow: 3px 3px 10px #999 !important;
+            -moz-box-shadow: 3px 3px 10px #999 !important;
+            box-shadow: 3px 3px 10px #999 !important;
+            font-family: Verdana, sans-serif;
+        }
+
+        .mmsgLetter a:link,
+        .mmsgLetter a:visited {
+            color: #407700;
+        }
+
+        .mmsgLetterContent {
+            text-align: left;
+            padding: 30px;
+            font-size: 14px;
+            line-height: 1.5;
+            /*background: url('images/ting.jpg') no-repeat top right;*/
+        }
+
+        .mmsgLetterContent h3 {
+            color: #000;
+            font-size: 20px;
+            font-weight: bold;
+            margin: 20px 0 20px;
+            border-top: 2px solid #eee;
+            padding: 20px 0 0 0;
+            font-family: "微软雅黑", "黑体", "Lucida Grande", Verdana, sans-serif;
+        }
+
+        .mmsgLetterContent p {
+            margin: 20px 0;
+            padding: 0;
+        }
+
+        .mmsgLetterContent .salutation {
+            font-weight: bold;
+        }
+
+        .mmsgLetterContent .mmsgMoreInfo {
+        }
+
+        .mmsgLetterContent a.mmsgButton {
+            display: block;
+            float: left;
+            height: 40px;
+            text-decoration: none;
+            text-align: center;
+            cursor: pointer;
+        }
+
+        .mmsgLetterContent a.mmsgButton span {
+            display: block;
+            float: left;
+            padding: 0 25px;
+            height: 40px;
+            line-height: 36px;
+            font-size: 14px;
+            font-weight: bold;
+            color: #fff;
+            text-shadow: 1px 0 0 #235e00;
+        }
+
+        .mmsgLetterContent a.mmsgButton:link,
+        .mmsgLetterContent a.mmsgButton:visited {
+            background: #338702 url('images/mmsgletter_2_btn.png') no-repeat right -40px;
+        }
+
+        .mmsgLetterContent a.mmsgButton:link span,
+        .mmsgLetterContent a.mmsgButton:visited span {
+            background: url('images/mmsgletter_2_btn.png') no-repeat 0 0;
+        }
+
+        .mmsgLetterContent a.mmsgButton:hover,
+        .mmsgLetterContent a.mmsgButton:active {
+            background: #338702 url('images/mmsgletter_2_btn.png') no-repeat right -120px;
+        }
+
+        .mmsgLetterContent a.mmsgButton:hover span,
+        .mmsgLetterContent a.mmsgButton:active span {
+            background: url('images/mmsgletter_2_btn.png') no-repeat 0 -80px;
+        }
+
+        .mmsgLetterInscribe {
+            padding: 40px 0 0;
+        }
+
+        .mmsgLetterInscribe .mmsgAvatar {
+            float: left;
+        }
+
+        .mmsgLetterInscribe .mmsgName {
+            margin: 0 0 10px;
+        }
+
+        .mmsgLetterInscribe .mmsgSender {
+            margin: 0 0 0 54px;
+        }
+
+        .mmsgLetterInscribe .mmsgInfo {
+            font-size: 12px;
+            margin: 0;
+            line-height: 1.2;
+        }
+
+        .mmsgLetterHeader {
+            height: 23px;
+            /*background: url('images/mmsgletter_2_bg_topline.png') repeat-x 0 0;*/
+            background: 7px 0 repeat-x #FFF;
+            background-image: -webkit-repeating-linear-gradient(135deg, #4882CE, #4882CE 20px, #FFF 20px, #FFF 35px, #EB1B2E 35px, #EB1B2E 55px, #FFF 55px, #FFF 70px);
+            background-image: repeating-linear-gradient(-45deg, #4882CE, #4882CE 20px, #FFF 20px, #FFF 35px, #EB1B2E 35px, #EB1B2E 55px, #FFF 55px, #FFF 70px);
+            background-size: 110px 10px;
+        }
+
+        .mmsgLetterFooter {
+            margin: 16px;
+            text-align: center;
+            font-size: 12px;
+            color: #888;
+            text-shadow: 1px 0px 0px #eee;
+        }
+
+        .mmsgLetterClr {
+            clear: both;
+            overflow: hidden;
+            height: 1px;
+        }
+
+        .mmsgLetterUser {
+            padding: 10px 0;
+        }
+
+        .mmsgLetterUserItem {
+            padding: 0 0 20px 0;
+        }
+
+        .mmsgLetterUserAvatar {
+            height: 40px;
+            border: 1px solid #ccc;
+            padding: 2px;
+            display: block;
+            float: left;
+        }
+
+        .mmsgLetterUserAvatar img {
+            width: 40px;
+            height: 40px;
+        }
+
+        .mmsgLetterInfo {
+            margin-left: 48px;
+        }
+
+        .mmsgLetterName {
+            display: block;
+            color: #5fa207;
+            font-weight: bold;
+            margin-left: 10px;
+        }
+
+        .mmsgLetterDesc {
+            font-size: 12px;
+            float: left;
+            height: 43px;
+            background: url('images/mmsgletter_chat_right.gif') no-repeat right top;
+        }
+
+        .mmsgLetterDesc div {
+            white-space: nowrap;
+            float: left;
+            height: 43px;
+            padding: 0 20px;
+            line-height: 40px;
+            background: url('images/mmsgletter_chat_left.gif') no-repeat left top;
+        }
+
+        .mmsgLetterUser {
+        }
+
+        .mmsgLetterAvatar {
+            float: left;
+        }
+
+        .mmsgLetterInfo {
+            margin: 0 0 0 60px;
+        }
+
+        .mmsgLetterNickName {
+            font-size: 14px;
+            font-weight: bold;
+        }
+
+        .mmsgLetterUin {
+            font-size: 12px;
+            color: #666;
+        }
+
+        .mmsgLetterUser {
+            padding: 10px 0;
+        }
+
+        .mmsgLetterUserItem {
+            padding: 0 0 20px 0;
+        }
+
+        .mmsgLetterUserAvatar {
+            height: 40px;
+            border: 1px solid #ccc;
+            padding: 2px;
+            display: block;
+            float: left;
+        }
+
+        .mmsgLetterUserAvatar img {
+            width: 40px;
+            height: 40px;
+        }
+
+        .mmsgLetterInfo {
+            margin-left: 48px;
+        }
+
+        .mmsgLetterName {
+            display: block;
+            color: #5fa207;
+            font-weight: bold;
+            margin-left: 10px;
+            padding-top: 10px;
+        }
+
+        .mmsgLetterDesc {
+            font-size: 12px;
+            float: left;
+            height: 43px;
+            background: url('images/mmsgletter_chat_right.gif') no-repeat right top;
+        }
+
+        .mmsgLetterDesc div {
+            white-space: nowrap;
+            float: left;
+            height: 43px;
+            padding: 0 20px;
+            line-height: 40px;
+            background: url('images/mmsgletter_chat_left.gif') no-repeat left top;
+        }
+
+        .qmbox style,
+        .qmbox script,
+        .qmbox head,
+        .qmbox link,
+        .qmbox meta {
+            display: none !important;
+        }
+
+        #mailContentContainer .txt {
+            height: auto;
+        }
+
+        a {
+            color: #407700 !important;
+        }
+
+        .renewed {
+            color: #4882CE !important;
+        }
+
+        .notRenewed {
+            color: #EB1B2E !important;
+        }
+
+        .domainDays a {
+            color: #407700 !important;
+        }
+
+        .renewed a,
+        .notRenewed a,
+        .domainDays a {
+            /*color: inherit !important;*/
+            display: inline-block;
+            background: #ffffff;
+            min-width: 22px;
+            text-align: center;
+            padding: 4px 6px;
+            border-radius: 22px;
+            font-size: 14px;
+            font-weight: bold;
+            transition: 0.2s all ease-in-out;
+            text-decoration: none !important;
+            margin-right: 12px;
+        }
+
+        .renewed a {
+            color: inherit !important;
+            border: 2px solid #4882CE;
+        }
+
+        .notRenewed a {
+            color: inherit !important;
+            border: 2px solid #EB1B2E;
+        }
+
+        .domainDays a {
+            border: 2px solid #407700;
+        }
+
+        .renewed a:hover {
+            background-color: #4882CE;
+            color: #ffffff !important;
+        }
+
+        .notRenewed a:hover {
+            background-color: #EB1B2E;
+            color: #ffffff !important;
+        }
+
+        .domainDays a:hover {
+            background-color: #407700;
+            color: #ffffff !important;
+        }
+    </style>
+</head>
+<body>
+<div id="mailContentContainer" class="qmbox qm_con_body_content qqmail_webmail_only" style="">
+    <div style="background-color:#d0d0d0;background-image:url('images/mmsgletter_2_bg.png');text-align:center;padding:40px;">
+        <div class="mmsgLetter">
+            <div class="mmsgLetterHeader" style="height:23px;"></div>
+            <div class="mmsgLetterContent" style="text-align:left;padding:30px;font-size:14px;line-height:1.5;/*background:url('images/ting.jpg') no-repeat top right;background-size: 224px 224px;*/">
+                <div>
+                    <p>主人
+                        <br>刚刚萌萌哒我有帮你续期域名哦~账户<a href="#" rel="noopener">%s</a>这次续期的结果如下
+                        <br>
+                        <span class="renewed">%s</span>
+                        <span class="notRenewed">%s</span>
+                        <br>emmmm,除以上内容外,我还帮小主看了一下:<br><span class="domainDays">%s</span>更多信息可以参考<a href="https://my.freenom.com/domains.php?a=renewals" target="_blank" rel="noopener">Freenom官网</a>哦~
+                    </p>
+                </div>
+                <div class="mmsgLetterInscribe" style="padding:40px 0 0;">
+                    <img class="mmsgAvatar" src="https://q2.qlogo.cn/headimg_dl?dst_uin=593198779&spec=100" style="float:left;width:40px;height:40px;">
+                    <div class="mmsgSender" style="margin:0 0 0 54px;">
+                        <p class="mmsgName" style="margin:0 0 10px;">Im Robot</p>
+                        <p class="mmsgInfo" style="font-size:12px;margin:0;line-height:1.2;">
+                            邮件推送机器人<br>
+                            <a href="mailto:[email protected]" rel="noopener" target="_blank">[email protected]</a>
+                        </p>
+                    </div>
+                </div>
+            </div>
+            <div class="mmsgLetterFooter" style="margin:16px;text-align:center;font-size:12px;color:#888;text-shadow:1px 0px 0px #eee;"></div>
+        </div>
+    </div>
+</div>
+</body>
+</html>

BIN
resources/mail/images/163/163mail01.png


BIN
resources/mail/images/163/163mail02.png


BIN
resources/mail/images/163/163mail03.png


BIN
resources/mail/images/Snipaste_2018-08-13_15-58-52.png


BIN
resources/mail/images/github_actions/ga01.png


BIN
resources/mail/images/github_actions/ga02.png


BIN
resources/mail/images/github_actions/ga03.png


BIN
resources/mail/images/github_actions/ga04.png


BIN
resources/mail/images/github_actions/ga05.png


BIN
resources/mail/images/github_actions/ga06.png


BIN
resources/mail/images/github_actions/ga07.png


BIN
resources/mail/images/github_actions/ga08.png


BIN
resources/mail/images/gmail/gmail01.png


BIN
resources/mail/images/gmail/gmail01_en.png


BIN
resources/mail/images/gmail/gmail02.png


BIN
resources/mail/images/mmsgletter_2_bg.png


BIN
resources/mail/images/mmsgletter_2_bg_topline.png


BIN
resources/mail/images/mmsgletter_2_btn.png


BIN
resources/mail/images/mmsgletter_chat_left.gif


BIN
resources/mail/images/mmsgletter_chat_right.gif


BIN
resources/mail/images/pay.png


BIN
resources/mail/images/qq/qq01.png


BIN
resources/mail/images/qq/qq02.png


BIN
resources/mail/images/qq/qq03.png


BIN
resources/mail/images/qq/qq04.png


BIN
resources/mail/images/ting.jpg


+ 366 - 0
resources/mail/notice.html

@@ -0,0 +1,366 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <title>邮件通知</title>
+    <style>
+        .mmsgLetter {
+            width: 580px;
+            margin: 0 auto;
+            padding: 10px;
+            color: #333;
+            background: #fff;
+            border: 0px solid #aaa;
+            border-radius: 5px;
+            -webkit-box-shadow: 3px 3px 10px #999 !important;
+            -moz-box-shadow: 3px 3px 10px #999 !important;
+            box-shadow: 3px 3px 10px #999 !important;
+            font-family: Verdana, sans-serif;
+        }
+
+        .mmsgLetter a:link,
+        .mmsgLetter a:visited {
+            color: #407700;
+        }
+
+        .mmsgLetterContent {
+            text-align: left;
+            padding: 30px;
+            font-size: 14px;
+            line-height: 1.5;
+            /*background: url('images/ting.jpg') no-repeat top right;*/
+        }
+
+        .mmsgLetterContent h3 {
+            color: #000;
+            font-size: 20px;
+            font-weight: bold;
+            margin: 20px 0 20px;
+            border-top: 2px solid #eee;
+            padding: 20px 0 0 0;
+            font-family: "微软雅黑", "黑体", "Lucida Grande", Verdana, sans-serif;
+        }
+
+        .mmsgLetterContent p {
+            margin: 20px 0;
+            padding: 0;
+        }
+
+        .mmsgLetterContent .salutation {
+            font-weight: bold;
+        }
+
+        .mmsgLetterContent .mmsgMoreInfo {
+        }
+
+        .mmsgLetterContent a.mmsgButton {
+            display: block;
+            float: left;
+            height: 40px;
+            text-decoration: none;
+            text-align: center;
+            cursor: pointer;
+        }
+
+        .mmsgLetterContent a.mmsgButton span {
+            display: block;
+            float: left;
+            padding: 0 25px;
+            height: 40px;
+            line-height: 36px;
+            font-size: 14px;
+            font-weight: bold;
+            color: #fff;
+            text-shadow: 1px 0 0 #235e00;
+        }
+
+        .mmsgLetterContent a.mmsgButton:link,
+        .mmsgLetterContent a.mmsgButton:visited {
+            background: #338702 url('images/mmsgletter_2_btn.png') no-repeat right -40px;
+        }
+
+        .mmsgLetterContent a.mmsgButton:link span,
+        .mmsgLetterContent a.mmsgButton:visited span {
+            background: url('images/mmsgletter_2_btn.png') no-repeat 0 0;
+        }
+
+        .mmsgLetterContent a.mmsgButton:hover,
+        .mmsgLetterContent a.mmsgButton:active {
+            background: #338702 url('images/mmsgletter_2_btn.png') no-repeat right -120px;
+        }
+
+        .mmsgLetterContent a.mmsgButton:hover span,
+        .mmsgLetterContent a.mmsgButton:active span {
+            background: url('images/mmsgletter_2_btn.png') no-repeat 0 -80px;
+        }
+
+        .mmsgLetterInscribe {
+            padding: 40px 0 0;
+        }
+
+        .mmsgLetterInscribe .mmsgAvatar {
+            float: left;
+        }
+
+        .mmsgLetterInscribe .mmsgName {
+            margin: 0 0 10px;
+        }
+
+        .mmsgLetterInscribe .mmsgSender {
+            margin: 0 0 0 54px;
+        }
+
+        .mmsgLetterInscribe .mmsgInfo {
+            font-size: 12px;
+            margin: 0;
+            line-height: 1.2;
+        }
+
+        .mmsgLetterHeader {
+            height: 23px;
+            /*background: url('images/mmsgletter_2_bg_topline.png') repeat-x 0 0;*/
+            background: 7px 0 repeat-x #FFF;
+            background-image: -webkit-repeating-linear-gradient(135deg, #4882CE, #4882CE 20px, #FFF 20px, #FFF 35px, #EB1B2E 35px, #EB1B2E 55px, #FFF 55px, #FFF 70px);
+            background-image: repeating-linear-gradient(-45deg, #4882CE, #4882CE 20px, #FFF 20px, #FFF 35px, #EB1B2E 35px, #EB1B2E 55px, #FFF 55px, #FFF 70px);
+            background-size: 110px 10px;
+        }
+
+        .mmsgLetterFooter {
+            margin: 16px;
+            text-align: center;
+            font-size: 12px;
+            color: #888;
+            text-shadow: 1px 0px 0px #eee;
+        }
+
+        .mmsgLetterClr {
+            clear: both;
+            overflow: hidden;
+            height: 1px;
+        }
+
+        .mmsgLetterUser {
+            padding: 10px 0;
+        }
+
+        .mmsgLetterUserItem {
+            padding: 0 0 20px 0;
+        }
+
+        .mmsgLetterUserAvatar {
+            height: 40px;
+            border: 1px solid #ccc;
+            padding: 2px;
+            display: block;
+            float: left;
+        }
+
+        .mmsgLetterUserAvatar img {
+            width: 40px;
+            height: 40px;
+        }
+
+        .mmsgLetterInfo {
+            margin-left: 48px;
+        }
+
+        .mmsgLetterName {
+            display: block;
+            color: #5fa207;
+            font-weight: bold;
+            margin-left: 10px;
+        }
+
+        .mmsgLetterDesc {
+            font-size: 12px;
+            float: left;
+            height: 43px;
+            background: url('images/mmsgletter_chat_right.gif') no-repeat right top;
+        }
+
+        .mmsgLetterDesc div {
+            white-space: nowrap;
+            float: left;
+            height: 43px;
+            padding: 0 20px;
+            line-height: 40px;
+            background: url('images/mmsgletter_chat_left.gif') no-repeat left top;
+        }
+
+        .mmsgLetterUser {
+        }
+
+        .mmsgLetterAvatar {
+            float: left;
+        }
+
+        .mmsgLetterInfo {
+            margin: 0 0 0 60px;
+        }
+
+        .mmsgLetterNickName {
+            font-size: 14px;
+            font-weight: bold;
+        }
+
+        .mmsgLetterUin {
+            font-size: 12px;
+            color: #666;
+        }
+
+        .mmsgLetterUser {
+            padding: 10px 0;
+        }
+
+        .mmsgLetterUserItem {
+            padding: 0 0 20px 0;
+        }
+
+        .mmsgLetterUserAvatar {
+            height: 40px;
+            border: 1px solid #ccc;
+            padding: 2px;
+            display: block;
+            float: left;
+        }
+
+        .mmsgLetterUserAvatar img {
+            width: 40px;
+            height: 40px;
+        }
+
+        .mmsgLetterInfo {
+            margin-left: 48px;
+        }
+
+        .mmsgLetterName {
+            display: block;
+            color: #5fa207;
+            font-weight: bold;
+            margin-left: 10px;
+            padding-top: 10px;
+        }
+
+        .mmsgLetterDesc {
+            font-size: 12px;
+            float: left;
+            height: 43px;
+            background: url('images/mmsgletter_chat_right.gif') no-repeat right top;
+        }
+
+        .mmsgLetterDesc div {
+            white-space: nowrap;
+            float: left;
+            height: 43px;
+            padding: 0 20px;
+            line-height: 40px;
+            background: url('images/mmsgletter_chat_left.gif') no-repeat left top;
+        }
+
+        .qmbox style,
+        .qmbox script,
+        .qmbox head,
+        .qmbox link,
+        .qmbox meta {
+            display: none !important;
+        }
+
+        #mailContentContainer .txt {
+            height: auto;
+        }
+
+        a {
+            color: #407700 !important;
+        }
+
+        .renewed {
+            color: #4882CE !important;
+        }
+
+        .notRenewed {
+            color: #EB1B2E !important;
+        }
+
+        .domainDays a {
+            color: #407700 !important;
+        }
+
+        .renewed a,
+        .notRenewed a,
+        .domainDays a {
+            /*color: inherit !important;*/
+            display: inline-block;
+            background: #ffffff;
+            min-width: 22px;
+            text-align: center;
+            padding: 4px 6px;
+            border-radius: 22px;
+            font-size: 14px;
+            font-weight: bold;
+            transition: 0.2s all ease-in-out;
+            text-decoration: none !important;
+            margin-right: 12px;
+        }
+
+        .renewed a {
+            color: inherit !important;
+            border: 2px solid #4882CE;
+        }
+
+        .notRenewed a {
+            color: inherit !important;
+            border: 2px solid #EB1B2E;
+        }
+
+        .domainDays a {
+            border: 2px solid #407700;
+        }
+
+        .renewed a:hover {
+            background-color: #4882CE;
+            color: #ffffff !important;
+        }
+
+        .notRenewed a:hover {
+            background-color: #EB1B2E;
+            color: #ffffff !important;
+        }
+
+        .domainDays a:hover {
+            background-color: #407700;
+            color: #ffffff !important;
+        }
+    </style>
+</head>
+<body>
+<div id="mailContentContainer" class="qmbox qm_con_body_content qqmail_webmail_only" style="">
+    <div style="background-color:#d0d0d0;background-image:url('images/mmsgletter_2_bg.png');text-align:center;padding:40px;">
+        <div class="mmsgLetter">
+            <div class="mmsgLetterHeader" style="height:23px;"></div>
+            <div class="mmsgLetterContent" style="text-align:left;padding:30px;font-size:14px;line-height:1.5;/*background:url('images/ting.jpg') no-repeat top right;background-size: 224px 224px;*/">
+                <div>
+                    <p>
+                        <br>我刚刚帮小主看了一下,账户<a href="#" rel="noopener">%s</a>今天并没有需要续期的域名。所有域名情况如下:
+                        <br>
+                        <br><span class="domainDays">%s</span>更多信息可以参考<a href="https://my.freenom.com/domains.php?a=renewals" target="_blank" rel="noopener">Freenom官网</a>哦~
+                        <br>
+                        <br>(如果你不想每次执行都收到推送,请将 .env 中 NOTICE_FREQ 的值设为0,使程序只在有续期操作时才推送)
+                    </p>
+                </div>
+                <div class="mmsgLetterInscribe" style="padding:40px 0 0;">
+                    <img class="mmsgAvatar" src="https://q2.qlogo.cn/headimg_dl?dst_uin=593198779&spec=100" style="float:left;width:40px;height:40px;">
+                    <div class="mmsgSender" style="margin:0 0 0 54px;">
+                        <p class="mmsgName" style="margin:0 0 10px;">Im Robot</p>
+                        <p class="mmsgInfo" style="font-size:12px;margin:0;line-height:1.2;">
+                            邮件推送机器人<br>
+                            <a href="mailto:[email protected]" rel="noopener" target="_blank">[email protected]</a>
+                        </p>
+                    </div>
+                </div>
+            </div>
+            <div class="mmsgLetterFooter" style="margin:16px;text-align:center;font-size:12px;color:#888;text-shadow:1px 0px 0px #eee;"></div>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 0 - 0
resources/screenshot/.gitkeep


BIN
resources/screenshot/lie.jpg


BIN
resources/screenshot/lizhi.jpg


BIN
resources/screenshot/scf.png


+ 74 - 0
run

@@ -0,0 +1,74 @@
+#!/usr/bin/env php
+<?php
+/**
+ * 入口文件
+ *
+ * @author mybsdc <[email protected]>
+ * @date 2019/3/2
+ * @time 11:05
+ * @link https://github.com/luolongfei/freenom
+ */
+
+error_reporting(E_ERROR);
+ini_set('display_errors', 1);
+set_time_limit(0);
+
+define('IS_CLI', PHP_SAPI === 'cli');
+define('DS', DIRECTORY_SEPARATOR);
+define('ROOT_PATH', realpath(__DIR__));
+define('VENDOR_PATH', realpath(ROOT_PATH . '/vendor'));
+define('APP_PATH', realpath(ROOT_PATH . '/app'));
+define('RESOURCES_PATH', realpath(ROOT_PATH . '/resources'));
+
+date_default_timezone_set('Asia/Shanghai');
+
+/**
+ * 注册错误处理
+ */
+register_shutdown_function('customize_error_handler');
+
+/**
+ * 注册异常处理
+ */
+set_exception_handler('exception_handler');
+
+require VENDOR_PATH . '/autoload.php';
+
+use Luolongfei\Lib\Log;
+use Luolongfei\Lib\Mail;
+use Luolongfei\Lib\TelegramBot;
+
+/**
+ * @throws Exception
+ */
+function customize_error_handler()
+{
+    if (!is_null($error = error_get_last())) {
+        Log::error('程序意外终止', $error);
+//        Mail::send('主人,程序意外终止', '具体情况我也不清楚,请查看服务器日志定位问题。');
+//        TelegramBot::send('主人,程序意外终止,具体情况我也不清楚,请查看服务器日志定位问题。');
+    }
+}
+
+/**
+ * @param \Exception $e
+ *
+ * @throws \Exception
+ */
+function exception_handler($e)
+{
+    Log::error('未捕获的异常:' . $e->getMessage());
+    Mail::send('主人,未捕获的异常', "具体的异常内容是:\n" . $e->getMessage());
+    TelegramBot::send('主人,发现未捕获的异常:' . $e->getMessage());
+}
+
+try {
+    system_check();
+
+    $class = sprintf('Luolongfei\App\Console\%s', get_argv('c', 'FreeNom'));
+    $fn = get_argv('m', 'handle');
+
+    $class::instance()->$fn();
+} catch (\Exception $e) {
+    system_log(sprintf('执行出错:<red>%s</red>', $e->getMessage()), $e->getTrace());
+}

+ 7 - 0
vendor/autoload.php

@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInit99b73a665e0d2066a2cb8dd066883cba::getLoader();

+ 1 - 0
vendor/bramus/ansi-php/.gitignore

@@ -0,0 +1 @@
+vendor

+ 12 - 0
vendor/bramus/ansi-php/.travis.yml

@@ -0,0 +1,12 @@
+language: php
+
+dist: trusty
+
+php:
+  - 5.6
+  - 7.0
+  - 7.1
+  - 7.2
+  - 7.3
+
+before_script: composer install

+ 19 - 0
vendor/bramus/ansi-php/LICENSE

@@ -0,0 +1,19 @@
+Copyright Bram(us) Van Damme - https://www.bram.us/
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 26 - 0
vendor/bramus/ansi-php/composer.json

@@ -0,0 +1,26 @@
+{
+    "name": "bramus/ansi-php",
+    "description": "ANSI Control Functions and ANSI Control Sequences (Colors, Erasing, etc.) for PHP CLI Apps",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Bramus Van Damme",
+            "email": "[email protected]",
+            "homepage": "https://www.bram.us/"
+        }
+    ],
+    "require": {
+        "php": ">=5.4.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "~4.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Bramus\\Ansi\\": "src/"
+        }
+    },
+    "scripts": {
+        "test": "vendor/bin/phpunit"
+    }
+}

+ 1199 - 0
vendor/bramus/ansi-php/composer.lock

@@ -0,0 +1,1199 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "2bd803cd6028d92b2de740f9bff2af1d",
+    "packages": [],
+    "packages-dev": [
+        {
+            "name": "doctrine/instantiator",
+            "version": "1.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/instantiator.git",
+                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
+                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3,<8.0-DEV"
+            },
+            "require-dev": {
+                "athletic/athletic": "~0.1.8",
+                "ext-pdo": "*",
+                "ext-phar": "*",
+                "phpunit/phpunit": "~4.0",
+                "squizlabs/php_codesniffer": "~2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "[email protected]",
+                    "homepage": "http://ocramius.github.com/"
+                }
+            ],
+            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+            "homepage": "https://github.com/doctrine/instantiator",
+            "keywords": [
+                "constructor",
+                "instantiate"
+            ],
+            "time": "2015-06-14T21:17:01+00:00"
+        },
+        {
+            "name": "phpdocumentor/reflection-common",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": [
+                        "src"
+                    ]
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jaap van Otterdijk",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+            "homepage": "http://www.phpdoc.org",
+            "keywords": [
+                "FQSEN",
+                "phpDocumentor",
+                "phpdoc",
+                "reflection",
+                "static analysis"
+            ],
+            "time": "2017-09-11T18:02:19+00:00"
+        },
+        {
+            "name": "phpdocumentor/reflection-docblock",
+            "version": "3.3.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+                "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bf329f6c1aadea3299f08ee804682b7c45b326a2",
+                "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.6 || ^7.0",
+                "phpdocumentor/reflection-common": "^1.0.0",
+                "phpdocumentor/type-resolver": "^0.4.0",
+                "webmozart/assert": "^1.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "^0.9.4",
+                "phpunit/phpunit": "^4.4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": [
+                        "src/"
+                    ]
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+            "time": "2017-11-10T14:09:06+00:00"
+        },
+        {
+            "name": "phpdocumentor/type-resolver",
+            "version": "0.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/TypeResolver.git",
+                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
+                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.5 || ^7.0",
+                "phpdocumentor/reflection-common": "^1.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "^0.9.4",
+                "phpunit/phpunit": "^5.2||^4.8.24"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": [
+                        "src/"
+                    ]
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "[email protected]"
+                }
+            ],
+            "time": "2017-07-14T14:27:02+00:00"
+        },
+        {
+            "name": "phpspec/prophecy",
+            "version": "1.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpspec/prophecy.git",
+                "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
+                "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/instantiator": "^1.0.2",
+                "php": "^5.3|^7.0",
+                "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
+                "sebastian/comparator": "^1.1|^2.0|^3.0",
+                "sebastian/recursion-context": "^1.0|^2.0|^3.0"
+            },
+            "require-dev": {
+                "phpspec/phpspec": "^2.5|^3.2",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.8.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Prophecy\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Konstantin Kudryashov",
+                    "email": "[email protected]",
+                    "homepage": "http://everzet.com"
+                },
+                {
+                    "name": "Marcello Duarte",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Highly opinionated mocking framework for PHP 5.3+",
+            "homepage": "https://github.com/phpspec/prophecy",
+            "keywords": [
+                "Double",
+                "Dummy",
+                "fake",
+                "mock",
+                "spy",
+                "stub"
+            ],
+            "time": "2018-08-05T17:53:17+00:00"
+        },
+        {
+            "name": "phpunit/php-code-coverage",
+            "version": "2.2.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+                "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979",
+                "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "phpunit/php-file-iterator": "~1.3",
+                "phpunit/php-text-template": "~1.2",
+                "phpunit/php-token-stream": "~1.3",
+                "sebastian/environment": "^1.3.2",
+                "sebastian/version": "~1.0"
+            },
+            "require-dev": {
+                "ext-xdebug": ">=2.1.4",
+                "phpunit/phpunit": "~4"
+            },
+            "suggest": {
+                "ext-dom": "*",
+                "ext-xdebug": ">=2.2.1",
+                "ext-xmlwriter": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.2.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+            "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+            "keywords": [
+                "coverage",
+                "testing",
+                "xunit"
+            ],
+            "time": "2015-10-06T15:47:00+00:00"
+        },
+        {
+            "name": "phpunit/php-file-iterator",
+            "version": "1.4.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+                "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
+                "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.4.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+            "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+            "keywords": [
+                "filesystem",
+                "iterator"
+            ],
+            "time": "2017-11-27T13:52:08+00:00"
+        },
+        {
+            "name": "phpunit/php-text-template",
+            "version": "1.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-text-template.git",
+                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "Simple template engine.",
+            "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+            "keywords": [
+                "template"
+            ],
+            "time": "2015-06-21T13:50:34+00:00"
+        },
+        {
+            "name": "phpunit/php-timer",
+            "version": "1.0.9",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-timer.git",
+                "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
+                "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.3 || ^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "Utility class for timing",
+            "homepage": "https://github.com/sebastianbergmann/php-timer/",
+            "keywords": [
+                "timer"
+            ],
+            "time": "2017-02-26T11:10:40+00:00"
+        },
+        {
+            "name": "phpunit/php-token-stream",
+            "version": "1.4.12",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-token-stream.git",
+                "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16",
+                "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16",
+                "shasum": ""
+            },
+            "require": {
+                "ext-tokenizer": "*",
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.4-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Wrapper around PHP's tokenizer extension.",
+            "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
+            "keywords": [
+                "tokenizer"
+            ],
+            "time": "2017-12-04T08:55:13+00:00"
+        },
+        {
+            "name": "phpunit/phpunit",
+            "version": "4.8.36",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phpunit.git",
+                "reference": "46023de9a91eec7dfb06cc56cb4e260017298517"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/46023de9a91eec7dfb06cc56cb4e260017298517",
+                "reference": "46023de9a91eec7dfb06cc56cb4e260017298517",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-json": "*",
+                "ext-pcre": "*",
+                "ext-reflection": "*",
+                "ext-spl": "*",
+                "php": ">=5.3.3",
+                "phpspec/prophecy": "^1.3.1",
+                "phpunit/php-code-coverage": "~2.1",
+                "phpunit/php-file-iterator": "~1.4",
+                "phpunit/php-text-template": "~1.2",
+                "phpunit/php-timer": "^1.0.6",
+                "phpunit/phpunit-mock-objects": "~2.3",
+                "sebastian/comparator": "~1.2.2",
+                "sebastian/diff": "~1.2",
+                "sebastian/environment": "~1.3",
+                "sebastian/exporter": "~1.2",
+                "sebastian/global-state": "~1.0",
+                "sebastian/version": "~1.0",
+                "symfony/yaml": "~2.1|~3.0"
+            },
+            "suggest": {
+                "phpunit/php-invoker": "~1.1"
+            },
+            "bin": [
+                "phpunit"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.8.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "The PHP Unit Testing framework.",
+            "homepage": "https://phpunit.de/",
+            "keywords": [
+                "phpunit",
+                "testing",
+                "xunit"
+            ],
+            "time": "2017-06-21T08:07:12+00:00"
+        },
+        {
+            "name": "phpunit/phpunit-mock-objects",
+            "version": "2.3.8",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
+                "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983",
+                "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/instantiator": "^1.0.2",
+                "php": ">=5.3.3",
+                "phpunit/php-text-template": "~1.2",
+                "sebastian/exporter": "~1.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.4"
+            },
+            "suggest": {
+                "ext-soap": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.3.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "Mock Object library for PHPUnit",
+            "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
+            "keywords": [
+                "mock",
+                "xunit"
+            ],
+            "time": "2015-10-02T06:51:40+00:00"
+        },
+        {
+            "name": "sebastian/comparator",
+            "version": "1.2.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/comparator.git",
+                "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
+                "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "sebastian/diff": "~1.2",
+                "sebastian/exporter": "~1.2 || ~2.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.2.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jeff Welch",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Provides the functionality to compare PHP values for equality",
+            "homepage": "http://www.github.com/sebastianbergmann/comparator",
+            "keywords": [
+                "comparator",
+                "compare",
+                "equality"
+            ],
+            "time": "2017-01-29T09:50:25+00:00"
+        },
+        {
+            "name": "sebastian/diff",
+            "version": "1.4.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/diff.git",
+                "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4",
+                "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.3 || ^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.4-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Kore Nordmann",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Diff implementation",
+            "homepage": "https://github.com/sebastianbergmann/diff",
+            "keywords": [
+                "diff"
+            ],
+            "time": "2017-05-22T07:24:03+00:00"
+        },
+        {
+            "name": "sebastian/environment",
+            "version": "1.3.8",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/environment.git",
+                "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea",
+                "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.3 || ^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8 || ^5.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Provides functionality to handle HHVM/PHP environments",
+            "homepage": "http://www.github.com/sebastianbergmann/environment",
+            "keywords": [
+                "Xdebug",
+                "environment",
+                "hhvm"
+            ],
+            "time": "2016-08-18T05:49:44+00:00"
+        },
+        {
+            "name": "sebastian/exporter",
+            "version": "1.2.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/exporter.git",
+                "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4",
+                "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "sebastian/recursion-context": "~1.0"
+            },
+            "require-dev": {
+                "ext-mbstring": "*",
+                "phpunit/phpunit": "~4.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jeff Welch",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Provides the functionality to export PHP variables for visualization",
+            "homepage": "http://www.github.com/sebastianbergmann/exporter",
+            "keywords": [
+                "export",
+                "exporter"
+            ],
+            "time": "2016-06-17T09:04:28+00:00"
+        },
+        {
+            "name": "sebastian/global-state",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/global-state.git",
+                "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
+                "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.2"
+            },
+            "suggest": {
+                "ext-uopz": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Snapshotting of global state",
+            "homepage": "http://www.github.com/sebastianbergmann/global-state",
+            "keywords": [
+                "global state"
+            ],
+            "time": "2015-10-12T03:26:01+00:00"
+        },
+        {
+            "name": "sebastian/recursion-context",
+            "version": "1.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/recursion-context.git",
+                "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7",
+                "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jeff Welch",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Provides functionality to recursively process PHP variables",
+            "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+            "time": "2016-10-03T07:41:43+00:00"
+        },
+        {
+            "name": "sebastian/version",
+            "version": "1.0.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/version.git",
+                "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
+                "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+            "homepage": "https://github.com/sebastianbergmann/version",
+            "time": "2015-06-21T13:59:46+00:00"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.10.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
+                "reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "ext-ctype": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.9-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                },
+                {
+                    "name": "Gert de Pagter",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "time": "2018-08-06T14:22:27+00:00"
+        },
+        {
+            "name": "symfony/yaml",
+            "version": "v3.4.22",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/yaml.git",
+                "reference": "ba11776e9e6c15ad5759a07bffb15899bac75c2d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/ba11776e9e6c15ad5759a07bffb15899bac75c2d",
+                "reference": "ba11776e9e6c15ad5759a07bffb15899bac75c2d",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.5.9|>=7.0.8",
+                "symfony/polyfill-ctype": "~1.8"
+            },
+            "conflict": {
+                "symfony/console": "<3.4"
+            },
+            "require-dev": {
+                "symfony/console": "~3.4|~4.0"
+            },
+            "suggest": {
+                "symfony/console": "For validating YAML files using the lint command"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.4-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Yaml\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Yaml Component",
+            "homepage": "https://symfony.com",
+            "time": "2019-01-16T10:59:17+00:00"
+        },
+        {
+            "name": "webmozart/assert",
+            "version": "1.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/webmozart/assert.git",
+                "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
+                "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.3 || ^7.0",
+                "symfony/polyfill-ctype": "^1.8"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.6",
+                "sebastian/version": "^1.0.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Webmozart\\Assert\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Assertions to validate method input/output with nice error messages.",
+            "keywords": [
+                "assert",
+                "check",
+                "validate"
+            ],
+            "time": "2018-12-25T11:19:39+00:00"
+        }
+    ],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "php": ">=5.4.0"
+    },
+    "platform-dev": []
+}

+ 14 - 0
vendor/bramus/ansi-php/phpunit.xml.dist

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" syntaxCheck="false" bootstrap="tests/bootstrap.php">
+	<testsuites>
+		<testsuite name="Ansi Tests">
+			<directory>tests/</directory>
+		</testsuite>
+	</testsuites>
+	<filter>
+		<whitelist>
+			<directory suffix=".php">src/</directory>
+		</whitelist>
+	</filter>
+</phpunit>

+ 337 - 0
vendor/bramus/ansi-php/readme.md

@@ -0,0 +1,337 @@
+# ANSI PHP
+
+[![Build Status](https://img.shields.io/travis/bramus/ansi-php.svg?style=flat-square)](http://travis-ci.org/bramus/ansi-php) [![Source](http://img.shields.io/badge/source-bramus/ansi--php-blue.svg?style=flat-square)](https://github.com/bramus/ansi-php) [![Version](https://img.shields.io/packagist/v/bramus/ansi-php.svg?style=flat-square)](https://packagist.org/packages/bramus/ansi-php) [![Downloads](https://img.shields.io/packagist/dt/bramus/ansi-php.svg?style=flat-square)](https://packagist.org/packages/bramus/ansi-php/stats) [![License](https://img.shields.io/packagist/l/bramus/ansi-php.svg?style=flat-square)](https://github.com/bramus/ansi-php/blob/master/LICENSE)
+
+ANSI Control Functions and ANSI Control Sequences for PHP CLI Apps
+
+Built by Bramus! - [https://www.bram.us/](https://www.bram.us/)
+
+## About
+
+`bramus/ansi-php` is a set of classes to working with ANSI Control Functions and ANSI Control Sequences on text based terminals.
+
+- ANSI Control Functions control an action such as line spacing, paging, or data flow.
+- ANSI Control Sequences allow one to clear the screen, move the cursor, set text colors, etc.
+
+_(Sidenote: An “ANSI Escape Sequence” is a special type of “ANSI Control Sequence” which starts with the ESC ANSI Control Function. The terms are not interchangeable.)_
+
+## Features
+
+When it comes to ANSI Control Functions `bramus/ansi-php` supports:
+
+- `BS`: Backspace
+- `BEL`: Bell
+- `CR`: Carriage Return
+- `ESC`: Escape
+- `LF`: Line Feed
+- `TAB`: Tab
+
+When it comes to ANSI Escape Sequences `bramus/ansi-php` supports:
+
+- SGR _(Select Graphic Rendition)_: Manipulate text styling (bold, underline, blink, colors, etc.).
+- ED _(Erase Display)_: Erase (parts of) the display.
+- EL _(Erase In Line)_: Erase (parts of) the current line.
+
+Other Control Sequences – such as moving the cursor – are not (yet) supported.
+
+An example library that uses `bramus/ansi-php` is [`bramus/monolog-colored-line-formatter`](https://github.com/bramus/monolog-colored-line-formatter). It uses `bramus/ansi-php`'s SGR support to colorize the output:
+
+![Monolog Colored Line Formatter](https://user-images.githubusercontent.com/11269635/28756233-c9f63abe-756a-11e7-883f-a084f35c55e7.gif)
+
+## Prerequisites/Requirements
+
+- PHP 5.4.0 or greater
+
+## Installation
+
+Installation is possible using Composer
+
+```shell
+composer require bramus/ansi-php ~3.0
+```
+
+## Usage
+
+The easiest way to use _ANSI PHP_ is to use the bundled `Ansi` helper class which provides easy shorthands to working with `bramus/ansi-php`. The `Ansi` class is written in such a way that you can chain calls to one another.
+
+If you're feeling adventurous, you're of course free to use the raw `ControlFunction` and `ControlSequence` classes.
+
+### Quick example
+
+```php
+use \Bramus\Ansi\Ansi;
+use \Bramus\Ansi\Writers\StreamWriter;
+use \Bramus\Ansi\ControlSequences\EscapeSequences\Enums\SGR;
+
+// Create Ansi Instance
+$ansi = new Ansi(new StreamWriter('php://stdout'));
+
+// Output some styled text on screen, along with a Line Feed and a Bell
+$ansi->color(array(SGR::COLOR_FG_RED, SGR::COLOR_BG_WHITE))
+     ->blink()
+     ->text('I will be blinking red on a white background.')
+     ->nostyle()
+     ->text(' And I will be normally styled.')
+     ->lf()
+     ->text('Ooh, a bell is coming ...')
+     ->bell();
+```
+
+See more examples further down on how to use these.
+
+## Concepts
+
+Since v3.0 `bramus/ansi-php` uses the concept of writers to write the data to. By default a `StreamWriter` writing to `php://stdout` is used.
+
+The following writers are provided
+
+- `StreamWriter`: Writes the data to a stream. Just pass in the path to a file and it will open a stream for you. Defaults to writing to `php://stdout`.
+- `BufferWriter`: Writes the data to a buffer. When calling `flush()` the contents of the buffer will be returned.
+- `ProxyWriter`: Acts as a proxy to another writer. Writes the data to an internal buffer. When calling `flush()` the writer will first write the data to the other writer before returning it.
+
+## The `Ansi` helper class functions
+
+### Core functions:
+
+- `text($text)`: Write a piece of data to the writer
+- `setWriter(\Bramus\Ansi\Writers\WriterInterface $writer)`: Sets the writer
+- `getWriter()`: Gets the writer
+
+### ANSI Control Function shorthands:
+
+These shorthands write a Control Character to the writer.
+
+- `bell()`:  Bell Control Character (`\a`)
+- `backspace()`:  Backspace Control Character (`\b`)
+- `tab()`:  Tab Control Character (`\t`)
+- `lf()`:  Line Feed Control Character (`\n`)
+- `cr()`:  Carriage Return Control Character (`\r`)
+- `esc()`:  Escape Control Character
+
+### SGR ANSI Escape Sequence shorthands:
+
+These shorthands write SGR ANSI Escape Sequences to the writer.
+
+- `nostyle()` or `reset()`: Remove all text styling (colors, bold, etc)
+- `color()`: Set the foreground and/or backgroundcolor of the text. _(see further)_
+- `bold()` or `bright()`: Bold: On. On some systems "Intensity: Bright"
+- `normal()`: Bold: Off. On some systems "Intensity: Normal"
+- `faint()`: Intensity: Faint. _(Not widely supported)_
+- `italic()`: Italic: On. _(Not widely supported)_
+- `underline()`: Underline: On.
+- `blink()`: Blink: On.
+- `negative()`: Inverse or Reverse. Swap foreground and background.
+- `strikethrough()`: Strikethrough: On. _(Not widely supported)_
+
+__IMPORTANT:__ Select Graphic Rendition works in such a way that text styling  you have set will remain active until you call `nostyle()` or `reset()` to return to the default styling.
+
+### ED ANSI Escape Sequence shorthands:
+
+These shorthands write ED ANSI Escape Sequences to the writer.
+
+- `eraseDisplay()`: Erase the entire screen and moves the cursor to home.
+- `eraseDisplayUp()`: Erase the screen from the current line up to the top of the screen.
+- `eraseDisplayDown()`: Erase the screen from the current line down to the bottom of the screen.
+
+### EL ANSI Escape Sequence shorthands:
+
+These shorthands write EL ANSI Escape Sequences to the writer.
+
+- `eraseLine()`: Erase the entire current line.
+- `eraseLineToEOL()`: Erase from the current cursor position to the end of the current line.
+- `eraseLineToSOL()`: Erases from the current cursor position to the start of the current line.
+
+### Extra functions
+
+- `flush()` or `get()`: Retrieve contents of a `FlushableWriter` writer.
+- `e()`: Echo the contents of a `FlushableWriter` writer.
+
+## Examples
+
+### The Basics
+
+```php
+// Create Ansi Instance
+$ansi = new \Bramus\Ansi\Ansi();
+
+// This will output a Bell
+$ansi->bell();
+
+// This will output some text
+$ansi->text('Hello World!');
+```
+
+_NOTE:_ As no `$writer` is passed into the constructor of `\Bramus\Ansi\Ansi`, the default `StreamWriter` writing to `php://stdout` is used.
+
+### Using a `FlushableWriter`
+
+Flushable Writers are writers that cache the data and only output it when flushed using its `flush()` function. The `BufferWriter` and `ProxyWriter` implement this interface.
+
+```php
+// Create Ansi Instance
+$ansi = new \Bramus\Ansi\Ansi(new \Bramus\Ansi\Writers\BufferWriter());
+
+// This will append a bell to the buffer. It will not output it.
+$ansi->bell();
+
+// This will append a bell to the buffer. It will not output it.
+$ansi->text('Hello World!');
+
+// Now we'll output it
+echo $ansi->get();
+```
+
+### Chaining
+
+`bramus/ansi-php`'s wrapper `Ansi` class supports chaining.
+
+```php
+// Create Ansi Instance
+$ansi = new \Bramus\Ansi\Ansi();
+
+// This will output a Line Feed, some text, a Bell, and a Line Feed
+$ansi->lf()->text('hello')->bell()->lf();
+
+```
+
+### Styling Text: The Basics
+
+```php
+$ansi = new \Bramus\Ansi\Ansi();
+$ansi->bold()->underline()->text('I will be bold and underlined')->lf();
+```
+
+__IMPORTANT__ Select Graphic Rendition works in such a way that text styling  you have set will remain active until you call `nostyle()` or `reset()` to return to the default styling.
+
+
+```php
+$ansi = new \Bramus\Ansi\Ansi();
+
+$ansi->bold()->underline()->text('I will be bold and underlined')->lf();
+$ansi->text('I will also be bold because nostyle() has not been called yet')->lf();
+$ansi->nostyle()->blink()->text('I will be blinking')->nostyle()->lf();
+$ansi->text('I will be normal because nostyle() was called on the previous line');
+
+```
+
+### Styling Text: Colors
+
+Colors, and other text styling options, are defined as contants on `\Bramus\Ansi\ControlSequences\EscapeSequences\Enums\SGR`.
+
+#### Foreground (Text) Colors
+
+- `SGR::COLOR_FG_BLACK`: Black Foreground Color
+- `SGR::COLOR_FG_RED`: Red Foreground Color
+- `SGR::COLOR_FG_GREEN`: Green Foreground Color
+- `SGR::COLOR_FG_YELLOW`: Yellow Foreground Color
+- `SGR::COLOR_FG_BLUE`: Blue Foreground Color
+- `SGR::COLOR_FG_PURPLE`: Purple Foreground Color
+- `SGR::COLOR_FG_CYAN`: Cyan Foreground Color
+- `SGR::COLOR_FG_WHITE`: White Foreground Color
+- `SGR::COLOR_FG_BLACK_BRIGHT`: Black Foreground Color (Bright)
+- `SGR::COLOR_FG_RED_BRIGHT`: Red Foreground Color (Bright)
+- `SGR::COLOR_FG_GREEN_BRIGHT`: Green Foreground Color (Bright)
+- `SGR::COLOR_FG_YELLOW_BRIGHT`: Yellow Foreground Color (Bright)
+- `SGR::COLOR_FG_BLUE_BRIGHT`: Blue Foreground Color (Bright)
+- `SGR::COLOR_FG_PURPLE_BRIGHT`: Purple Foreground Color (Bright)
+- `SGR::COLOR_FG_CYAN_BRIGHT`: Cyan Foreground Color (Bright)
+- `SGR::COLOR_FG_WHITE_BRIGHT`: White Foreground Color (Bright)
+- `SGR::COLOR_FG_RESET`: Default Foreground Color
+
+#### Background Colors
+
+- `SGR::COLOR_BG_BLACK`: Black Background Color
+- `SGR::COLOR_BG_RED`: Red Background Color
+- `SGR::COLOR_BG_GREEN`: Green Background Color
+- `SGR::COLOR_BG_YELLOW`: Yellow Background Color
+- `SGR::COLOR_BG_BLUE`: Blue Background Color
+- `SGR::COLOR_BG_PURPLE`: Purple Background Color
+- `SGR::COLOR_BG_CYAN`: Cyan Background Color
+- `SGR::COLOR_BG_WHITE`: White Background Color
+- `SGR::COLOR_BG_BLACK_BRIGHT`: Black Background Color (Bright)
+- `SGR::COLOR_BG_RED_BRIGHT`: Red Background Color (Bright)
+- `SGR::COLOR_BG_GREEN_BRIGHT`: Green Background Color (Bright)
+- `SGR::COLOR_BG_YELLOW_BRIGHT`: Yellow Background Color (Bright)
+- `SGR::COLOR_BG_BLUE_BRIGHT`: Blue Background Color (Bright)
+- `SGR::COLOR_BG_PURPLE_BRIGHT`: Purple Background Color (Bright)
+- `SGR::COLOR_BG_CYAN_BRIGHT`: Cyan Background Color (Bright)
+- `SGR::COLOR_BG_WHITE_BRIGHT`: White Background Color (Bright)
+- `SGR::COLOR_BG_RESET`: Default Background Color
+
+Pass one of these into `$ansi->color()` and the color will be set.
+
+```php
+use \Bramus\Ansi\ControlSequences\EscapeSequences\Enums\SGR;
+
+$ansi = new \Bramus\Ansi\Ansi();
+
+$ansi->color(SGR::COLOR_FG_RED)
+     ->text('I will be red')
+     ->nostyle();
+```
+
+To set the foreground and background color in one call, pass them using an array to `$ansi->color()`
+
+```php
+use \Bramus\Ansi\ControlSequences\EscapeSequences\Enums\SGR;
+
+$ansi = new \Bramus\Ansi\Ansi();
+
+$ansi->color(array(SGR::COLOR_FG_RED, SGR::COLOR_BG_WHITE))
+     ->blink()
+     ->text('I will be blinking red on a wrhite background.')
+     ->nostyle();
+```
+
+### Using the raw classes
+
+As all raw `ControlFunction` and `ControlSequence` classes are provided with a `__toString()` function it's perfectly possible to directly `echo` some `bramus/ansi-php` instance.
+
+```php
+// Output a Bell Control Character
+echo new \Bramus\Ansi\ControlFunctions\Bell();
+
+// Output an ED instruction, to erase the entire screen
+echo new \Bramus\Ansi\ControlSequences\EscapeSequences\ED(
+    \Bramus\Ansi\ControlSequences\EscapeSequences\Enums\ED::ALL
+);
+```
+
+To fetch their contents, use the `get()` function:
+
+```php
+// Get ANSI string for a Bell Control Character
+$bell = (new \Bramus\Ansi\ControlFunctions\Bell())->get();
+
+// Get ANSI string for an ED instruction, to erase the entire screen
+$eraseDisplay = (new \Bramus\Ansi\ControlSequences\EscapeSequences\ED(
+    \Bramus\Ansi\ControlSequences\EscapeSequences\Enums\ED::ALL
+))->get();
+
+echo $bell . $bell . $eraseDisplay . $bell;
+```
+
+## Unit Testing
+
+`bramus/ansi-php` ships with unit tests using [PHPUnit](https://github.com/sebastianbergmann/phpunit/).
+
+- If PHPUnit is installed globally run `phpunit` to run the tests.
+
+- If PHPUnit is not installed globally, install it locally throuh composer by running `composer install --dev`. Run the tests themselves by calling `vendor/bin/phpunit` or `composer test`
+
+Unit tests are also automatically run [on Travis CI](http://travis-ci.org/bramus/ansi-php)
+
+## License
+
+`bramus/ansi-php` is released under the MIT public license. See the enclosed `LICENSE` for details.
+
+## ANSI References
+
+- [http://en.wikipedia.org/wiki/ANSI_escape_code](http://en.wikipedia.org/wiki/ANSI_escape_code)
+- [http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf](http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf)
+- [http://wiki.bash-hackers.org/scripting/terminalcodes](http://wiki.bash-hackers.org/scripting/terminalcodes)
+- [http://web.mit.edu/gnu/doc/html/screen_10.html](http://web.mit.edu/gnu/doc/html/screen_10.html)
+- [http://www.isthe.com/chongo/tech/comp/ansi_escapes.html](http://www.isthe.com/chongo/tech/comp/ansi_escapes.html)
+- [http://www.termsys.demon.co.uk/vtansi.htm](http://www.termsys.demon.co.uk/vtansi.htm)
+- [http://rrbrandt.dee.ufcg.edu.br/en/docs/ansi/](http://rrbrandt.dee.ufcg.edu.br/en/docs/ansi/)
+- [http://tldp.org/HOWTO/Bash-Prompt-HOWTO/c327.html](http://tldp.org/HOWTO/Bash-Prompt-HOWTO/c327.html)

+ 111 - 0
vendor/bramus/ansi-php/src/Ansi.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace Bramus\Ansi;
+
+/**
+ * ANSI Wrapper Class to work with \Bramus\Ansi more easily
+ */
+class Ansi
+{
+    /**
+     * Traits to use
+     */
+    use Traits\ControlFunctions;
+    use Traits\EscapeSequences\SGR;
+    use Traits\EscapeSequences\ED;
+    use Traits\EscapeSequences\EL;
+
+    /**
+     * The writer to write the data to
+     * @var Writer\WriterInterface
+     */
+    protected $writer;
+
+    /**
+     * ANSI Wrapper Class to work with \Bramus\Ansi more easily
+     * @param Writer\WriterInterface $writer writer to use
+     */
+    public function __construct($writer = null)
+    {
+        // Enforce having a writer
+        if (!$writer) {
+            $writer = new Writers\StreamWriter();
+        }
+
+        // Set the writer
+        $this->setWriter($writer);
+    }
+
+    /**
+     * Sets the writer
+     * @param Writer\WriterInterface $writer The writer to use
+     */
+    public function setWriter(Writers\WriterInterface $writer)
+    {
+        $this->writer = $writer;
+    }
+
+    /**
+     * Gets the writer
+     * @return Writer\WriterInterface $writer The writer used
+     */
+    public function getWriter()
+    {
+        return $this->writer;
+    }
+
+    /**
+     * Write a piece of text onto the writer
+     * @param  string $text The text to write
+     * @return Ansi   self, for chaining
+     */
+    public function text($text)
+    {
+        // Write the text to the writer
+        $this->writer->write($text);
+
+        // Afford chaining
+        return $this;
+    }
+
+    /**
+     * Flush the contents of the writer
+     * @param  $resetAfterwards Reset the writer contents after flushing?
+     * @return string The writer contents
+     */
+    public function flush($resetAfterwards = true)
+    {
+        if ($this->writer instanceof Writers\FlushableInterface) {
+            return $this->writer->flush($resetAfterwards);
+        } else {
+            throw new \Exception('Flushing a non FlushableInterface is not possible');
+        }
+    }
+
+    public function get($resetAfterwards = true)
+    {
+        try {
+            return $this->flush($resetAfterwards);
+        } catch (\Exception $e) {
+            throw $e;
+        }
+    }
+
+    /**
+     * Echo the contents of the writer
+     * @param  $resetAfterwards Reset the writer contents after flushing?
+     * @return Ansi self, for chaining
+     */
+    public function e($resetAfterwards = true)
+    {
+        try {
+            // Get the contents and echo them
+            echo $this->flush($resetAfterwards);
+
+            // Afford chaining
+            return $this;
+        } catch (\Exception $e) {
+            throw $e;
+        }
+    }
+}

+ 13 - 0
vendor/bramus/ansi-php/src/ControlFunctions/Backspace.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ *
+ */
+namespace Bramus\Ansi\ControlFunctions;
+
+class Backspace extends Base
+{
+    public function __construct()
+    {
+        parent::__construct(Enums\C0::BACKSPACE);
+    }
+}

+ 71 - 0
vendor/bramus/ansi-php/src/ControlFunctions/Base.php

@@ -0,0 +1,71 @@
+<?php
+/**
+ * ANSI Control Function
+ *
+ * An element of a character set that effects the recording, processing,
+ * transmission, or interpretation of data, and that has a coded
+ * representation consisting of one or more bit combinations.
+ *
+ */
+namespace Bramus\Ansi\ControlFunctions;
+
+class Base
+{
+    /**
+     * The Control Character used
+     * @var string
+     */
+    protected $controlCharacter;
+
+    /**
+     * ANSI Control Function
+     * @param string $controlCharacter The Control Character to use
+     */
+    public function __construct($controlCharacter)
+    {
+        // Store the Control Character
+        $this->setControlCharacter($controlCharacter);
+    }
+
+    /**
+     * Set the control character
+     * @param string $controlCharacter The Control Character
+     */
+    public function setControlCharacter($controlCharacter)
+    {
+        // @TODO: Check Validity
+        $this->controlCharacter = $controlCharacter;
+
+        return $this;
+    }
+
+    /**
+     * Build and return the ANSI Code
+     * @return string The ANSI Code
+     */
+    public function get()
+    {
+        return $this->controlCharacter;
+    }
+
+    /**
+     * Return the ANSI Code upon __toString
+     * @return string The ANSI Code
+     */
+    public function __toString()
+    {
+        return $this->get();
+    }
+
+    /**
+     * Echo the ANSI Code
+     */
+    public function e()
+    {
+        echo $this->get();
+
+        return $this;
+    }
+}
+
+// EOF

+ 13 - 0
vendor/bramus/ansi-php/src/ControlFunctions/Bell.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ *
+ */
+namespace Bramus\Ansi\ControlFunctions;
+
+class Bell extends Base
+{
+    public function __construct()
+    {
+        parent::__construct(Enums\C0::BELL);
+    }
+}

+ 13 - 0
vendor/bramus/ansi-php/src/ControlFunctions/CarriageReturn.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ *
+ */
+namespace Bramus\Ansi\ControlFunctions;
+
+class CarriageReturn extends Base
+{
+    public function __construct()
+    {
+        parent::__construct(Enums\C0::CR);
+    }
+}

+ 361 - 0
vendor/bramus/ansi-php/src/ControlFunctions/Enums/C0.php

@@ -0,0 +1,361 @@
+<?php
+/**
+ * The ANSI C0 Set
+ *
+ * Based upon http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf, section 5.2
+ *
+ */
+namespace Bramus\Ansi\ControlFunctions\Enums;
+
+class C0
+{
+    /**
+     * NULL
+     *
+     * NUL is used for media-fill or time-fill. NUL characters may be inserted
+     * into, or removed from, a data stream without affecting the information
+     * content of that stream, but such action may affect the information layout
+     * and/or the control of equipment.
+     *
+     * @var String
+     */
+    const NUL = "\000";
+
+    /**
+     * START OF HEADING
+     *
+     * SOH is used to indicate the beginning of a heading.
+     * The use of SOH is defined in ISO 1745.
+     *
+     * @var String
+     */
+    const SOH = "\001";
+
+    /**
+     * START OF TEXT
+     *
+     * STX is used to indicate the beginning of a text and the end of a heading.
+     * The use of STX is defined in ISO 1745.
+     *
+     * @var String
+     */
+    const STX = "\002";
+
+    /**
+     * END OF TEXT
+     *
+     * ETX is used to indicate the end of a text.
+     * The use of ETX is defined in ISO 1745.
+     *
+     * @var String
+     */
+    const ETX = "\003";
+
+    /**
+     * END OF TRANSMISSION
+     *
+     * EOT is used to indicate the conclusion of the transmission of one or more texts.
+     * The use of EOT is defined in ISO 1745.
+     *
+     * @var String
+     */
+    const EOT = "\004";
+
+    /**
+     * ENQUIRY
+     *
+     * ENQ is transmitted by a sender as a request for a response from a receiver.
+     * The use of ENQ is defined in ISO 1745.
+     *
+     * @var String
+     */
+    const ENQ = "\005";
+
+    /**
+     * ACKNOWLEDGE
+     *
+     * ACK is transmitted by a receiver as an affirmative response to the sender.
+     * The use of ACK is defined in ISO 1745.
+     *
+     * @var String
+     */
+    const ACK = "\006";
+
+    /**
+     * BELL
+     *
+     * BEL is used when there is a need to call for attention; it may control alarm
+     * or attention devices.
+     *
+     * @var string
+     */
+    const BEL = "\007";
+    const BELL = "\007";
+
+    /**
+     * BACKSPACE
+     *
+     * BS causes the active data position to be moved one character position in the
+     * data component in the direction opposite to that of the implicit movement.
+     *
+     * @var string
+     */
+    const BS = "\010";
+    const BACKSPACE = "\010";
+
+    /**
+     * CHARACTER TABULATION (HORIZONTAL TAB)
+     *
+     * HT causes the active presentation position to be moved to the following
+     * character tabulation stop in the presentation component.
+     *
+     * @var string
+     */
+    const HT = "\011";
+    const TAB = "\011";
+
+    /**
+     * LINE FEED
+     *
+     * LF causes the active data position to be moved to the corresponding
+     * character position of the following line in the data component.
+     *
+     * @var string
+     */
+    const LF = "\012";
+
+    /**
+     * LINE TABULATION (VERTICAL TAB)
+     *
+     * VT causes the active presentation position to be moved in the presentation
+     * component to the corresponding character position on the line at which the
+     * following line tabulation stop is set.
+     *
+     * @var String
+     */
+    const VT = "\013";
+
+    /**
+     * FORM FEED
+     *
+     * FF causes the active presentation position to be moved to the corresponding
+     * character position of the line at the page home position of the next form or
+     * page in the presentation component
+     *
+     * @var String
+     */
+    const FF = "\014";
+
+    /**
+     * CARRIAGE RETURN
+     *
+     * CR causes the active data position to be moved to the line home position of
+     * the same line in the data component
+     *
+     * @var string
+     */
+    const CR = "\015";
+
+    /**
+     * LOCKING-SHIFT ONE
+     *
+     * LS1 is used for code extension purposes. It causes the meanings of the bit
+     * combinations following it in the data stream to be changed.
+     * The use of LS1 is defined in Standard ECMA-35.
+     *
+     * @var String
+     */
+    const LS1 = "\016";
+
+    /**
+     * LOCKING-SHIFT ZERO
+     *
+     * LS0 is used for code extension purposes. It causes the meanings of the bit
+     * combinations following it in the data stream to be changed.
+     * The use of LS0 is defined in Standard ECMA-35.
+     *
+     * @var String
+     */
+    const LS0 = "\017";
+
+    /**
+     * DATA LINK ESCAPE
+     *
+     * DLE is used exclusively to provide supplementary transmission control functions.
+     * The use of DLE is defined in ISO 1745.
+     *
+     * @var String
+     */
+    const DLE = "\020";
+
+    /**
+     * DEVICE CONTROL ONE
+     *
+     * DC1 is primarily intended for turning on or starting an ancillary device.
+     * If it is not required for this purpose, it may be used to restore a device to
+     * the basic mode of operation (see also DC2 and DC3), or any other device control
+     * function not provided by other DCs.
+     *
+     * @var String
+     */
+    const DC1 = "\021";
+
+    /**
+     * DEVICE CONTROL TWO
+     *
+     * DC2 is primarily intended for turning on or starting an ancillary device.
+     * If it is not required for this purpose, it may be used to set a device to a
+     * special mode of operation (in which case DC1 is used to restore the device to
+     * the basic mode), or for any other device control function not provided
+     * by other DCs.
+     *
+     * @var String
+     */
+    const DC2 = "\022";
+
+    /**
+     * DEVICE CONTROL THREE
+     *
+     * DC3 is primarily intended for turning off or stopping an ancillary device.
+     * This function may be a secondary level stop, for example wait, pause,
+     * stand-by or halt (in which case DC1 is used to restore normal operation).
+     * If it is not required for this purpose, it may be used for any other device control
+     * function not provided by other DCs.
+     *
+     * @var String
+     */
+    const DC3 = "\023";
+
+    /**
+     * DEVICE CONTROL FOUR
+     *
+     * DC4 is primarily intended for turning off, stopping or interrupting an ancillary
+     * device. If it is not required for this purpose, it may be used for any other device
+     * control function not provided by other DCs
+     *
+     * @var String
+     */
+    const DC4 = "\024";
+
+    /**
+     * NEGATIVE ACKNOWLEDGE
+     *
+     * NAK is transmitted by a receiver as a negative response to the sender.
+     * The use of NAK is defined in ISO 1745.
+     *
+     * @var String
+     */
+    const NAK = "\025";
+
+    /**
+     * SYNCHRONOUS IDLE
+     *
+     * SYN is used by a synchronous transmission system in the absence of any other
+     * character (idle condition) to provide a signal from which synchronism may be
+     * achieved or retained between data terminal equipment.
+     * The use of SYN is defined in ISO 1745.
+     *
+     * @var String
+     */
+    const SYN = "\026";
+
+    /**
+     * END OF TRANSMISSION BLOCK
+     *
+     * ETB is used to indicate the end of a block of data where the data are
+     * divided into such blocks for transmission purposes.
+     * The use of ETB is defined in ISO 1745.
+     *
+     * @var String
+     */
+    const ETB = "\027";
+
+    /**
+     * CANCEL
+     *
+     * CAN is used to indicate that the data preceding it in the data stream is
+     * in error. As a result, this data shall be ignored. The specific meaning
+     * of this control function shall be defined for each application and/or
+     * between sender and recipient.
+     *
+     * @var String
+     */
+    const CAN = "\030";
+    const CANCEL = "\030";
+
+    /**
+     * END OF MEDIUM
+     *
+     * EM is used to identify the physical end of a medium, or the end of the used
+     * portion of a medium, or the end of the wanted portion of data recorded on
+     * a medium.
+     *
+     * @var String
+     */
+    const EM = "\031";
+
+    /**
+     * SUBSTITUTE
+     *
+     * SUB is used in the place of a character that has been found to be invalid
+     * or in error. SUB is intended to be introduced by automatic means.
+     *
+     * @var String
+     */
+    const SUB = "\032";
+
+    /**
+     * ESCAPE
+     *
+     * ESC is used for code extension purposes. It causes the meanings of a limited
+     * number of bit combinations following it in the data stream to be changed.
+     * The use of ESC is defined in Standard ECMA-35.
+     *
+     * @var string
+     */
+    const ESC = "\033";
+
+    /**
+     * INFORMATION SEPARATOR FOUR (FS - FILE SEPARATOR)
+     *
+     * IS4 is used to separate and qualify data logically; its specific meaning has
+     * to be defined for each application. If this control function is used in
+     * hierarchical order, it may delimit a data item called a file.
+     *
+     * @var String
+     */
+    const IS4 = "\034";
+
+    /**
+     * INFORMATION SEPARATOR THREE (GS - GROUP SEPARATOR)
+     *
+     * IS3 is used to separate and qualify data logically; its specific meaning has
+     * to be defined for each application. If this control function is used in
+     * hierarchical order, it may delimit a data item called a group.
+     *
+     * @var String
+     */
+    const IS3 = "\035";
+
+    /**
+     * INFORMATION SEPARATOR TWO (RS - RECORD SEPARATOR)
+     *
+     * IS2 is used to separate and qualify data logically; its specific meaning has
+     * to be defined for each application. If this control function is used in
+     * hierarchical order, it may delimit a data item called a record.
+     *
+     * @var String
+     */
+    const IS2 = "\036";
+
+    /**
+     * INFORMATION SEPARATOR ONE (US - UNIT SEPARATOR)
+     *
+     * IS1 is used to separate and qualify data logically; its specific meaning has
+     * to be defined for each application. If this control function is used in
+     * hierarchical order, it may delimit a data item called a unit.
+     *
+     * @var String
+     */
+    const IS1 = "\037";
+}

+ 13 - 0
vendor/bramus/ansi-php/src/ControlFunctions/Escape.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ *
+ */
+namespace Bramus\Ansi\ControlFunctions;
+
+class Escape extends Base
+{
+    public function __construct()
+    {
+        parent::__construct(Enums\C0::ESC);
+    }
+}

+ 13 - 0
vendor/bramus/ansi-php/src/ControlFunctions/LineFeed.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ *
+ */
+namespace Bramus\Ansi\ControlFunctions;
+
+class LineFeed extends Base
+{
+    public function __construct()
+    {
+        parent::__construct(Enums\C0::LF);
+    }
+}

+ 13 - 0
vendor/bramus/ansi-php/src/ControlFunctions/Tab.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ *
+ */
+namespace Bramus\Ansi\ControlFunctions;
+
+class Tab extends Base
+{
+    public function __construct()
+    {
+        parent::__construct(Enums\C0::TAB);
+    }
+}

+ 105 - 0
vendor/bramus/ansi-php/src/ControlSequences/Base.php

@@ -0,0 +1,105 @@
+<?php
+/**
+ * ANSI Control Sequence
+ *
+ * A string of bit combinations starting with the control
+ * function CONTROL SEQUENCE INTRODUCER (CSI), and used for
+ * the coded representation of control functions with
+ * or without parameters.
+ */
+namespace Bramus\Ansi\ControlSequences;
+
+class Base
+{
+    /**
+     * A ControlFunction that acts as the Control Sequence Introducer (CSI)
+     * @var \Bramus\Ansi\ControlFunction
+     */
+    protected $controlSequenceIntroducer;
+
+    /**
+     * ANSI Control Sequence
+     * @param \Bramus\Ansi\ControlFunction $controlSequenceIntroducer A ControlFunction that acts as the Control Sequence Introducer (CSI)
+     * @param boolean                      $outputNow                 Output the resulting ANSI Code right now?
+     */
+    public function __construct($controlSequenceIntroducer)
+    {
+        // Store datamembers
+        $this->setControlSequenceIntroducer($controlSequenceIntroducer);
+    }
+
+    /**
+     * Set the control sequence introducer
+     * @param  \Bramus\Ansi\ControlFunction $controlSequenceIntroducer A ControlFunction that acts as the Control Sequence Introducer (CSI)
+     * @return ControlSequence              self, for chaining
+     */
+    public function setControlSequenceIntroducer($controlSequenceIntroducer)
+    {
+        // Make sure it's a ControlFunction instance
+        if (is_string($controlSequenceIntroducer)) {
+            $controlSequenceIntroducer = new ControlFunction($controlSequenceIntroducer);
+        }
+
+        // @TODO: Check Validity
+        $this->controlSequenceIntroducer = $controlSequenceIntroducer;
+
+        return $this;
+    }
+
+    /**
+     * Gets the CSI
+     * @return string The CSI
+     */
+    public function getControlSequenceIntroducer()
+    {
+        return $this->controlSequenceIntroducer;
+    }
+
+    /**
+     * Build and return the ANSI Code
+     * @return string The ANSI Code
+     */
+    public function get()
+    {
+        $toReturn = '';
+
+        // Append CSI
+        $toReturn = $this->controlSequenceIntroducer->get().'[';
+
+        // Append Parameter Byte (if any)
+        if (isset($this->parameterBytes) && sizeof((array) $this->parameterBytes) > 0) {
+            $toReturn .= implode(';', $this->parameterBytes);
+        }
+
+        // Append Intermediate Bytes (if any)
+        if (isset($this->intermediateBytes) && sizeof((array) $this->intermediateBytes) > 0) {
+            $toReturn .= implode(';', $this->intermediateBytes); // @TODO: Verify that ';' is the glue for intermediate bytes
+        }
+
+        // Append Final Byte (if any)
+        if (isset($this->finalByte)) {
+            $toReturn .= $this->getFinalByte();
+        }
+
+        return $toReturn;
+    }
+
+    /**
+     * Return the ANSI Code upon __toString
+     * @return string The ANSI Code
+     */
+    public function __toString()
+    {
+        return $this->get();
+    }
+
+    /**
+     * Echo the ANSI Code
+     */
+    public function e()
+    {
+        echo $this->get();
+    }
+}
+
+// EOF

+ 30 - 0
vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/Base.php

@@ -0,0 +1,30 @@
+<?php
+/**
+ * ANSI Escape Sequence
+ *
+ * A string of bit combinations that is used for control purposes in
+ * code extension procedures. The first of these bit combinations
+ * represents the control function ESCAPE.
+ */
+namespace Bramus\Ansi\ControlSequences\EscapeSequences;
+
+class Base extends \Bramus\Ansi\ControlSequences\Base
+{
+    // Escape Sequences have a final byte
+    use \Bramus\Ansi\ControlSequences\Traits\HasFinalByte;
+
+    /**
+     * ANSI Escape Sequence
+     * @param string  $finalByte The Final Byte of the Escape Sequence
+     */
+    public function __construct($finalByte)
+    {
+        // Store the final byte
+        $this->setFinalByte($finalByte);
+
+        // Call Parent Constructor
+        parent::__construct(
+            new \Bramus\Ansi\ControlFunctions\Escape()
+        );
+    }
+}

+ 31 - 0
vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/ED.php

@@ -0,0 +1,31 @@
+<?php
+/**
+ * ED - ERASE DISPLAY (ERASE IN PAGE)
+ *
+ * ED causes some or all character positions of the active page
+ * (the page which contains the active presentation position in the
+ * presentation component) to be put into the erased state, depending
+ * on the parameter values
+ */
+namespace Bramus\Ansi\ControlSequences\EscapeSequences;
+
+class ED extends Base
+{
+    // This EscapeSequence has ParameterByte(s)
+    use \Bramus\Ansi\ControlSequences\Traits\HasParameterBytes;
+
+    /**
+     * ED - ERASE DISPLAY (ERASE IN PAGE)
+     * @param mixed   $parameterBytes The Parameter Bytes
+     */
+    public function __construct($parameterBytes)
+    {
+        // Store the parameter bytes
+        $this->setParameterBytes($parameterBytes);
+
+        // Call Parent Constructor (which will store finalByte)
+        parent::__construct(
+            \Bramus\Ansi\ControlSequences\EscapeSequences\Enums\FinalByte::ED
+        );
+    }
+}

+ 31 - 0
vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/EL.php

@@ -0,0 +1,31 @@
+<?php
+/**
+ * EL - ERASE IN LINE
+ *
+ * EL causes some or all character positions of the active line (the
+ * line which contains the active data position in the data component)
+ * to be put into the erased state, depending on the parameter values
+ *
+ */
+namespace Bramus\Ansi\ControlSequences\EscapeSequences;
+
+class EL extends Base
+{
+    // This EscapeSequence has ParameterByte(s)
+    use \Bramus\Ansi\ControlSequences\Traits\HasParameterBytes;
+
+    /**
+     * EL - ERASE IN LINE
+     * @param mixed   $parameterBytes The Parameter Bytes
+     */
+    public function __construct($parameterBytes)
+    {
+        // Store the parameter bytes
+        $this->setParameterBytes($parameterBytes);
+
+        // Call Parent Constructor (which will store finalByte)
+        parent::__construct(
+            \Bramus\Ansi\ControlSequences\EscapeSequences\Enums\FinalByte::EL
+        );
+    }
+}

+ 26 - 0
vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/Enums/ED.php

@@ -0,0 +1,26 @@
+<?php
+/**
+ * Possible Parameter Byte Values for ED
+ */
+namespace Bramus\Ansi\ControlSequences\EscapeSequences\Enums;
+
+class ED
+{
+    /**
+     * Erases the screen from the current line down to the bottom of the screen.
+     * @type string
+     */
+    const DOWN = '0';
+
+    /**
+     * Erases the screen from the current line up to the top of the screen.
+     * @type string
+     */
+    const UP = '1';
+
+    /**
+     * Erases the screen with the background colour and moves the cursor to home.
+     * @type string
+     */
+    const ALL = '2';
+}

+ 26 - 0
vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/Enums/EL.php

@@ -0,0 +1,26 @@
+<?php
+/**
+ * Possible Parameter Byte Values for EL
+ */
+namespace Bramus\Ansi\ControlSequences\EscapeSequences\Enums;
+
+class EL
+{
+    /**
+     * Erases from the current cursor position to the end of the current line.
+     * @type string
+     */
+    const TO_EOL = '0';
+
+    /**
+     * Erases from the current cursor position to the start of the current line.
+     * @type string
+     */
+    const TO_SOL = '1';
+
+    /**
+     * Erases the entire current line.
+     * @type string
+     */
+    const ALL = '2';
+}

+ 49 - 0
vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/Enums/FinalByte.php

@@ -0,0 +1,49 @@
+<?php
+/**
+ * Possible Final Byte Values for Escape Sequences
+ */
+namespace Bramus\Ansi\ControlSequences\EscapeSequences\Enums;
+
+class FinalByte
+{
+    /**
+     * ED - ERASE DISPLAY (ERASE IN PAGE)
+     *
+     * ED causes some or all character positions of the active page
+     * (the page which contains the active presentation position in the
+     * presentation component) to be put into the erased state, depending
+     * on the parameter values
+     *
+     * @type string
+     */
+    const ED = 'J';
+
+    /**
+     * EL - ERASE IN LINE
+     *
+     * EL causes some or all character positions of the active line (the
+     * line which contains the active data position in the data component)
+     * to be put into the erased state, depending on the parameter values
+     *
+     * @type string
+     */
+    const EL = 'K';
+
+    /**
+     * SGR - SELECT GRAPHIC RENDITION
+     *
+     * SGR is used to establish one or more graphic rendition aspects for
+     * subsequent text. The established aspects remain in effect until the
+     * next occurrence of SGR in the data stream, depending on the setting
+     * of the GRAPHIC RENDITION COMBINATION MODE (GRCM). Each graphic
+     * rendition aspect is specified by a parameter value
+     *
+     * @type string
+     */
+    const SGR = 'm';
+
+    // @TODO: Add more Escape Code Final Bytes
+    // @see http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf
+    // @see http://www.termsys.demon.co.uk/vtansi.htm
+    // @see http://en.wikipedia.org/wiki/ANSI_escape_code
+}

+ 385 - 0
vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/Enums/SGR.php

@@ -0,0 +1,385 @@
+<?php
+/**
+ * Possible Parameter Byte Values for SGR
+ */
+namespace Bramus\Ansi\ControlSequences\EscapeSequences\Enums;
+
+class SGR
+{
+    /**
+     * Default rendition, cancels the effect of any preceding occurrence of SGR in the data stream
+     * @type string
+     */
+    const STYLE_NONE = '0';
+
+    /**
+     * Bold: On ~or~ increased intensity
+     * @type string
+     */
+    const STYLE_INTENSITY_BRIGHT = '1';
+
+    /**
+     * Bold: On ~or~ increased intensity
+     * @type string
+     */
+    const STYLE_BOLD = '1';
+
+    /**
+     * Faint, decreased intensity
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_INTENSITY_FAINT = '2';
+
+    /**
+     * Italic: On
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_ITALIC = '3';
+
+    /**
+     * Underline: On
+     * @type string
+     */
+    const STYLE_UNDERLINE = '4';
+
+    /**
+     * Blink: On
+     * @type string
+     */
+    const STYLE_BLINK = '5';
+
+    /**
+     * Blink (Rapid): On
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_BLINK_RAPID = '6';
+
+    /**
+     * Inverse or reverse colors (viz. swap foreground and background)
+     * @type string
+     */
+    const STYLE_NEGATIVE = '7';
+
+    /**
+     * Conceal (Hide) text
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_CONCEAL = '8';
+
+    /**
+     * Cross-out / strikethrough: On
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_STRIKETHROUGH = '9';
+
+    /**
+     * Bold: Off ~or~ normal intensity
+     * @type string
+     */
+    const STYLE_INTENSITY_NORMAL = '22';
+
+    /**
+     * Bold: Off ~or~ normal intensity
+     * @type string
+     */
+    const STYLE_BOLD_OFF = '22';
+
+    /**
+     * Italic: Off
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_ITALIC_OFF = '23';
+
+    /**
+     * Underline: Off
+     * @type string
+     */
+    const STYLE_UNDERLINE_OFF = '24';
+
+    /**
+     * Blink: Off (Steady)
+     * @type string
+     */
+    const STYLE_STEADY = '5';
+    const STYLE_BLINK_OFF = '5';
+
+    /**
+     * Positive Image (viz. don't swap foreground and background)
+     * @type string
+     */
+    const STYLE_POSITIVE = '27';
+
+    /**
+     * Revealed Charcters (inverse of CONCEAL)
+     * @type string
+     */
+    const STYLE_REVEAL = '28';
+
+    /**
+     * Strikethrough: Off
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_STRIKETHROUGH_OFF = '29';
+
+    /**
+     * Black Foreground Color
+     * @type string
+     */
+    const COLOR_FG_BLACK = '30';
+
+    /**
+     * Red Foreground Color
+     * @type string
+     */
+    const COLOR_FG_RED = '31';
+
+    /**
+     * Green Foreground Color
+     * @type string
+     */
+    const COLOR_FG_GREEN = '32';
+
+    /**
+     * Yellow Foreground Color
+     * @type string
+     */
+    const COLOR_FG_YELLOW = '33';
+
+    /**
+     * Blue Foreground Color
+     * @type string
+     */
+    const COLOR_FG_BLUE = '34';
+
+    /**
+     * Purple Foreground Color
+     * @type string
+     */
+    const COLOR_FG_PURPLE = '35';
+
+    /**
+     * Cyan Foreground Color
+     * @type string
+     */
+    const COLOR_FG_CYAN = '36';
+
+    /**
+     * White Foreground Color
+     * @type string
+     */
+    const COLOR_FG_WHITE = '37';
+
+    /**
+     * Default Foreground Color
+     * @type string
+     */
+    const COLOR_FG_RESET = '39';
+
+    /**
+     * Black Background Color
+     * @type string
+     */
+    const COLOR_BG_BLACK = '40';
+
+    /**
+     * Red Background Color
+     * @type string
+     */
+    const COLOR_BG_RED = '41';
+
+    /**
+     * Green Background Color
+     * @type string
+     */
+    const COLOR_BG_GREEN = '42';
+
+    /**
+     * Yellow Background Color
+     * @type string
+     */
+    const COLOR_BG_YELLOW = '43';
+
+    /**
+     * Blue Background Color
+     * @type string
+     */
+    const COLOR_BG_BLUE = '44';
+
+    /**
+     * Purple Background Color
+     * @type string
+     */
+    const COLOR_BG_PURPLE = '45';
+
+    /**
+     * Cyan Background Color
+     * @type string
+     */
+    const COLOR_BG_CYAN = '46';
+
+    /**
+     * White Background Color
+     * @type string
+     */
+    const COLOR_BG_WHITE = '47';
+
+    /**
+     * Default Background Color
+     * @type string
+     */
+    const COLOR_BG_RESET = '49';
+
+    /**
+     * Framed: On
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_FRAMED = '51';
+
+    /**
+     * Encircled
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_ENCIRCLED = '52';
+
+    /**
+     * Overlined: On
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_OVERLINED = '53';
+
+    /**
+     * Framed: Off
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_FRAMED_ENCIRCLED_OFF = '54';
+
+    /**
+     * Overlined: Off
+     * @note Not widely supported
+     * @type string
+     */
+    const STYLE_OVERLINED_OFF = '55';
+
+    /**
+     * Black Foreground Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_FG_BLACK_BRIGHT = '90';
+
+    /**
+     * Red Foreground Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_FG_RED_BRIGHT = '91';
+
+    /**
+     * Green Foreground Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_FG_GREEN_BRIGHT = '92';
+
+    /**
+     * Yellow Foreground Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_FG_YELLOW_BRIGHT = '93';
+
+    /**
+     * Blue Foreground Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_FG_BLUE_BRIGHT = '94';
+
+    /**
+     * Purple Foreground Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_FG_PURPLE_BRIGHT = '95';
+
+    /**
+     * Bright Foreground Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_FG_CYAN_BRIGHT = '96';
+
+    /**
+     * White Foreground Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_FG_WHITE_BRIGHT = '97';
+
+    /**
+     * Black Background Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_BG_BLACK_BRIGHT = '100';
+
+    /**
+     * Red Background Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_BG_RED_BRIGHT = '101';
+
+    /**
+     * Green Background Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_BG_GREEN_BRIGHT = '102';
+
+    /**
+     * Yellow Background Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_BG_YELLOW_BRIGHT = '103';
+
+    /**
+     * Blue Background Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_BG_BLUE_BRIGHT = '104';
+
+    /**
+     * Purple Background Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_BG_PURPLE_BRIGHT = '105';
+
+    /**
+     * Cyan Background Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_BG_CYAN_BRIGHT = '106';
+
+    /**
+     * White Background Color (High Intensity)
+     * @note Not part of the ANSI standard
+     * @type string
+     */
+    const COLOR_BG_WHITE_BRIGHT = '107';
+}

+ 37 - 0
vendor/bramus/ansi-php/src/ControlSequences/EscapeSequences/SGR.php

@@ -0,0 +1,37 @@
+<?php
+/**
+ * SGR - SELECT GRAPHIC RENDITION
+ *
+ * SGR is used to establish one or more graphic rendition aspects for
+ * subsequent text. The established aspects remain in effect until the
+ * next occurrence of SGR in the data stream, depending on the setting
+ * of the GRAPHIC RENDITION COMBINATION MODE (GRCM). Each graphic
+ * rendition aspect is specified by a parameter value
+ */
+namespace Bramus\Ansi\ControlSequences\EscapeSequences;
+
+class SGR extends Base
+{
+    // This EscapeSequence has ParameterByte(s)
+    use \Bramus\Ansi\ControlSequences\Traits\HasParameterBytes;
+
+    /**
+     * SGR - SELECT GRAPHIC RENDITION
+     * @param mixed   $parameterBytes The Parameter Bytes
+     */
+    public function __construct($parameterBytes = null)
+    {
+        // Make sure we have parameter bytes
+        if (!$parameterBytes) {
+            $parameterBytes = array(Enums\SGR::STYLE_NONE);
+        }
+
+        // Store the parameter bytes
+        $this->setParameterBytes($parameterBytes);
+
+        // Call Parent Constructor (which will store finalByte)
+        parent::__construct(
+            \Bramus\Ansi\ControlSequences\EscapeSequences\Enums\FinalByte::SGR
+        );
+    }
+}

+ 34 - 0
vendor/bramus/ansi-php/src/ControlSequences/Traits/HasFinalByte.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Bramus\Ansi\ControlSequences\Traits;
+
+trait HasFinalByte
+{
+    /**
+     * Final Byte: The bit combination that terminates an escape sequence or a control sequence.
+     * @var string
+     */
+    protected $finalByte;
+
+    /**
+     * Set the finalByte
+     * @param  string $finalByte The bit combination that terminates an escape sequence or a control sequence.
+     * @return Base   self, for chaining
+     */
+    public function setFinalByte($finalByte)
+    {
+        // @TODO Verify Validity
+        $this->finalByte = $finalByte;
+
+        return $this;
+    }
+
+    /**
+     * Get the Final Byte
+     * @return string
+     */
+    public function getFinalByte()
+    {
+        return $this->finalByte;
+    }
+}

+ 52 - 0
vendor/bramus/ansi-php/src/ControlSequences/Traits/HasIntermediateBytes.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Bramus\Ansi\ControlSequences\Traits;
+
+trait HasIntermediateBytes
+{
+    /**
+     * Intermediate Byte: In a control sequence, a bit combination that may occur between the ControlFunction CSI and the Final Byte, or between a Parameter Byte and the Final Byte.
+     * @var array
+     */
+    protected $intermediateBytes = array();
+
+    /**
+     * Add a Intermediate Byte
+     * @param  string $intermediateByte The byte to add
+     * @return Base   self, for chaining
+     */
+    public function addIntermediateByte($intermediateByte)
+    {
+        $this->intermediateBytes[] = (string) $intermediateByte;
+
+        return $this;
+    }
+
+    /**
+     * Set the Intermediate Byte
+     * @param  array $parameterByte The byte to add
+     * @return Base  self, for chaining
+     */
+    public function setIntermediateBytes($intermediateBytes)
+    {
+        foreach ((array) $intermediateBytes as $byte) {
+            $this->addIntermediateByte($byte);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get the Intermediate Byte
+     * @param  bool $asString As a string, or as an array?
+     * @return Base self, for chaining
+     */
+    public function getIntermediateBytes($asString = true)
+    {
+        if ($asString === true) {
+            return implode($this->intermediateBytes);
+        } else {
+            return $this->intermediateBytes;
+        }
+    }
+}

+ 52 - 0
vendor/bramus/ansi-php/src/ControlSequences/Traits/HasParameterBytes.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Bramus\Ansi\ControlSequences\Traits;
+
+trait HasParameterBytes
+{
+    /**
+     * Parameter Byte: In a control sequence, a bit combination that may occur between the ControlFunction CSI and the Final Byte, or between CSI and an Intermediate Byte.
+     * @var array
+     */
+    protected $parameterBytes = array();
+
+    /**
+     * Add a Parameter Byte
+     * @param  string $parameterByte The byte to add
+     * @return Base   self, for chaining
+     */
+    public function addParameterByte($parameterByte)
+    {
+        $this->parameterBytes[] = (string) $parameterByte;
+
+        return $this;
+    }
+
+    /**
+     * Set the Parameter Byte
+     * @param  array $parameterByte The byte to add
+     * @return Base  self, for chaining
+     */
+    public function setParameterBytes($parameterBytes)
+    {
+        foreach ((array) $parameterBytes as $byte) {
+            $this->addParameterByte($byte);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get the Parameter Byte
+     * @param  bool $asString As a string, or as an array?
+     * @return Base self, for chaining
+     */
+    public function getParameterBytes($asString = true)
+    {
+        if ($asString === true) {
+            return implode($this->parameterBytes);
+        } else {
+            return $this->parameterBytes;
+        }
+    }
+}

+ 74 - 0
vendor/bramus/ansi-php/src/Traits/ControlFunctions.php

@@ -0,0 +1,74 @@
+<?php
+
+namespace Bramus\Ansi\Traits;
+
+/**
+ * Trait containing the Control Function Shorthands
+ */
+trait ControlFunctions
+{
+    /**
+     * Add a Bell Control Character to the buffer / echo it on screen
+     * @return Ansi self, for chaining
+     */
+    public function bell()
+    {
+        // Write character onto writer
+        $this->writer->write(new \Bramus\Ansi\ControlFunctions\Bell());
+
+        // Afford chaining
+        return $this;
+    }
+
+    /**
+     * Add a Backspace Control Character to the buffer / echo it on screen
+     * @return Ansi self, for chaining
+     */
+    public function backspace()
+    {
+        // Write character onto writer
+        $this->writer->write(new \Bramus\Ansi\ControlFunctions\Backspace());
+
+        // Afford chaining
+        return $this;
+    }
+
+    /**
+     * Add a Tab Control Character to the buffer / echo it on screen
+     * @return Ansi self, for chaining
+     */
+    public function tab()
+    {
+        // Write character onto writer
+        $this->writer->write(new \Bramus\Ansi\ControlFunctions\Tab());
+
+        // Afford chaining
+        return $this;
+    }
+
+    /**
+     * Add a Line Feed Control Character to the buffer / echo it on screen
+     * @return Ansi self, for chaining
+     */
+    public function lf()
+    {
+        // Write character onto writer
+        $this->writer->write(new \Bramus\Ansi\ControlFunctions\LineFeed());
+
+        // Afford chaining
+        return $this;
+    }
+
+    /**
+     * Add a Carriage Return Control Character to the buffer / echo it on screen
+     * @return Ansi self, for chaining
+     */
+    public function cr()
+    {
+        // Write character onto writer
+        $this->writer->write(new \Bramus\Ansi\ControlFunctions\CarriageReturn());
+
+        // Afford chaining
+        return $this;
+    }
+}

+ 54 - 0
vendor/bramus/ansi-php/src/Traits/EscapeSequences/ED.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace Bramus\Ansi\Traits\EscapeSequences;
+
+use Bramus\Ansi\ControlSequences\EscapeSequences\Enums\ED as EnumED;
+
+/**
+ * Trait containing the ED Escape Function Shorthands
+ */
+trait ED
+{
+    /**
+     * Manually use ED (Select Graphic Rendition)
+     * @param  array $parameterByte Parameter byte to the SGR Escape Code
+     * @return Ansi  self, for chaining
+     */
+    public function ed($data)
+    {
+        // Write data to the writer
+        $this->writer->write(
+            new \Bramus\Ansi\ControlSequences\EscapeSequences\ED($data)
+        );
+
+        // Afford chaining
+        return $this;
+    }
+
+    /**
+     * Erase the screen from the current line up to the top of the screen
+     * @return Ansi self, for chaining
+     */
+    public function eraseDisplayUp()
+    {
+        return $this->ed(EnumED::UP);
+    }
+
+    /**
+     * Erase the screen from the current line down to the bottom of the screen
+     * @return Ansi self, for chaining
+     */
+    public function eraseDisplayDown()
+    {
+        return $this->ed(EnumED::DOWN);
+    }
+
+    /**
+     * Erase the entire screen and moves the cursor to home
+     * @return Ansi self, for chaining
+     */
+    public function eraseDisplay()
+    {
+        return $this->ed(EnumED::ALL);
+    }
+}

+ 57 - 0
vendor/bramus/ansi-php/src/Traits/EscapeSequences/EL.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace Bramus\Ansi\Traits\EscapeSequences;
+
+use Bramus\Ansi\ControlSequences\EscapeSequences\Enums\EL as EnumEL;
+
+/**
+ * Trait containing the EL Escape Function Shorthands
+ */
+trait EL
+{
+    /**
+     * Manually use EL (ERASE IN LINE)
+     * @param  array $parameterByte Parameter byte to the EL Escape Code
+     * @return Ansi  self, for chaining
+     */
+    public function el($data)
+    {
+        // Write data to the writer
+        $this->writer->write(
+            new \Bramus\Ansi\ControlSequences\EscapeSequences\EL($data)
+        );
+
+        // Afford chaining
+        return $this;
+    }
+
+    /**
+     * Erase from the current cursor position to the end of the current line.
+     * @param  boolean $outputNow Echo the character right now, or add it to the buffer building?
+     * @return Ansi    self, for chaining
+     */
+    public function eraseLineToEOL()
+    {
+        return $this->el(EnumEL::TO_EOL);
+    }
+
+    /**
+     * Erases from the current cursor position to the start of the current line.
+     * @param  boolean $outputNow Echo the character right now, or add it to the buffer building?
+     * @return Ansi    self, for chaining
+     */
+    public function eraseLineToSOL()
+    {
+        return $this->el(EnumEL::TO_SOL);
+    }
+
+    /**
+     * Erase the entire current line.
+     * @param  boolean $outputNow Echo the character right now, or add it to the buffer building?
+     * @return Ansi    self, for chaining
+     */
+    public function eraseLine()
+    {
+        return $this->el(EnumEL::ALL);
+    }
+}

+ 136 - 0
vendor/bramus/ansi-php/src/Traits/EscapeSequences/SGR.php

@@ -0,0 +1,136 @@
+<?php
+
+namespace Bramus\Ansi\Traits\EscapeSequences;
+
+use Bramus\Ansi\ControlSequences\EscapeSequences\Enums\SGR as EnumSGR;
+
+/**
+ * Trait containing the SGR Escape Function Shorthands
+ */
+trait SGR
+{
+    /**
+     * Manually use SGR (Select Graphic Rendition)
+     * @param  array $parameterByte Parameter byte to the SGR Escape Code
+     * @return Ansi  self, for chaining
+     */
+    public function sgr($data = array())
+    {
+        // Write data to the writer
+        $this->writer->write(
+            new \Bramus\Ansi\ControlSequences\EscapeSequences\SGR($data)
+        );
+
+        // Afford chaining
+        return $this;
+    }
+
+    /**
+     * Shorthand to remove all text styling (colors, bold, etc)
+     * @return Ansi self, for chaining
+     */
+    public function nostyle()
+    {
+        return $this->sgr(array(EnumSGR::STYLE_NONE));
+    }
+
+    /**
+     * Shorthand to remove all text styling (colors, bold, etc)
+     * @return Ansi self, for chaining
+     */
+    public function reset()
+    {
+        return $this->nostyle();
+    }
+
+    /**
+     * Shorthand to set the color.
+     * @param  array $color The color you want to set. Use an array filled with ControlSequences\EscapeSequences\Enums\SGR::COLOR_* constants
+     * @return Ansi  self, for chaining
+     */
+    public function color($color = array())
+    {
+        return $this->sgr($color);
+    }
+
+    /**
+     * Shorthand to set make text styling to bold (on some systems bright intensity)
+     * @return Ansi self, for chaining
+     */
+    public function bold()
+    {
+        return $this->sgr(array(EnumSGR::STYLE_BOLD));
+    }
+
+    /**
+     * Shorthand to set the text intensity to bright (on some systems bold)
+     * @return Ansi self, for chaining
+     */
+    public function bright()
+    {
+        return $this->sgr(array(EnumSGR::STYLE_INTENSITY_BRIGHT));
+    }
+
+    /**
+     * Shorthand to set the text styling to normal (no bold/bright)
+     * @return Ansi self, for chaining
+     */
+    public function normal()
+    {
+        return $this->sgr(array(EnumSGR::STYLE_INTENSITY_NORMAL));
+    }
+
+    /**
+     * (Not widely supported) Shorthand to set the text intensity to faint
+     * @return Ansi self, for chaining
+     */
+    public function faint()
+    {
+        return $this->sgr(array(EnumSGR::STYLE_INTENSITY_FAINT));
+    }
+
+    /**
+     * (Not widely supported) Shorthand to set the text styling to italic
+     * @return Ansi self, for chaining
+     */
+    public function italic()
+    {
+        return $this->sgr(array(EnumSGR::STYLE_ITALIC));
+    }
+
+    /**
+     * Shorthand to set the text styling to underline
+     * @return Ansi self, for chaining
+     */
+    public function underline()
+    {
+        return $this->sgr(array(EnumSGR::STYLE_UNDERLINE));
+    }
+
+    /**
+     * Shorthand to set the text styling to blink
+     * @return Ansi self, for chaining
+     */
+    public function blink()
+    {
+        return $this->sgr(array(EnumSGR::STYLE_BLINK));
+    }
+
+    /**
+     * Shorthand to set the text styling to reserved (viz. swap background & foreground color)
+     * @return Ansi self, for chaining
+     */
+    public function negative()
+    {
+        return $this->sgr(array(EnumSGR::STYLE_NEGATIVE));
+    }
+
+    /**
+     * (Not widely supported) Shorthand to set the text styling to strikethrough
+     * @return Ansi self, for chaining
+     */
+    public function strikethrough()
+    {
+        return $this->sgr(array(EnumSGR::STYLE_STRIKETHROUGH));
+    }
+}

+ 61 - 0
vendor/bramus/ansi-php/src/Writers/BufferWriter.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Bramus\Ansi\Writers;
+
+/**
+ * Buffers data for another WriterInterface until asked to flush the buffer to it
+ */
+class BufferWriter implements WriterInterface, FlushableInterface
+{
+    /**
+     * The buffer that holds the data
+     * @var string
+     */
+    private $buffer = '';
+
+    /**
+     * Write Data
+     * @param  string          $data Data to write
+     * @return WriterInterface Self, for chaining
+     */
+    public function write($data)
+    {
+        // Add data to the buffer
+        $this->buffer .= $data;
+
+        // Afford chaining
+        return $this;
+    }
+
+    /**
+     * Get/Flush the data
+     * @param  boolean $resetAfterwards Reset the data afterwards?
+     * @return string  The data
+     */
+    public function flush($resetAfterwards = true)
+    {
+        // Get buffer contents
+        $buffer = $this->buffer;
+
+        // Clear buffer contents
+        if ($resetAfterwards) {
+            $this->clear();
+        }
+
+        // Return data that was flushed
+        return $buffer;
+    }
+
+    /**
+     * Reset/Clear the buffer
+     * @return BufferedStreamWriter self, for chaining
+     */
+    public function clear()
+    {
+        // Clear the buffer
+        $this->buffer = '';
+
+        // Afford chaining
+        return $this;
+    }
+}

+ 19 - 0
vendor/bramus/ansi-php/src/Writers/FlushableInterface.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Bramus\Ansi\Writers;
+
+interface FlushableInterface
+{
+    /**
+     * Get/Flush the data
+     * @param  boolean $resetAfterwards Reset the data afterwards?
+     * @return string  The data
+     */
+    public function flush($resetAfterwards = true);
+
+    /**
+     * Reset/Clear the buffer
+     * @return BufferedStreamWriter self, for chaining
+     */
+    public function clear();
+}

+ 60 - 0
vendor/bramus/ansi-php/src/Writers/ProxyWriter.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace Bramus\Ansi\Writers;
+
+/**
+ * Writer that acts as a proxy to write to another Writer
+ */
+class ProxyWriter extends BufferWriter
+{
+    /**
+     * The writer to proxy for
+     * @var WriterInterface
+     */
+    private $writer;
+
+    /**
+     * ProxyWriter — Writer that acts as a proxy to write to another Writer
+     * @param WriterInterface $writer The writer to proxy for
+     */
+    public function __construct(WriterInterface $writer)
+    {
+        // Store writer
+        $this->setWriter($writer);
+    }
+
+    /**
+     * Set the writer to proxy for
+     * @param WriterInterface $writer The writer to proxy for
+     */
+    public function setWriter(WriterInterface $writer)
+    {
+        $this->writer = $writer;
+    }
+
+    /**
+     * Get the writer we are proxying for
+     * @return WriterInterface The writer we are proxying for
+     */
+    public function getWriter()
+    {
+        return $this->writer;
+    }
+
+    /**
+     * Get/Flush the data
+     * @param  boolean $resetAfterwards Reset the data afterwards?
+     * @return string  The data
+     */
+    public function flush($resetAfterwards = true)
+    {
+        // Get the data from the buffer
+        $data = parent::flush($resetAfterwards);
+
+        // Write the data to the writer we are proxying for
+        $this->writer->write($data);
+
+        // Return the data
+        return $data;
+    }
+}

+ 78 - 0
vendor/bramus/ansi-php/src/Writers/StreamWriter.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace Bramus\Ansi\Writers;
+
+/**
+ * Writes data to a stream
+ */
+class StreamWriter implements WriterInterface
+{
+    /**
+     * Stream to write to
+     * @var resource
+     */
+    private $stream;
+
+    /**
+     * Constructor
+     * @param mixed $stream Stream to write to (Default: php://stdout)
+     */
+    public function __construct($stream = 'php://stdout')
+    {
+        $this->setStream($stream);
+    }
+
+    /**
+     * Destructor
+     */
+    public function __destruct()
+    {
+        @fclose($this->stream);
+    }
+
+    /**
+     * Set the stream to write to
+     * @param mixed $stream Stream to write to
+     */
+    public function setStream($stream = null)
+    {
+        // String passed in? Try converting it to a stream
+        if (is_string($stream)) {
+            $stream = @fopen($stream, 'a');
+        }
+
+        // Make sure the stream is a resource
+        if (!is_resource($stream)) {
+            throw new \InvalidArgumentException('Invalid Stream');
+        }
+
+        // Store it
+        $this->stream = $stream;
+
+        // Afford chaining
+        return $this;
+    }
+
+    /**
+     * Get stream we are writing to
+     * @return resource Stream we are writing to
+     */
+    public function getStream()
+    {
+        return $this->stream;
+    }
+
+    /**
+     * Write Data
+     * @param  string          $data Data to write
+     * @return WriterInterface Self, for chaining
+     */
+    public function write($data)
+    {
+        // Write data on the stream
+        fwrite($this->stream, $data);
+
+        // Afford chaining
+        return $this;
+    }
+}

+ 16 - 0
vendor/bramus/ansi-php/src/Writers/WriterInterface.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Bramus\Ansi\Writers;
+
+/**
+ * Writer Interface
+ */
+interface WriterInterface
+{
+    /**
+     * Write Data
+     * @param  string          $data Data to write
+     * @return WriterInterface Self, for chaining
+     */
+    public function write($data);
+}

+ 45 - 0
vendor/bramus/ansi-php/tests/AnsiTest.php

@@ -0,0 +1,45 @@
+<?php
+
+use \Bramus\Ansi\Ansi;
+use \Bramus\Ansi\Writers\StreamWriter;
+use \Bramus\Ansi\Writers\BufferWriter;
+use \Bramus\Ansi\ControlFunctions\Enums\C0;
+
+/**
+ * Test the Ansi Class and its core functions
+ */
+class AnsiTest extends PHPUnit_Framework_TestCase
+{
+
+    public function testInstantiation()
+    {
+
+        // Create Ansi Instance (using default writer)
+        $a = new Ansi();
+        $this->assertInstanceOf('\Bramus\Ansi\Ansi', $a);
+        $this->assertInstanceOf('\Bramus\Ansi\Writers\StreamWriter', $a->getWriter());
+
+        // Create Ansi Instance (using custom writer)
+        $a = new Ansi(new BufferWriter());
+        $this->assertInstanceOf('\Bramus\Ansi\Ansi', $a);
+        $this->assertInstanceOf('\Bramus\Ansi\Writers\BufferWriter', $a->getWriter());
+
+    }
+
+    public function testFunctions()
+    {
+
+    }
+
+    public function testChaining()
+    {
+        $a = new Ansi(new BufferWriter());
+        $test = $a->text('foo')->text('bar')->get();
+
+        $this->assertEquals(
+            $test,
+            'foobar'
+        );
+    }
+
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików