├── Procfile ├── tests ├── bootstrap.php ├── RedirectTest.php ├── GenerateDeviceCodeTest.php ├── VerifyUserCodeTest.php └── AccessTokenRequestTest.php ├── .gitignore ├── heroku-env.png ├── public ├── assets │ └── styles.css └── index.php ├── views ├── error.php ├── signed-in.php ├── index.php ├── layout.php └── device.php ├── CONTRIBUTING.md ├── phpunit.xml ├── .env.example ├── app.json ├── .travis.yml ├── composer.json ├── nginx.conf ├── LICENSE.txt ├── client.php ├── lib └── helpers.php ├── README.md ├── controllers └── Controller.php └── composer.lock /Procfile: -------------------------------------------------------------------------------- 1 | web: vendor/bin/heroku-php-nginx -C nginx.conf public/ 2 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => 'Error']); ?> 2 | 3 |

4 | 5 |

6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | By submitting code to this project, you agree to irrevocably release it under the same license as this project. See README.md for more details. 2 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/ 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | BASE_URL=http://localhost:8080 2 | LIMIT_REQUESTS_PER_MINUTE=12 3 | AUTHORIZATION_ENDPOINT=https://example.com/oauth2/authorize 4 | TOKEN_ENDPOINT=https://example.com/oauth2/token 5 | REDIS_URL=localhost:6379 6 | -------------------------------------------------------------------------------- /views/signed-in.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => $title]); ?> 2 | 3 |

You successfully signed in! Now return to your device to finish.

4 | 5 | 8 | -------------------------------------------------------------------------------- /views/index.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => $title]); ?> 2 | 3 |

OAuth 2.0 Device Flow

4 |

This is a server that is used to sign in to OAuth services from devices like an Apple TV. You'll need to start the process from the device.

5 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OAuth Device Flow Proxy Server", 3 | "description": "An application showing how to create a Device Flow Proxy Server in PHP on Heroku.", 4 | "keywords": ["oauth", "php", "demo", "example"], 5 | "repository": "https://github.com/aaronpk/Device-Flow-Proxy-Server", 6 | "addons": [ 7 | "heroku-redis" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 7.2 4 | sudo: false 5 | services: 6 | - redis 7 | before_script: 8 | - composer self-update 9 | - composer install --prefer-dist --no-interaction 10 | env: 11 | - BASE_URL=http://localhost:8080 LIMIT_REQUESTS_PER_MINUTE=12 AUTHORIZATION_ENDPOINT=https://example.com/oauth2/authorize TOKEN_ENDPOINT=https://example.com/oauth2/token 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "php": "^7.2.0", 4 | "league/route": "~1.2", 5 | "league/plates": "~3.1", 6 | "predis/predis": "^1.1", 7 | "vlucas/phpdotenv": "^3.6" 8 | }, 9 | "require-dev": { 10 | "phpunit/phpunit": "*" 11 | }, 12 | "autoload": { 13 | "files": [ 14 | "lib/helpers.php", 15 | "controllers/Controller.php" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | add_header X-Frame-Options "SAMEORIGIN"; 2 | add_header X-XSS-Protection "1; mode=block"; 3 | add_header X-Content-Type-Options "nosniff"; 4 | 5 | index index.php; 6 | 7 | charset utf-8; 8 | 9 | location / { 10 | try_files $uri $uri/ /index.php?$query_string; 11 | } 12 | 13 | location = /favicon.ico { access_log off; log_not_found off; } 14 | location = /robots.txt { access_log off; log_not_found off; } 15 | 16 | error_page 404 /index.php; 17 | -------------------------------------------------------------------------------- /views/layout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <?= $this->e($title) ?> 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | section('content') ?> 13 |
14 | 15 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 6 | -------------------------------------------------------------------------------- /views/device.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => 'Enter Device Code']); ?> 2 | 3 | 4 |

Confirm the code below matches the code shown on the device.

5 | 6 |

Enter the code shown on your device to continue.

7 | 8 | 9 |
10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | addRoute('GET', '/', 'Controller::index'); 11 | 12 | # Browser routes 13 | $router->addRoute('GET', '/device', 'Controller::device'); 14 | $router->addRoute('GET', '/auth/verify_code', 'Controller::verify_code'); 15 | $router->addRoute('GET', '/auth/redirect', 'Controller::redirect'); 16 | 17 | # Device API 18 | $router->addRoute('POST', '/device/code', 'Controller::generate_code'); 19 | $router->addRoute('POST', '/device/token', 'Controller::access_token'); 20 | 21 | $dispatcher = $router->getDispatcher(); 22 | $request = Request::createFromGlobals(); 23 | $response = $dispatcher->dispatch($request->getMethod(), $request->getPathInfo()); 24 | $response->send(); 25 | -------------------------------------------------------------------------------- /tests/RedirectTest.php: -------------------------------------------------------------------------------- 1 | redirect($request, $response); 15 | 16 | $html = $response->getContent(); 17 | $this->assertStringContainsString('Invalid Request', $html); 18 | } 19 | 20 | public function testInvalidState() { 21 | $controller = new Controller(); 22 | 23 | $request = new Request(['code'=>'foo', 'state'=>'foo']); 24 | $response = new Response(); 25 | $response = $controller->redirect($request, $response); 26 | 27 | $html = $response->getContent(); 28 | $this->assertStringContainsString('Invalid State', $html); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /client.php: -------------------------------------------------------------------------------- 1 | $client_id, 14 | ]; 15 | curl_setopt($ch, CURLOPT_POST, true); 16 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); 17 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 18 | $response = curl_exec($ch); 19 | $start = json_decode($response, true); 20 | 21 | if(!isset($start['device_code'])) { 22 | echo "Something went wrong trying to start the Device Flow\n"; 23 | echo "Here is the raw response from the server:\n"; 24 | echo $response."\n"; 25 | die(); 26 | } 27 | 28 | echo "Please visit this URL in your browser, and confirm the code in the browser matches the code shown:\n"; 29 | 30 | echo $start['verification_uri'].'?code='.$start['user_code']."\n"; 31 | echo $start['user_code']."\n"; 32 | 33 | $done = false; 34 | while($done == false) { 35 | sleep($start['interval']); 36 | 37 | $ch = curl_init($base_url.'/device/token'); 38 | $params = [ 39 | 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', 40 | 'client_id' => $client_id, 41 | 'device_code' => $start['device_code'], 42 | ]; 43 | curl_setopt($ch, CURLOPT_POST, true); 44 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); 45 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 46 | $response = curl_exec($ch); 47 | $token = json_decode($response, true); 48 | 49 | if(isset($token['access_token'])) { 50 | echo "You successfully logged in!\n"; 51 | echo "Here is the access token returned from the server:\n"; 52 | echo $token['access_token']."\n"; 53 | die(); 54 | } 55 | if(isset($token['error']) && $token['error'] != 'authorization_pending') { 56 | echo "The token endpoint returned an unrecoverable error:\n"; 57 | echo $response."\n"; 58 | die(); 59 | } 60 | # go back to the top and wait... 61 | } 62 | -------------------------------------------------------------------------------- /lib/helpers.php: -------------------------------------------------------------------------------- 1 | load(); 8 | } 9 | 10 | // Check if environment variables are defined, or return an error 11 | $required = ['BASE_URL', 'LIMIT_REQUESTS_PER_MINUTE', 'AUTHORIZATION_ENDPOINT', 'TOKEN_ENDPOINT']; 12 | $complete = true; 13 | foreach($required as $r) { 14 | if(!getenv($r)) 15 | $complete = false; 16 | } 17 | if(!$complete) { 18 | echo "Missing app configuration.\n"; 19 | echo "Please copy .env.example to .env and fill out the variables, or\n"; 20 | echo "define all environment variables accordingly.\n"; 21 | die(1); 22 | } 23 | 24 | if(getenv('REDIS_URL')) { 25 | $result = Cache::connect(getenv('REDIS_URL')); 26 | } 27 | 28 | 29 | function view($template, $data=[]) { 30 | global $templates; 31 | return $templates->render($template, $data); 32 | } 33 | 34 | function base64_urlencode($string) { 35 | return rtrim(strtr(base64_encode($string), '+/', '-_'), '='); 36 | } 37 | 38 | function random_alpha_string($len) { 39 | $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; 40 | $str = ''; 41 | for($i=0; $i<$len; $i++) 42 | $str .= substr($chars, random_int(0, strlen($chars)-1), 1); 43 | return $str; 44 | } 45 | 46 | class Cache { 47 | private static $redis; 48 | 49 | public static function connect($host=false) { 50 | if(!isset(self::$redis)) { 51 | if($host) { 52 | self::$redis = new Predis\Client($host); 53 | } else { 54 | self::$redis = new Predis\Client(); 55 | } 56 | } 57 | } 58 | 59 | public static function set($key, $value, $exp=600) { 60 | self::connect(); 61 | self::$redis->setex($key, $exp, json_encode($value)); 62 | } 63 | 64 | public static function get($key) { 65 | self::connect(); 66 | $data = self::$redis->get($key); 67 | if($data) { 68 | return json_decode($data); 69 | } else { 70 | return null; 71 | } 72 | } 73 | 74 | public static function add($key, $value, $exp=600) { 75 | self::connect(); 76 | self::$redis->setex($key, $exp, json_encode($value)); 77 | } 78 | 79 | public static function expire($key, $exp) { 80 | self::connect(); 81 | self::$redis->expire($key, $exp); 82 | } 83 | 84 | public static function incr($key, $value=1) { 85 | self::connect(); 86 | self::$redis->incrby($key, $value); 87 | } 88 | 89 | public static function delete($key) { 90 | self::connect(); 91 | self::$redis->del($key); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/GenerateDeviceCodeTest.php: -------------------------------------------------------------------------------- 1 | generate_code($request, $response); 12 | $data = json_decode($response->getContent()); 13 | $this->assertEquals($data->error, 'invalid_request'); 14 | } 15 | 16 | public function testGeneratesCode() { 17 | $controller = new Controller(); 18 | $request = new Request(['client_id'=>'x']); 19 | $response = new Response(); 20 | $response = $controller->generate_code($request, $response); 21 | $data = json_decode($response->getContent()); 22 | # Make sure there's no error 23 | $this->assertObjectNotHasAttribute('error', $data); 24 | # Check the expected properties exist 25 | $this->assertObjectHasAttribute('device_code', $data); 26 | $this->assertObjectHasAttribute('user_code', $data); 27 | $this->assertObjectHasAttribute('verification_uri', $data); 28 | # Make sure the values are as expected 29 | $this->assertStringMatchesFormat('%x', $data->device_code); 30 | $this->assertStringMatchesFormat('%s', $data->user_code); 31 | $this->assertStringMatchesFormat('%s/device', $data->verification_uri); 32 | # Check that the info is cached against the user code 33 | $cache = Cache::get(str_replace('-','',$data->user_code)); 34 | $this->assertNotNull($cache); 35 | $this->assertEquals($cache->client_id, 'x'); 36 | $this->assertEquals($cache->device_code, $data->device_code); 37 | } 38 | 39 | public function testGeneratesCodeWithScope() { 40 | $controller = new Controller(); 41 | $request = new Request(['response_type'=>'device_code', 'client_id'=>'x', 'scope'=>'user']); 42 | $response = new Response(); 43 | $response = $controller->generate_code($request, $response); 44 | $data = json_decode($response->getContent()); 45 | # Make sure there's no error 46 | $this->assertObjectNotHasAttribute('error', $data); 47 | # Check that the info is cached against the user code 48 | $cache = Cache::get(str_replace('-','',$data->user_code)); 49 | $this->assertNotNull($cache); 50 | $this->assertEquals($cache->client_id, 'x'); 51 | $this->assertEquals($cache->device_code, $data->device_code); 52 | $this->assertEquals($cache->scope, 'user'); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /tests/VerifyUserCodeTest.php: -------------------------------------------------------------------------------- 1 | verify_code($request, $response); 13 | 14 | $error = $response->getContent(); 15 | $this->assertStringContainsString('No code was entered', $error); 16 | } 17 | 18 | public function testInvalidUserCode() { 19 | $controller = new Controller(); 20 | 21 | $request = new Request(['code'=>'xxxx']); 22 | $response = new Response(); 23 | $response = $controller->verify_code($request, $response); 24 | 25 | $error = $response->getContent(); 26 | $this->assertStringContainsString('invalid_request', $error); 27 | $this->assertStringContainsString('Code not found', $error); 28 | } 29 | 30 | public function testRedirectsToAuthServerGivenCode() { 31 | $controller = new Controller(); 32 | 33 | # First generate a code 34 | $request = new Request(['response_type'=>'device_code', 'client_id'=>'x']); 35 | $response = new Response(); 36 | $response = $controller->generate_code($request, $response); 37 | $data = json_decode($response->getContent()); 38 | 39 | $request = new Request(['code'=>$data->user_code]); 40 | $response = new Response(); 41 | $response = $controller->verify_code($request, $response); 42 | 43 | $responseString = $response->__toString(); 44 | preg_match('/Location:\s+([^\s]+)/', $responseString, $location); 45 | $authURL = parse_url($location[1]); 46 | parse_str($authURL['query'], $params); 47 | 48 | $this->assertEquals('code', $params['response_type']); 49 | $this->assertEquals('x', $params['client_id']); 50 | $this->assertEquals(getenv('BASE_URL') . '/auth/redirect', $params['redirect_uri']); 51 | $this->assertArrayNotHasKey('scope', $params); 52 | $this->assertNotEmpty($params['state']); 53 | } 54 | 55 | public function testRedirectsToAuthServerWithScopeGivenCode() { 56 | $controller = new Controller(); 57 | 58 | # First generate a code 59 | $request = new Request(['response_type'=>'device_code', 'client_id'=>'x', 'scope'=>'foo']); 60 | $response = new Response(); 61 | $response = $controller->generate_code($request, $response); 62 | $data = json_decode($response->getContent()); 63 | 64 | $request = new Request(['code'=>$data->user_code]); 65 | $response = new Response(); 66 | $response = $controller->verify_code($request, $response); 67 | 68 | $responseString = $response->__toString(); 69 | preg_match('/Location:\s+([^\s]+)/', $responseString, $location); 70 | $authURL = parse_url($location[1]); 71 | parse_str($authURL['query'], $params); 72 | 73 | $this->assertEquals('code', $params['response_type']); 74 | $this->assertEquals('x', $params['client_id']); 75 | $this->assertEquals('foo', $params['scope']); 76 | $this->assertEquals(getenv('BASE_URL') . '/auth/redirect', $params['redirect_uri']); 77 | $this->assertNotEmpty($params['state']); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OAuth 2.0 Device Flow Proxy Server 2 | ================================== 3 | 4 | A demonstration of the OAuth 2.0 Device Code flow for devices without a browser or with limited keyboard entry. 5 | 6 | This service acts as an OAuth server that implements the device code flow, proxying to a real OAuth server behind the scenes. 7 | 8 | Installation 9 | ------------ 10 | 11 | ``` 12 | composer install 13 | cp .env.example .env 14 | ``` 15 | 16 | In the `.env` file, fill out the required variables. 17 | 18 | You will need to install Redis if it is not already on your system, or point to an existing redis server in the config file. 19 | 20 | Define your OAuth server's authorization endpoint and token endpoint URL. 21 | 22 | ### Heroku Deploy 23 | 24 | To deploy this in Heroku, you'll need to do the following: 25 | 26 | Create a new Heroku application, and take note of the name. 27 | 28 | Define environment variables in Heroku's admin interface based on the values from `.env.example` with the exception of `REDIS_URL`. 29 | 30 | ![Heroku Config](heroku-env.png) 31 | 32 | ``` 33 | # Log in to your Heroku account 34 | heroku login 35 | 36 | # Define the Heroku upstream git repo from your app name 37 | heroku git:remote -a oauth-device-flow-demo 38 | 39 | # Enable Redis for your application 40 | heroku addons:create heroku-redis:hobby-dev 41 | 42 | # Deploy to Heroku 43 | git push heroku master 44 | ``` 45 | 46 | 47 | Usage 48 | ----- 49 | 50 | The device will need to register an application at the OAuth server to get a client ID. You'll need to set the proxy's URL as the callback URL in the OAuth application registration: 51 | 52 | ``` 53 | http://localhost:8080/auth/redirect 54 | ``` 55 | 56 | The device can begin the flow by making a POST request to this proxy: 57 | 58 | ``` 59 | curl http://localhost:8080/device/code -d client_id=1234567890 60 | ``` 61 | 62 | The response will contain the URL the user should visit and the code they should enter, as well as a long device code. 63 | 64 | ```json 65 | { 66 | "device_code": "5cb3a6029c967a7b04f642a5b92b5cca237ec19d41853f55dcce98a4d2aa528f", 67 | "user_code": "248707", 68 | "verification_uri": "http://localhost:8080/device", 69 | "expires_in": 300, 70 | "interval": 5 71 | } 72 | ``` 73 | 74 | The device should instruct the user to visit the URL and enter the code, or can provide a full link that pre-fills the code for the user in case the device is displaying a QR code. 75 | 76 | `http://localhost:8080/device?code=248707` 77 | 78 | The device should then poll the token endpoint at the interval provided, making a POST request like the below: 79 | 80 | ``` 81 | curl http://localhost:8080/device/token -d grant_type=urn:ietf:params:oauth:grant-type:device_code \ 82 | -d client_id=1234567890 \ 83 | -d device_code=5cb3a6029c967a7b04f642a5b92b5cca237ec19d41853f55dcce98a4d2aa528f 84 | ``` 85 | 86 | While the user is busy logging in, the response will be 87 | 88 | ``` 89 | {"error":"authorization_pending"} 90 | ``` 91 | 92 | Once the user has finished logging in and granting access to the application, the response will contain an access token. 93 | 94 | ```json 95 | { 96 | "access_token": "FmcTZiYmJzeWpoeTdUSTBoNyIsInVpZCI6IjAwdWJ1NG1", 97 | "token_type": "Bearer", 98 | "expires_in": 7200 99 | } 100 | ``` 101 | -------------------------------------------------------------------------------- /tests/AccessTokenRequestTest.php: -------------------------------------------------------------------------------- 1 | access_token($request, $response); 13 | 14 | $data = json_decode($response->getContent()); 15 | $this->assertEquals('invalid_request', $data->error); 16 | } 17 | 18 | public function testRequestMissingParameters() { 19 | $controller = new Controller(); 20 | 21 | $request = new Request(['grant_type' => 'urn:ietf:params:oauth:grant-type:device_code']); 22 | $response = new Response(); 23 | $response = $controller->access_token($request, $response); 24 | 25 | $data = json_decode($response->getContent()); 26 | $this->assertEquals('invalid_request', $data->error); 27 | 28 | $request = new Request(['grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', 'code' => 'foo']); 29 | $response = new Response(); 30 | $response = $controller->access_token($request, $response); 31 | 32 | $data = json_decode($response->getContent()); 33 | $this->assertEquals('invalid_request', $data->error); 34 | } 35 | 36 | public function testInvalidGrantType() { 37 | $controller = new Controller(); 38 | 39 | $request = new Request(['grant_type' => 'foo', 'code' => 'foo'.microtime(true), 'client_id' => 'bar']); 40 | $response = new Response(); 41 | $response = $controller->access_token($request, $response); 42 | 43 | $data = json_decode($response->getContent()); 44 | $this->assertEquals('invalid_request', $data->error); 45 | } 46 | 47 | public function testInvalidAuthorizationCode() { 48 | $controller = new Controller(); 49 | 50 | $request = new Request(['grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', 'device_code' => 'foo.'.microtime(true), 'client_id' => 'bar']); 51 | $response = new Response(); 52 | $response = $controller->access_token($request, $response); 53 | 54 | $data = json_decode($response->getContent()); 55 | $this->assertEquals('invalid_grant', $data->error); 56 | } 57 | 58 | public function testRateLimiting() { 59 | $controller = new Controller(); 60 | 61 | $request = new Request(['grant_type'=>'urn:ietf:params:oauth:grant-type:device_code', 'device_code'=>'foo.'.microtime(true), 'client_id'=>'bar']); 62 | $response = new Response(); 63 | 64 | for($i=0; $i<12; $i++) { 65 | $response_data = $controller->access_token($request, $response); 66 | $data = json_decode($response_data->getContent()); 67 | $this->assertNotEquals('slow_down', $data->error); 68 | } 69 | 70 | $response_data = $controller->access_token($request, $response); 71 | $data = json_decode($response_data->getContent()); 72 | $this->assertEquals('slow_down', $data->error); 73 | } 74 | 75 | public function testAuthorizationPending() { 76 | # obtain a device code 77 | $controller = new Controller(); 78 | $response = new Response(); 79 | 80 | $request = new Request(['response_type'=>'device_code', 'client_id'=>'x']); 81 | $response_data = $controller->generate_code($request, $response); 82 | $data = json_decode($response_data->getContent()); 83 | $this->assertObjectNotHasAttribute('error', $data); 84 | 85 | $device_code = $data->device_code; 86 | 87 | # check the status of the device code 88 | $request = new Request(['grant_type'=>'urn:ietf:params:oauth:grant-type:device_code', 'client_id'=>'x', 'device_code'=>$device_code]); 89 | $response_data = $controller->access_token($request, $response); 90 | $data = json_decode($response_data->getContent()); 91 | 92 | $this->assertEquals('authorization_pending', $data->error); 93 | } 94 | 95 | public function testAccessTokenGranted() { 96 | # obtain a device code 97 | $controller = new Controller(); 98 | $response = new Response(); 99 | 100 | $request = new Request(['response_type'=>'device_code', 'client_id'=>'x']); 101 | $response_data = $controller->generate_code($request, $response); 102 | $data = json_decode($response_data->getContent()); 103 | $this->assertObjectNotHasAttribute('error', $data); 104 | 105 | $device_code = $data->device_code; 106 | 107 | # simulate the access token being granted 108 | Cache::set($device_code, [ 109 | 'status' => 'complete', 110 | 'token_response' => [ 111 | 'access_token' => 'abc123', 112 | 'expires_in' => 600, 113 | 'custom' => 'foo' 114 | ] 115 | ]); 116 | 117 | # check the status of the device code 118 | $request = new Request(['grant_type'=>'urn:ietf:params:oauth:grant-type:device_code', 'client_id'=>'x', 'device_code'=>$device_code]); 119 | $response_data = $controller->access_token($request, $response); 120 | $data = json_decode($response_data->getContent()); 121 | 122 | $this->assertObjectNotHasAttribute('error', $data); 123 | $this->assertEquals('abc123', $data->access_token); 124 | $this->assertEquals(600, $data->expires_in); 125 | $this->assertEquals('foo', $data->custom); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /controllers/Controller.php: -------------------------------------------------------------------------------- 1 | $error 10 | ]; 11 | if($error_description) { 12 | $data['error_description'] = $error_description; 13 | } 14 | 15 | $response->setStatusCode(400); 16 | $response->setContent($this->_json($data)); 17 | $response->headers->set('Content-Type', 'application/json'); 18 | return $response; 19 | } 20 | 21 | private function html_error(Response $response, $error, $error_description) { 22 | $response->setStatusCode(400); 23 | $response->setContent(view('error', [ 24 | 'error' => $error, 25 | 'error_description' => $error_description 26 | ])); 27 | return $response; 28 | } 29 | 30 | private function success(Response $response, $data) { 31 | $response->setContent($this->_json($data)); 32 | $response->headers->set('Content-Type', 'application/json'); 33 | return $response; 34 | } 35 | 36 | # Home Page 37 | public function index(Request $request, Response $response) { 38 | $response->setContent(view('index', [ 39 | 'title' => 'Device Flow Proxy Server' 40 | ])); 41 | return $response; 42 | } 43 | 44 | # A device submits a request here to generate a new device and user code 45 | public function generate_code(Request $request, Response $response) { 46 | # Params: 47 | # client_id 48 | # scope 49 | 50 | # client_id is required 51 | if($request->get('client_id') == null) { 52 | return $this->error($response, 'invalid_request', 'Missing client_id'); 53 | } 54 | 55 | # We've validated everything we can at this stage. 56 | # Generate a verification code and cache it along with the other values in the request. 57 | $device_code = bin2hex(random_bytes(32)); 58 | # Generate a PKCE code_verifier and store it in the cache too 59 | $pkce_verifier = bin2hex(random_bytes(32)); 60 | $cache = [ 61 | 'client_id' => $request->get('client_id'), 62 | 'client_secret' => $request->get('client_secret'), 63 | 'scope' => $request->get('scope'), 64 | 'device_code' => $device_code, 65 | 'pkce_verifier' => $pkce_verifier, 66 | ]; 67 | $user_code = random_alpha_string(4).'-'.random_alpha_string(4); 68 | Cache::set(str_replace('-', '', $user_code), $cache, 300); # store without the hyphen 69 | 70 | # Add a placeholder entry with the device code so that the token route knows the request is pending 71 | Cache::set($device_code, [ 72 | 'timestamp' => time(), 73 | 'status' => 'pending' 74 | ], 300); 75 | 76 | $data = [ 77 | 'device_code' => $device_code, 78 | 'user_code' => $user_code, 79 | 'verification_uri' => getenv('BASE_URL') . '/device', 80 | 'expires_in' => 300, 81 | 'interval' => round(60/getenv('LIMIT_REQUESTS_PER_MINUTE')) 82 | ]; 83 | 84 | return $this->success($response, $data); 85 | } 86 | 87 | # The user visits this page in a web browser 88 | # This interface provides a prompt to enter a device code, which then begins the actual OAuth flow 89 | public function device(Request $request, Response $response) { 90 | $response->setContent(view('device', ['code' => $request->get('code')])); 91 | return $response; 92 | } 93 | 94 | # The browser submits a form that is a GET request to this route, which verifies 95 | # and looks up the user code, and then redirects to the real authorization server 96 | public function verify_code(Request $request, Response $response) { 97 | if($request->get('code') == null) { 98 | return $this->html_error($response, 'invalid_request', 'No code was entered'); 99 | } 100 | 101 | $user_code = $request->get('code'); 102 | # Remove hyphens and convert to uppercase to make it easier for users to enter the code 103 | $user_code = strtoupper(str_replace('-', '', $user_code)); 104 | 105 | $cache = Cache::get($user_code); 106 | if(!$cache) { 107 | return $this->html_error($response, 'invalid_request', 'Code not found'); 108 | } 109 | 110 | $state = bin2hex(random_bytes(16)); 111 | Cache::set('state:'.$state, [ 112 | 'user_code' => $user_code, 113 | 'timestamp' => time(), 114 | ], 300); 115 | 116 | $pkce_challenge = base64_urlencode(hash('sha256', $cache->pkce_verifier, true)); 117 | 118 | // TODO: might need to make this configurable to support OAuth servers that have 119 | // custom parameters for the auth endpoint 120 | $query = [ 121 | 'response_type' => 'code', 122 | 'client_id' => $cache->client_id, 123 | 'redirect_uri' => getenv('BASE_URL') . '/auth/redirect', 124 | 'state' => $state, 125 | 'code_challenge' => $pkce_challenge, 126 | 'code_challenge_method' => 'S256', 127 | ]; 128 | if($cache->scope) 129 | $query['scope'] = $cache->scope; 130 | 131 | $authURL = getenv('AUTHORIZATION_ENDPOINT') . '?' . http_build_query($query); 132 | 133 | $response->setStatusCode(302); 134 | $response->headers->set('Location', $authURL); 135 | return $response; 136 | } 137 | 138 | # After the user logs in and authorizes the app on the real auth server, they will 139 | # be redirected back to here. We'll need to exchange the auth code for an access token, 140 | # and then show a message that instructs the user to go back to their TV and wait. 141 | public function redirect(Request $request, Response $response) { 142 | # Verify input params 143 | if($request->get('state') == false || $request->get('code') == false) { 144 | return $this->html_error($response, 'Invalid Request', 'Request was missing parameters'); 145 | } 146 | 147 | # Check that the state parameter matches 148 | if(!($state=Cache::get('state:'.$request->get('state')))) { 149 | return $this->html_error($response, 'Invalid State', 'The state parameter was invalid'); 150 | } 151 | 152 | # Look up the info from the user code provided in the state parameter 153 | $cache = Cache::get($state->user_code); 154 | 155 | # Exchange the authorization code for an access token 156 | 157 | # TODO: Might need to provide a way to customize this request in case of 158 | # non-standard OAuth 2 services 159 | 160 | $params = [ 161 | 'grant_type' => 'authorization_code', 162 | 'code' => $request->get('code'), 163 | 'redirect_uri' => getenv('BASE_URL') . '/auth/redirect', 164 | 'client_id' => $cache->client_id, 165 | 'code_verifier' => $cache->pkce_verifier, 166 | ]; 167 | if($cache->client_secret) { 168 | $params['client_secret'] = $cache->client_secret; 169 | } 170 | 171 | $ch = curl_init(); 172 | curl_setopt($ch, CURLOPT_URL, getenv('TOKEN_ENDPOINT')); 173 | curl_setopt($ch, CURLOPT_POST, true); 174 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); 175 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 176 | $token_response = curl_exec($ch); 177 | $access_token = json_decode($token_response); 178 | 179 | if(!$access_token || !property_exists($access_token, 'access_token')) { 180 | # If there are any problems getting an access token, kill the request and display an error 181 | Cache::delete($state->user_code); 182 | Cache::delete($cache->device_code); 183 | return $this->html_error($response, 'Error Logging In', 'There was an error getting an access token from the service

'.$token_response.'

'); 184 | } 185 | 186 | # Stash the access token in the cache and display a success message 187 | Cache::set($cache->device_code, [ 188 | 'status' => 'complete', 189 | 'token_response' => $access_token 190 | ], 120); 191 | Cache::delete($state->user_code); 192 | 193 | $response->setContent(view('signed-in', [ 194 | 'title' => 'Signed In' 195 | ])); 196 | return $response; 197 | } 198 | 199 | # Meanwhile, the device is continually posting to this route waiting for the user to 200 | # approve the request. Once the user approves the request, this route returns the access token. 201 | # In addition to the standard OAuth error responses defined in https://tools.ietf.org/html/rfc6749#section-4.2.2.1 202 | # the server should return: authorization_pending and slow_down 203 | public function access_token(Request $request, Response $response) { 204 | 205 | if($request->get('device_code') == null || $request->get('client_id') == null || $request->get('grant_type') == null) { 206 | return $this->error($response, 'invalid_request'); 207 | } 208 | 209 | # This server only supports the device_code response type 210 | if($request->get('grant_type') != 'urn:ietf:params:oauth:grant-type:device_code') { 211 | return $this->error($response, 'unsupported_grant_type', 'Only \'urn:ietf:params:oauth:grant-type:device_code\' is supported.'); 212 | } 213 | 214 | $device_code = $request->get('device_code'); 215 | 216 | ##################### 217 | ## RATE LIMITING 218 | 219 | # Count the number of requests per minute 220 | $bucket = 'ratelimit-'.floor(time()/60).'-'.$device_code; 221 | 222 | if(Cache::get($bucket) >= getenv('LIMIT_REQUESTS_PER_MINUTE')) { 223 | return $this->error($response, 'slow_down'); 224 | } 225 | 226 | # Mark for rate limiting 227 | Cache::incr($bucket); 228 | Cache::expire($bucket, 60); 229 | ##################### 230 | 231 | # Check if the device code is in the cache 232 | $data = Cache::get($device_code); 233 | 234 | if(!$data) { 235 | return $this->error($response, 'invalid_grant'); 236 | } 237 | 238 | if($data && $data->status == 'pending') { 239 | return $this->error($response, 'authorization_pending'); 240 | } else if($data && $data->status == 'complete') { 241 | # return the raw access token response from the real authorization server 242 | Cache::delete($device_code); 243 | return $this->success($response, $data->token_response); 244 | } else { 245 | return $this->error($response, 'invalid_grant'); 246 | } 247 | } 248 | 249 | private function _json($data) { 250 | return json_encode($data, JSON_PRETTY_PRINT+JSON_UNESCAPED_SLASHES)."\n"; 251 | } 252 | 253 | } 254 | -------------------------------------------------------------------------------- /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#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "b12d8cb01958e4b6937470f9842b9c00", 8 | "packages": [ 9 | { 10 | "name": "ircmaxell/password-compat", 11 | "version": "v1.0.4", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/ircmaxell/password_compat.git", 15 | "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", 20 | "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", 21 | "shasum": "" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "4.*" 25 | }, 26 | "type": "library", 27 | "autoload": { 28 | "files": [ 29 | "lib/password.php" 30 | ] 31 | }, 32 | "notification-url": "https://packagist.org/downloads/", 33 | "license": [ 34 | "MIT" 35 | ], 36 | "authors": [ 37 | { 38 | "name": "Anthony Ferrara", 39 | "email": "ircmaxell@php.net", 40 | "homepage": "http://blog.ircmaxell.com" 41 | } 42 | ], 43 | "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", 44 | "homepage": "https://github.com/ircmaxell/password_compat", 45 | "keywords": [ 46 | "hashing", 47 | "password" 48 | ], 49 | "time": "2014-11-20T16:49:30+00:00" 50 | }, 51 | { 52 | "name": "league/container", 53 | "version": "1.3.2", 54 | "source": { 55 | "type": "git", 56 | "url": "https://github.com/thephpleague/container.git", 57 | "reference": "7e6c17fe48f76f3b97aeca70dc29c3f3c7c88d15" 58 | }, 59 | "dist": { 60 | "type": "zip", 61 | "url": "https://api.github.com/repos/thephpleague/container/zipball/7e6c17fe48f76f3b97aeca70dc29c3f3c7c88d15", 62 | "reference": "7e6c17fe48f76f3b97aeca70dc29c3f3c7c88d15", 63 | "shasum": "" 64 | }, 65 | "require": { 66 | "php": ">=5.4.0" 67 | }, 68 | "replace": { 69 | "orno/di": "~2.0" 70 | }, 71 | "require-dev": { 72 | "phpunit/phpunit": "4.*" 73 | }, 74 | "type": "library", 75 | "extra": { 76 | "branch-alias": { 77 | "dev-master": "2.0-dev", 78 | "dev-1.x": "1.3-dev" 79 | } 80 | }, 81 | "autoload": { 82 | "psr-4": { 83 | "League\\Container\\": "src" 84 | } 85 | }, 86 | "notification-url": "https://packagist.org/downloads/", 87 | "license": [ 88 | "MIT" 89 | ], 90 | "authors": [ 91 | { 92 | "name": "Phil Bennett", 93 | "email": "philipobenito@gmail.com", 94 | "homepage": "http://philipobenito.github.io", 95 | "role": "Developer" 96 | } 97 | ], 98 | "description": "A fast and intuitive dependency injection container.", 99 | "homepage": "https://github.com/thephpleague/container", 100 | "keywords": [ 101 | "container", 102 | "dependency", 103 | "di", 104 | "injection", 105 | "league" 106 | ], 107 | "time": "2015-04-05T17:14:48+00:00" 108 | }, 109 | { 110 | "name": "league/plates", 111 | "version": "3.3.0", 112 | "source": { 113 | "type": "git", 114 | "url": "https://github.com/thephpleague/plates.git", 115 | "reference": "b1684b6f127714497a0ef927ce42c0b44b45a8af" 116 | }, 117 | "dist": { 118 | "type": "zip", 119 | "url": "https://api.github.com/repos/thephpleague/plates/zipball/b1684b6f127714497a0ef927ce42c0b44b45a8af", 120 | "reference": "b1684b6f127714497a0ef927ce42c0b44b45a8af", 121 | "shasum": "" 122 | }, 123 | "require": { 124 | "php": "^5.3 | ^7.0" 125 | }, 126 | "require-dev": { 127 | "mikey179/vfsstream": "^1.4", 128 | "phpunit/phpunit": "~4.0", 129 | "squizlabs/php_codesniffer": "~1.5" 130 | }, 131 | "type": "library", 132 | "extra": { 133 | "branch-alias": { 134 | "dev-master": "3.0-dev" 135 | } 136 | }, 137 | "autoload": { 138 | "psr-4": { 139 | "League\\Plates\\": "src" 140 | } 141 | }, 142 | "notification-url": "https://packagist.org/downloads/", 143 | "license": [ 144 | "MIT" 145 | ], 146 | "authors": [ 147 | { 148 | "name": "Jonathan Reinink", 149 | "email": "jonathan@reinink.ca", 150 | "role": "Developer" 151 | } 152 | ], 153 | "description": "Plates, the native PHP template system that's fast, easy to use and easy to extend.", 154 | "homepage": "http://platesphp.com", 155 | "keywords": [ 156 | "league", 157 | "package", 158 | "templates", 159 | "templating", 160 | "views" 161 | ], 162 | "time": "2016-12-28T00:14:17+00:00" 163 | }, 164 | { 165 | "name": "league/route", 166 | "version": "1.2.3", 167 | "source": { 168 | "type": "git", 169 | "url": "https://github.com/thephpleague/route.git", 170 | "reference": "079e87a4653b43e2cba47b9e0563179c1c49fcf8" 171 | }, 172 | "dist": { 173 | "type": "zip", 174 | "url": "https://api.github.com/repos/thephpleague/route/zipball/079e87a4653b43e2cba47b9e0563179c1c49fcf8", 175 | "reference": "079e87a4653b43e2cba47b9e0563179c1c49fcf8", 176 | "shasum": "" 177 | }, 178 | "require": { 179 | "league/container": "~1.0", 180 | "nikic/fast-route": "~0.3", 181 | "php": ">=5.4.0", 182 | "symfony/http-foundation": "~2.6" 183 | }, 184 | "replace": { 185 | "orno/http": "~1.0", 186 | "orno/route": "~1.0" 187 | }, 188 | "require-dev": { 189 | "phpunit/phpunit": "4.*" 190 | }, 191 | "type": "library", 192 | "extra": { 193 | "branch-alias": { 194 | "dev-master": "1.0-dev" 195 | } 196 | }, 197 | "autoload": { 198 | "psr-4": { 199 | "League\\Route\\": "src" 200 | } 201 | }, 202 | "notification-url": "https://packagist.org/downloads/", 203 | "license": [ 204 | "MIT" 205 | ], 206 | "authors": [ 207 | { 208 | "name": "Phil Bennett", 209 | "email": "philipobenito@gmail.com", 210 | "homepage": "http://philipobenito.github.io", 211 | "role": "Developer" 212 | } 213 | ], 214 | "description": "A fast routing and dispatch package built on top of FastRoute.", 215 | "homepage": "https://github.com/thephpleague/route", 216 | "keywords": [ 217 | "league", 218 | "route" 219 | ], 220 | "time": "2015-09-11T07:40:31+00:00" 221 | }, 222 | { 223 | "name": "nikic/fast-route", 224 | "version": "v0.8.0", 225 | "source": { 226 | "type": "git", 227 | "url": "https://github.com/nikic/FastRoute.git", 228 | "reference": "5e1f431ed2afe2be5d2bd97fa69b0e99b9ba45e6" 229 | }, 230 | "dist": { 231 | "type": "zip", 232 | "url": "https://api.github.com/repos/nikic/FastRoute/zipball/5e1f431ed2afe2be5d2bd97fa69b0e99b9ba45e6", 233 | "reference": "5e1f431ed2afe2be5d2bd97fa69b0e99b9ba45e6", 234 | "shasum": "" 235 | }, 236 | "require": { 237 | "php": ">=5.4.0" 238 | }, 239 | "type": "library", 240 | "autoload": { 241 | "psr-4": { 242 | "FastRoute\\": "src/" 243 | }, 244 | "files": [ 245 | "src/functions.php" 246 | ] 247 | }, 248 | "notification-url": "https://packagist.org/downloads/", 249 | "license": [ 250 | "BSD-3-Clause" 251 | ], 252 | "authors": [ 253 | { 254 | "name": "Nikita Popov", 255 | "email": "nikic@php.net" 256 | } 257 | ], 258 | "description": "Fast request router for PHP", 259 | "keywords": [ 260 | "router", 261 | "routing" 262 | ], 263 | "time": "2016-03-25T23:46:52+00:00" 264 | }, 265 | { 266 | "name": "phpoption/phpoption", 267 | "version": "1.5.0", 268 | "source": { 269 | "type": "git", 270 | "url": "https://github.com/schmittjoh/php-option.git", 271 | "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed" 272 | }, 273 | "dist": { 274 | "type": "zip", 275 | "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed", 276 | "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed", 277 | "shasum": "" 278 | }, 279 | "require": { 280 | "php": ">=5.3.0" 281 | }, 282 | "require-dev": { 283 | "phpunit/phpunit": "4.7.*" 284 | }, 285 | "type": "library", 286 | "extra": { 287 | "branch-alias": { 288 | "dev-master": "1.3-dev" 289 | } 290 | }, 291 | "autoload": { 292 | "psr-0": { 293 | "PhpOption\\": "src/" 294 | } 295 | }, 296 | "notification-url": "https://packagist.org/downloads/", 297 | "license": [ 298 | "Apache2" 299 | ], 300 | "authors": [ 301 | { 302 | "name": "Johannes M. Schmitt", 303 | "email": "schmittjoh@gmail.com" 304 | } 305 | ], 306 | "description": "Option Type for PHP", 307 | "keywords": [ 308 | "language", 309 | "option", 310 | "php", 311 | "type" 312 | ], 313 | "time": "2015-07-25T16:39:46+00:00" 314 | }, 315 | { 316 | "name": "predis/predis", 317 | "version": "v1.1.1", 318 | "source": { 319 | "type": "git", 320 | "url": "https://github.com/nrk/predis.git", 321 | "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1" 322 | }, 323 | "dist": { 324 | "type": "zip", 325 | "url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1", 326 | "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1", 327 | "shasum": "" 328 | }, 329 | "require": { 330 | "php": ">=5.3.9" 331 | }, 332 | "require-dev": { 333 | "phpunit/phpunit": "~4.8" 334 | }, 335 | "suggest": { 336 | "ext-curl": "Allows access to Webdis when paired with phpiredis", 337 | "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" 338 | }, 339 | "type": "library", 340 | "autoload": { 341 | "psr-4": { 342 | "Predis\\": "src/" 343 | } 344 | }, 345 | "notification-url": "https://packagist.org/downloads/", 346 | "license": [ 347 | "MIT" 348 | ], 349 | "authors": [ 350 | { 351 | "name": "Daniele Alessandri", 352 | "email": "suppakilla@gmail.com", 353 | "homepage": "http://clorophilla.net" 354 | } 355 | ], 356 | "description": "Flexible and feature-complete Redis client for PHP and HHVM", 357 | "homepage": "http://github.com/nrk/predis", 358 | "keywords": [ 359 | "nosql", 360 | "predis", 361 | "redis" 362 | ], 363 | "time": "2016-06-16T16:22:20+00:00" 364 | }, 365 | { 366 | "name": "symfony/http-foundation", 367 | "version": "v2.8.52", 368 | "source": { 369 | "type": "git", 370 | "url": "https://github.com/symfony/http-foundation.git", 371 | "reference": "3929d9fe8148d17819ad0178c748b8d339420709" 372 | }, 373 | "dist": { 374 | "type": "zip", 375 | "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3929d9fe8148d17819ad0178c748b8d339420709", 376 | "reference": "3929d9fe8148d17819ad0178c748b8d339420709", 377 | "shasum": "" 378 | }, 379 | "require": { 380 | "php": ">=5.3.9", 381 | "symfony/polyfill-mbstring": "~1.1", 382 | "symfony/polyfill-php54": "~1.0", 383 | "symfony/polyfill-php55": "~1.0" 384 | }, 385 | "require-dev": { 386 | "symfony/expression-language": "~2.4|~3.0.0" 387 | }, 388 | "type": "library", 389 | "extra": { 390 | "branch-alias": { 391 | "dev-master": "2.8-dev" 392 | } 393 | }, 394 | "autoload": { 395 | "psr-4": { 396 | "Symfony\\Component\\HttpFoundation\\": "" 397 | }, 398 | "exclude-from-classmap": [ 399 | "/Tests/" 400 | ] 401 | }, 402 | "notification-url": "https://packagist.org/downloads/", 403 | "license": [ 404 | "MIT" 405 | ], 406 | "authors": [ 407 | { 408 | "name": "Fabien Potencier", 409 | "email": "fabien@symfony.com" 410 | }, 411 | { 412 | "name": "Symfony Community", 413 | "homepage": "https://symfony.com/contributors" 414 | } 415 | ], 416 | "description": "Symfony HttpFoundation Component", 417 | "homepage": "https://symfony.com", 418 | "time": "2019-11-12T12:34:41+00:00" 419 | }, 420 | { 421 | "name": "symfony/polyfill-ctype", 422 | "version": "v1.12.0", 423 | "source": { 424 | "type": "git", 425 | "url": "https://github.com/symfony/polyfill-ctype.git", 426 | "reference": "550ebaac289296ce228a706d0867afc34687e3f4" 427 | }, 428 | "dist": { 429 | "type": "zip", 430 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", 431 | "reference": "550ebaac289296ce228a706d0867afc34687e3f4", 432 | "shasum": "" 433 | }, 434 | "require": { 435 | "php": ">=5.3.3" 436 | }, 437 | "suggest": { 438 | "ext-ctype": "For best performance" 439 | }, 440 | "type": "library", 441 | "extra": { 442 | "branch-alias": { 443 | "dev-master": "1.12-dev" 444 | } 445 | }, 446 | "autoload": { 447 | "psr-4": { 448 | "Symfony\\Polyfill\\Ctype\\": "" 449 | }, 450 | "files": [ 451 | "bootstrap.php" 452 | ] 453 | }, 454 | "notification-url": "https://packagist.org/downloads/", 455 | "license": [ 456 | "MIT" 457 | ], 458 | "authors": [ 459 | { 460 | "name": "Gert de Pagter", 461 | "email": "BackEndTea@gmail.com" 462 | }, 463 | { 464 | "name": "Symfony Community", 465 | "homepage": "https://symfony.com/contributors" 466 | } 467 | ], 468 | "description": "Symfony polyfill for ctype functions", 469 | "homepage": "https://symfony.com", 470 | "keywords": [ 471 | "compatibility", 472 | "ctype", 473 | "polyfill", 474 | "portable" 475 | ], 476 | "time": "2019-08-06T08:03:45+00:00" 477 | }, 478 | { 479 | "name": "symfony/polyfill-mbstring", 480 | "version": "v1.13.1", 481 | "source": { 482 | "type": "git", 483 | "url": "https://github.com/symfony/polyfill-mbstring.git", 484 | "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f" 485 | }, 486 | "dist": { 487 | "type": "zip", 488 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f", 489 | "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f", 490 | "shasum": "" 491 | }, 492 | "require": { 493 | "php": ">=5.3.3" 494 | }, 495 | "suggest": { 496 | "ext-mbstring": "For best performance" 497 | }, 498 | "type": "library", 499 | "extra": { 500 | "branch-alias": { 501 | "dev-master": "1.13-dev" 502 | } 503 | }, 504 | "autoload": { 505 | "psr-4": { 506 | "Symfony\\Polyfill\\Mbstring\\": "" 507 | }, 508 | "files": [ 509 | "bootstrap.php" 510 | ] 511 | }, 512 | "notification-url": "https://packagist.org/downloads/", 513 | "license": [ 514 | "MIT" 515 | ], 516 | "authors": [ 517 | { 518 | "name": "Nicolas Grekas", 519 | "email": "p@tchwork.com" 520 | }, 521 | { 522 | "name": "Symfony Community", 523 | "homepage": "https://symfony.com/contributors" 524 | } 525 | ], 526 | "description": "Symfony polyfill for the Mbstring extension", 527 | "homepage": "https://symfony.com", 528 | "keywords": [ 529 | "compatibility", 530 | "mbstring", 531 | "polyfill", 532 | "portable", 533 | "shim" 534 | ], 535 | "time": "2019-11-27T14:18:11+00:00" 536 | }, 537 | { 538 | "name": "symfony/polyfill-php54", 539 | "version": "v1.13.1", 540 | "source": { 541 | "type": "git", 542 | "url": "https://github.com/symfony/polyfill-php54.git", 543 | "reference": "dd1618047426412036e98d159940d58a81fc392c" 544 | }, 545 | "dist": { 546 | "type": "zip", 547 | "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/dd1618047426412036e98d159940d58a81fc392c", 548 | "reference": "dd1618047426412036e98d159940d58a81fc392c", 549 | "shasum": "" 550 | }, 551 | "require": { 552 | "php": ">=5.3.3" 553 | }, 554 | "type": "library", 555 | "extra": { 556 | "branch-alias": { 557 | "dev-master": "1.13-dev" 558 | } 559 | }, 560 | "autoload": { 561 | "psr-4": { 562 | "Symfony\\Polyfill\\Php54\\": "" 563 | }, 564 | "files": [ 565 | "bootstrap.php" 566 | ], 567 | "classmap": [ 568 | "Resources/stubs" 569 | ] 570 | }, 571 | "notification-url": "https://packagist.org/downloads/", 572 | "license": [ 573 | "MIT" 574 | ], 575 | "authors": [ 576 | { 577 | "name": "Nicolas Grekas", 578 | "email": "p@tchwork.com" 579 | }, 580 | { 581 | "name": "Symfony Community", 582 | "homepage": "https://symfony.com/contributors" 583 | } 584 | ], 585 | "description": "Symfony polyfill backporting some PHP 5.4+ features to lower PHP versions", 586 | "homepage": "https://symfony.com", 587 | "keywords": [ 588 | "compatibility", 589 | "polyfill", 590 | "portable", 591 | "shim" 592 | ], 593 | "time": "2019-11-27T13:56:44+00:00" 594 | }, 595 | { 596 | "name": "symfony/polyfill-php55", 597 | "version": "v1.13.1", 598 | "source": { 599 | "type": "git", 600 | "url": "https://github.com/symfony/polyfill-php55.git", 601 | "reference": "b0d838f225725e2951af1aafc784d2e5ea7b656e" 602 | }, 603 | "dist": { 604 | "type": "zip", 605 | "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/b0d838f225725e2951af1aafc784d2e5ea7b656e", 606 | "reference": "b0d838f225725e2951af1aafc784d2e5ea7b656e", 607 | "shasum": "" 608 | }, 609 | "require": { 610 | "ircmaxell/password-compat": "~1.0", 611 | "php": ">=5.3.3" 612 | }, 613 | "type": "library", 614 | "extra": { 615 | "branch-alias": { 616 | "dev-master": "1.13-dev" 617 | } 618 | }, 619 | "autoload": { 620 | "psr-4": { 621 | "Symfony\\Polyfill\\Php55\\": "" 622 | }, 623 | "files": [ 624 | "bootstrap.php" 625 | ] 626 | }, 627 | "notification-url": "https://packagist.org/downloads/", 628 | "license": [ 629 | "MIT" 630 | ], 631 | "authors": [ 632 | { 633 | "name": "Nicolas Grekas", 634 | "email": "p@tchwork.com" 635 | }, 636 | { 637 | "name": "Symfony Community", 638 | "homepage": "https://symfony.com/contributors" 639 | } 640 | ], 641 | "description": "Symfony polyfill backporting some PHP 5.5+ features to lower PHP versions", 642 | "homepage": "https://symfony.com", 643 | "keywords": [ 644 | "compatibility", 645 | "polyfill", 646 | "portable", 647 | "shim" 648 | ], 649 | "time": "2019-11-27T13:56:44+00:00" 650 | }, 651 | { 652 | "name": "vlucas/phpdotenv", 653 | "version": "v3.6.0", 654 | "source": { 655 | "type": "git", 656 | "url": "https://github.com/vlucas/phpdotenv.git", 657 | "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156" 658 | }, 659 | "dist": { 660 | "type": "zip", 661 | "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1bdf24f065975594f6a117f0f1f6cabf1333b156", 662 | "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156", 663 | "shasum": "" 664 | }, 665 | "require": { 666 | "php": "^5.4 || ^7.0", 667 | "phpoption/phpoption": "^1.5", 668 | "symfony/polyfill-ctype": "^1.9" 669 | }, 670 | "require-dev": { 671 | "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0" 672 | }, 673 | "type": "library", 674 | "extra": { 675 | "branch-alias": { 676 | "dev-master": "3.6-dev" 677 | } 678 | }, 679 | "autoload": { 680 | "psr-4": { 681 | "Dotenv\\": "src/" 682 | } 683 | }, 684 | "notification-url": "https://packagist.org/downloads/", 685 | "license": [ 686 | "BSD-3-Clause" 687 | ], 688 | "authors": [ 689 | { 690 | "name": "Graham Campbell", 691 | "email": "graham@alt-three.com", 692 | "homepage": "https://gjcampbell.co.uk/" 693 | }, 694 | { 695 | "name": "Vance Lucas", 696 | "email": "vance@vancelucas.com", 697 | "homepage": "https://vancelucas.com/" 698 | } 699 | ], 700 | "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", 701 | "keywords": [ 702 | "dotenv", 703 | "env", 704 | "environment" 705 | ], 706 | "time": "2019-09-10T21:37:39+00:00" 707 | } 708 | ], 709 | "packages-dev": [ 710 | { 711 | "name": "doctrine/instantiator", 712 | "version": "1.2.0", 713 | "source": { 714 | "type": "git", 715 | "url": "https://github.com/doctrine/instantiator.git", 716 | "reference": "a2c590166b2133a4633738648b6b064edae0814a" 717 | }, 718 | "dist": { 719 | "type": "zip", 720 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", 721 | "reference": "a2c590166b2133a4633738648b6b064edae0814a", 722 | "shasum": "" 723 | }, 724 | "require": { 725 | "php": "^7.1" 726 | }, 727 | "require-dev": { 728 | "doctrine/coding-standard": "^6.0", 729 | "ext-pdo": "*", 730 | "ext-phar": "*", 731 | "phpbench/phpbench": "^0.13", 732 | "phpstan/phpstan-phpunit": "^0.11", 733 | "phpstan/phpstan-shim": "^0.11", 734 | "phpunit/phpunit": "^7.0" 735 | }, 736 | "type": "library", 737 | "extra": { 738 | "branch-alias": { 739 | "dev-master": "1.2.x-dev" 740 | } 741 | }, 742 | "autoload": { 743 | "psr-4": { 744 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 745 | } 746 | }, 747 | "notification-url": "https://packagist.org/downloads/", 748 | "license": [ 749 | "MIT" 750 | ], 751 | "authors": [ 752 | { 753 | "name": "Marco Pivetta", 754 | "email": "ocramius@gmail.com", 755 | "homepage": "http://ocramius.github.com/" 756 | } 757 | ], 758 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 759 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 760 | "keywords": [ 761 | "constructor", 762 | "instantiate" 763 | ], 764 | "time": "2019-03-17T17:37:11+00:00" 765 | }, 766 | { 767 | "name": "myclabs/deep-copy", 768 | "version": "1.9.3", 769 | "source": { 770 | "type": "git", 771 | "url": "https://github.com/myclabs/DeepCopy.git", 772 | "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea" 773 | }, 774 | "dist": { 775 | "type": "zip", 776 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea", 777 | "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea", 778 | "shasum": "" 779 | }, 780 | "require": { 781 | "php": "^7.1" 782 | }, 783 | "replace": { 784 | "myclabs/deep-copy": "self.version" 785 | }, 786 | "require-dev": { 787 | "doctrine/collections": "^1.0", 788 | "doctrine/common": "^2.6", 789 | "phpunit/phpunit": "^7.1" 790 | }, 791 | "type": "library", 792 | "autoload": { 793 | "psr-4": { 794 | "DeepCopy\\": "src/DeepCopy/" 795 | }, 796 | "files": [ 797 | "src/DeepCopy/deep_copy.php" 798 | ] 799 | }, 800 | "notification-url": "https://packagist.org/downloads/", 801 | "license": [ 802 | "MIT" 803 | ], 804 | "description": "Create deep copies (clones) of your objects", 805 | "keywords": [ 806 | "clone", 807 | "copy", 808 | "duplicate", 809 | "object", 810 | "object graph" 811 | ], 812 | "time": "2019-08-09T12:45:53+00:00" 813 | }, 814 | { 815 | "name": "phar-io/manifest", 816 | "version": "1.0.3", 817 | "source": { 818 | "type": "git", 819 | "url": "https://github.com/phar-io/manifest.git", 820 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" 821 | }, 822 | "dist": { 823 | "type": "zip", 824 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 825 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 826 | "shasum": "" 827 | }, 828 | "require": { 829 | "ext-dom": "*", 830 | "ext-phar": "*", 831 | "phar-io/version": "^2.0", 832 | "php": "^5.6 || ^7.0" 833 | }, 834 | "type": "library", 835 | "extra": { 836 | "branch-alias": { 837 | "dev-master": "1.0.x-dev" 838 | } 839 | }, 840 | "autoload": { 841 | "classmap": [ 842 | "src/" 843 | ] 844 | }, 845 | "notification-url": "https://packagist.org/downloads/", 846 | "license": [ 847 | "BSD-3-Clause" 848 | ], 849 | "authors": [ 850 | { 851 | "name": "Arne Blankerts", 852 | "email": "arne@blankerts.de", 853 | "role": "Developer" 854 | }, 855 | { 856 | "name": "Sebastian Heuer", 857 | "email": "sebastian@phpeople.de", 858 | "role": "Developer" 859 | }, 860 | { 861 | "name": "Sebastian Bergmann", 862 | "email": "sebastian@phpunit.de", 863 | "role": "Developer" 864 | } 865 | ], 866 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 867 | "time": "2018-07-08T19:23:20+00:00" 868 | }, 869 | { 870 | "name": "phar-io/version", 871 | "version": "2.0.1", 872 | "source": { 873 | "type": "git", 874 | "url": "https://github.com/phar-io/version.git", 875 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" 876 | }, 877 | "dist": { 878 | "type": "zip", 879 | "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", 880 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", 881 | "shasum": "" 882 | }, 883 | "require": { 884 | "php": "^5.6 || ^7.0" 885 | }, 886 | "type": "library", 887 | "autoload": { 888 | "classmap": [ 889 | "src/" 890 | ] 891 | }, 892 | "notification-url": "https://packagist.org/downloads/", 893 | "license": [ 894 | "BSD-3-Clause" 895 | ], 896 | "authors": [ 897 | { 898 | "name": "Arne Blankerts", 899 | "email": "arne@blankerts.de", 900 | "role": "Developer" 901 | }, 902 | { 903 | "name": "Sebastian Heuer", 904 | "email": "sebastian@phpeople.de", 905 | "role": "Developer" 906 | }, 907 | { 908 | "name": "Sebastian Bergmann", 909 | "email": "sebastian@phpunit.de", 910 | "role": "Developer" 911 | } 912 | ], 913 | "description": "Library for handling version information and constraints", 914 | "time": "2018-07-08T19:19:57+00:00" 915 | }, 916 | { 917 | "name": "phpdocumentor/reflection-common", 918 | "version": "2.0.0", 919 | "source": { 920 | "type": "git", 921 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 922 | "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" 923 | }, 924 | "dist": { 925 | "type": "zip", 926 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", 927 | "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", 928 | "shasum": "" 929 | }, 930 | "require": { 931 | "php": ">=7.1" 932 | }, 933 | "require-dev": { 934 | "phpunit/phpunit": "~6" 935 | }, 936 | "type": "library", 937 | "extra": { 938 | "branch-alias": { 939 | "dev-master": "2.x-dev" 940 | } 941 | }, 942 | "autoload": { 943 | "psr-4": { 944 | "phpDocumentor\\Reflection\\": "src/" 945 | } 946 | }, 947 | "notification-url": "https://packagist.org/downloads/", 948 | "license": [ 949 | "MIT" 950 | ], 951 | "authors": [ 952 | { 953 | "name": "Jaap van Otterdijk", 954 | "email": "opensource@ijaap.nl" 955 | } 956 | ], 957 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 958 | "homepage": "http://www.phpdoc.org", 959 | "keywords": [ 960 | "FQSEN", 961 | "phpDocumentor", 962 | "phpdoc", 963 | "reflection", 964 | "static analysis" 965 | ], 966 | "time": "2018-08-07T13:53:10+00:00" 967 | }, 968 | { 969 | "name": "phpdocumentor/reflection-docblock", 970 | "version": "4.3.2", 971 | "source": { 972 | "type": "git", 973 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 974 | "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e" 975 | }, 976 | "dist": { 977 | "type": "zip", 978 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e", 979 | "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e", 980 | "shasum": "" 981 | }, 982 | "require": { 983 | "php": "^7.0", 984 | "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", 985 | "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", 986 | "webmozart/assert": "^1.0" 987 | }, 988 | "require-dev": { 989 | "doctrine/instantiator": "^1.0.5", 990 | "mockery/mockery": "^1.0", 991 | "phpunit/phpunit": "^6.4" 992 | }, 993 | "type": "library", 994 | "extra": { 995 | "branch-alias": { 996 | "dev-master": "4.x-dev" 997 | } 998 | }, 999 | "autoload": { 1000 | "psr-4": { 1001 | "phpDocumentor\\Reflection\\": [ 1002 | "src/" 1003 | ] 1004 | } 1005 | }, 1006 | "notification-url": "https://packagist.org/downloads/", 1007 | "license": [ 1008 | "MIT" 1009 | ], 1010 | "authors": [ 1011 | { 1012 | "name": "Mike van Riel", 1013 | "email": "me@mikevanriel.com" 1014 | } 1015 | ], 1016 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 1017 | "time": "2019-09-12T14:27:41+00:00" 1018 | }, 1019 | { 1020 | "name": "phpdocumentor/type-resolver", 1021 | "version": "1.0.1", 1022 | "source": { 1023 | "type": "git", 1024 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 1025 | "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" 1026 | }, 1027 | "dist": { 1028 | "type": "zip", 1029 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", 1030 | "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", 1031 | "shasum": "" 1032 | }, 1033 | "require": { 1034 | "php": "^7.1", 1035 | "phpdocumentor/reflection-common": "^2.0" 1036 | }, 1037 | "require-dev": { 1038 | "ext-tokenizer": "^7.1", 1039 | "mockery/mockery": "~1", 1040 | "phpunit/phpunit": "^7.0" 1041 | }, 1042 | "type": "library", 1043 | "extra": { 1044 | "branch-alias": { 1045 | "dev-master": "1.x-dev" 1046 | } 1047 | }, 1048 | "autoload": { 1049 | "psr-4": { 1050 | "phpDocumentor\\Reflection\\": "src" 1051 | } 1052 | }, 1053 | "notification-url": "https://packagist.org/downloads/", 1054 | "license": [ 1055 | "MIT" 1056 | ], 1057 | "authors": [ 1058 | { 1059 | "name": "Mike van Riel", 1060 | "email": "me@mikevanriel.com" 1061 | } 1062 | ], 1063 | "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", 1064 | "time": "2019-08-22T18:11:29+00:00" 1065 | }, 1066 | { 1067 | "name": "phpspec/prophecy", 1068 | "version": "1.9.0", 1069 | "source": { 1070 | "type": "git", 1071 | "url": "https://github.com/phpspec/prophecy.git", 1072 | "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203" 1073 | }, 1074 | "dist": { 1075 | "type": "zip", 1076 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203", 1077 | "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203", 1078 | "shasum": "" 1079 | }, 1080 | "require": { 1081 | "doctrine/instantiator": "^1.0.2", 1082 | "php": "^5.3|^7.0", 1083 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", 1084 | "sebastian/comparator": "^1.1|^2.0|^3.0", 1085 | "sebastian/recursion-context": "^1.0|^2.0|^3.0" 1086 | }, 1087 | "require-dev": { 1088 | "phpspec/phpspec": "^2.5|^3.2", 1089 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" 1090 | }, 1091 | "type": "library", 1092 | "extra": { 1093 | "branch-alias": { 1094 | "dev-master": "1.8.x-dev" 1095 | } 1096 | }, 1097 | "autoload": { 1098 | "psr-4": { 1099 | "Prophecy\\": "src/Prophecy" 1100 | } 1101 | }, 1102 | "notification-url": "https://packagist.org/downloads/", 1103 | "license": [ 1104 | "MIT" 1105 | ], 1106 | "authors": [ 1107 | { 1108 | "name": "Konstantin Kudryashov", 1109 | "email": "ever.zet@gmail.com", 1110 | "homepage": "http://everzet.com" 1111 | }, 1112 | { 1113 | "name": "Marcello Duarte", 1114 | "email": "marcello.duarte@gmail.com" 1115 | } 1116 | ], 1117 | "description": "Highly opinionated mocking framework for PHP 5.3+", 1118 | "homepage": "https://github.com/phpspec/prophecy", 1119 | "keywords": [ 1120 | "Double", 1121 | "Dummy", 1122 | "fake", 1123 | "mock", 1124 | "spy", 1125 | "stub" 1126 | ], 1127 | "time": "2019-10-03T11:07:50+00:00" 1128 | }, 1129 | { 1130 | "name": "phpunit/php-code-coverage", 1131 | "version": "7.0.8", 1132 | "source": { 1133 | "type": "git", 1134 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 1135 | "reference": "aa0d179a13284c7420fc281fc32750e6cc7c9e2f" 1136 | }, 1137 | "dist": { 1138 | "type": "zip", 1139 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa0d179a13284c7420fc281fc32750e6cc7c9e2f", 1140 | "reference": "aa0d179a13284c7420fc281fc32750e6cc7c9e2f", 1141 | "shasum": "" 1142 | }, 1143 | "require": { 1144 | "ext-dom": "*", 1145 | "ext-xmlwriter": "*", 1146 | "php": "^7.2", 1147 | "phpunit/php-file-iterator": "^2.0.2", 1148 | "phpunit/php-text-template": "^1.2.1", 1149 | "phpunit/php-token-stream": "^3.1.1", 1150 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 1151 | "sebastian/environment": "^4.2.2", 1152 | "sebastian/version": "^2.0.1", 1153 | "theseer/tokenizer": "^1.1.3" 1154 | }, 1155 | "require-dev": { 1156 | "phpunit/phpunit": "^8.2.2" 1157 | }, 1158 | "suggest": { 1159 | "ext-xdebug": "^2.7.2" 1160 | }, 1161 | "type": "library", 1162 | "extra": { 1163 | "branch-alias": { 1164 | "dev-master": "7.0-dev" 1165 | } 1166 | }, 1167 | "autoload": { 1168 | "classmap": [ 1169 | "src/" 1170 | ] 1171 | }, 1172 | "notification-url": "https://packagist.org/downloads/", 1173 | "license": [ 1174 | "BSD-3-Clause" 1175 | ], 1176 | "authors": [ 1177 | { 1178 | "name": "Sebastian Bergmann", 1179 | "email": "sebastian@phpunit.de", 1180 | "role": "lead" 1181 | } 1182 | ], 1183 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 1184 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 1185 | "keywords": [ 1186 | "coverage", 1187 | "testing", 1188 | "xunit" 1189 | ], 1190 | "time": "2019-09-17T06:24:36+00:00" 1191 | }, 1192 | { 1193 | "name": "phpunit/php-file-iterator", 1194 | "version": "2.0.2", 1195 | "source": { 1196 | "type": "git", 1197 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 1198 | "reference": "050bedf145a257b1ff02746c31894800e5122946" 1199 | }, 1200 | "dist": { 1201 | "type": "zip", 1202 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", 1203 | "reference": "050bedf145a257b1ff02746c31894800e5122946", 1204 | "shasum": "" 1205 | }, 1206 | "require": { 1207 | "php": "^7.1" 1208 | }, 1209 | "require-dev": { 1210 | "phpunit/phpunit": "^7.1" 1211 | }, 1212 | "type": "library", 1213 | "extra": { 1214 | "branch-alias": { 1215 | "dev-master": "2.0.x-dev" 1216 | } 1217 | }, 1218 | "autoload": { 1219 | "classmap": [ 1220 | "src/" 1221 | ] 1222 | }, 1223 | "notification-url": "https://packagist.org/downloads/", 1224 | "license": [ 1225 | "BSD-3-Clause" 1226 | ], 1227 | "authors": [ 1228 | { 1229 | "name": "Sebastian Bergmann", 1230 | "email": "sebastian@phpunit.de", 1231 | "role": "lead" 1232 | } 1233 | ], 1234 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 1235 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 1236 | "keywords": [ 1237 | "filesystem", 1238 | "iterator" 1239 | ], 1240 | "time": "2018-09-13T20:33:42+00:00" 1241 | }, 1242 | { 1243 | "name": "phpunit/php-text-template", 1244 | "version": "1.2.1", 1245 | "source": { 1246 | "type": "git", 1247 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 1248 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 1249 | }, 1250 | "dist": { 1251 | "type": "zip", 1252 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 1253 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 1254 | "shasum": "" 1255 | }, 1256 | "require": { 1257 | "php": ">=5.3.3" 1258 | }, 1259 | "type": "library", 1260 | "autoload": { 1261 | "classmap": [ 1262 | "src/" 1263 | ] 1264 | }, 1265 | "notification-url": "https://packagist.org/downloads/", 1266 | "license": [ 1267 | "BSD-3-Clause" 1268 | ], 1269 | "authors": [ 1270 | { 1271 | "name": "Sebastian Bergmann", 1272 | "email": "sebastian@phpunit.de", 1273 | "role": "lead" 1274 | } 1275 | ], 1276 | "description": "Simple template engine.", 1277 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 1278 | "keywords": [ 1279 | "template" 1280 | ], 1281 | "time": "2015-06-21T13:50:34+00:00" 1282 | }, 1283 | { 1284 | "name": "phpunit/php-timer", 1285 | "version": "2.1.2", 1286 | "source": { 1287 | "type": "git", 1288 | "url": "https://github.com/sebastianbergmann/php-timer.git", 1289 | "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" 1290 | }, 1291 | "dist": { 1292 | "type": "zip", 1293 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", 1294 | "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", 1295 | "shasum": "" 1296 | }, 1297 | "require": { 1298 | "php": "^7.1" 1299 | }, 1300 | "require-dev": { 1301 | "phpunit/phpunit": "^7.0" 1302 | }, 1303 | "type": "library", 1304 | "extra": { 1305 | "branch-alias": { 1306 | "dev-master": "2.1-dev" 1307 | } 1308 | }, 1309 | "autoload": { 1310 | "classmap": [ 1311 | "src/" 1312 | ] 1313 | }, 1314 | "notification-url": "https://packagist.org/downloads/", 1315 | "license": [ 1316 | "BSD-3-Clause" 1317 | ], 1318 | "authors": [ 1319 | { 1320 | "name": "Sebastian Bergmann", 1321 | "email": "sebastian@phpunit.de", 1322 | "role": "lead" 1323 | } 1324 | ], 1325 | "description": "Utility class for timing", 1326 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 1327 | "keywords": [ 1328 | "timer" 1329 | ], 1330 | "time": "2019-06-07T04:22:29+00:00" 1331 | }, 1332 | { 1333 | "name": "phpunit/php-token-stream", 1334 | "version": "3.1.1", 1335 | "source": { 1336 | "type": "git", 1337 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 1338 | "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" 1339 | }, 1340 | "dist": { 1341 | "type": "zip", 1342 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", 1343 | "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", 1344 | "shasum": "" 1345 | }, 1346 | "require": { 1347 | "ext-tokenizer": "*", 1348 | "php": "^7.1" 1349 | }, 1350 | "require-dev": { 1351 | "phpunit/phpunit": "^7.0" 1352 | }, 1353 | "type": "library", 1354 | "extra": { 1355 | "branch-alias": { 1356 | "dev-master": "3.1-dev" 1357 | } 1358 | }, 1359 | "autoload": { 1360 | "classmap": [ 1361 | "src/" 1362 | ] 1363 | }, 1364 | "notification-url": "https://packagist.org/downloads/", 1365 | "license": [ 1366 | "BSD-3-Clause" 1367 | ], 1368 | "authors": [ 1369 | { 1370 | "name": "Sebastian Bergmann", 1371 | "email": "sebastian@phpunit.de" 1372 | } 1373 | ], 1374 | "description": "Wrapper around PHP's tokenizer extension.", 1375 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 1376 | "keywords": [ 1377 | "tokenizer" 1378 | ], 1379 | "time": "2019-09-17T06:23:10+00:00" 1380 | }, 1381 | { 1382 | "name": "phpunit/phpunit", 1383 | "version": "8.4.2", 1384 | "source": { 1385 | "type": "git", 1386 | "url": "https://github.com/sebastianbergmann/phpunit.git", 1387 | "reference": "a142a7e66c0ea7b5b6c04ee27f08d10d1137cd9b" 1388 | }, 1389 | "dist": { 1390 | "type": "zip", 1391 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a142a7e66c0ea7b5b6c04ee27f08d10d1137cd9b", 1392 | "reference": "a142a7e66c0ea7b5b6c04ee27f08d10d1137cd9b", 1393 | "shasum": "" 1394 | }, 1395 | "require": { 1396 | "doctrine/instantiator": "^1.2.0", 1397 | "ext-dom": "*", 1398 | "ext-json": "*", 1399 | "ext-libxml": "*", 1400 | "ext-mbstring": "*", 1401 | "ext-xml": "*", 1402 | "ext-xmlwriter": "*", 1403 | "myclabs/deep-copy": "^1.9.1", 1404 | "phar-io/manifest": "^1.0.3", 1405 | "phar-io/version": "^2.0.1", 1406 | "php": "^7.2", 1407 | "phpspec/prophecy": "^1.8.1", 1408 | "phpunit/php-code-coverage": "^7.0.7", 1409 | "phpunit/php-file-iterator": "^2.0.2", 1410 | "phpunit/php-text-template": "^1.2.1", 1411 | "phpunit/php-timer": "^2.1.2", 1412 | "sebastian/comparator": "^3.0.2", 1413 | "sebastian/diff": "^3.0.2", 1414 | "sebastian/environment": "^4.2.2", 1415 | "sebastian/exporter": "^3.1.1", 1416 | "sebastian/global-state": "^3.0.0", 1417 | "sebastian/object-enumerator": "^3.0.3", 1418 | "sebastian/resource-operations": "^2.0.1", 1419 | "sebastian/type": "^1.1.3", 1420 | "sebastian/version": "^2.0.1" 1421 | }, 1422 | "require-dev": { 1423 | "ext-pdo": "*" 1424 | }, 1425 | "suggest": { 1426 | "ext-soap": "*", 1427 | "ext-xdebug": "*", 1428 | "phpunit/php-invoker": "^2.0.0" 1429 | }, 1430 | "bin": [ 1431 | "phpunit" 1432 | ], 1433 | "type": "library", 1434 | "extra": { 1435 | "branch-alias": { 1436 | "dev-master": "8.4-dev" 1437 | } 1438 | }, 1439 | "autoload": { 1440 | "classmap": [ 1441 | "src/" 1442 | ] 1443 | }, 1444 | "notification-url": "https://packagist.org/downloads/", 1445 | "license": [ 1446 | "BSD-3-Clause" 1447 | ], 1448 | "authors": [ 1449 | { 1450 | "name": "Sebastian Bergmann", 1451 | "email": "sebastian@phpunit.de", 1452 | "role": "lead" 1453 | } 1454 | ], 1455 | "description": "The PHP Unit Testing framework.", 1456 | "homepage": "https://phpunit.de/", 1457 | "keywords": [ 1458 | "phpunit", 1459 | "testing", 1460 | "xunit" 1461 | ], 1462 | "time": "2019-10-28T10:39:51+00:00" 1463 | }, 1464 | { 1465 | "name": "sebastian/code-unit-reverse-lookup", 1466 | "version": "1.0.1", 1467 | "source": { 1468 | "type": "git", 1469 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 1470 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" 1471 | }, 1472 | "dist": { 1473 | "type": "zip", 1474 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 1475 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 1476 | "shasum": "" 1477 | }, 1478 | "require": { 1479 | "php": "^5.6 || ^7.0" 1480 | }, 1481 | "require-dev": { 1482 | "phpunit/phpunit": "^5.7 || ^6.0" 1483 | }, 1484 | "type": "library", 1485 | "extra": { 1486 | "branch-alias": { 1487 | "dev-master": "1.0.x-dev" 1488 | } 1489 | }, 1490 | "autoload": { 1491 | "classmap": [ 1492 | "src/" 1493 | ] 1494 | }, 1495 | "notification-url": "https://packagist.org/downloads/", 1496 | "license": [ 1497 | "BSD-3-Clause" 1498 | ], 1499 | "authors": [ 1500 | { 1501 | "name": "Sebastian Bergmann", 1502 | "email": "sebastian@phpunit.de" 1503 | } 1504 | ], 1505 | "description": "Looks up which function or method a line of code belongs to", 1506 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 1507 | "time": "2017-03-04T06:30:41+00:00" 1508 | }, 1509 | { 1510 | "name": "sebastian/comparator", 1511 | "version": "3.0.2", 1512 | "source": { 1513 | "type": "git", 1514 | "url": "https://github.com/sebastianbergmann/comparator.git", 1515 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" 1516 | }, 1517 | "dist": { 1518 | "type": "zip", 1519 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 1520 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 1521 | "shasum": "" 1522 | }, 1523 | "require": { 1524 | "php": "^7.1", 1525 | "sebastian/diff": "^3.0", 1526 | "sebastian/exporter": "^3.1" 1527 | }, 1528 | "require-dev": { 1529 | "phpunit/phpunit": "^7.1" 1530 | }, 1531 | "type": "library", 1532 | "extra": { 1533 | "branch-alias": { 1534 | "dev-master": "3.0-dev" 1535 | } 1536 | }, 1537 | "autoload": { 1538 | "classmap": [ 1539 | "src/" 1540 | ] 1541 | }, 1542 | "notification-url": "https://packagist.org/downloads/", 1543 | "license": [ 1544 | "BSD-3-Clause" 1545 | ], 1546 | "authors": [ 1547 | { 1548 | "name": "Jeff Welch", 1549 | "email": "whatthejeff@gmail.com" 1550 | }, 1551 | { 1552 | "name": "Volker Dusch", 1553 | "email": "github@wallbash.com" 1554 | }, 1555 | { 1556 | "name": "Bernhard Schussek", 1557 | "email": "bschussek@2bepublished.at" 1558 | }, 1559 | { 1560 | "name": "Sebastian Bergmann", 1561 | "email": "sebastian@phpunit.de" 1562 | } 1563 | ], 1564 | "description": "Provides the functionality to compare PHP values for equality", 1565 | "homepage": "https://github.com/sebastianbergmann/comparator", 1566 | "keywords": [ 1567 | "comparator", 1568 | "compare", 1569 | "equality" 1570 | ], 1571 | "time": "2018-07-12T15:12:46+00:00" 1572 | }, 1573 | { 1574 | "name": "sebastian/diff", 1575 | "version": "3.0.2", 1576 | "source": { 1577 | "type": "git", 1578 | "url": "https://github.com/sebastianbergmann/diff.git", 1579 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" 1580 | }, 1581 | "dist": { 1582 | "type": "zip", 1583 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 1584 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 1585 | "shasum": "" 1586 | }, 1587 | "require": { 1588 | "php": "^7.1" 1589 | }, 1590 | "require-dev": { 1591 | "phpunit/phpunit": "^7.5 || ^8.0", 1592 | "symfony/process": "^2 || ^3.3 || ^4" 1593 | }, 1594 | "type": "library", 1595 | "extra": { 1596 | "branch-alias": { 1597 | "dev-master": "3.0-dev" 1598 | } 1599 | }, 1600 | "autoload": { 1601 | "classmap": [ 1602 | "src/" 1603 | ] 1604 | }, 1605 | "notification-url": "https://packagist.org/downloads/", 1606 | "license": [ 1607 | "BSD-3-Clause" 1608 | ], 1609 | "authors": [ 1610 | { 1611 | "name": "Kore Nordmann", 1612 | "email": "mail@kore-nordmann.de" 1613 | }, 1614 | { 1615 | "name": "Sebastian Bergmann", 1616 | "email": "sebastian@phpunit.de" 1617 | } 1618 | ], 1619 | "description": "Diff implementation", 1620 | "homepage": "https://github.com/sebastianbergmann/diff", 1621 | "keywords": [ 1622 | "diff", 1623 | "udiff", 1624 | "unidiff", 1625 | "unified diff" 1626 | ], 1627 | "time": "2019-02-04T06:01:07+00:00" 1628 | }, 1629 | { 1630 | "name": "sebastian/environment", 1631 | "version": "4.2.2", 1632 | "source": { 1633 | "type": "git", 1634 | "url": "https://github.com/sebastianbergmann/environment.git", 1635 | "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404" 1636 | }, 1637 | "dist": { 1638 | "type": "zip", 1639 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404", 1640 | "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404", 1641 | "shasum": "" 1642 | }, 1643 | "require": { 1644 | "php": "^7.1" 1645 | }, 1646 | "require-dev": { 1647 | "phpunit/phpunit": "^7.5" 1648 | }, 1649 | "suggest": { 1650 | "ext-posix": "*" 1651 | }, 1652 | "type": "library", 1653 | "extra": { 1654 | "branch-alias": { 1655 | "dev-master": "4.2-dev" 1656 | } 1657 | }, 1658 | "autoload": { 1659 | "classmap": [ 1660 | "src/" 1661 | ] 1662 | }, 1663 | "notification-url": "https://packagist.org/downloads/", 1664 | "license": [ 1665 | "BSD-3-Clause" 1666 | ], 1667 | "authors": [ 1668 | { 1669 | "name": "Sebastian Bergmann", 1670 | "email": "sebastian@phpunit.de" 1671 | } 1672 | ], 1673 | "description": "Provides functionality to handle HHVM/PHP environments", 1674 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1675 | "keywords": [ 1676 | "Xdebug", 1677 | "environment", 1678 | "hhvm" 1679 | ], 1680 | "time": "2019-05-05T09:05:15+00:00" 1681 | }, 1682 | { 1683 | "name": "sebastian/exporter", 1684 | "version": "3.1.2", 1685 | "source": { 1686 | "type": "git", 1687 | "url": "https://github.com/sebastianbergmann/exporter.git", 1688 | "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" 1689 | }, 1690 | "dist": { 1691 | "type": "zip", 1692 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", 1693 | "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", 1694 | "shasum": "" 1695 | }, 1696 | "require": { 1697 | "php": "^7.0", 1698 | "sebastian/recursion-context": "^3.0" 1699 | }, 1700 | "require-dev": { 1701 | "ext-mbstring": "*", 1702 | "phpunit/phpunit": "^6.0" 1703 | }, 1704 | "type": "library", 1705 | "extra": { 1706 | "branch-alias": { 1707 | "dev-master": "3.1.x-dev" 1708 | } 1709 | }, 1710 | "autoload": { 1711 | "classmap": [ 1712 | "src/" 1713 | ] 1714 | }, 1715 | "notification-url": "https://packagist.org/downloads/", 1716 | "license": [ 1717 | "BSD-3-Clause" 1718 | ], 1719 | "authors": [ 1720 | { 1721 | "name": "Sebastian Bergmann", 1722 | "email": "sebastian@phpunit.de" 1723 | }, 1724 | { 1725 | "name": "Jeff Welch", 1726 | "email": "whatthejeff@gmail.com" 1727 | }, 1728 | { 1729 | "name": "Volker Dusch", 1730 | "email": "github@wallbash.com" 1731 | }, 1732 | { 1733 | "name": "Adam Harvey", 1734 | "email": "aharvey@php.net" 1735 | }, 1736 | { 1737 | "name": "Bernhard Schussek", 1738 | "email": "bschussek@gmail.com" 1739 | } 1740 | ], 1741 | "description": "Provides the functionality to export PHP variables for visualization", 1742 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1743 | "keywords": [ 1744 | "export", 1745 | "exporter" 1746 | ], 1747 | "time": "2019-09-14T09:02:43+00:00" 1748 | }, 1749 | { 1750 | "name": "sebastian/global-state", 1751 | "version": "3.0.0", 1752 | "source": { 1753 | "type": "git", 1754 | "url": "https://github.com/sebastianbergmann/global-state.git", 1755 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" 1756 | }, 1757 | "dist": { 1758 | "type": "zip", 1759 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", 1760 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", 1761 | "shasum": "" 1762 | }, 1763 | "require": { 1764 | "php": "^7.2", 1765 | "sebastian/object-reflector": "^1.1.1", 1766 | "sebastian/recursion-context": "^3.0" 1767 | }, 1768 | "require-dev": { 1769 | "ext-dom": "*", 1770 | "phpunit/phpunit": "^8.0" 1771 | }, 1772 | "suggest": { 1773 | "ext-uopz": "*" 1774 | }, 1775 | "type": "library", 1776 | "extra": { 1777 | "branch-alias": { 1778 | "dev-master": "3.0-dev" 1779 | } 1780 | }, 1781 | "autoload": { 1782 | "classmap": [ 1783 | "src/" 1784 | ] 1785 | }, 1786 | "notification-url": "https://packagist.org/downloads/", 1787 | "license": [ 1788 | "BSD-3-Clause" 1789 | ], 1790 | "authors": [ 1791 | { 1792 | "name": "Sebastian Bergmann", 1793 | "email": "sebastian@phpunit.de" 1794 | } 1795 | ], 1796 | "description": "Snapshotting of global state", 1797 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1798 | "keywords": [ 1799 | "global state" 1800 | ], 1801 | "time": "2019-02-01T05:30:01+00:00" 1802 | }, 1803 | { 1804 | "name": "sebastian/object-enumerator", 1805 | "version": "3.0.3", 1806 | "source": { 1807 | "type": "git", 1808 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1809 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" 1810 | }, 1811 | "dist": { 1812 | "type": "zip", 1813 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1814 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1815 | "shasum": "" 1816 | }, 1817 | "require": { 1818 | "php": "^7.0", 1819 | "sebastian/object-reflector": "^1.1.1", 1820 | "sebastian/recursion-context": "^3.0" 1821 | }, 1822 | "require-dev": { 1823 | "phpunit/phpunit": "^6.0" 1824 | }, 1825 | "type": "library", 1826 | "extra": { 1827 | "branch-alias": { 1828 | "dev-master": "3.0.x-dev" 1829 | } 1830 | }, 1831 | "autoload": { 1832 | "classmap": [ 1833 | "src/" 1834 | ] 1835 | }, 1836 | "notification-url": "https://packagist.org/downloads/", 1837 | "license": [ 1838 | "BSD-3-Clause" 1839 | ], 1840 | "authors": [ 1841 | { 1842 | "name": "Sebastian Bergmann", 1843 | "email": "sebastian@phpunit.de" 1844 | } 1845 | ], 1846 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1847 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1848 | "time": "2017-08-03T12:35:26+00:00" 1849 | }, 1850 | { 1851 | "name": "sebastian/object-reflector", 1852 | "version": "1.1.1", 1853 | "source": { 1854 | "type": "git", 1855 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1856 | "reference": "773f97c67f28de00d397be301821b06708fca0be" 1857 | }, 1858 | "dist": { 1859 | "type": "zip", 1860 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", 1861 | "reference": "773f97c67f28de00d397be301821b06708fca0be", 1862 | "shasum": "" 1863 | }, 1864 | "require": { 1865 | "php": "^7.0" 1866 | }, 1867 | "require-dev": { 1868 | "phpunit/phpunit": "^6.0" 1869 | }, 1870 | "type": "library", 1871 | "extra": { 1872 | "branch-alias": { 1873 | "dev-master": "1.1-dev" 1874 | } 1875 | }, 1876 | "autoload": { 1877 | "classmap": [ 1878 | "src/" 1879 | ] 1880 | }, 1881 | "notification-url": "https://packagist.org/downloads/", 1882 | "license": [ 1883 | "BSD-3-Clause" 1884 | ], 1885 | "authors": [ 1886 | { 1887 | "name": "Sebastian Bergmann", 1888 | "email": "sebastian@phpunit.de" 1889 | } 1890 | ], 1891 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1892 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1893 | "time": "2017-03-29T09:07:27+00:00" 1894 | }, 1895 | { 1896 | "name": "sebastian/recursion-context", 1897 | "version": "3.0.0", 1898 | "source": { 1899 | "type": "git", 1900 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1901 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" 1902 | }, 1903 | "dist": { 1904 | "type": "zip", 1905 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1906 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1907 | "shasum": "" 1908 | }, 1909 | "require": { 1910 | "php": "^7.0" 1911 | }, 1912 | "require-dev": { 1913 | "phpunit/phpunit": "^6.0" 1914 | }, 1915 | "type": "library", 1916 | "extra": { 1917 | "branch-alias": { 1918 | "dev-master": "3.0.x-dev" 1919 | } 1920 | }, 1921 | "autoload": { 1922 | "classmap": [ 1923 | "src/" 1924 | ] 1925 | }, 1926 | "notification-url": "https://packagist.org/downloads/", 1927 | "license": [ 1928 | "BSD-3-Clause" 1929 | ], 1930 | "authors": [ 1931 | { 1932 | "name": "Jeff Welch", 1933 | "email": "whatthejeff@gmail.com" 1934 | }, 1935 | { 1936 | "name": "Sebastian Bergmann", 1937 | "email": "sebastian@phpunit.de" 1938 | }, 1939 | { 1940 | "name": "Adam Harvey", 1941 | "email": "aharvey@php.net" 1942 | } 1943 | ], 1944 | "description": "Provides functionality to recursively process PHP variables", 1945 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1946 | "time": "2017-03-03T06:23:57+00:00" 1947 | }, 1948 | { 1949 | "name": "sebastian/resource-operations", 1950 | "version": "2.0.1", 1951 | "source": { 1952 | "type": "git", 1953 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1954 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" 1955 | }, 1956 | "dist": { 1957 | "type": "zip", 1958 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 1959 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 1960 | "shasum": "" 1961 | }, 1962 | "require": { 1963 | "php": "^7.1" 1964 | }, 1965 | "type": "library", 1966 | "extra": { 1967 | "branch-alias": { 1968 | "dev-master": "2.0-dev" 1969 | } 1970 | }, 1971 | "autoload": { 1972 | "classmap": [ 1973 | "src/" 1974 | ] 1975 | }, 1976 | "notification-url": "https://packagist.org/downloads/", 1977 | "license": [ 1978 | "BSD-3-Clause" 1979 | ], 1980 | "authors": [ 1981 | { 1982 | "name": "Sebastian Bergmann", 1983 | "email": "sebastian@phpunit.de" 1984 | } 1985 | ], 1986 | "description": "Provides a list of PHP built-in functions that operate on resources", 1987 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1988 | "time": "2018-10-04T04:07:39+00:00" 1989 | }, 1990 | { 1991 | "name": "sebastian/type", 1992 | "version": "1.1.3", 1993 | "source": { 1994 | "type": "git", 1995 | "url": "https://github.com/sebastianbergmann/type.git", 1996 | "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" 1997 | }, 1998 | "dist": { 1999 | "type": "zip", 2000 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", 2001 | "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", 2002 | "shasum": "" 2003 | }, 2004 | "require": { 2005 | "php": "^7.2" 2006 | }, 2007 | "require-dev": { 2008 | "phpunit/phpunit": "^8.2" 2009 | }, 2010 | "type": "library", 2011 | "extra": { 2012 | "branch-alias": { 2013 | "dev-master": "1.1-dev" 2014 | } 2015 | }, 2016 | "autoload": { 2017 | "classmap": [ 2018 | "src/" 2019 | ] 2020 | }, 2021 | "notification-url": "https://packagist.org/downloads/", 2022 | "license": [ 2023 | "BSD-3-Clause" 2024 | ], 2025 | "authors": [ 2026 | { 2027 | "name": "Sebastian Bergmann", 2028 | "email": "sebastian@phpunit.de", 2029 | "role": "lead" 2030 | } 2031 | ], 2032 | "description": "Collection of value objects that represent the types of the PHP type system", 2033 | "homepage": "https://github.com/sebastianbergmann/type", 2034 | "time": "2019-07-02T08:10:15+00:00" 2035 | }, 2036 | { 2037 | "name": "sebastian/version", 2038 | "version": "2.0.1", 2039 | "source": { 2040 | "type": "git", 2041 | "url": "https://github.com/sebastianbergmann/version.git", 2042 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 2043 | }, 2044 | "dist": { 2045 | "type": "zip", 2046 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 2047 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 2048 | "shasum": "" 2049 | }, 2050 | "require": { 2051 | "php": ">=5.6" 2052 | }, 2053 | "type": "library", 2054 | "extra": { 2055 | "branch-alias": { 2056 | "dev-master": "2.0.x-dev" 2057 | } 2058 | }, 2059 | "autoload": { 2060 | "classmap": [ 2061 | "src/" 2062 | ] 2063 | }, 2064 | "notification-url": "https://packagist.org/downloads/", 2065 | "license": [ 2066 | "BSD-3-Clause" 2067 | ], 2068 | "authors": [ 2069 | { 2070 | "name": "Sebastian Bergmann", 2071 | "email": "sebastian@phpunit.de", 2072 | "role": "lead" 2073 | } 2074 | ], 2075 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 2076 | "homepage": "https://github.com/sebastianbergmann/version", 2077 | "time": "2016-10-03T07:35:21+00:00" 2078 | }, 2079 | { 2080 | "name": "theseer/tokenizer", 2081 | "version": "1.1.3", 2082 | "source": { 2083 | "type": "git", 2084 | "url": "https://github.com/theseer/tokenizer.git", 2085 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" 2086 | }, 2087 | "dist": { 2088 | "type": "zip", 2089 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 2090 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 2091 | "shasum": "" 2092 | }, 2093 | "require": { 2094 | "ext-dom": "*", 2095 | "ext-tokenizer": "*", 2096 | "ext-xmlwriter": "*", 2097 | "php": "^7.0" 2098 | }, 2099 | "type": "library", 2100 | "autoload": { 2101 | "classmap": [ 2102 | "src/" 2103 | ] 2104 | }, 2105 | "notification-url": "https://packagist.org/downloads/", 2106 | "license": [ 2107 | "BSD-3-Clause" 2108 | ], 2109 | "authors": [ 2110 | { 2111 | "name": "Arne Blankerts", 2112 | "email": "arne@blankerts.de", 2113 | "role": "Developer" 2114 | } 2115 | ], 2116 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 2117 | "time": "2019-06-13T22:48:21+00:00" 2118 | }, 2119 | { 2120 | "name": "webmozart/assert", 2121 | "version": "1.5.0", 2122 | "source": { 2123 | "type": "git", 2124 | "url": "https://github.com/webmozart/assert.git", 2125 | "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" 2126 | }, 2127 | "dist": { 2128 | "type": "zip", 2129 | "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", 2130 | "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", 2131 | "shasum": "" 2132 | }, 2133 | "require": { 2134 | "php": "^5.3.3 || ^7.0", 2135 | "symfony/polyfill-ctype": "^1.8" 2136 | }, 2137 | "require-dev": { 2138 | "phpunit/phpunit": "^4.8.36 || ^7.5.13" 2139 | }, 2140 | "type": "library", 2141 | "extra": { 2142 | "branch-alias": { 2143 | "dev-master": "1.3-dev" 2144 | } 2145 | }, 2146 | "autoload": { 2147 | "psr-4": { 2148 | "Webmozart\\Assert\\": "src/" 2149 | } 2150 | }, 2151 | "notification-url": "https://packagist.org/downloads/", 2152 | "license": [ 2153 | "MIT" 2154 | ], 2155 | "authors": [ 2156 | { 2157 | "name": "Bernhard Schussek", 2158 | "email": "bschussek@gmail.com" 2159 | } 2160 | ], 2161 | "description": "Assertions to validate method input/output with nice error messages.", 2162 | "keywords": [ 2163 | "assert", 2164 | "check", 2165 | "validate" 2166 | ], 2167 | "time": "2019-08-24T08:43:50+00:00" 2168 | } 2169 | ], 2170 | "aliases": [], 2171 | "minimum-stability": "stable", 2172 | "stability-flags": [], 2173 | "prefer-stable": false, 2174 | "prefer-lowest": false, 2175 | "platform": { 2176 | "php": "^7.2.0" 2177 | }, 2178 | "platform-dev": [] 2179 | } 2180 | --------------------------------------------------------------------------------