├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── commands └── GithubController.php ├── components └── Github.php ├── composer.json ├── composer.lock ├── config.php ├── controllers ├── IssuesController.php └── SiteController.php ├── phpunit.xml.dist ├── tests ├── GithubControllerTest.php ├── GithubTest.php ├── IssuesControllerTest.php ├── TestCase.php ├── bootstrap.php └── mocks │ ├── CachedHttpClientMock.php │ ├── GithubControllerMock.php │ ├── HooksMock.php │ ├── RepoMock.php │ └── sleep.php ├── web ├── index.php └── robots.txt └── yii /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /config.local.php 3 | /tmp 4 | /logs 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # faster builds on new travis setup not using sudo 2 | sudo: false 3 | language: php 4 | 5 | php: 6 | - 5.5 7 | - 5.6 8 | - 7.0 9 | - 7.1 10 | - nightly 11 | 12 | matrix: 13 | fast_finish: true 14 | allow_failures: 15 | - php: nightly 16 | 17 | # cache vendor dirs 18 | cache: 19 | directories: 20 | - vendor 21 | - $HOME/.composer/cache 22 | 23 | install: 24 | - travis_retry composer self-update && composer --version 25 | - travis_retry composer install --prefer-dist --no-interaction 26 | 27 | script: 28 | # validate composer.json 29 | - composer validate 30 | # run PHPUnit 31 | - vendor/bin/phpunit --verbose --coverage-text 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Carsten Brandt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-bot 2 | 3 | This is a bot that helps with automated comments on github issues. 4 | 5 | ## Features 6 | 7 | - Reply to issues when a label is added. E.g. add a comment to tell user to use forum instead when an issues does not contain a bug report or feature request. 8 | - move issues to other repositories, if the issue is specific to an extension. 9 | This action will be triggered by adding a label to an issue which is not closed (allowing to edit labels after an issue has been closed). 10 | 11 | ## Installation 12 | 13 | - deploy this repo on a webserver. 14 | - run `composer install` 15 | - create a `config.local.php` and override things from `config.php` there. 16 | - run `./yii github/register` to install webhooks in the repos. 17 | - The bot user needs Admin privilege to do this, this can be removed after registering the hooks, so it only needs Write privilege for normal actions. Alternatively you can specify the `--noAdmin` option for running the command, which allows you to specify a github auth token for a user that has admin privileges. 18 | - register can safely run multiple times, it does check whether a hook already exists and updates the hook instead of adding duplicates. 19 | 20 | ## Uninstall 21 | 22 | - run `./yii github/un-register` to remove webhooks from the repos 23 | 24 | -------------------------------------------------------------------------------- /commands/GithubController.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class GithubController extends Controller 18 | { 19 | /** 20 | * @var bool specify this flag if the bot github user has no admin privileges for managing hooks. 21 | * You can enter your github name and password instead. 22 | */ 23 | public $noAdmin = false; 24 | 25 | 26 | public function options($actionID) 27 | { 28 | return array_merge(parent::options($actionID), ['noAdmin']); 29 | } 30 | 31 | public function init() 32 | { 33 | parent::init(); 34 | if (empty(Yii::$app->params['hook_secret'])) { 35 | throw new Exception('Config param "hook_secret" is not configured!'); 36 | } 37 | } 38 | 39 | public function hooks() 40 | { 41 | $base = rtrim(Yii::$app->params['webUrl'], '/'); 42 | return [ 43 | // register for issues events and send them to issues controller 44 | // also send pull request events to issues controller 45 | 'issues,pull_request' => $base . '/index.php?r=issues', 46 | ]; 47 | } 48 | 49 | 50 | /** 51 | * Check on which repos events are registered on the github API. 52 | * 53 | * @param array $limitRepos limit this call to a certain set of repos. This is a comma separated list of repos. 54 | * The default is to run against all configured repos. 55 | */ 56 | public function actionStatus(array $limitRepos = []) 57 | { 58 | $client = $this->getClient(); 59 | 60 | $repositories = Yii::$app->params['repositories']; 61 | if (!empty($limitRepos)) { 62 | $repositories = array_intersect($limitRepos, $repositories); 63 | } 64 | 65 | // check hooks: 66 | foreach($repositories as $urepo) { 67 | foreach($this->hooks() as $hookName => $hookUrl) { 68 | 69 | $this->stdout("checking "); 70 | $this->stdout("$hookName", Console::BOLD); 71 | $this->stdout(" hook on "); 72 | $this->stdout($urepo, Console::BOLD); 73 | $this->stdout('...'); 74 | list($user, $repo) = explode('/', $urepo); 75 | 76 | // https://developer.github.com/v3/repos/hooks/#create-a-hook 77 | $api = Yii::createObject(\Github\Api\Repo::class, [$client]); 78 | 79 | // check if hook exists 80 | $hookId = null; 81 | foreach ($api->hooks()->all($user, $repo) as $hook) { 82 | if ($hook['name'] === 'web' && isset($hook['config']['url']) && $hook['config']['url'] === $hookUrl) { 83 | $hookId = $hook['id']; 84 | break; 85 | } 86 | } 87 | if ($hookId) { 88 | $this->stdout("registered.\n", Console::BOLD, Console::FG_GREEN); 89 | } else { 90 | $this->stdout("not registered.\n", Console::BOLD, Console::FG_RED); 91 | } 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Register for events on the github API. 98 | * 99 | * The bot user needs Admin privilege to do this. The privilege can be removed 100 | * after register action has run, it only needs Write privilege for normal actions. 101 | * 102 | * Register can safely run multiple times, it does check whether a hook already exists 103 | * and updates the hook instead of adding duplicates. 104 | * 105 | * @param array $limitRepos limit this call to a certain set of repos. This is a comma separated list of repos. 106 | * The default is to run against all configured repos. 107 | */ 108 | public function actionRegister(array $limitRepos = []) 109 | { 110 | $client = $this->getClient(); 111 | 112 | $repositories = Yii::$app->params['repositories']; 113 | if (!empty($limitRepos)) { 114 | $repositories = array_intersect($limitRepos, $repositories); 115 | } 116 | 117 | // create hooks: 118 | foreach($repositories as $urepo) { 119 | foreach($this->hooks() as $hookName => $hookUrl) { 120 | 121 | $this->stdout("registering "); 122 | $this->stdout("$hookName", Console::BOLD); 123 | $this->stdout(" hook on "); 124 | $this->stdout($urepo, Console::BOLD); 125 | $this->stdout('...'); 126 | list($user, $repo) = explode('/', $urepo); 127 | 128 | // https://developer.github.com/v3/repos/hooks/#create-a-hook 129 | $api = Yii::createObject(\Github\Api\Repo::class, [$client]); 130 | 131 | // check if hook exists 132 | $hookId = null; 133 | foreach ($api->hooks()->all($user, $repo) as $hook) { 134 | if ($hook['name'] === 'web' && isset($hook['config']['url']) && $hook['config']['url'] === $hookUrl) { 135 | $this->stdout("already registered, updating..."); 136 | $hookId = $hook['id']; 137 | break; 138 | } 139 | } 140 | 141 | $params = [ 142 | 'name' => 'web', 143 | 'config' => [ 144 | 'url' => $hookUrl, 145 | 'content_type' => 'json', 146 | 'secret' => Yii::$app->params['hook_secret'], 147 | ], 148 | 'events' => explode(',', $hookName), 149 | 'active' => true, 150 | ]; 151 | if ($hookId) { 152 | $response = $api->hooks()->update($user, $repo, $hookId, $params); 153 | //print_r($response); 154 | $this->stdout("updated.\n", Console::FG_GREEN, Console::BOLD); 155 | } else { 156 | $response = $api->hooks()->create($user, $repo, $params); 157 | //print_r($response); 158 | $this->stdout("added.\n", Console::FG_GREEN, Console::BOLD); 159 | } 160 | } 161 | } 162 | } 163 | 164 | public function actionUnRegister(array $limitRepos = []) 165 | { 166 | $client = $this->getClient(); 167 | 168 | $repositories = Yii::$app->params['repositories']; 169 | if (!empty($limitRepos)) { 170 | $repositories = array_intersect($limitRepos, $repositories); 171 | } 172 | 173 | // remove hooks: 174 | foreach($repositories as $urepo) { 175 | foreach($this->hooks() as $hookName => $hookUrl) { 176 | 177 | $this->stdout("un-registering "); 178 | $this->stdout("$hookName", Console::BOLD); 179 | $this->stdout(" hook on "); 180 | $this->stdout($urepo, Console::BOLD); 181 | $this->stdout('...'); 182 | list($user, $repo) = explode('/', $urepo); 183 | 184 | // https://developer.github.com/v3/repos/hooks/#create-a-hook 185 | $api = new Repo($client); 186 | 187 | // check if hook exists 188 | foreach ($api->hooks()->all($user, $repo) as $hook) { 189 | if ($hook['name'] === 'web' && isset($hook['config']['url']) && $hook['config']['url'] === $hookUrl) { 190 | $api->hooks()->remove($user, $repo, $hook['id']); 191 | break; 192 | } 193 | } 194 | $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); 195 | } 196 | } 197 | } 198 | 199 | /** 200 | * @return Client 201 | */ 202 | private function getClient() 203 | { 204 | $token = null; 205 | if ($this->noAdmin) { 206 | $this->stdout("Please go to https://github.com/settings/tokens and generate a token.\n\n"); 207 | $this->stdout("Required permissions: "); 208 | $this->stdout("admin:org, admin:repo_hook", Console::BOLD); 209 | $this->stdout("\n\n"); 210 | 211 | $token = $this->prompt('Token? '); 212 | } 213 | 214 | /** @var $client \Github\Client */ 215 | $client = Yii::$app->github->client($token); 216 | 217 | return $client; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /components/Github.php: -------------------------------------------------------------------------------- 1 | github`. 14 | * 15 | * @author Carsten Brandt 16 | */ 17 | class Github extends Component 18 | { 19 | /** 20 | * @return \Github\Client 21 | */ 22 | public function client($token = null) 23 | { 24 | // create client 25 | $client = Yii::createObject(\Github\HttpClient\CachedHttpClient::class); 26 | $client->setCache(new \Github\HttpClient\Cache\FilesystemCache(__DIR__ . '/../tmp/github-cache')); 27 | $client = new \Github\Client($client); 28 | 29 | if (empty(Yii::$app->params['github_token'])) { 30 | throw new Exception('Config param "github_token" is not configured!'); 31 | } 32 | if (empty(Yii::$app->params['github_username'])) { 33 | throw new Exception('Config param "github_username" is not configured!'); 34 | } 35 | 36 | // authenticate 37 | $client->authenticate($token ?: Yii::$app->params['github_token'], '', \Github\Client::AUTH_HTTP_TOKEN); 38 | 39 | return $client; 40 | } 41 | 42 | public function verifyRequest($body) 43 | { 44 | // 'sha1='+OpenSSL::HMAC.hexdigest(HMAC_DIGEST, secret, body) 45 | // https://github.com/github/github-services/blob/f3bb3dd780feb6318c42b2db064ed6d481b70a1f/lib/service/http_helper.rb#L77 46 | if (empty(Yii::$app->params['hook_secret'])) { 47 | throw new Exception('Config param "hook_secret" is not configured!'); 48 | } 49 | $secret = Yii::$app->params['hook_secret']; 50 | $signHeader = \Yii::$app->request->headers->get('X-Hub-Signature'); 51 | if (!$signHeader || strpos($signHeader, '=') === false) { 52 | throw new BadRequestHttpException('X-Hub-Signature header is missing.'); 53 | } 54 | list($algo, $hash) = explode('=', $signHeader, 2); 55 | if (!in_array($algo, ['sha1', 'sha256', 'sha384', 'sha512'])) { 56 | throw new BadRequestHttpException('Unknown algorithm in X-Hub-Signature header.'); 57 | } 58 | 59 | $oldAlgo = Yii::$app->security->macHash; 60 | Yii::$app->security->macHash = $algo; 61 | if (Yii::$app->security->validateData($hash.$body, $secret) === false) { 62 | throw new BadRequestHttpException('Unable to validate submitted data.'); 63 | } 64 | Yii::$app->security->macHash = $oldAlgo; 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft-contrib/github-bot", 3 | "description": "This is a github bot that helps with managing issues, used on Yii Framework Github Issues.", 4 | "type": "project", 5 | "license": "MIT", 6 | "provide": { 7 | "bower-asset/jquery": "*", 8 | "bower-asset/jquery.inputmask": "*", 9 | "bower-asset/punycode": "*", 10 | "bower-asset/yii2-pjax": "*" 11 | }, 12 | "require": { 13 | "php": ">=5.5", 14 | "knplabs/github-api": "~1.5", 15 | "yiisoft/yii2": "~2.0.6", 16 | "yiisoft/yii2-swiftmailer": "~2.0.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "~4.0", 20 | "yiisoft/yii2-shell": "~2.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "1c745ea6f6000a2990598187bbf2f3d4", 8 | "content-hash": "985c8140e8ff876715e27db3c08b4b4a", 9 | "packages": [ 10 | { 11 | "name": "bower-asset/jquery", 12 | "version": "2.1.4", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/jquery/jquery-dist.git", 16 | "reference": "7751e69b615c6eca6f783a81e292a55725af6b85" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/7751e69b615c6eca6f783a81e292a55725af6b85", 21 | "reference": "7751e69b615c6eca6f783a81e292a55725af6b85", 22 | "shasum": "" 23 | }, 24 | "require-dev": { 25 | "bower-asset/qunit": "1.14.0", 26 | "bower-asset/requirejs": "2.1.10", 27 | "bower-asset/sinon": "1.8.1", 28 | "bower-asset/sizzle": "2.1.1-patch2" 29 | }, 30 | "type": "bower-asset-library", 31 | "extra": { 32 | "bower-asset-main": "dist/jquery.js", 33 | "bower-asset-ignore": [ 34 | "**/.*", 35 | "build", 36 | "dist/cdn", 37 | "speed", 38 | "test", 39 | "*.md", 40 | "AUTHORS.txt", 41 | "Gruntfile.js", 42 | "package.json" 43 | ] 44 | }, 45 | "license": [ 46 | "MIT" 47 | ], 48 | "keywords": [ 49 | "javascript", 50 | "jquery", 51 | "library" 52 | ] 53 | }, 54 | { 55 | "name": "bower-asset/jquery.inputmask", 56 | "version": "3.1.63", 57 | "source": { 58 | "type": "git", 59 | "url": "https://github.com/RobinHerbots/jquery.inputmask.git", 60 | "reference": "c40c7287eadc31e341ebbf0c02352eb55b9cbc48" 61 | }, 62 | "dist": { 63 | "type": "zip", 64 | "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/c40c7287eadc31e341ebbf0c02352eb55b9cbc48", 65 | "reference": "c40c7287eadc31e341ebbf0c02352eb55b9cbc48", 66 | "shasum": "" 67 | }, 68 | "require": { 69 | "bower-asset/jquery": ">=1.7" 70 | }, 71 | "type": "bower-asset-library", 72 | "extra": { 73 | "bower-asset-main": [ 74 | "./dist/inputmask/jquery.inputmask.js", 75 | "./dist/inputmask/jquery.inputmask.extensions.js", 76 | "./dist/inputmask/jquery.inputmask.date.extensions.js", 77 | "./dist/inputmask/jquery.inputmask.numeric.extensions.js", 78 | "./dist/inputmask/jquery.inputmask.phone.extensions.js", 79 | "./dist/inputmask/jquery.inputmask.regex.extensions.js" 80 | ], 81 | "bower-asset-ignore": [ 82 | "**/.*", 83 | "qunit/", 84 | "nuget/", 85 | "tools/", 86 | "js/", 87 | "*.md", 88 | "build.properties", 89 | "build.xml", 90 | "jquery.inputmask.jquery.json" 91 | ] 92 | }, 93 | "license": [ 94 | "http://opensource.org/licenses/mit-license.php" 95 | ], 96 | "description": "jquery.inputmask is a jquery plugin which create an input mask.", 97 | "keywords": [ 98 | "form", 99 | "input", 100 | "inputmask", 101 | "jquery", 102 | "mask", 103 | "plugins" 104 | ] 105 | }, 106 | { 107 | "name": "bower-asset/punycode", 108 | "version": "v1.3.2", 109 | "source": { 110 | "type": "git", 111 | "url": "https://github.com/bestiejs/punycode.js.git", 112 | "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" 113 | }, 114 | "dist": { 115 | "type": "zip", 116 | "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", 117 | "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", 118 | "shasum": "" 119 | }, 120 | "type": "bower-asset-library", 121 | "extra": { 122 | "bower-asset-main": "punycode.js", 123 | "bower-asset-ignore": [ 124 | "coverage", 125 | "tests", 126 | ".*", 127 | "component.json", 128 | "Gruntfile.js", 129 | "node_modules", 130 | "package.json" 131 | ] 132 | } 133 | }, 134 | { 135 | "name": "bower-asset/yii2-pjax", 136 | "version": "v2.0.5", 137 | "source": { 138 | "type": "git", 139 | "url": "https://github.com/yiisoft/jquery-pjax.git", 140 | "reference": "6818718408086db6bdcf33649cecb86b6b4f9b67" 141 | }, 142 | "dist": { 143 | "type": "zip", 144 | "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/6818718408086db6bdcf33649cecb86b6b4f9b67", 145 | "reference": "6818718408086db6bdcf33649cecb86b6b4f9b67", 146 | "shasum": "" 147 | }, 148 | "require": { 149 | "bower-asset/jquery": ">=1.8" 150 | }, 151 | "type": "bower-asset-library", 152 | "extra": { 153 | "bower-asset-main": "./jquery.pjax.js", 154 | "bower-asset-ignore": [ 155 | ".travis.yml", 156 | "Gemfile", 157 | "Gemfile.lock", 158 | "vendor/", 159 | "script/", 160 | "test/" 161 | ] 162 | }, 163 | "license": [ 164 | "MIT" 165 | ] 166 | }, 167 | { 168 | "name": "cebe/markdown", 169 | "version": "1.1.1", 170 | "source": { 171 | "type": "git", 172 | "url": "https://github.com/cebe/markdown.git", 173 | "reference": "c30eb5e01fe021cc5bba2f9ee0eeef96d4931166" 174 | }, 175 | "dist": { 176 | "type": "zip", 177 | "url": "https://api.github.com/repos/cebe/markdown/zipball/c30eb5e01fe021cc5bba2f9ee0eeef96d4931166", 178 | "reference": "c30eb5e01fe021cc5bba2f9ee0eeef96d4931166", 179 | "shasum": "" 180 | }, 181 | "require": { 182 | "lib-pcre": "*", 183 | "php": ">=5.4.0" 184 | }, 185 | "require-dev": { 186 | "cebe/indent": "*", 187 | "facebook/xhprof": "*@dev", 188 | "phpunit/phpunit": "4.1.*" 189 | }, 190 | "bin": [ 191 | "bin/markdown" 192 | ], 193 | "type": "library", 194 | "extra": { 195 | "branch-alias": { 196 | "dev-master": "1.1.x-dev" 197 | } 198 | }, 199 | "autoload": { 200 | "psr-4": { 201 | "cebe\\markdown\\": "" 202 | } 203 | }, 204 | "notification-url": "https://packagist.org/downloads/", 205 | "license": [ 206 | "MIT" 207 | ], 208 | "authors": [ 209 | { 210 | "name": "Carsten Brandt", 211 | "email": "mail@cebe.cc", 212 | "homepage": "http://cebe.cc/", 213 | "role": "Creator" 214 | } 215 | ], 216 | "description": "A super fast, highly extensible markdown parser for PHP", 217 | "homepage": "https://github.com/cebe/markdown#readme", 218 | "keywords": [ 219 | "extensible", 220 | "fast", 221 | "gfm", 222 | "markdown", 223 | "markdown-extra" 224 | ], 225 | "time": "2016-09-14 20:40:20" 226 | }, 227 | { 228 | "name": "ezyang/htmlpurifier", 229 | "version": "v4.6.0", 230 | "source": { 231 | "type": "git", 232 | "url": "https://github.com/ezyang/htmlpurifier.git", 233 | "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd" 234 | }, 235 | "dist": { 236 | "type": "zip", 237 | "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/6f389f0f25b90d0b495308efcfa073981177f0fd", 238 | "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd", 239 | "shasum": "" 240 | }, 241 | "require": { 242 | "php": ">=5.2" 243 | }, 244 | "type": "library", 245 | "autoload": { 246 | "psr-0": { 247 | "HTMLPurifier": "library/" 248 | }, 249 | "files": [ 250 | "library/HTMLPurifier.composer.php" 251 | ] 252 | }, 253 | "notification-url": "https://packagist.org/downloads/", 254 | "license": [ 255 | "LGPL" 256 | ], 257 | "authors": [ 258 | { 259 | "name": "Edward Z. Yang", 260 | "email": "admin@htmlpurifier.org", 261 | "homepage": "http://ezyang.com" 262 | } 263 | ], 264 | "description": "Standards compliant HTML filter written in PHP", 265 | "homepage": "http://htmlpurifier.org/", 266 | "keywords": [ 267 | "html" 268 | ], 269 | "time": "2013-11-30 08:25:19" 270 | }, 271 | { 272 | "name": "guzzle/guzzle", 273 | "version": "v3.9.3", 274 | "source": { 275 | "type": "git", 276 | "url": "https://github.com/guzzle/guzzle3.git", 277 | "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" 278 | }, 279 | "dist": { 280 | "type": "zip", 281 | "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", 282 | "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", 283 | "shasum": "" 284 | }, 285 | "require": { 286 | "ext-curl": "*", 287 | "php": ">=5.3.3", 288 | "symfony/event-dispatcher": "~2.1" 289 | }, 290 | "replace": { 291 | "guzzle/batch": "self.version", 292 | "guzzle/cache": "self.version", 293 | "guzzle/common": "self.version", 294 | "guzzle/http": "self.version", 295 | "guzzle/inflection": "self.version", 296 | "guzzle/iterator": "self.version", 297 | "guzzle/log": "self.version", 298 | "guzzle/parser": "self.version", 299 | "guzzle/plugin": "self.version", 300 | "guzzle/plugin-async": "self.version", 301 | "guzzle/plugin-backoff": "self.version", 302 | "guzzle/plugin-cache": "self.version", 303 | "guzzle/plugin-cookie": "self.version", 304 | "guzzle/plugin-curlauth": "self.version", 305 | "guzzle/plugin-error-response": "self.version", 306 | "guzzle/plugin-history": "self.version", 307 | "guzzle/plugin-log": "self.version", 308 | "guzzle/plugin-md5": "self.version", 309 | "guzzle/plugin-mock": "self.version", 310 | "guzzle/plugin-oauth": "self.version", 311 | "guzzle/service": "self.version", 312 | "guzzle/stream": "self.version" 313 | }, 314 | "require-dev": { 315 | "doctrine/cache": "~1.3", 316 | "monolog/monolog": "~1.0", 317 | "phpunit/phpunit": "3.7.*", 318 | "psr/log": "~1.0", 319 | "symfony/class-loader": "~2.1", 320 | "zendframework/zend-cache": "2.*,<2.3", 321 | "zendframework/zend-log": "2.*,<2.3" 322 | }, 323 | "suggest": { 324 | "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." 325 | }, 326 | "type": "library", 327 | "extra": { 328 | "branch-alias": { 329 | "dev-master": "3.9-dev" 330 | } 331 | }, 332 | "autoload": { 333 | "psr-0": { 334 | "Guzzle": "src/", 335 | "Guzzle\\Tests": "tests/" 336 | } 337 | }, 338 | "notification-url": "https://packagist.org/downloads/", 339 | "license": [ 340 | "MIT" 341 | ], 342 | "authors": [ 343 | { 344 | "name": "Michael Dowling", 345 | "email": "mtdowling@gmail.com", 346 | "homepage": "https://github.com/mtdowling" 347 | }, 348 | { 349 | "name": "Guzzle Community", 350 | "homepage": "https://github.com/guzzle/guzzle/contributors" 351 | } 352 | ], 353 | "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", 354 | "homepage": "http://guzzlephp.org/", 355 | "keywords": [ 356 | "client", 357 | "curl", 358 | "framework", 359 | "http", 360 | "http client", 361 | "rest", 362 | "web service" 363 | ], 364 | "abandoned": "guzzlehttp/guzzle", 365 | "time": "2015-03-18 18:23:50" 366 | }, 367 | { 368 | "name": "knplabs/github-api", 369 | "version": "1.7.1", 370 | "source": { 371 | "type": "git", 372 | "url": "https://github.com/KnpLabs/php-github-api.git", 373 | "reference": "98d0bcd2c4c96a40ded9081f8f6289907f73823c" 374 | }, 375 | "dist": { 376 | "type": "zip", 377 | "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/98d0bcd2c4c96a40ded9081f8f6289907f73823c", 378 | "reference": "98d0bcd2c4c96a40ded9081f8f6289907f73823c", 379 | "shasum": "" 380 | }, 381 | "require": { 382 | "ext-curl": "*", 383 | "guzzle/guzzle": "~3.7", 384 | "php": ">=5.3.2" 385 | }, 386 | "require-dev": { 387 | "phpunit/phpunit": "~4.0", 388 | "sllh/php-cs-fixer-styleci-bridge": "~1.3" 389 | }, 390 | "suggest": { 391 | "knplabs/gaufrette": "Needed for optional Gaufrette cache" 392 | }, 393 | "type": "library", 394 | "extra": { 395 | "branch-alias": { 396 | "dev-master": "1.8.x-dev" 397 | } 398 | }, 399 | "autoload": { 400 | "psr-4": { 401 | "Github\\": "lib/Github/" 402 | } 403 | }, 404 | "notification-url": "https://packagist.org/downloads/", 405 | "license": [ 406 | "MIT" 407 | ], 408 | "authors": [ 409 | { 410 | "name": "Thibault Duplessis", 411 | "email": "thibault.duplessis@gmail.com", 412 | "homepage": "http://ornicar.github.com" 413 | }, 414 | { 415 | "name": "KnpLabs Team", 416 | "homepage": "http://knplabs.com" 417 | } 418 | ], 419 | "description": "GitHub API v3 client", 420 | "homepage": "https://github.com/KnpLabs/php-github-api", 421 | "keywords": [ 422 | "api", 423 | "gh", 424 | "gist", 425 | "github" 426 | ], 427 | "time": "2016-07-26 08:49:38" 428 | }, 429 | { 430 | "name": "swiftmailer/swiftmailer", 431 | "version": "v5.4.3", 432 | "source": { 433 | "type": "git", 434 | "url": "https://github.com/swiftmailer/swiftmailer.git", 435 | "reference": "4cc92842069c2bbc1f28daaaf1d2576ec4dfe153" 436 | }, 437 | "dist": { 438 | "type": "zip", 439 | "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/4cc92842069c2bbc1f28daaaf1d2576ec4dfe153", 440 | "reference": "4cc92842069c2bbc1f28daaaf1d2576ec4dfe153", 441 | "shasum": "" 442 | }, 443 | "require": { 444 | "php": ">=5.3.3" 445 | }, 446 | "require-dev": { 447 | "mockery/mockery": "~0.9.1" 448 | }, 449 | "type": "library", 450 | "extra": { 451 | "branch-alias": { 452 | "dev-master": "5.4-dev" 453 | } 454 | }, 455 | "autoload": { 456 | "files": [ 457 | "lib/swift_required.php" 458 | ] 459 | }, 460 | "notification-url": "https://packagist.org/downloads/", 461 | "license": [ 462 | "MIT" 463 | ], 464 | "authors": [ 465 | { 466 | "name": "Chris Corbyn" 467 | }, 468 | { 469 | "name": "Fabien Potencier", 470 | "email": "fabien@symfony.com" 471 | } 472 | ], 473 | "description": "Swiftmailer, free feature-rich PHP mailer", 474 | "homepage": "http://swiftmailer.org", 475 | "keywords": [ 476 | "email", 477 | "mail", 478 | "mailer" 479 | ], 480 | "time": "2016-07-08 11:51:25" 481 | }, 482 | { 483 | "name": "symfony/event-dispatcher", 484 | "version": "v2.8.14", 485 | "source": { 486 | "type": "git", 487 | "url": "https://github.com/symfony/event-dispatcher.git", 488 | "reference": "25c576abd4e0f212e678fe8b2bd9a9a98c7ea934" 489 | }, 490 | "dist": { 491 | "type": "zip", 492 | "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/25c576abd4e0f212e678fe8b2bd9a9a98c7ea934", 493 | "reference": "25c576abd4e0f212e678fe8b2bd9a9a98c7ea934", 494 | "shasum": "" 495 | }, 496 | "require": { 497 | "php": ">=5.3.9" 498 | }, 499 | "require-dev": { 500 | "psr/log": "~1.0", 501 | "symfony/config": "~2.0,>=2.0.5|~3.0.0", 502 | "symfony/dependency-injection": "~2.6|~3.0.0", 503 | "symfony/expression-language": "~2.6|~3.0.0", 504 | "symfony/stopwatch": "~2.3|~3.0.0" 505 | }, 506 | "suggest": { 507 | "symfony/dependency-injection": "", 508 | "symfony/http-kernel": "" 509 | }, 510 | "type": "library", 511 | "extra": { 512 | "branch-alias": { 513 | "dev-master": "2.8-dev" 514 | } 515 | }, 516 | "autoload": { 517 | "psr-4": { 518 | "Symfony\\Component\\EventDispatcher\\": "" 519 | }, 520 | "exclude-from-classmap": [ 521 | "/Tests/" 522 | ] 523 | }, 524 | "notification-url": "https://packagist.org/downloads/", 525 | "license": [ 526 | "MIT" 527 | ], 528 | "authors": [ 529 | { 530 | "name": "Fabien Potencier", 531 | "email": "fabien@symfony.com" 532 | }, 533 | { 534 | "name": "Symfony Community", 535 | "homepage": "https://symfony.com/contributors" 536 | } 537 | ], 538 | "description": "Symfony EventDispatcher Component", 539 | "homepage": "https://symfony.com", 540 | "time": "2016-10-13 01:43:15" 541 | }, 542 | { 543 | "name": "yiisoft/yii2", 544 | "version": "2.0.6", 545 | "source": { 546 | "type": "git", 547 | "url": "https://github.com/yiisoft/yii2-framework.git", 548 | "reference": "f42b2eb80f61992438661b01d0d74c6738e2ff38" 549 | }, 550 | "dist": { 551 | "type": "zip", 552 | "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/f42b2eb80f61992438661b01d0d74c6738e2ff38", 553 | "reference": "f42b2eb80f61992438661b01d0d74c6738e2ff38", 554 | "shasum": "" 555 | }, 556 | "require": { 557 | "bower-asset/jquery": "2.1.*@stable | 1.11.*@stable", 558 | "bower-asset/jquery.inputmask": "3.1.*", 559 | "bower-asset/punycode": "1.3.*", 560 | "bower-asset/yii2-pjax": ">=2.0.1", 561 | "cebe/markdown": "~1.0.0 | ~1.1.0", 562 | "ext-mbstring": "*", 563 | "ezyang/htmlpurifier": "4.6.*", 564 | "lib-pcre": "*", 565 | "php": ">=5.4.0", 566 | "yiisoft/yii2-composer": "*" 567 | }, 568 | "bin": [ 569 | "yii" 570 | ], 571 | "type": "library", 572 | "extra": { 573 | "branch-alias": { 574 | "dev-master": "2.0.x-dev" 575 | } 576 | }, 577 | "autoload": { 578 | "psr-4": { 579 | "yii\\": "" 580 | } 581 | }, 582 | "notification-url": "https://packagist.org/downloads/", 583 | "license": [ 584 | "BSD-3-Clause" 585 | ], 586 | "authors": [ 587 | { 588 | "name": "Qiang Xue", 589 | "email": "qiang.xue@gmail.com", 590 | "homepage": "http://www.yiiframework.com/", 591 | "role": "Founder and project lead" 592 | }, 593 | { 594 | "name": "Alexander Makarov", 595 | "email": "sam@rmcreative.ru", 596 | "homepage": "http://rmcreative.ru/", 597 | "role": "Core framework development" 598 | }, 599 | { 600 | "name": "Maurizio Domba", 601 | "homepage": "http://mdomba.info/", 602 | "role": "Core framework development" 603 | }, 604 | { 605 | "name": "Carsten Brandt", 606 | "email": "mail@cebe.cc", 607 | "homepage": "http://cebe.cc/", 608 | "role": "Core framework development" 609 | }, 610 | { 611 | "name": "Timur Ruziev", 612 | "email": "resurtm@gmail.com", 613 | "homepage": "http://resurtm.com/", 614 | "role": "Core framework development" 615 | }, 616 | { 617 | "name": "Paul Klimov", 618 | "email": "klimov.paul@gmail.com", 619 | "role": "Core framework development" 620 | } 621 | ], 622 | "description": "Yii PHP Framework Version 2", 623 | "homepage": "http://www.yiiframework.com/", 624 | "keywords": [ 625 | "framework", 626 | "yii2" 627 | ], 628 | "time": "2015-08-05 22:00:30" 629 | }, 630 | { 631 | "name": "yiisoft/yii2-composer", 632 | "version": "2.0.4", 633 | "source": { 634 | "type": "git", 635 | "url": "https://github.com/yiisoft/yii2-composer.git", 636 | "reference": "7452fd908a5023b8bb5ea1b123a174ca080de464" 637 | }, 638 | "dist": { 639 | "type": "zip", 640 | "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/7452fd908a5023b8bb5ea1b123a174ca080de464", 641 | "reference": "7452fd908a5023b8bb5ea1b123a174ca080de464", 642 | "shasum": "" 643 | }, 644 | "require": { 645 | "composer-plugin-api": "^1.0" 646 | }, 647 | "type": "composer-plugin", 648 | "extra": { 649 | "class": "yii\\composer\\Plugin", 650 | "branch-alias": { 651 | "dev-master": "2.0.x-dev" 652 | } 653 | }, 654 | "autoload": { 655 | "psr-4": { 656 | "yii\\composer\\": "" 657 | } 658 | }, 659 | "notification-url": "https://packagist.org/downloads/", 660 | "license": [ 661 | "BSD-3-Clause" 662 | ], 663 | "authors": [ 664 | { 665 | "name": "Qiang Xue", 666 | "email": "qiang.xue@gmail.com" 667 | } 668 | ], 669 | "description": "The composer plugin for Yii extension installer", 670 | "keywords": [ 671 | "composer", 672 | "extension installer", 673 | "yii2" 674 | ], 675 | "time": "2016-02-06 00:49:24" 676 | }, 677 | { 678 | "name": "yiisoft/yii2-swiftmailer", 679 | "version": "2.0.6", 680 | "source": { 681 | "type": "git", 682 | "url": "https://github.com/yiisoft/yii2-swiftmailer.git", 683 | "reference": "26b900767f1031ff3a4668dfa36c10595875f0a5" 684 | }, 685 | "dist": { 686 | "type": "zip", 687 | "url": "https://api.github.com/repos/yiisoft/yii2-swiftmailer/zipball/26b900767f1031ff3a4668dfa36c10595875f0a5", 688 | "reference": "26b900767f1031ff3a4668dfa36c10595875f0a5", 689 | "shasum": "" 690 | }, 691 | "require": { 692 | "swiftmailer/swiftmailer": "~5.0", 693 | "yiisoft/yii2": "~2.0.4" 694 | }, 695 | "type": "yii2-extension", 696 | "extra": { 697 | "branch-alias": { 698 | "dev-master": "2.0.x-dev" 699 | } 700 | }, 701 | "autoload": { 702 | "psr-4": { 703 | "yii\\swiftmailer\\": "" 704 | } 705 | }, 706 | "notification-url": "https://packagist.org/downloads/", 707 | "license": [ 708 | "BSD-3-Clause" 709 | ], 710 | "authors": [ 711 | { 712 | "name": "Paul Klimov", 713 | "email": "klimov.paul@gmail.com" 714 | } 715 | ], 716 | "description": "The SwiftMailer integration for the Yii framework", 717 | "keywords": [ 718 | "email", 719 | "mail", 720 | "mailer", 721 | "swift", 722 | "swiftmailer", 723 | "yii2" 724 | ], 725 | "time": "2016-09-09 11:48:11" 726 | } 727 | ], 728 | "packages-dev": [ 729 | { 730 | "name": "dnoegel/php-xdg-base-dir", 731 | "version": "0.1", 732 | "source": { 733 | "type": "git", 734 | "url": "https://github.com/dnoegel/php-xdg-base-dir.git", 735 | "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a" 736 | }, 737 | "dist": { 738 | "type": "zip", 739 | "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/265b8593498b997dc2d31e75b89f053b5cc9621a", 740 | "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a", 741 | "shasum": "" 742 | }, 743 | "require": { 744 | "php": ">=5.3.2" 745 | }, 746 | "require-dev": { 747 | "phpunit/phpunit": "@stable" 748 | }, 749 | "type": "project", 750 | "autoload": { 751 | "psr-4": { 752 | "XdgBaseDir\\": "src/" 753 | } 754 | }, 755 | "notification-url": "https://packagist.org/downloads/", 756 | "license": [ 757 | "MIT" 758 | ], 759 | "description": "implementation of xdg base directory specification for php", 760 | "time": "2014-10-24 07:27:01" 761 | }, 762 | { 763 | "name": "doctrine/instantiator", 764 | "version": "1.0.5", 765 | "source": { 766 | "type": "git", 767 | "url": "https://github.com/doctrine/instantiator.git", 768 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" 769 | }, 770 | "dist": { 771 | "type": "zip", 772 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", 773 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", 774 | "shasum": "" 775 | }, 776 | "require": { 777 | "php": ">=5.3,<8.0-DEV" 778 | }, 779 | "require-dev": { 780 | "athletic/athletic": "~0.1.8", 781 | "ext-pdo": "*", 782 | "ext-phar": "*", 783 | "phpunit/phpunit": "~4.0", 784 | "squizlabs/php_codesniffer": "~2.0" 785 | }, 786 | "type": "library", 787 | "extra": { 788 | "branch-alias": { 789 | "dev-master": "1.0.x-dev" 790 | } 791 | }, 792 | "autoload": { 793 | "psr-4": { 794 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 795 | } 796 | }, 797 | "notification-url": "https://packagist.org/downloads/", 798 | "license": [ 799 | "MIT" 800 | ], 801 | "authors": [ 802 | { 803 | "name": "Marco Pivetta", 804 | "email": "ocramius@gmail.com", 805 | "homepage": "http://ocramius.github.com/" 806 | } 807 | ], 808 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 809 | "homepage": "https://github.com/doctrine/instantiator", 810 | "keywords": [ 811 | "constructor", 812 | "instantiate" 813 | ], 814 | "time": "2015-06-14 21:17:01" 815 | }, 816 | { 817 | "name": "jakub-onderka/php-console-color", 818 | "version": "0.1", 819 | "source": { 820 | "type": "git", 821 | "url": "https://github.com/JakubOnderka/PHP-Console-Color.git", 822 | "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1" 823 | }, 824 | "dist": { 825 | "type": "zip", 826 | "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/e0b393dacf7703fc36a4efc3df1435485197e6c1", 827 | "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1", 828 | "shasum": "" 829 | }, 830 | "require": { 831 | "php": ">=5.3.2" 832 | }, 833 | "require-dev": { 834 | "jakub-onderka/php-code-style": "1.0", 835 | "jakub-onderka/php-parallel-lint": "0.*", 836 | "jakub-onderka/php-var-dump-check": "0.*", 837 | "phpunit/phpunit": "3.7.*", 838 | "squizlabs/php_codesniffer": "1.*" 839 | }, 840 | "type": "library", 841 | "autoload": { 842 | "psr-0": { 843 | "JakubOnderka\\PhpConsoleColor": "src/" 844 | } 845 | }, 846 | "notification-url": "https://packagist.org/downloads/", 847 | "license": [ 848 | "BSD-2-Clause" 849 | ], 850 | "authors": [ 851 | { 852 | "name": "Jakub Onderka", 853 | "email": "jakub.onderka@gmail.com", 854 | "homepage": "http://www.acci.cz" 855 | } 856 | ], 857 | "time": "2014-04-08 15:00:19" 858 | }, 859 | { 860 | "name": "jakub-onderka/php-console-highlighter", 861 | "version": "v0.3.2", 862 | "source": { 863 | "type": "git", 864 | "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git", 865 | "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5" 866 | }, 867 | "dist": { 868 | "type": "zip", 869 | "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/7daa75df45242c8d5b75a22c00a201e7954e4fb5", 870 | "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5", 871 | "shasum": "" 872 | }, 873 | "require": { 874 | "jakub-onderka/php-console-color": "~0.1", 875 | "php": ">=5.3.0" 876 | }, 877 | "require-dev": { 878 | "jakub-onderka/php-code-style": "~1.0", 879 | "jakub-onderka/php-parallel-lint": "~0.5", 880 | "jakub-onderka/php-var-dump-check": "~0.1", 881 | "phpunit/phpunit": "~4.0", 882 | "squizlabs/php_codesniffer": "~1.5" 883 | }, 884 | "type": "library", 885 | "autoload": { 886 | "psr-0": { 887 | "JakubOnderka\\PhpConsoleHighlighter": "src/" 888 | } 889 | }, 890 | "notification-url": "https://packagist.org/downloads/", 891 | "license": [ 892 | "MIT" 893 | ], 894 | "authors": [ 895 | { 896 | "name": "Jakub Onderka", 897 | "email": "acci@acci.cz", 898 | "homepage": "http://www.acci.cz/" 899 | } 900 | ], 901 | "time": "2015-04-20 18:58:01" 902 | }, 903 | { 904 | "name": "nikic/php-parser", 905 | "version": "v2.1.1", 906 | "source": { 907 | "type": "git", 908 | "url": "https://github.com/nikic/PHP-Parser.git", 909 | "reference": "4dd659edadffdc2143e4753df655d866dbfeedf0" 910 | }, 911 | "dist": { 912 | "type": "zip", 913 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4dd659edadffdc2143e4753df655d866dbfeedf0", 914 | "reference": "4dd659edadffdc2143e4753df655d866dbfeedf0", 915 | "shasum": "" 916 | }, 917 | "require": { 918 | "ext-tokenizer": "*", 919 | "php": ">=5.4" 920 | }, 921 | "require-dev": { 922 | "phpunit/phpunit": "~4.0" 923 | }, 924 | "bin": [ 925 | "bin/php-parse" 926 | ], 927 | "type": "library", 928 | "extra": { 929 | "branch-alias": { 930 | "dev-master": "2.1-dev" 931 | } 932 | }, 933 | "autoload": { 934 | "psr-4": { 935 | "PhpParser\\": "lib/PhpParser" 936 | } 937 | }, 938 | "notification-url": "https://packagist.org/downloads/", 939 | "license": [ 940 | "BSD-3-Clause" 941 | ], 942 | "authors": [ 943 | { 944 | "name": "Nikita Popov" 945 | } 946 | ], 947 | "description": "A PHP parser written in PHP", 948 | "keywords": [ 949 | "parser", 950 | "php" 951 | ], 952 | "time": "2016-09-16 12:04:44" 953 | }, 954 | { 955 | "name": "phpdocumentor/reflection-common", 956 | "version": "1.0", 957 | "source": { 958 | "type": "git", 959 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 960 | "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" 961 | }, 962 | "dist": { 963 | "type": "zip", 964 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", 965 | "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", 966 | "shasum": "" 967 | }, 968 | "require": { 969 | "php": ">=5.5" 970 | }, 971 | "require-dev": { 972 | "phpunit/phpunit": "^4.6" 973 | }, 974 | "type": "library", 975 | "extra": { 976 | "branch-alias": { 977 | "dev-master": "1.0.x-dev" 978 | } 979 | }, 980 | "autoload": { 981 | "psr-4": { 982 | "phpDocumentor\\Reflection\\": [ 983 | "src" 984 | ] 985 | } 986 | }, 987 | "notification-url": "https://packagist.org/downloads/", 988 | "license": [ 989 | "MIT" 990 | ], 991 | "authors": [ 992 | { 993 | "name": "Jaap van Otterdijk", 994 | "email": "opensource@ijaap.nl" 995 | } 996 | ], 997 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 998 | "homepage": "http://www.phpdoc.org", 999 | "keywords": [ 1000 | "FQSEN", 1001 | "phpDocumentor", 1002 | "phpdoc", 1003 | "reflection", 1004 | "static analysis" 1005 | ], 1006 | "time": "2015-12-27 11:43:31" 1007 | }, 1008 | { 1009 | "name": "phpdocumentor/reflection-docblock", 1010 | "version": "3.1.1", 1011 | "source": { 1012 | "type": "git", 1013 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 1014 | "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" 1015 | }, 1016 | "dist": { 1017 | "type": "zip", 1018 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", 1019 | "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", 1020 | "shasum": "" 1021 | }, 1022 | "require": { 1023 | "php": ">=5.5", 1024 | "phpdocumentor/reflection-common": "^1.0@dev", 1025 | "phpdocumentor/type-resolver": "^0.2.0", 1026 | "webmozart/assert": "^1.0" 1027 | }, 1028 | "require-dev": { 1029 | "mockery/mockery": "^0.9.4", 1030 | "phpunit/phpunit": "^4.4" 1031 | }, 1032 | "type": "library", 1033 | "autoload": { 1034 | "psr-4": { 1035 | "phpDocumentor\\Reflection\\": [ 1036 | "src/" 1037 | ] 1038 | } 1039 | }, 1040 | "notification-url": "https://packagist.org/downloads/", 1041 | "license": [ 1042 | "MIT" 1043 | ], 1044 | "authors": [ 1045 | { 1046 | "name": "Mike van Riel", 1047 | "email": "me@mikevanriel.com" 1048 | } 1049 | ], 1050 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 1051 | "time": "2016-09-30 07:12:33" 1052 | }, 1053 | { 1054 | "name": "phpdocumentor/type-resolver", 1055 | "version": "0.2", 1056 | "source": { 1057 | "type": "git", 1058 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 1059 | "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443" 1060 | }, 1061 | "dist": { 1062 | "type": "zip", 1063 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b39c7a5b194f9ed7bd0dd345c751007a41862443", 1064 | "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443", 1065 | "shasum": "" 1066 | }, 1067 | "require": { 1068 | "php": ">=5.5", 1069 | "phpdocumentor/reflection-common": "^1.0" 1070 | }, 1071 | "require-dev": { 1072 | "mockery/mockery": "^0.9.4", 1073 | "phpunit/phpunit": "^5.2||^4.8.24" 1074 | }, 1075 | "type": "library", 1076 | "extra": { 1077 | "branch-alias": { 1078 | "dev-master": "1.0.x-dev" 1079 | } 1080 | }, 1081 | "autoload": { 1082 | "psr-4": { 1083 | "phpDocumentor\\Reflection\\": [ 1084 | "src/" 1085 | ] 1086 | } 1087 | }, 1088 | "notification-url": "https://packagist.org/downloads/", 1089 | "license": [ 1090 | "MIT" 1091 | ], 1092 | "authors": [ 1093 | { 1094 | "name": "Mike van Riel", 1095 | "email": "me@mikevanriel.com" 1096 | } 1097 | ], 1098 | "time": "2016-06-10 07:14:17" 1099 | }, 1100 | { 1101 | "name": "phpspec/prophecy", 1102 | "version": "v1.6.2", 1103 | "source": { 1104 | "type": "git", 1105 | "url": "https://github.com/phpspec/prophecy.git", 1106 | "reference": "6c52c2722f8460122f96f86346600e1077ce22cb" 1107 | }, 1108 | "dist": { 1109 | "type": "zip", 1110 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb", 1111 | "reference": "6c52c2722f8460122f96f86346600e1077ce22cb", 1112 | "shasum": "" 1113 | }, 1114 | "require": { 1115 | "doctrine/instantiator": "^1.0.2", 1116 | "php": "^5.3|^7.0", 1117 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", 1118 | "sebastian/comparator": "^1.1", 1119 | "sebastian/recursion-context": "^1.0|^2.0" 1120 | }, 1121 | "require-dev": { 1122 | "phpspec/phpspec": "^2.0", 1123 | "phpunit/phpunit": "^4.8 || ^5.6.5" 1124 | }, 1125 | "type": "library", 1126 | "extra": { 1127 | "branch-alias": { 1128 | "dev-master": "1.6.x-dev" 1129 | } 1130 | }, 1131 | "autoload": { 1132 | "psr-0": { 1133 | "Prophecy\\": "src/" 1134 | } 1135 | }, 1136 | "notification-url": "https://packagist.org/downloads/", 1137 | "license": [ 1138 | "MIT" 1139 | ], 1140 | "authors": [ 1141 | { 1142 | "name": "Konstantin Kudryashov", 1143 | "email": "ever.zet@gmail.com", 1144 | "homepage": "http://everzet.com" 1145 | }, 1146 | { 1147 | "name": "Marcello Duarte", 1148 | "email": "marcello.duarte@gmail.com" 1149 | } 1150 | ], 1151 | "description": "Highly opinionated mocking framework for PHP 5.3+", 1152 | "homepage": "https://github.com/phpspec/prophecy", 1153 | "keywords": [ 1154 | "Double", 1155 | "Dummy", 1156 | "fake", 1157 | "mock", 1158 | "spy", 1159 | "stub" 1160 | ], 1161 | "time": "2016-11-21 14:58:47" 1162 | }, 1163 | { 1164 | "name": "phpunit/php-code-coverage", 1165 | "version": "2.2.4", 1166 | "source": { 1167 | "type": "git", 1168 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 1169 | "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" 1170 | }, 1171 | "dist": { 1172 | "type": "zip", 1173 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", 1174 | "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", 1175 | "shasum": "" 1176 | }, 1177 | "require": { 1178 | "php": ">=5.3.3", 1179 | "phpunit/php-file-iterator": "~1.3", 1180 | "phpunit/php-text-template": "~1.2", 1181 | "phpunit/php-token-stream": "~1.3", 1182 | "sebastian/environment": "^1.3.2", 1183 | "sebastian/version": "~1.0" 1184 | }, 1185 | "require-dev": { 1186 | "ext-xdebug": ">=2.1.4", 1187 | "phpunit/phpunit": "~4" 1188 | }, 1189 | "suggest": { 1190 | "ext-dom": "*", 1191 | "ext-xdebug": ">=2.2.1", 1192 | "ext-xmlwriter": "*" 1193 | }, 1194 | "type": "library", 1195 | "extra": { 1196 | "branch-alias": { 1197 | "dev-master": "2.2.x-dev" 1198 | } 1199 | }, 1200 | "autoload": { 1201 | "classmap": [ 1202 | "src/" 1203 | ] 1204 | }, 1205 | "notification-url": "https://packagist.org/downloads/", 1206 | "license": [ 1207 | "BSD-3-Clause" 1208 | ], 1209 | "authors": [ 1210 | { 1211 | "name": "Sebastian Bergmann", 1212 | "email": "sb@sebastian-bergmann.de", 1213 | "role": "lead" 1214 | } 1215 | ], 1216 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 1217 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 1218 | "keywords": [ 1219 | "coverage", 1220 | "testing", 1221 | "xunit" 1222 | ], 1223 | "time": "2015-10-06 15:47:00" 1224 | }, 1225 | { 1226 | "name": "phpunit/php-file-iterator", 1227 | "version": "1.4.1", 1228 | "source": { 1229 | "type": "git", 1230 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 1231 | "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" 1232 | }, 1233 | "dist": { 1234 | "type": "zip", 1235 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", 1236 | "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", 1237 | "shasum": "" 1238 | }, 1239 | "require": { 1240 | "php": ">=5.3.3" 1241 | }, 1242 | "type": "library", 1243 | "extra": { 1244 | "branch-alias": { 1245 | "dev-master": "1.4.x-dev" 1246 | } 1247 | }, 1248 | "autoload": { 1249 | "classmap": [ 1250 | "src/" 1251 | ] 1252 | }, 1253 | "notification-url": "https://packagist.org/downloads/", 1254 | "license": [ 1255 | "BSD-3-Clause" 1256 | ], 1257 | "authors": [ 1258 | { 1259 | "name": "Sebastian Bergmann", 1260 | "email": "sb@sebastian-bergmann.de", 1261 | "role": "lead" 1262 | } 1263 | ], 1264 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 1265 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 1266 | "keywords": [ 1267 | "filesystem", 1268 | "iterator" 1269 | ], 1270 | "time": "2015-06-21 13:08:43" 1271 | }, 1272 | { 1273 | "name": "phpunit/php-text-template", 1274 | "version": "1.2.1", 1275 | "source": { 1276 | "type": "git", 1277 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 1278 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 1279 | }, 1280 | "dist": { 1281 | "type": "zip", 1282 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 1283 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 1284 | "shasum": "" 1285 | }, 1286 | "require": { 1287 | "php": ">=5.3.3" 1288 | }, 1289 | "type": "library", 1290 | "autoload": { 1291 | "classmap": [ 1292 | "src/" 1293 | ] 1294 | }, 1295 | "notification-url": "https://packagist.org/downloads/", 1296 | "license": [ 1297 | "BSD-3-Clause" 1298 | ], 1299 | "authors": [ 1300 | { 1301 | "name": "Sebastian Bergmann", 1302 | "email": "sebastian@phpunit.de", 1303 | "role": "lead" 1304 | } 1305 | ], 1306 | "description": "Simple template engine.", 1307 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 1308 | "keywords": [ 1309 | "template" 1310 | ], 1311 | "time": "2015-06-21 13:50:34" 1312 | }, 1313 | { 1314 | "name": "phpunit/php-timer", 1315 | "version": "1.0.8", 1316 | "source": { 1317 | "type": "git", 1318 | "url": "https://github.com/sebastianbergmann/php-timer.git", 1319 | "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" 1320 | }, 1321 | "dist": { 1322 | "type": "zip", 1323 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", 1324 | "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", 1325 | "shasum": "" 1326 | }, 1327 | "require": { 1328 | "php": ">=5.3.3" 1329 | }, 1330 | "require-dev": { 1331 | "phpunit/phpunit": "~4|~5" 1332 | }, 1333 | "type": "library", 1334 | "autoload": { 1335 | "classmap": [ 1336 | "src/" 1337 | ] 1338 | }, 1339 | "notification-url": "https://packagist.org/downloads/", 1340 | "license": [ 1341 | "BSD-3-Clause" 1342 | ], 1343 | "authors": [ 1344 | { 1345 | "name": "Sebastian Bergmann", 1346 | "email": "sb@sebastian-bergmann.de", 1347 | "role": "lead" 1348 | } 1349 | ], 1350 | "description": "Utility class for timing", 1351 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 1352 | "keywords": [ 1353 | "timer" 1354 | ], 1355 | "time": "2016-05-12 18:03:57" 1356 | }, 1357 | { 1358 | "name": "phpunit/php-token-stream", 1359 | "version": "1.4.9", 1360 | "source": { 1361 | "type": "git", 1362 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 1363 | "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b" 1364 | }, 1365 | "dist": { 1366 | "type": "zip", 1367 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3b402f65a4cc90abf6e1104e388b896ce209631b", 1368 | "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b", 1369 | "shasum": "" 1370 | }, 1371 | "require": { 1372 | "ext-tokenizer": "*", 1373 | "php": ">=5.3.3" 1374 | }, 1375 | "require-dev": { 1376 | "phpunit/phpunit": "~4.2" 1377 | }, 1378 | "type": "library", 1379 | "extra": { 1380 | "branch-alias": { 1381 | "dev-master": "1.4-dev" 1382 | } 1383 | }, 1384 | "autoload": { 1385 | "classmap": [ 1386 | "src/" 1387 | ] 1388 | }, 1389 | "notification-url": "https://packagist.org/downloads/", 1390 | "license": [ 1391 | "BSD-3-Clause" 1392 | ], 1393 | "authors": [ 1394 | { 1395 | "name": "Sebastian Bergmann", 1396 | "email": "sebastian@phpunit.de" 1397 | } 1398 | ], 1399 | "description": "Wrapper around PHP's tokenizer extension.", 1400 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 1401 | "keywords": [ 1402 | "tokenizer" 1403 | ], 1404 | "time": "2016-11-15 14:06:22" 1405 | }, 1406 | { 1407 | "name": "phpunit/phpunit", 1408 | "version": "4.8.29", 1409 | "source": { 1410 | "type": "git", 1411 | "url": "https://github.com/sebastianbergmann/phpunit.git", 1412 | "reference": "f19d481b468b76a7fb55eb2b772ed487e484891e" 1413 | }, 1414 | "dist": { 1415 | "type": "zip", 1416 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f19d481b468b76a7fb55eb2b772ed487e484891e", 1417 | "reference": "f19d481b468b76a7fb55eb2b772ed487e484891e", 1418 | "shasum": "" 1419 | }, 1420 | "require": { 1421 | "ext-dom": "*", 1422 | "ext-json": "*", 1423 | "ext-pcre": "*", 1424 | "ext-reflection": "*", 1425 | "ext-spl": "*", 1426 | "php": ">=5.3.3", 1427 | "phpspec/prophecy": "^1.3.1", 1428 | "phpunit/php-code-coverage": "~2.1", 1429 | "phpunit/php-file-iterator": "~1.4", 1430 | "phpunit/php-text-template": "~1.2", 1431 | "phpunit/php-timer": "^1.0.6", 1432 | "phpunit/phpunit-mock-objects": "~2.3", 1433 | "sebastian/comparator": "~1.2.2", 1434 | "sebastian/diff": "~1.2", 1435 | "sebastian/environment": "~1.3", 1436 | "sebastian/exporter": "~1.2", 1437 | "sebastian/global-state": "~1.0", 1438 | "sebastian/version": "~1.0", 1439 | "symfony/yaml": "~2.1|~3.0" 1440 | }, 1441 | "suggest": { 1442 | "phpunit/php-invoker": "~1.1" 1443 | }, 1444 | "bin": [ 1445 | "phpunit" 1446 | ], 1447 | "type": "library", 1448 | "extra": { 1449 | "branch-alias": { 1450 | "dev-master": "4.8.x-dev" 1451 | } 1452 | }, 1453 | "autoload": { 1454 | "classmap": [ 1455 | "src/" 1456 | ] 1457 | }, 1458 | "notification-url": "https://packagist.org/downloads/", 1459 | "license": [ 1460 | "BSD-3-Clause" 1461 | ], 1462 | "authors": [ 1463 | { 1464 | "name": "Sebastian Bergmann", 1465 | "email": "sebastian@phpunit.de", 1466 | "role": "lead" 1467 | } 1468 | ], 1469 | "description": "The PHP Unit Testing framework.", 1470 | "homepage": "https://phpunit.de/", 1471 | "keywords": [ 1472 | "phpunit", 1473 | "testing", 1474 | "xunit" 1475 | ], 1476 | "time": "2016-11-20 10:35:28" 1477 | }, 1478 | { 1479 | "name": "phpunit/phpunit-mock-objects", 1480 | "version": "2.3.8", 1481 | "source": { 1482 | "type": "git", 1483 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 1484 | "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" 1485 | }, 1486 | "dist": { 1487 | "type": "zip", 1488 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", 1489 | "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", 1490 | "shasum": "" 1491 | }, 1492 | "require": { 1493 | "doctrine/instantiator": "^1.0.2", 1494 | "php": ">=5.3.3", 1495 | "phpunit/php-text-template": "~1.2", 1496 | "sebastian/exporter": "~1.2" 1497 | }, 1498 | "require-dev": { 1499 | "phpunit/phpunit": "~4.4" 1500 | }, 1501 | "suggest": { 1502 | "ext-soap": "*" 1503 | }, 1504 | "type": "library", 1505 | "extra": { 1506 | "branch-alias": { 1507 | "dev-master": "2.3.x-dev" 1508 | } 1509 | }, 1510 | "autoload": { 1511 | "classmap": [ 1512 | "src/" 1513 | ] 1514 | }, 1515 | "notification-url": "https://packagist.org/downloads/", 1516 | "license": [ 1517 | "BSD-3-Clause" 1518 | ], 1519 | "authors": [ 1520 | { 1521 | "name": "Sebastian Bergmann", 1522 | "email": "sb@sebastian-bergmann.de", 1523 | "role": "lead" 1524 | } 1525 | ], 1526 | "description": "Mock Object library for PHPUnit", 1527 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 1528 | "keywords": [ 1529 | "mock", 1530 | "xunit" 1531 | ], 1532 | "time": "2015-10-02 06:51:40" 1533 | }, 1534 | { 1535 | "name": "psr/log", 1536 | "version": "1.0.2", 1537 | "source": { 1538 | "type": "git", 1539 | "url": "https://github.com/php-fig/log.git", 1540 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" 1541 | }, 1542 | "dist": { 1543 | "type": "zip", 1544 | "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", 1545 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", 1546 | "shasum": "" 1547 | }, 1548 | "require": { 1549 | "php": ">=5.3.0" 1550 | }, 1551 | "type": "library", 1552 | "extra": { 1553 | "branch-alias": { 1554 | "dev-master": "1.0.x-dev" 1555 | } 1556 | }, 1557 | "autoload": { 1558 | "psr-4": { 1559 | "Psr\\Log\\": "Psr/Log/" 1560 | } 1561 | }, 1562 | "notification-url": "https://packagist.org/downloads/", 1563 | "license": [ 1564 | "MIT" 1565 | ], 1566 | "authors": [ 1567 | { 1568 | "name": "PHP-FIG", 1569 | "homepage": "http://www.php-fig.org/" 1570 | } 1571 | ], 1572 | "description": "Common interface for logging libraries", 1573 | "homepage": "https://github.com/php-fig/log", 1574 | "keywords": [ 1575 | "log", 1576 | "psr", 1577 | "psr-3" 1578 | ], 1579 | "time": "2016-10-10 12:19:37" 1580 | }, 1581 | { 1582 | "name": "psy/psysh", 1583 | "version": "v0.7.2", 1584 | "source": { 1585 | "type": "git", 1586 | "url": "https://github.com/bobthecow/psysh.git", 1587 | "reference": "e64e10b20f8d229cac76399e1f3edddb57a0f280" 1588 | }, 1589 | "dist": { 1590 | "type": "zip", 1591 | "url": "https://api.github.com/repos/bobthecow/psysh/zipball/e64e10b20f8d229cac76399e1f3edddb57a0f280", 1592 | "reference": "e64e10b20f8d229cac76399e1f3edddb57a0f280", 1593 | "shasum": "" 1594 | }, 1595 | "require": { 1596 | "dnoegel/php-xdg-base-dir": "0.1", 1597 | "jakub-onderka/php-console-highlighter": "0.3.*", 1598 | "nikic/php-parser": "^1.2.1|~2.0", 1599 | "php": ">=5.3.9", 1600 | "symfony/console": "~2.3.10|^2.4.2|~3.0", 1601 | "symfony/var-dumper": "~2.7|~3.0" 1602 | }, 1603 | "require-dev": { 1604 | "fabpot/php-cs-fixer": "~1.5", 1605 | "phpunit/phpunit": "~3.7|~4.0|~5.0", 1606 | "squizlabs/php_codesniffer": "~2.0", 1607 | "symfony/finder": "~2.1|~3.0" 1608 | }, 1609 | "suggest": { 1610 | "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", 1611 | "ext-pdo-sqlite": "The doc command requires SQLite to work.", 1612 | "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", 1613 | "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history." 1614 | }, 1615 | "bin": [ 1616 | "bin/psysh" 1617 | ], 1618 | "type": "library", 1619 | "extra": { 1620 | "branch-alias": { 1621 | "dev-develop": "0.8.x-dev" 1622 | } 1623 | }, 1624 | "autoload": { 1625 | "files": [ 1626 | "src/Psy/functions.php" 1627 | ], 1628 | "psr-4": { 1629 | "Psy\\": "src/Psy/" 1630 | } 1631 | }, 1632 | "notification-url": "https://packagist.org/downloads/", 1633 | "license": [ 1634 | "MIT" 1635 | ], 1636 | "authors": [ 1637 | { 1638 | "name": "Justin Hileman", 1639 | "email": "justin@justinhileman.info", 1640 | "homepage": "http://justinhileman.com" 1641 | } 1642 | ], 1643 | "description": "An interactive shell for modern PHP.", 1644 | "homepage": "http://psysh.org", 1645 | "keywords": [ 1646 | "REPL", 1647 | "console", 1648 | "interactive", 1649 | "shell" 1650 | ], 1651 | "time": "2016-03-09 05:03:14" 1652 | }, 1653 | { 1654 | "name": "sebastian/comparator", 1655 | "version": "1.2.2", 1656 | "source": { 1657 | "type": "git", 1658 | "url": "https://github.com/sebastianbergmann/comparator.git", 1659 | "reference": "6a1ed12e8b2409076ab22e3897126211ff8b1f7f" 1660 | }, 1661 | "dist": { 1662 | "type": "zip", 1663 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a1ed12e8b2409076ab22e3897126211ff8b1f7f", 1664 | "reference": "6a1ed12e8b2409076ab22e3897126211ff8b1f7f", 1665 | "shasum": "" 1666 | }, 1667 | "require": { 1668 | "php": ">=5.3.3", 1669 | "sebastian/diff": "~1.2", 1670 | "sebastian/exporter": "~1.2 || ~2.0" 1671 | }, 1672 | "require-dev": { 1673 | "phpunit/phpunit": "~4.4" 1674 | }, 1675 | "type": "library", 1676 | "extra": { 1677 | "branch-alias": { 1678 | "dev-master": "1.2.x-dev" 1679 | } 1680 | }, 1681 | "autoload": { 1682 | "classmap": [ 1683 | "src/" 1684 | ] 1685 | }, 1686 | "notification-url": "https://packagist.org/downloads/", 1687 | "license": [ 1688 | "BSD-3-Clause" 1689 | ], 1690 | "authors": [ 1691 | { 1692 | "name": "Jeff Welch", 1693 | "email": "whatthejeff@gmail.com" 1694 | }, 1695 | { 1696 | "name": "Volker Dusch", 1697 | "email": "github@wallbash.com" 1698 | }, 1699 | { 1700 | "name": "Bernhard Schussek", 1701 | "email": "bschussek@2bepublished.at" 1702 | }, 1703 | { 1704 | "name": "Sebastian Bergmann", 1705 | "email": "sebastian@phpunit.de" 1706 | } 1707 | ], 1708 | "description": "Provides the functionality to compare PHP values for equality", 1709 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 1710 | "keywords": [ 1711 | "comparator", 1712 | "compare", 1713 | "equality" 1714 | ], 1715 | "time": "2016-11-19 09:18:40" 1716 | }, 1717 | { 1718 | "name": "sebastian/diff", 1719 | "version": "1.4.1", 1720 | "source": { 1721 | "type": "git", 1722 | "url": "https://github.com/sebastianbergmann/diff.git", 1723 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" 1724 | }, 1725 | "dist": { 1726 | "type": "zip", 1727 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", 1728 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", 1729 | "shasum": "" 1730 | }, 1731 | "require": { 1732 | "php": ">=5.3.3" 1733 | }, 1734 | "require-dev": { 1735 | "phpunit/phpunit": "~4.8" 1736 | }, 1737 | "type": "library", 1738 | "extra": { 1739 | "branch-alias": { 1740 | "dev-master": "1.4-dev" 1741 | } 1742 | }, 1743 | "autoload": { 1744 | "classmap": [ 1745 | "src/" 1746 | ] 1747 | }, 1748 | "notification-url": "https://packagist.org/downloads/", 1749 | "license": [ 1750 | "BSD-3-Clause" 1751 | ], 1752 | "authors": [ 1753 | { 1754 | "name": "Kore Nordmann", 1755 | "email": "mail@kore-nordmann.de" 1756 | }, 1757 | { 1758 | "name": "Sebastian Bergmann", 1759 | "email": "sebastian@phpunit.de" 1760 | } 1761 | ], 1762 | "description": "Diff implementation", 1763 | "homepage": "https://github.com/sebastianbergmann/diff", 1764 | "keywords": [ 1765 | "diff" 1766 | ], 1767 | "time": "2015-12-08 07:14:41" 1768 | }, 1769 | { 1770 | "name": "sebastian/environment", 1771 | "version": "1.3.8", 1772 | "source": { 1773 | "type": "git", 1774 | "url": "https://github.com/sebastianbergmann/environment.git", 1775 | "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" 1776 | }, 1777 | "dist": { 1778 | "type": "zip", 1779 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", 1780 | "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", 1781 | "shasum": "" 1782 | }, 1783 | "require": { 1784 | "php": "^5.3.3 || ^7.0" 1785 | }, 1786 | "require-dev": { 1787 | "phpunit/phpunit": "^4.8 || ^5.0" 1788 | }, 1789 | "type": "library", 1790 | "extra": { 1791 | "branch-alias": { 1792 | "dev-master": "1.3.x-dev" 1793 | } 1794 | }, 1795 | "autoload": { 1796 | "classmap": [ 1797 | "src/" 1798 | ] 1799 | }, 1800 | "notification-url": "https://packagist.org/downloads/", 1801 | "license": [ 1802 | "BSD-3-Clause" 1803 | ], 1804 | "authors": [ 1805 | { 1806 | "name": "Sebastian Bergmann", 1807 | "email": "sebastian@phpunit.de" 1808 | } 1809 | ], 1810 | "description": "Provides functionality to handle HHVM/PHP environments", 1811 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1812 | "keywords": [ 1813 | "Xdebug", 1814 | "environment", 1815 | "hhvm" 1816 | ], 1817 | "time": "2016-08-18 05:49:44" 1818 | }, 1819 | { 1820 | "name": "sebastian/exporter", 1821 | "version": "1.2.2", 1822 | "source": { 1823 | "type": "git", 1824 | "url": "https://github.com/sebastianbergmann/exporter.git", 1825 | "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" 1826 | }, 1827 | "dist": { 1828 | "type": "zip", 1829 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", 1830 | "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", 1831 | "shasum": "" 1832 | }, 1833 | "require": { 1834 | "php": ">=5.3.3", 1835 | "sebastian/recursion-context": "~1.0" 1836 | }, 1837 | "require-dev": { 1838 | "ext-mbstring": "*", 1839 | "phpunit/phpunit": "~4.4" 1840 | }, 1841 | "type": "library", 1842 | "extra": { 1843 | "branch-alias": { 1844 | "dev-master": "1.3.x-dev" 1845 | } 1846 | }, 1847 | "autoload": { 1848 | "classmap": [ 1849 | "src/" 1850 | ] 1851 | }, 1852 | "notification-url": "https://packagist.org/downloads/", 1853 | "license": [ 1854 | "BSD-3-Clause" 1855 | ], 1856 | "authors": [ 1857 | { 1858 | "name": "Jeff Welch", 1859 | "email": "whatthejeff@gmail.com" 1860 | }, 1861 | { 1862 | "name": "Volker Dusch", 1863 | "email": "github@wallbash.com" 1864 | }, 1865 | { 1866 | "name": "Bernhard Schussek", 1867 | "email": "bschussek@2bepublished.at" 1868 | }, 1869 | { 1870 | "name": "Sebastian Bergmann", 1871 | "email": "sebastian@phpunit.de" 1872 | }, 1873 | { 1874 | "name": "Adam Harvey", 1875 | "email": "aharvey@php.net" 1876 | } 1877 | ], 1878 | "description": "Provides the functionality to export PHP variables for visualization", 1879 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1880 | "keywords": [ 1881 | "export", 1882 | "exporter" 1883 | ], 1884 | "time": "2016-06-17 09:04:28" 1885 | }, 1886 | { 1887 | "name": "sebastian/global-state", 1888 | "version": "1.1.1", 1889 | "source": { 1890 | "type": "git", 1891 | "url": "https://github.com/sebastianbergmann/global-state.git", 1892 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" 1893 | }, 1894 | "dist": { 1895 | "type": "zip", 1896 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", 1897 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", 1898 | "shasum": "" 1899 | }, 1900 | "require": { 1901 | "php": ">=5.3.3" 1902 | }, 1903 | "require-dev": { 1904 | "phpunit/phpunit": "~4.2" 1905 | }, 1906 | "suggest": { 1907 | "ext-uopz": "*" 1908 | }, 1909 | "type": "library", 1910 | "extra": { 1911 | "branch-alias": { 1912 | "dev-master": "1.0-dev" 1913 | } 1914 | }, 1915 | "autoload": { 1916 | "classmap": [ 1917 | "src/" 1918 | ] 1919 | }, 1920 | "notification-url": "https://packagist.org/downloads/", 1921 | "license": [ 1922 | "BSD-3-Clause" 1923 | ], 1924 | "authors": [ 1925 | { 1926 | "name": "Sebastian Bergmann", 1927 | "email": "sebastian@phpunit.de" 1928 | } 1929 | ], 1930 | "description": "Snapshotting of global state", 1931 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1932 | "keywords": [ 1933 | "global state" 1934 | ], 1935 | "time": "2015-10-12 03:26:01" 1936 | }, 1937 | { 1938 | "name": "sebastian/recursion-context", 1939 | "version": "1.0.2", 1940 | "source": { 1941 | "type": "git", 1942 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1943 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791" 1944 | }, 1945 | "dist": { 1946 | "type": "zip", 1947 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", 1948 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791", 1949 | "shasum": "" 1950 | }, 1951 | "require": { 1952 | "php": ">=5.3.3" 1953 | }, 1954 | "require-dev": { 1955 | "phpunit/phpunit": "~4.4" 1956 | }, 1957 | "type": "library", 1958 | "extra": { 1959 | "branch-alias": { 1960 | "dev-master": "1.0.x-dev" 1961 | } 1962 | }, 1963 | "autoload": { 1964 | "classmap": [ 1965 | "src/" 1966 | ] 1967 | }, 1968 | "notification-url": "https://packagist.org/downloads/", 1969 | "license": [ 1970 | "BSD-3-Clause" 1971 | ], 1972 | "authors": [ 1973 | { 1974 | "name": "Jeff Welch", 1975 | "email": "whatthejeff@gmail.com" 1976 | }, 1977 | { 1978 | "name": "Sebastian Bergmann", 1979 | "email": "sebastian@phpunit.de" 1980 | }, 1981 | { 1982 | "name": "Adam Harvey", 1983 | "email": "aharvey@php.net" 1984 | } 1985 | ], 1986 | "description": "Provides functionality to recursively process PHP variables", 1987 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1988 | "time": "2015-11-11 19:50:13" 1989 | }, 1990 | { 1991 | "name": "sebastian/version", 1992 | "version": "1.0.6", 1993 | "source": { 1994 | "type": "git", 1995 | "url": "https://github.com/sebastianbergmann/version.git", 1996 | "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" 1997 | }, 1998 | "dist": { 1999 | "type": "zip", 2000 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", 2001 | "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", 2002 | "shasum": "" 2003 | }, 2004 | "type": "library", 2005 | "autoload": { 2006 | "classmap": [ 2007 | "src/" 2008 | ] 2009 | }, 2010 | "notification-url": "https://packagist.org/downloads/", 2011 | "license": [ 2012 | "BSD-3-Clause" 2013 | ], 2014 | "authors": [ 2015 | { 2016 | "name": "Sebastian Bergmann", 2017 | "email": "sebastian@phpunit.de", 2018 | "role": "lead" 2019 | } 2020 | ], 2021 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 2022 | "homepage": "https://github.com/sebastianbergmann/version", 2023 | "time": "2015-06-21 13:59:46" 2024 | }, 2025 | { 2026 | "name": "symfony/console", 2027 | "version": "v3.1.7", 2028 | "source": { 2029 | "type": "git", 2030 | "url": "https://github.com/symfony/console.git", 2031 | "reference": "5be36e1f3ac7ecbe7e34fb641480ad8497b83aa6" 2032 | }, 2033 | "dist": { 2034 | "type": "zip", 2035 | "url": "https://api.github.com/repos/symfony/console/zipball/5be36e1f3ac7ecbe7e34fb641480ad8497b83aa6", 2036 | "reference": "5be36e1f3ac7ecbe7e34fb641480ad8497b83aa6", 2037 | "shasum": "" 2038 | }, 2039 | "require": { 2040 | "php": ">=5.5.9", 2041 | "symfony/debug": "~2.8|~3.0", 2042 | "symfony/polyfill-mbstring": "~1.0" 2043 | }, 2044 | "require-dev": { 2045 | "psr/log": "~1.0", 2046 | "symfony/event-dispatcher": "~2.8|~3.0", 2047 | "symfony/process": "~2.8|~3.0" 2048 | }, 2049 | "suggest": { 2050 | "psr/log": "For using the console logger", 2051 | "symfony/event-dispatcher": "", 2052 | "symfony/process": "" 2053 | }, 2054 | "type": "library", 2055 | "extra": { 2056 | "branch-alias": { 2057 | "dev-master": "3.1-dev" 2058 | } 2059 | }, 2060 | "autoload": { 2061 | "psr-4": { 2062 | "Symfony\\Component\\Console\\": "" 2063 | }, 2064 | "exclude-from-classmap": [ 2065 | "/Tests/" 2066 | ] 2067 | }, 2068 | "notification-url": "https://packagist.org/downloads/", 2069 | "license": [ 2070 | "MIT" 2071 | ], 2072 | "authors": [ 2073 | { 2074 | "name": "Fabien Potencier", 2075 | "email": "fabien@symfony.com" 2076 | }, 2077 | { 2078 | "name": "Symfony Community", 2079 | "homepage": "https://symfony.com/contributors" 2080 | } 2081 | ], 2082 | "description": "Symfony Console Component", 2083 | "homepage": "https://symfony.com", 2084 | "time": "2016-11-16 22:17:09" 2085 | }, 2086 | { 2087 | "name": "symfony/debug", 2088 | "version": "v3.1.7", 2089 | "source": { 2090 | "type": "git", 2091 | "url": "https://github.com/symfony/debug.git", 2092 | "reference": "c058661c32f5b462722e36d120905940089cbd9a" 2093 | }, 2094 | "dist": { 2095 | "type": "zip", 2096 | "url": "https://api.github.com/repos/symfony/debug/zipball/c058661c32f5b462722e36d120905940089cbd9a", 2097 | "reference": "c058661c32f5b462722e36d120905940089cbd9a", 2098 | "shasum": "" 2099 | }, 2100 | "require": { 2101 | "php": ">=5.5.9", 2102 | "psr/log": "~1.0" 2103 | }, 2104 | "conflict": { 2105 | "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" 2106 | }, 2107 | "require-dev": { 2108 | "symfony/class-loader": "~2.8|~3.0", 2109 | "symfony/http-kernel": "~2.8|~3.0" 2110 | }, 2111 | "type": "library", 2112 | "extra": { 2113 | "branch-alias": { 2114 | "dev-master": "3.1-dev" 2115 | } 2116 | }, 2117 | "autoload": { 2118 | "psr-4": { 2119 | "Symfony\\Component\\Debug\\": "" 2120 | }, 2121 | "exclude-from-classmap": [ 2122 | "/Tests/" 2123 | ] 2124 | }, 2125 | "notification-url": "https://packagist.org/downloads/", 2126 | "license": [ 2127 | "MIT" 2128 | ], 2129 | "authors": [ 2130 | { 2131 | "name": "Fabien Potencier", 2132 | "email": "fabien@symfony.com" 2133 | }, 2134 | { 2135 | "name": "Symfony Community", 2136 | "homepage": "https://symfony.com/contributors" 2137 | } 2138 | ], 2139 | "description": "Symfony Debug Component", 2140 | "homepage": "https://symfony.com", 2141 | "time": "2016-11-15 12:55:20" 2142 | }, 2143 | { 2144 | "name": "symfony/polyfill-mbstring", 2145 | "version": "v1.3.0", 2146 | "source": { 2147 | "type": "git", 2148 | "url": "https://github.com/symfony/polyfill-mbstring.git", 2149 | "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" 2150 | }, 2151 | "dist": { 2152 | "type": "zip", 2153 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", 2154 | "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", 2155 | "shasum": "" 2156 | }, 2157 | "require": { 2158 | "php": ">=5.3.3" 2159 | }, 2160 | "suggest": { 2161 | "ext-mbstring": "For best performance" 2162 | }, 2163 | "type": "library", 2164 | "extra": { 2165 | "branch-alias": { 2166 | "dev-master": "1.3-dev" 2167 | } 2168 | }, 2169 | "autoload": { 2170 | "psr-4": { 2171 | "Symfony\\Polyfill\\Mbstring\\": "" 2172 | }, 2173 | "files": [ 2174 | "bootstrap.php" 2175 | ] 2176 | }, 2177 | "notification-url": "https://packagist.org/downloads/", 2178 | "license": [ 2179 | "MIT" 2180 | ], 2181 | "authors": [ 2182 | { 2183 | "name": "Nicolas Grekas", 2184 | "email": "p@tchwork.com" 2185 | }, 2186 | { 2187 | "name": "Symfony Community", 2188 | "homepage": "https://symfony.com/contributors" 2189 | } 2190 | ], 2191 | "description": "Symfony polyfill for the Mbstring extension", 2192 | "homepage": "https://symfony.com", 2193 | "keywords": [ 2194 | "compatibility", 2195 | "mbstring", 2196 | "polyfill", 2197 | "portable", 2198 | "shim" 2199 | ], 2200 | "time": "2016-11-14 01:06:16" 2201 | }, 2202 | { 2203 | "name": "symfony/var-dumper", 2204 | "version": "v3.1.7", 2205 | "source": { 2206 | "type": "git", 2207 | "url": "https://github.com/symfony/var-dumper.git", 2208 | "reference": "0c2d613e890e33f4da810159ac97931068f5bd17" 2209 | }, 2210 | "dist": { 2211 | "type": "zip", 2212 | "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0c2d613e890e33f4da810159ac97931068f5bd17", 2213 | "reference": "0c2d613e890e33f4da810159ac97931068f5bd17", 2214 | "shasum": "" 2215 | }, 2216 | "require": { 2217 | "php": ">=5.5.9", 2218 | "symfony/polyfill-mbstring": "~1.0" 2219 | }, 2220 | "require-dev": { 2221 | "twig/twig": "~1.20|~2.0" 2222 | }, 2223 | "suggest": { 2224 | "ext-symfony_debug": "" 2225 | }, 2226 | "type": "library", 2227 | "extra": { 2228 | "branch-alias": { 2229 | "dev-master": "3.1-dev" 2230 | } 2231 | }, 2232 | "autoload": { 2233 | "files": [ 2234 | "Resources/functions/dump.php" 2235 | ], 2236 | "psr-4": { 2237 | "Symfony\\Component\\VarDumper\\": "" 2238 | }, 2239 | "exclude-from-classmap": [ 2240 | "/Tests/" 2241 | ] 2242 | }, 2243 | "notification-url": "https://packagist.org/downloads/", 2244 | "license": [ 2245 | "MIT" 2246 | ], 2247 | "authors": [ 2248 | { 2249 | "name": "Nicolas Grekas", 2250 | "email": "p@tchwork.com" 2251 | }, 2252 | { 2253 | "name": "Symfony Community", 2254 | "homepage": "https://symfony.com/contributors" 2255 | } 2256 | ], 2257 | "description": "Symfony mechanism for exploring and dumping PHP variables", 2258 | "homepage": "https://symfony.com", 2259 | "keywords": [ 2260 | "debug", 2261 | "dump" 2262 | ], 2263 | "time": "2016-11-03 08:04:31" 2264 | }, 2265 | { 2266 | "name": "symfony/yaml", 2267 | "version": "v3.1.7", 2268 | "source": { 2269 | "type": "git", 2270 | "url": "https://github.com/symfony/yaml.git", 2271 | "reference": "9da375317228e54f4ea1b013b30fa47417e84943" 2272 | }, 2273 | "dist": { 2274 | "type": "zip", 2275 | "url": "https://api.github.com/repos/symfony/yaml/zipball/9da375317228e54f4ea1b013b30fa47417e84943", 2276 | "reference": "9da375317228e54f4ea1b013b30fa47417e84943", 2277 | "shasum": "" 2278 | }, 2279 | "require": { 2280 | "php": ">=5.5.9" 2281 | }, 2282 | "type": "library", 2283 | "extra": { 2284 | "branch-alias": { 2285 | "dev-master": "3.1-dev" 2286 | } 2287 | }, 2288 | "autoload": { 2289 | "psr-4": { 2290 | "Symfony\\Component\\Yaml\\": "" 2291 | }, 2292 | "exclude-from-classmap": [ 2293 | "/Tests/" 2294 | ] 2295 | }, 2296 | "notification-url": "https://packagist.org/downloads/", 2297 | "license": [ 2298 | "MIT" 2299 | ], 2300 | "authors": [ 2301 | { 2302 | "name": "Fabien Potencier", 2303 | "email": "fabien@symfony.com" 2304 | }, 2305 | { 2306 | "name": "Symfony Community", 2307 | "homepage": "https://symfony.com/contributors" 2308 | } 2309 | ], 2310 | "description": "Symfony Yaml Component", 2311 | "homepage": "https://symfony.com", 2312 | "time": "2016-11-18 21:05:29" 2313 | }, 2314 | { 2315 | "name": "webmozart/assert", 2316 | "version": "1.1.0", 2317 | "source": { 2318 | "type": "git", 2319 | "url": "https://github.com/webmozart/assert.git", 2320 | "reference": "bb2d123231c095735130cc8f6d31385a44c7b308" 2321 | }, 2322 | "dist": { 2323 | "type": "zip", 2324 | "url": "https://api.github.com/repos/webmozart/assert/zipball/bb2d123231c095735130cc8f6d31385a44c7b308", 2325 | "reference": "bb2d123231c095735130cc8f6d31385a44c7b308", 2326 | "shasum": "" 2327 | }, 2328 | "require": { 2329 | "php": "^5.3.3|^7.0" 2330 | }, 2331 | "require-dev": { 2332 | "phpunit/phpunit": "^4.6", 2333 | "sebastian/version": "^1.0.1" 2334 | }, 2335 | "type": "library", 2336 | "extra": { 2337 | "branch-alias": { 2338 | "dev-master": "1.2-dev" 2339 | } 2340 | }, 2341 | "autoload": { 2342 | "psr-4": { 2343 | "Webmozart\\Assert\\": "src/" 2344 | } 2345 | }, 2346 | "notification-url": "https://packagist.org/downloads/", 2347 | "license": [ 2348 | "MIT" 2349 | ], 2350 | "authors": [ 2351 | { 2352 | "name": "Bernhard Schussek", 2353 | "email": "bschussek@gmail.com" 2354 | } 2355 | ], 2356 | "description": "Assertions to validate method input/output with nice error messages.", 2357 | "keywords": [ 2358 | "assert", 2359 | "check", 2360 | "validate" 2361 | ], 2362 | "time": "2016-08-09 15:02:57" 2363 | }, 2364 | { 2365 | "name": "yiisoft/yii2-shell", 2366 | "version": "2.0.0", 2367 | "source": { 2368 | "type": "git", 2369 | "url": "https://github.com/yiisoft/yii2-shell.git", 2370 | "reference": "0584eb26ce5cb029d52e13d2a74edc849866e405" 2371 | }, 2372 | "dist": { 2373 | "type": "zip", 2374 | "url": "https://api.github.com/repos/yiisoft/yii2-shell/zipball/0584eb26ce5cb029d52e13d2a74edc849866e405", 2375 | "reference": "0584eb26ce5cb029d52e13d2a74edc849866e405", 2376 | "shasum": "" 2377 | }, 2378 | "require": { 2379 | "psy/psysh": "0.7.*", 2380 | "yiisoft/yii2": "~2.0.0" 2381 | }, 2382 | "type": "yii2-extension", 2383 | "extra": { 2384 | "bootstrap": "yii\\shell\\Bootstrap", 2385 | "branch-alias": { 2386 | "dev-master": "2.0.x-dev" 2387 | } 2388 | }, 2389 | "autoload": { 2390 | "psr-4": { 2391 | "yii\\shell\\": "" 2392 | } 2393 | }, 2394 | "notification-url": "https://packagist.org/downloads/", 2395 | "license": [ 2396 | "BSD-3-Clause" 2397 | ], 2398 | "authors": [ 2399 | { 2400 | "name": "Daniel Gomez Pan", 2401 | "email": "pana_1990@hotmail.com" 2402 | }, 2403 | { 2404 | "name": "Sascha Vincent Kurowski", 2405 | "email": "svkurowski@gmail.com" 2406 | } 2407 | ], 2408 | "description": "The interactive shell extension for Yii framework", 2409 | "keywords": [ 2410 | "shell", 2411 | "yii2" 2412 | ], 2413 | "time": "2016-11-22 21:30:25" 2414 | } 2415 | ], 2416 | "aliases": [], 2417 | "minimum-stability": "stable", 2418 | "stability-flags": [], 2419 | "prefer-stable": false, 2420 | "prefer-lowest": false, 2421 | "platform": { 2422 | "php": ">=5.5" 2423 | }, 2424 | "platform-dev": [] 2425 | } 2426 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 'http://bot.cebe.cc/', 7 | 8 | // SET THIS IN config.local.php 9 | // needs the following permissions: 10 | // - repo 11 | // - admin:repo_hook 12 | 'github_token' => '', 13 | 'github_username' => '', 14 | 15 | // a secret to verify hooks are really coming from github 16 | 'hook_secret' => '', 17 | 18 | // the repositories to install hooks for 19 | 'repositories' => [ 20 | 'cebe-test/testrepo', 21 | 'cebe-test/testrepo-redis', 22 | ], 23 | 24 | // comment on issues that are labeled with a specific label 25 | // - action: "comment" 26 | // - label: on which label to react 27 | // - comment: what to post 28 | // - close: whether to close the issue after commenting 29 | 30 | // move issues to other repos based on labels 31 | // - action: "move" 32 | // - label: on which label to react 33 | // - repo: the repo to move the issue to 34 | 35 | 'actions' => [ 36 | 37 | /** 38 | * Questions 39 | */ 40 | 41 | /** 42 | * Status: Need more info 43 | */ 44 | [ 45 | 'label' => 'status:need more info', 46 | 'action' => 'comment', 47 | 'comment' => << 'missing formatting', 69 | 'action' => 'comment', 70 | 'comment' => << 'expired', 85 | 'action' => 'comment', 86 | 'comment' => << true, 98 | ], 99 | 100 | /** 101 | * PRs: closed 102 | */ 103 | [ 104 | 'label' => 'pr:too many objectives', 105 | 'action' => 'comment', 106 | 'comment' => << true, 121 | ], 122 | 123 | /** 124 | * PRs: kept open 125 | */ 126 | [ 127 | 'label' => 'pr:request for unit tests', 128 | 'action' => 'comment', 129 | 'comment' => << 'pr:missing usecase', 150 | 'action' => 'comment', 151 | 'comment' => << 'question', 169 | 'action' => 'comment', 170 | 'comment' => << true, 186 | ], 187 | 188 | /** 189 | * Extensions 190 | */ 191 | [ 192 | 'label' => 'ext:apidoc', 193 | 'action' => 'move', 194 | 'repo' => 'yiisoft/yii2-apidoc', 195 | ], 196 | 197 | [ 198 | 'label' => 'ext:authclient', 199 | 'action' => 'move', 200 | 'repo' => 'yiisoft/yii2-authclient', 201 | ], 202 | 203 | [ 204 | 'label' => 'ext:httpclient', 205 | 'action' => 'move', 206 | 'repo' => 'yiisoft/yii2-httpclient', 207 | ], 208 | 209 | [ 210 | 'label' => 'ext:bootstrap', 211 | 'action' => 'move', 212 | 'repo' => 'yiisoft/yii2-bootstrap', 213 | ], 214 | 215 | [ 216 | 'label' => 'ext:captcha', 217 | 'action' => 'move', 218 | 'repo' => 'yiisoft/yii2-captcha', 219 | ], 220 | 221 | [ 222 | 'label' => 'ext:codeception', 223 | 'action' => 'move', 224 | 'repo' => 'yiisoft/yii2-codeception', 225 | ], 226 | 227 | [ 228 | 'label' => 'ext:debug', 229 | 'action' => 'move', 230 | 'repo' => 'yiisoft/yii2-debug', 231 | ], 232 | 233 | [ 234 | 'label' => 'ext:elasticsearch', 235 | 'action' => 'move', 236 | 'repo' => 'yiisoft/yii2-elasticsearch', 237 | ], 238 | 239 | [ 240 | 'label' => 'ext:faker', 241 | 'action' => 'move', 242 | 'repo' => 'yiisoft/yii2-faker', 243 | ], 244 | 245 | [ 246 | 'label' => 'ext:gii', 247 | 'action' => 'move', 248 | 'repo' => 'yiisoft/yii2-gii', 249 | ], 250 | 251 | [ 252 | 'label' => 'ext:imagine', 253 | 'action' => 'move', 254 | 'repo' => 'yiisoft/yii2-imagine', 255 | ], 256 | 257 | [ 258 | 'label' => 'ext:jui', 259 | 'action' => 'move', 260 | 'repo' => 'yiisoft/yii2-jui', 261 | ], 262 | 263 | [ 264 | 'label' => 'ext:jquery', 265 | 'action' => 'move', 266 | 'repo' => 'yiisoft/yii2-jquery', 267 | ], 268 | 269 | [ 270 | 'label' => 'ext:mongodb', 271 | 'action' => 'move', 272 | 'repo' => 'yiisoft/yii2-mongodb', 273 | ], 274 | 275 | [ 276 | 'label' => 'ext:queue', 277 | 'action' => 'move', 278 | 'repo' => 'yiisoft/yii2-queue', 279 | ], 280 | 281 | [ 282 | 'label' => 'ext:redis', 283 | 'action' => 'move', 284 | 'repo' => 'yiisoft/yii2-redis', 285 | ], 286 | 287 | [ 288 | 'label' => 'ext:rest', 289 | 'action' => 'move', 290 | 'repo' => 'yiisoft/yii2-rest', 291 | ], 292 | 293 | [ 294 | 'label' => 'ext:sphinx', 295 | 'action' => 'move', 296 | 'repo' => 'yiisoft/yii2-sphinx', 297 | ], 298 | 299 | [ 300 | 'label' => 'ext:swiftmailer', 301 | 'action' => 'move', 302 | 'repo' => 'yiisoft/yii2-swiftmailer', 303 | ], 304 | 305 | [ 306 | 'label' => 'ext:twig', 307 | 'action' => 'move', 308 | 'repo' => 'yiisoft/yii2-twig', 309 | ], 310 | 311 | [ 312 | 'label' => 'ext:smarty', 313 | 'action' => 'move', 314 | 'repo' => 'yiisoft/yii2-smarty', 315 | ], 316 | 317 | [ // allow moving issue to Yii core 318 | 'label' => 'core issue', 319 | 'action' => 'move', 320 | 'repo' => 'yiisoft/yii2', 321 | ], 322 | 323 | ], 324 | 325 | 326 | 327 | ]; 328 | -------------------------------------------------------------------------------- /controllers/IssuesController.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class IssuesController extends Controller 20 | { 21 | /** 22 | * @see https://developer.github.com/v3/activity/events/types/#issuesevent 23 | */ 24 | const EVENTNAME_ISSUES = 'issues'; 25 | /** 26 | * @see https://developer.github.com/v3/activity/events/types/#pullrequestevent 27 | */ 28 | const EVENTNAME_PULL_REQUEST = 'pull_request'; 29 | 30 | 31 | public function behaviors() 32 | { 33 | return [ 34 | 'verb' => [ 35 | 'class' => VerbFilter::className(), 36 | 'actions' => [ 37 | 'index' => ['post'], 38 | ], 39 | ], 40 | ]; 41 | } 42 | 43 | public function actionIndex() 44 | { 45 | \Yii::$app->response->format = Response::FORMAT_JSON; 46 | 47 | // content of $params should look like here: https://developer.github.com/v3/activity/events/types/#issuesevent 48 | $params = \Yii::$app->request->bodyParams; 49 | $event = \Yii::$app->request->headers->get('X-Github-Event'); 50 | if (!$event) { 51 | \Yii::warning('event request without X-Github-Event header.'); 52 | throw new BadRequestHttpException('Event request without X-Github-Event header.'); 53 | } 54 | 55 | // verify request data to avoid data sent from sources other than github 56 | // this will throw BadRequestHttpException on invalid data 57 | Yii::$app->github->verifyRequest(Yii::$app->request->rawBody); 58 | 59 | // simple succes for 'ping' event 60 | if ($event === 'ping') { 61 | return ['success' => true, 'action' => 'pong']; 62 | } 63 | 64 | // only react on issue and pull request events 65 | if ($event !== self::EVENTNAME_ISSUES && $event !== self::EVENTNAME_PULL_REQUEST) { 66 | throw new BadRequestHttpException('Only issues and pull_request events should be deployed here.'); 67 | } 68 | 69 | // ignore events triggered by the bot itself to avoid loops 70 | if ($params['sender']['login'] === Yii::$app->params['github_username']) { 71 | \Yii::warning('ignoring event triggered by myself.'); 72 | return ['success' => true, 'action' => 'ignored']; 73 | } 74 | 75 | // dependent on the event perform some action 76 | switch($params['action']) 77 | { 78 | case 'labeled': 79 | // if label is added, check for actions 80 | 81 | if (isset($params['label'])) { 82 | foreach(\Yii::$app->params['actions'] as $action) { 83 | if ($params['label']['name'] == $action['label']) { 84 | $this->performActionByLabel($action, $params, $event); 85 | } 86 | } 87 | } 88 | 89 | return ['success' => true, 'action' => 'processed']; 90 | break; 91 | } 92 | 93 | return ['success' => true, 'action' => 'ignored']; 94 | } 95 | 96 | protected function performActionByLabel($action, $params, $event) 97 | { 98 | switch($action['action']) 99 | { 100 | case 'comment': 101 | // add a comment to issue or pull request 102 | if ($event === self::EVENTNAME_ISSUES) { 103 | $this->replyWithCommentToIssue($params['repository'], $params['issue'], $action['comment']); 104 | if (isset($action['close']) && $action['close']) { 105 | $this->closeIssue($params['repository'], $params['issue']); 106 | } 107 | } elseif($event === self::EVENTNAME_PULL_REQUEST) { 108 | $this->replyWithCommentToPr($params['repository'], $params['pull_request'], $action['comment']); 109 | if (isset($action['close']) && $action['close']) { 110 | $this->closePr($params['repository'], $params['pull_request']); 111 | } 112 | } 113 | break; 114 | case 'move': 115 | // move an issue to another repository 116 | if ($event === self::EVENTNAME_ISSUES) { 117 | if ($params['issue']['state'] !== 'open') { 118 | // do not move issue if it is closed, allow editing labels in closed state 119 | break; 120 | } 121 | $this->moveIssue($params['repository'], $action['repo'], $params['issue'], $params['sender']); 122 | } 123 | break; 124 | default: 125 | throw new InvalidConfigException('Action "' . $action['action'] . '" is not supported.'); 126 | } 127 | } 128 | 129 | protected function replyWithCommentToIssue($repository, $issue, $comment) 130 | { 131 | sleep(2); // wait 2sec before reply to have github issue events in order 132 | 133 | /** @var $client \Github\Client */ 134 | $client = Yii::$app->github->client(); 135 | 136 | $api = new \Github\Api\Issue($client); 137 | $api->comments()->create($repository['owner']['login'], $repository['name'], $issue['number'], [ 138 | 'body' => $comment, 139 | ]); 140 | Yii::info("commented on issue {$repository['owner']['login']}/{$repository['name']}#{$issue['number']}.", 'action'); 141 | } 142 | 143 | protected function closeIssue($repository, $issue) 144 | { 145 | sleep(2); // wait 2sec before reply to have github issue events in order 146 | 147 | /** @var $client \Github\Client */ 148 | $client = Yii::$app->github->client(); 149 | 150 | $api = new \Github\Api\Issue($client); 151 | $api->update($repository['owner']['login'], $repository['name'], $issue['number'], [ 152 | 'state' => 'closed', 153 | ]); 154 | Yii::info("closed issue {$repository['owner']['login']}/{$repository['name']}#{$issue['number']}.", 'action'); 155 | } 156 | 157 | protected function replyWithCommentToPr($repository, $pr, $comment) 158 | { 159 | sleep(2); // wait 2sec before reply to have github issue events in order 160 | 161 | /** @var $client \Github\Client */ 162 | $client = Yii::$app->github->client(); 163 | 164 | // create issue instead of PR to be able to post on the normal PR wall 165 | // otherwise the comment must be on a file or commit 166 | $api = new \Github\Api\Issue($client); 167 | $api->comments()->create($repository['owner']['login'], $repository['name'], $pr['number'], [ 168 | 'body' => $comment, 169 | ]); 170 | Yii::info("commented on pr {$repository['owner']['login']}/{$repository['name']}#{$pr['number']}.", 'action'); 171 | } 172 | 173 | protected function closePr($repository, $pr) 174 | { 175 | sleep(2); // wait 2sec before reply to have github issue events in order 176 | 177 | /** @var $client \Github\Client */ 178 | $client = Yii::$app->github->client(); 179 | 180 | $api = new \Github\Api\PullRequest($client); 181 | $api->update($repository['owner']['login'], $repository['name'], $pr['number'], [ 182 | 'state' => 'closed', 183 | ]); 184 | Yii::info("closed pr {$repository['owner']['login']}/{$repository['name']}#{$pr['number']}.", 'action'); 185 | } 186 | 187 | protected function moveIssue($fromRepository, $toRepository, $issue, $sender) 188 | { 189 | // do not move issue if from and to repo are the same (prevent loops) 190 | if ("{$fromRepository['owner']['login']}/{$fromRepository['name']}" === $toRepository) { 191 | Yii::warning("did NOT move issue {$fromRepository['owner']['login']}/{$fromRepository['name']}#{$issue['number']} to {$toRepository}.", 'action'); 192 | return; 193 | } 194 | // also do not move issues created by the bot itself (prevent loops) 195 | if ($issue['user']['login'] === Yii::$app->params['github_username']) { 196 | Yii::warning("did NOT move issue {$fromRepository['owner']['login']}/{$fromRepository['name']}#{$issue['number']} to {$toRepository} because it was created by me.", 'action'); 197 | return; 198 | } 199 | 200 | sleep(2); // wait 2sec before reply to have github issue events in order 201 | 202 | /** @var $client \Github\Client */ 203 | $client = Yii::$app->github->client(); 204 | 205 | $api = new \Github\Api\Issue($client); 206 | list($toUser, $toRepo) = explode('/', $toRepository); 207 | $newIssue = $api->create($toUser, $toRepo, [ 208 | 'title' => $issue['title'], 209 | 'body' => 'This issue has originally been reported by @' . $issue['user']['login'] . ' at ' . $issue['html_url'] . ".\n" 210 | . 'Moved here by @' . $sender['login'] . '.' 211 | . "\n\n-----\n\n" 212 | . $issue['body'], 213 | 'labels' => array_map(function($i) { return $i['name']; }, $issue['labels']), 214 | ]); 215 | Yii::info("moved issue {$fromRepository['owner']['login']}/{$fromRepository['name']}#{$issue['number']} to {$toRepository}#{$newIssue['number']}.", 'action'); 216 | sleep(2); // wait 2sec before reply to have github issue events in order 217 | $this->replyWithCommentToIssue($fromRepository, $issue, 'Issue moved to ' . $newIssue['html_url']); 218 | sleep(2); // wait 2sec before reply to have github issue events in order 219 | $this->closeIssue($fromRepository, $issue); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /controllers/SiteController.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace app\controllers; 9 | 10 | 11 | use Yii; 12 | use yii\base\Exception; 13 | use yii\base\UserException; 14 | use yii\web\Controller; 15 | use yii\web\HttpException; 16 | use yii\web\Response; 17 | 18 | class SiteController extends Controller 19 | { 20 | public function actionError() 21 | { 22 | \Yii::$app->response->format = Response::FORMAT_JSON; 23 | 24 | if (($exception = Yii::$app->getErrorHandler()->exception) === null) { 25 | // action has been invoked not from error handler, but by direct route, so we display '404 Not Found' 26 | $exception = new HttpException(404, Yii::t('yii', 'Page not found.')); 27 | } 28 | 29 | if ($exception instanceof HttpException) { 30 | $code = $exception->statusCode; 31 | Yii::$app->response->statusCode = $code; 32 | } else { 33 | $code = $exception->getCode(); 34 | Yii::$app->response->statusCode = 500; 35 | } 36 | if ($exception instanceof Exception) { 37 | $name = $exception->getName(); 38 | } else { 39 | $name = $this->defaultName ?: Yii::t('yii', 'Error'); 40 | } 41 | if ($code) { 42 | $name .= " (#$code)"; 43 | } 44 | 45 | if ($exception instanceof UserException) { 46 | $message = $exception->getMessage(); 47 | } else { 48 | $message = Yii::t('yii', 'An internal server error occurred.'); 49 | } 50 | 51 | return [ 52 | 'error' => $name, 53 | 'message' => $message, 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests 11 | 12 | 13 | 14 | 15 | . 16 | 17 | ./tmp 18 | ./vendor 19 | ./tests 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/GithubControllerTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class GithubControllerTest extends TestCase 16 | { 17 | public function testInit_HookSecretException() 18 | { 19 | $this->setExpectedException('yii\base\Exception', 'Config param "hook_secret" is not configured!'); 20 | new GithubController('github', Yii::$app); 21 | } 22 | 23 | public function testHooks_UndefinedWebUrl() 24 | { 25 | $this->mockApplication([ 26 | 'params' => [ 27 | 'hook_secret' => 'test-secret', 28 | ], 29 | ]); 30 | $controller = new GithubController('github', Yii::$app); 31 | 32 | $this->setExpectedException('PHPUnit_Framework_Error_Notice', 'Undefined index: webUrl'); 33 | $controller->hooks(); 34 | } 35 | 36 | public function testHooks() 37 | { 38 | $this->mockApplication([ 39 | 'params' => [ 40 | 'hook_secret' => 'test-secret', 41 | 'webUrl' => 'http://localhost' 42 | ], 43 | ]); 44 | $controller = new GithubController('github', Yii::$app); 45 | $this->assertEquals([ 46 | 'issues,pull_request' => 'http://localhost/index.php?r=issues' 47 | ], $controller->hooks()); 48 | } 49 | 50 | public function testActionRegister_RequiredGithubComponentException() 51 | { 52 | $this->mockApplication([ 53 | 'params' => [ 54 | 'hook_secret' => 'test-secret' 55 | ], 56 | ]); 57 | $controller = new GithubController('github', Yii::$app); 58 | $this->setExpectedException('\yii\base\UnknownPropertyException', 'Getting unknown property: yii\console\Application::github'); 59 | $controller->runAction('register'); 60 | } 61 | 62 | public function testActionRegister_RequiredRepositoriesParamException() 63 | { 64 | $this->mockApplication([ 65 | 'components' => [ 66 | 'github' => 'app\components\Github', 67 | ], 68 | 'params' => [ 69 | 'github_token' => 'test-token', 70 | 'github_username' => 'test-username', 71 | 'hook_secret' => 'test-secret' 72 | ], 73 | ]); 74 | $controller = new GithubController('github', Yii::$app); 75 | $this->setExpectedException('PHPUnit_Framework_Error_Notice', 'Undefined index: repositories'); 76 | $controller->runAction('register'); 77 | } 78 | 79 | public function testActionRegister_WrongTokenException() 80 | { 81 | Yii::$container->set('Github\Api\Repo', function ($container, $params, $config) { 82 | return new \yiiunit\extensions\githubbot\mocks\RepoMock($params[0]); 83 | }); 84 | Yii::$container->set('Github\HttpClient\CachedHttpClient', function () { 85 | return new CachedHttpClientMock(); 86 | }); 87 | 88 | $this->mockApplication([ 89 | 'components' => [ 90 | 'github' => 'app\components\Github', 91 | ], 92 | 'params' => [ 93 | 'github_token' => 'wrong' . CachedHttpClientMock::DUMMY_TOKEN, 94 | 'github_username' => 'username-test', 95 | 'repositories' => [ 96 | 'dummy-test/hook-test' 97 | ], 98 | 'hook_secret' => 'test-secret', 99 | 'webUrl' => 'http://www.domain.com/hookUrl' 100 | ], 101 | ]); 102 | $controller = new GithubControllerMock('github', Yii::$app); 103 | $this->setExpectedException('Github\Exception\RuntimeException', 'Bad credentials', 401); 104 | $controller->runAction('register'); 105 | } 106 | 107 | public function testActionRegister() 108 | { 109 | Yii::$container->set('Github\Api\Repo', function ($container, $params) { 110 | return new \yiiunit\extensions\githubbot\mocks\RepoMock($params[0]); 111 | }); 112 | Yii::$container->set('Github\HttpClient\CachedHttpClient', function () { 113 | return new CachedHttpClientMock(); 114 | }); 115 | 116 | $config = [ 117 | 'components' => [ 118 | 'github' => 'app\components\Github', 119 | ], 120 | 'params' => [ 121 | 'github_token' => CachedHttpClientMock::DUMMY_TOKEN, 122 | 'github_username' => 'username-test', 123 | 'repositories' => [ 124 | 'dummy-test/hook-test' 125 | ], 126 | 'hook_secret' => 'test-secret', 127 | 'webUrl' => 'http://www.domain.com/hookUrl' 128 | ], 129 | ]; 130 | $this->mockApplication($config); 131 | $controller = new GithubControllerMock('github', Yii::$app); 132 | $controller->runAction('register'); 133 | $actual = $controller->flushStdOutBuffer(); 134 | $this->assertEquals("registering issues,pull_request hook on " . $config['params']['repositories'][0] . "...added.\n", $actual); 135 | } 136 | } -------------------------------------------------------------------------------- /tests/GithubTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException('\Exception', 'Config param "github_token" is not configured!'); 17 | (new Github())->client(); 18 | } 19 | 20 | public function testClient_RequiredUserException() 21 | { 22 | $this->mockApplication([ 23 | 'params' => [ 24 | 'github_token' => 'test-token' 25 | ] 26 | ]); 27 | $this->setExpectedException('\Exception', 'Config param "github_username" is not configured!'); 28 | (new Github())->client(); 29 | } 30 | 31 | public function testClient_Authentication() 32 | { 33 | $this->mockApplication([ 34 | 'params' => [ 35 | 'github_token' => 'test-token', 36 | 'github_username' => 'test-username', 37 | ] 38 | ]); 39 | $client = (new Github())->client(); 40 | $this->assertInstanceOf('Github\Client', $client); 41 | $this->assertInstanceOf('Github\HttpClient\HttpClient', $this->getInvisibleProperty('httpClient', $client)); 42 | } 43 | 44 | public function testVerifyRequest_RequiredHookSecretException() 45 | { 46 | $this->setExpectedException('\yii\base\Exception', 'Config param "hook_secret" is not configured!'); 47 | (new Github())->verifyRequest(''); 48 | } 49 | 50 | public function testVerifyRequest_MissingSignatureException() 51 | { 52 | $this->mockWebApplication([ 53 | 'params' => [ 54 | 'hook_secret' => 'test-secret', 55 | ], 56 | ]); 57 | $this->setExpectedException('\yii\web\BadRequestHttpException', 'X-Hub-Signature header is missing.'); 58 | (new Github())->verifyRequest(''); 59 | } 60 | 61 | public function testVerifyRequest_UnknownSignatureException() 62 | { 63 | $this->mockWebApplication([ 64 | 'params' => [ 65 | 'hook_secret' => 'test-secret', 66 | ], 67 | ]); 68 | Yii::$app->request->getHeaders()->set('X-Hub-Signature', "unknown=dummy"); 69 | $this->setExpectedException('\yii\web\BadRequestHttpException', 'Unknown algorithm in X-Hub-Signature header.'); 70 | (new Github())->verifyRequest(''); 71 | } 72 | 73 | /** 74 | * @dataProvider signatureDataProvider 75 | */ 76 | public function testVerifyRequest_ValidationException($signature) 77 | { 78 | $this->mockWebApplication([ 79 | 'params' => [ 80 | 'hook_secret' => 'test-secret', 81 | ], 82 | ]); 83 | Yii::$app->request->getHeaders()->set('X-Hub-Signature', "$signature=hash"); 84 | $this->setExpectedException('\yii\web\BadRequestHttpException', 'Unable to validate submitted data.'); 85 | (new Github())->verifyRequest(''); 86 | } 87 | 88 | public function signatureDataProvider() 89 | { 90 | return [['sha1'], ['sha256'], ['sha384'], ['sha512']]; 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /tests/IssuesControllerTest.php: -------------------------------------------------------------------------------- 1 | httpClient = new CachedHttpClientMock(); 22 | Yii::$container->setSingleton(\Github\HttpClient\CachedHttpClient::class, function () { 23 | return $this->httpClient; 24 | }); 25 | 26 | $this->mockWebApplication([ 27 | 'components' => [ 28 | 'github' => \app\components\Github::class, 29 | 'request' => [ 30 | 'enableCsrfValidation' => false, 31 | 'enableCookieValidation' => false, 32 | 'parsers' => [ 33 | 'application/json' => 'yii\web\JsonParser', 34 | ], 35 | ], 36 | 'errorHandler' => [ 37 | 'errorAction' => 'site/error', 38 | ], 39 | ], 40 | 'params' => [ 41 | 'github_token' => CachedHttpClientMock::DUMMY_TOKEN, 42 | 'github_username' => 'username-test', 43 | 'repositories' => [ 44 | 'dummy-test/hook-test' 45 | ], 46 | 'hook_secret' => 'test-secret', 47 | 'webUrl' => 'http://www.domain.com/hookUrl', 48 | 'actions' => [ 49 | [ 50 | 'label' => 'expired', 51 | 'action' => 'comment', 52 | 'comment' => 'This issue is expired', 53 | 'close' => true, 54 | ], 55 | [ 56 | 'label' => 'info', 57 | 'action' => 'comment', 58 | 'comment' => 'This issue needs more info', 59 | 'close' => false, 60 | ], 61 | [ 62 | 'label' => 'ext:test', 63 | 'action' => 'move', 64 | 'repo' => 'cebe/test', 65 | ], 66 | ] 67 | ], 68 | ]); 69 | } 70 | 71 | protected function signRequest($request) 72 | { 73 | $hash = Yii::$app->security->hashData($request->rawBody, Yii::$app->params['hook_secret']); 74 | $request->headers->add('X-Hub-Signature', Yii::$app->security->macHash . '=' . substr($hash, 0, strlen(@hash_hmac(Yii::$app->security->macHash, '', '', false)))); 75 | } 76 | 77 | public function testIgnoreOwnAction() 78 | { 79 | $_SERVER['REQUEST_METHOD'] = 'POST'; 80 | $_SERVER["CONTENT_TYPE"] = 'application/json'; 81 | Yii::$app->request->headers->add('X-Github-Event', 'issues'); 82 | // body is reduced size, real github request is more verbose 83 | Yii::$app->request->rawBody = <<signRequest(Yii::$app->request); 112 | 113 | /** @var $controller IssuesController */ 114 | list($controller, ) = Yii::$app->createController('issues'); 115 | $response = $controller->runAction('index'); 116 | $this->assertEquals(['success' => true, 'action' => 'ignored'], $response); 117 | 118 | $this->assertCount(0, $this->httpClient->requests); 119 | } 120 | 121 | public function testIssueOnLabelComment() 122 | { 123 | $_SERVER['REQUEST_METHOD'] = 'POST'; 124 | $_SERVER["CONTENT_TYPE"] = 'application/json'; 125 | Yii::$app->request->headers->add('X-Github-Event', 'issues'); 126 | // body is reduced size, real github request is more verbose 127 | Yii::$app->request->rawBody = <<signRequest(Yii::$app->request); 156 | 157 | /** @var $controller IssuesController */ 158 | list($controller, ) = Yii::$app->createController('issues'); 159 | $response = $controller->runAction('index'); 160 | $this->assertEquals(['success' => true, 'action' => 'processed'], $response); 161 | 162 | $this->assertCount(1, $this->httpClient->requests); 163 | $request = reset($this->httpClient->requests); 164 | $this->assertEquals('repos/baxterthehacker/public-repo/issues/2/comments', $request['path']); 165 | $this->assertContains('This issue needs more info', $request['body']); 166 | } 167 | 168 | public function testIssueOnLabelCommentAndClose() 169 | { 170 | $_SERVER['REQUEST_METHOD'] = 'POST'; 171 | $_SERVER["CONTENT_TYPE"] = 'application/json'; 172 | Yii::$app->request->headers->add('X-Github-Event', 'issues'); 173 | // body is reduced size, real github request is more verbose 174 | Yii::$app->request->rawBody = <<signRequest(Yii::$app->request); 203 | 204 | /** @var $controller IssuesController */ 205 | list($controller, ) = Yii::$app->createController('issues'); 206 | $response = $controller->runAction('index'); 207 | $this->assertEquals(['success' => true, 'action' => 'processed'], $response); 208 | 209 | $this->assertCount(2, $this->httpClient->requests); 210 | $request1 = $this->httpClient->requests[0]; 211 | $this->assertEquals('repos/baxterthehacker/public-repo/issues/2/comments', $request1['path']); 212 | $this->assertContains('This issue is expired', $request1['body']); 213 | $request2 = $this->httpClient->requests[1]; 214 | $this->assertEquals('repos/baxterthehacker/public-repo/issues/2', $request2['path']); 215 | $this->assertContains('{"state":"closed"}', $request2['body']); 216 | } 217 | 218 | public function testPrOnLabelComment() 219 | { 220 | $_SERVER['REQUEST_METHOD'] = 'POST'; 221 | $_SERVER["CONTENT_TYPE"] = 'application/json'; 222 | Yii::$app->request->headers->add('X-Github-Event', 'pull_request'); 223 | // body is reduced size, real github request is more verbose 224 | Yii::$app->request->rawBody = <<signRequest(Yii::$app->request); 254 | 255 | /** @var $controller IssuesController */ 256 | list($controller, ) = Yii::$app->createController('issues'); 257 | $response = $controller->runAction('index'); 258 | $this->assertEquals(['success' => true, 'action' => 'processed'], $response); 259 | 260 | $this->assertCount(1, $this->httpClient->requests); 261 | $request = reset($this->httpClient->requests); 262 | $this->assertEquals('repos/baxterthehacker/public-repo/issues/1/comments', $request['path']); 263 | $this->assertContains('This issue needs more info', $request['body']); 264 | } 265 | 266 | public function testPrOnLabelCommentAndClose() 267 | { 268 | $_SERVER['REQUEST_METHOD'] = 'POST'; 269 | $_SERVER["CONTENT_TYPE"] = 'application/json'; 270 | Yii::$app->request->headers->add('X-Github-Event', 'pull_request'); 271 | // body is reduced size, real github request is more verbose 272 | Yii::$app->request->rawBody = <<signRequest(Yii::$app->request); 302 | 303 | /** @var $controller IssuesController */ 304 | list($controller, ) = Yii::$app->createController('issues'); 305 | $response = $controller->runAction('index'); 306 | $this->assertEquals(['success' => true, 'action' => 'processed'], $response); 307 | 308 | $this->assertCount(2, $this->httpClient->requests); 309 | $request1 = $this->httpClient->requests[0]; 310 | $this->assertEquals('repos/baxterthehacker/public-repo/issues/1/comments', $request1['path']); 311 | $this->assertContains('This issue is expired', $request1['body']); 312 | $request2 = $this->httpClient->requests[1]; 313 | $this->assertEquals('repos/baxterthehacker/public-repo/pulls/1', $request2['path']); 314 | $this->assertContains('{"state":"closed"}', $request2['body']); 315 | } 316 | 317 | public function testIssueOnLabelMove() 318 | { 319 | $_SERVER['REQUEST_METHOD'] = 'POST'; 320 | $_SERVER["CONTENT_TYPE"] = 'application/json'; 321 | Yii::$app->request->headers->add('X-Github-Event', 'issues'); 322 | // body is reduced size, real github request is more verbose 323 | Yii::$app->request->rawBody = <<signRequest(Yii::$app->request); 365 | 366 | /** @var $controller IssuesController */ 367 | list($controller, ) = Yii::$app->createController('issues'); 368 | $response = $controller->runAction('index'); 369 | $this->assertEquals(['success' => true, 'action' => 'processed'], $response); 370 | 371 | $this->assertCount(3, $this->httpClient->requests); 372 | $request1 = $this->httpClient->requests[0]; 373 | $this->assertEquals('repos/cebe/test/issues', $request1['path']); 374 | $this->assertContains('This issue has originally been reported by @baxterthehacker', $request1['body']); 375 | $this->assertContains('https:\/\/github.com\/baxterthehacker\/public-repo\/issues\/2', $request1['body']); 376 | $this->assertContains('Moved here by @cebe', $request1['body']); 377 | $this->assertContains("It looks like you accidently spelled 'commit' with two 't's.", $request1['body']); 378 | $request2 = $this->httpClient->requests[1]; 379 | $this->assertEquals('repos/baxterthehacker/public-repo/issues/2/comments', $request2['path']); 380 | $this->assertContains('Issue moved to', $request2['body']); 381 | $request3 = $this->httpClient->requests[2]; 382 | $this->assertEquals('repos/baxterthehacker/public-repo/issues/2', $request3['path']); 383 | $this->assertContains('{"state":"closed"}', $request3['body']); 384 | } 385 | 386 | public function testIssueOnLabelMoveIgnoreIfSentByMe() 387 | { 388 | $_SERVER['REQUEST_METHOD'] = 'POST'; 389 | $_SERVER["CONTENT_TYPE"] = 'application/json'; 390 | Yii::$app->request->headers->add('X-Github-Event', 'issues'); 391 | // body is reduced size, real github request is more verbose 392 | Yii::$app->request->rawBody = <<signRequest(Yii::$app->request); 434 | 435 | /** @var $controller IssuesController */ 436 | list($controller, ) = Yii::$app->createController('issues'); 437 | $response = $controller->runAction('index'); 438 | $this->assertEquals(['success' => true, 'action' => 'ignored'], $response); 439 | 440 | $this->assertCount(0, $this->httpClient->requests); 441 | } 442 | 443 | public function testIssueOnLabelMoveIgnoreIfSameRepo() 444 | { 445 | $_SERVER['REQUEST_METHOD'] = 'POST'; 446 | $_SERVER["CONTENT_TYPE"] = 'application/json'; 447 | Yii::$app->request->headers->add('X-Github-Event', 'issues'); 448 | // body is reduced size, real github request is more verbose 449 | Yii::$app->request->rawBody = <<signRequest(Yii::$app->request); 491 | 492 | /** @var $controller IssuesController */ 493 | list($controller, ) = Yii::$app->createController('issues'); 494 | $response = $controller->runAction('index'); 495 | $this->assertEquals(['success' => true, 'action' => 'processed'], $response); 496 | 497 | $this->assertCount(0, $this->httpClient->requests); 498 | } 499 | 500 | /** 501 | * moving PRs should not work 502 | */ 503 | public function testPrOnLabelDontMove() 504 | { 505 | $_SERVER['REQUEST_METHOD'] = 'POST'; 506 | $_SERVER["CONTENT_TYPE"] = 'application/json'; 507 | Yii::$app->request->headers->add('X-Github-Event', 'pull_request'); 508 | // body is reduced size, real github request is more verbose 509 | Yii::$app->request->rawBody = <<signRequest(Yii::$app->request); 539 | 540 | /** @var $controller IssuesController */ 541 | list($controller, ) = Yii::$app->createController('issues'); 542 | $response = $controller->runAction('index'); 543 | $this->assertEquals(['success' => true, 'action' => 'processed'], $response); 544 | 545 | $this->assertCount(0, $this->httpClient->requests); 546 | } 547 | 548 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | destroyApplication(); 22 | } 23 | 24 | /** 25 | * Destroys application in Yii::$app by setting it to null. 26 | */ 27 | protected function destroyApplication() 28 | { 29 | Yii::$app = null; 30 | Yii::$container = new Container(); 31 | } 32 | 33 | /** 34 | * Populates Yii::$app with a new application 35 | * The application will be destroyed on tearDown() automatically. 36 | * @param array $config The application configuration, if needed 37 | * @param string $appClass name of the application class to create 38 | */ 39 | protected function mockApplication($config = [], $appClass = '\yii\console\Application') 40 | { 41 | return new $appClass(ArrayHelper::merge([ 42 | 'id' => 'testapp', 43 | 'basePath' => dirname(__DIR__), 44 | 'vendorPath' => dirname(__DIR__) . '/vendor', 45 | ], $config)); 46 | } 47 | 48 | protected function mockWebApplication($config = [], $appClass = '\yii\web\Application') 49 | { 50 | return new $appClass(ArrayHelper::merge([ 51 | 'id' => 'testapp', 52 | 'basePath' => dirname(__DIR__), 53 | 'vendorPath' => dirname(__DIR__) . '/vendor', 54 | 'components' => [ 55 | 'request' => [ 56 | 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', 57 | 'scriptFile' => __DIR__ . '/index.php', 58 | 'scriptUrl' => '/index.php', 59 | ], 60 | ] 61 | ], $config)); 62 | } 63 | 64 | /** 65 | * Returns invisible property 66 | * @param string $property 67 | * @param string|object $mixed 68 | * @return mixed 69 | */ 70 | protected function getInvisibleProperty($propertyName, $mixed) 71 | { 72 | $class = new \ReflectionClass($mixed); 73 | $property = $class->getProperty($propertyName); 74 | $property->setAccessible(true); 75 | $value = $property->getValue($mixed); 76 | $property->setAccessible(false); 77 | return $value; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class CachedHttpClientMock extends CachedHttpClient 15 | { 16 | const DUMMY_TOKEN = 'correct-token'; 17 | const EXCEPTION_BAD_CREDENTIALS_MSG = 'Bad credentials'; 18 | const EXCEPTION_BAD_CREDENTIALS_CODE = 401; 19 | 20 | public $requests = []; 21 | 22 | /** 23 | * @inheritdoc 24 | */ 25 | public function request($path, $body = null, $httpMethod = 'GET', array $headers = array(), array $options = array()) 26 | { 27 | if (Yii::$app->params['github_token'] !== self::DUMMY_TOKEN) { 28 | throw new RuntimeException(self::EXCEPTION_BAD_CREDENTIALS_MSG, self::EXCEPTION_BAD_CREDENTIALS_CODE); 29 | } else { 30 | 31 | $this->requests[] = [ 32 | 'path' => $path, 33 | 'body' => $body, 34 | 'method' => $httpMethod, 35 | 'headers' => $headers, 36 | 'options' => $options, 37 | ]; 38 | 39 | $response = new \Guzzle\Http\Message\Response(200, [], []); 40 | switch($path) { 41 | case 'repos/cebe/test/issues': 42 | $response->addHeader('Content-Type', 'application/json'); 43 | $response->setBody(<< 11 | */ 12 | class GithubControllerMock extends GithubController 13 | { 14 | /** 15 | * @var string output buffer. 16 | */ 17 | private $stdOutBuffer = ''; 18 | 19 | public function stdout($string) 20 | { 21 | $this->stdOutBuffer .= $string; 22 | } 23 | 24 | public function flushStdOutBuffer() 25 | { 26 | $result = $this->stdOutBuffer; 27 | $this->stdOutBuffer = ''; 28 | return $result; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /tests/mocks/HooksMock.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class HooksMock extends Hooks 12 | { 13 | /** 14 | * Mock for all() 15 | * @see Repo::all() 16 | */ 17 | public function all($username, $repository) 18 | { 19 | return [ 20 | [ 21 | 'type' => 'Repository', 22 | 'id' => '12345', 23 | 'name' => 'web', 24 | 'active' => 1, 25 | 'events' => [ 26 | 0 => 'push' 27 | ], 28 | 'config' => [ 29 | 'content_type' => 'json', 30 | 'insecure_ssl' => 0, 31 | 'secret' => '********', 32 | 'url' => 'http://www.domain.com/payload', 33 | ], 34 | 'updated_at' => 'Repository', 35 | 'created_at' => 'Repository', 36 | 'url' => 'Repository', 37 | 'test_url' => 'Repository', 38 | 'ping_url' => 'Repository', 39 | 'last_response' => [ 40 | 'code' => 422, 41 | 'status' => 'misconfigured', 42 | 'message' => 'Invalid HTTP Response: 404' 43 | ] 44 | ] 45 | ]; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/mocks/RepoMock.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class RepoMock extends Repo 13 | { 14 | public function hooks() 15 | { 16 | return new HooksMock($this->client); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/mocks/sleep.php: -------------------------------------------------------------------------------- 1 | 'yii-web', 18 | 'basePath' => dirname(__DIR__), 19 | 'controllerNamespace' => 'app\controllers', 20 | 'params' => $config, 21 | 'vendorPath' => dirname(__DIR__) . '/vendor', 22 | 'runtimePath' => dirname(__DIR__) . '/tmp', 23 | 24 | 'bootstrap' => ['log'], 25 | 'components' => [ 26 | 'github' => 'app\components\Github', 27 | 28 | 'request' => [ 29 | 'enableCsrfValidation' => false, 30 | 'enableCookieValidation' => false, 31 | 'parsers' => [ 32 | 'application/json' => 'yii\web\JsonParser', 33 | ], 34 | ], 35 | 36 | 'cache' => [ 37 | 'class' => 'yii\caching\FileCache', 38 | ], 39 | 40 | 'errorHandler' => [ 41 | 'errorAction' => 'site/error', 42 | ], 43 | 44 | 'mailer' => [ 45 | 'class' => 'yii\swiftmailer\Mailer', 46 | // send all mails to a file by default. You have to set 47 | // 'useFileTransport' to false and configure a transport 48 | // for the mailer to send real emails. 49 | 'useFileTransport' => false, 50 | ], 51 | 52 | 'log' => [ 53 | 'traceLevel' => YII_DEBUG ? 3 : 0, 54 | 'targets' => [ 55 | [ 56 | 'class' => 'yii\log\FileTarget', 57 | 'logFile' => '@app/logs/access.log', 58 | 'categories' => ['request'], 59 | 'logVars' => [], 60 | 'enableRotation' => false, 61 | ], 62 | [ 63 | 'class' => 'yii\log\FileTarget', 64 | 'logFile' => '@app/logs/access-full.log', 65 | 'categories' => ['request'], 66 | 'logVars' => ['_GET', '_POST'], 67 | ], 68 | [ 69 | 'class' => 'yii\log\FileTarget', 70 | 'logFile' => '@app/logs/actions.log', 71 | 'categories' => ['action'], 72 | 'logVars' => [], 73 | 'enableRotation' => false, 74 | ], 75 | [ 76 | 'class' => 'yii\log\FileTarget', 77 | 'logFile' => '@app/logs/error.log', 78 | 'levels' => ['error', 'warning'], 79 | 'except' => ['yii\web\HttpException:404', 'action'], 80 | ], 81 | [ 82 | 'class' => 'yii\log\EmailTarget', 83 | 'levels' => ['error'], 84 | 'except' => ['yii\web\HttpException:404', 'action'], 85 | 'message' => [ 86 | 'from' => ['bot@cebe.cc'], 87 | 'to' => ['hostmaster+yiibot@cebe.cc'], 88 | 'subject' => 'Yii-bot error', 89 | ], 90 | ], 91 | ], 92 | ], 93 | ], 94 | 95 | 'on beforeAction' => function($event) { 96 | Yii::info('Request ' . Yii::$app->request->method . ' r"' . Yii::$app->requestedRoute . '" from ' . Yii::$app->request->userIP . ', UserAgent: ' . Yii::$app->request->userAgent, 'request'); 97 | if (!empty(Yii::$app->request->bodyParams)) { 98 | Yii::info(Yii::$app->request->bodyParams, 'request'); 99 | } 100 | } 101 | 102 | ]); 103 | 104 | $application->run(); 105 | -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /yii: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 'yii-console', 20 | 'basePath' => __DIR__, 21 | 'controllerNamespace' => 'app\commands', 22 | 'enableCoreCommands' => false, 23 | 'params' => $config, 24 | 'vendorPath' => __DIR__ . '/vendor', 25 | 26 | 'components' => [ 27 | 'github' => 'app\components\Github', 28 | ], 29 | 30 | ]); 31 | 32 | $exitCode = $application->run(); 33 | exit($exitCode); 34 | --------------------------------------------------------------------------------