├── 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 |
= $error_description ?>
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 |You successfully signed in! Now return to your device to finish.
4 | 5 | 8 | -------------------------------------------------------------------------------- /views/index.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => $title]); ?> 2 | 3 |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 |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 | 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 |  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 | --------------------------------------------------------------------------------