├── README.md ├── composer.json ├── license.md └── src └── Kdyby └── Github ├── Api ├── CurlClient.php ├── Request.php └── Response.php ├── Client.php ├── Configuration.php ├── DI └── GithubExtension.php ├── Diagnostics ├── GitHub-Mark-32px.png ├── Panel.php └── panel.phtml ├── HttpClient.php ├── Paginator.php ├── Profile.php ├── SessionStorage.php ├── UI └── LoginDialog.php └── exceptions.php /README.md: -------------------------------------------------------------------------------- 1 | Kdyby/Github 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/Kdyby/Github.svg?branch=master)](https://travis-ci.org/Kdyby/Github) 5 | [![Downloads this Month](https://img.shields.io/packagist/dm/kdyby/github.svg)](https://packagist.org/packages/kdyby/github) 6 | [![Latest stable](https://img.shields.io/packagist/v/kdyby/github.svg)](https://packagist.org/packages/kdyby/github) 7 | [![Join the chat at https://gitter.im/Kdyby/Help](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Kdyby/Help) 8 | 9 | Github API client with authorization for Nette Framework 10 | 11 | 12 | Requirements 13 | ------------ 14 | 15 | Kdyby/Github requires PHP 5.3.2 or higher with cUrl extension enabled. 16 | 17 | - [Nette Framework](https://github.com/nette/nette) 18 | - [Kdyby/CurlCaBundle](https://github.com/Kdyby/CurlCaBundle) 19 | 20 | 21 | Installation 22 | ------------ 23 | 24 | The best way to install Kdyby/Github is using [Composer](http://getcomposer.org/): 25 | 26 | ```sh 27 | $ composer require kdyby/github:~0.1 28 | ``` 29 | 30 | For Nette `2.1` and newer is `~0.1` 31 | 32 | 33 | Documentation 34 | ------------ 35 | 36 | Learn how to authenticate the user using Github's oauth or call Github's api in [documentation](https://github.com/Kdyby/Github/blob/master/docs/en/index.md). 37 | 38 | 39 | 40 | ----- 41 | 42 | Homepage [http://www.kdyby.org](http://www.kdyby.org) and repository [http://github.com/Kdyby/Github](http://github.com/Kdyby/Github). 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kdyby/github", 3 | "type": "library", 4 | "description": "Github API client with authorization for Nette Framework", 5 | "keywords": ["nette", "kdyby", "github", "sdk", "authorization"], 6 | "homepage": "http://kdyby.org", 7 | "license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"], 8 | "authors": [ 9 | { 10 | "name": "Filip Procházka", 11 | "homepage": "http://filip-prochazka.com", 12 | "email": "filip@prochazka.su" 13 | } 14 | ], 15 | "require": { 16 | "nette/application": "~2.2@dev", 17 | "nette/di": "~2.2@dev", 18 | "nette/http": "~2.2@dev", 19 | 20 | "kdyby/curl-ca-bundle": "~1.0", 21 | "ext-curl": "*", 22 | "ext-json": "*" 23 | }, 24 | "require-dev": { 25 | "nette/nette": "~2.2@dev", 26 | "nette/bootstrap": "~2.2@dev", 27 | "nette/caching": "~2.2@dev", 28 | "nette/component-model": "~2.2@dev", 29 | "nette/database": "~2.2@dev", 30 | "nette/deprecated": "~2.2@dev", 31 | "nette/finder": "~2.2@dev", 32 | "nette/forms": "~2.2@dev", 33 | "nette/mail": "~2.2@dev", 34 | "nette/neon": "~2.2@dev", 35 | "nette/php-generator": "~2.2@dev", 36 | "nette/reflection": "~2.2@dev", 37 | "nette/robot-loader": "~2.2@dev", 38 | "nette/safe-stream": "~2.2@dev", 39 | "nette/security": "~2.2@dev", 40 | "nette/tokenizer": "~2.2@dev", 41 | "nette/utils": "~2.2@dev", 42 | "latte/latte": "~2.2@dev", 43 | "tracy/tracy": "~2.2@dev", 44 | 45 | "nette/tester": "@dev", 46 | "jakub-onderka/php-parallel-lint": "~0.7" 47 | }, 48 | "support": { 49 | "email": "filip@prochazka.su", 50 | "issues": "https://github.com/kdyby/github/issues" 51 | }, 52 | "autoload": { 53 | "psr-0": { 54 | "Kdyby\\Github\\": "src/" 55 | }, 56 | "classmap": [ 57 | "src/Kdyby/Github/exceptions.php" 58 | ] 59 | }, 60 | "autoload-dev": { 61 | "classmap": ["tests/KdybyTests/"] 62 | }, 63 | "extra": { 64 | "branch-alias": { 65 | "dev-master": "1.0-dev" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Licenses 2 | ======== 3 | 4 | Good news! You may use Kdyby Framework under the terms of either 5 | the New BSD License or the GNU General Public License (GPL) version 2 or 3. 6 | 7 | The BSD License is recommended for most projects. It is easy to understand and it 8 | places almost no restrictions on what you can do with the framework. If the GPL 9 | fits better to your project, you can use the framework under this license. 10 | 11 | You don't have to notify anyone which license you are using. You can freely 12 | use Kdyby Framework in commercial projects as long as the copyright header 13 | remains intact. 14 | 15 | 16 | 17 | New BSD License 18 | --------------- 19 | 20 | Copyright (c) 2008 Filip Procházka (http://filip-prochazka.com) 21 | All rights reserved. 22 | 23 | Redistribution and use in source and binary forms, with or without modification, 24 | are permitted provided that the following conditions are met: 25 | 26 | * Redistributions of source code must retain the above copyright notice, 27 | this list of conditions and the following disclaimer. 28 | 29 | * Redistributions in binary form must reproduce the above copyright notice, 30 | this list of conditions and the following disclaimer in the documentation 31 | and/or other materials provided with the distribution. 32 | 33 | * Neither the name of "Kdyby Framework" nor the names of its contributors 34 | may be used to endorse or promote products derived from this software 35 | without specific prior written permission. 36 | 37 | This software is provided by the copyright holders and contributors "as is" and 38 | any express or implied warranties, including, but not limited to, the implied 39 | warranties of merchantability and fitness for a particular purpose are 40 | disclaimed. In no event shall the copyright owner or contributors be liable for 41 | any direct, indirect, incidental, special, exemplary, or consequential damages 42 | (including, but not limited to, procurement of substitute goods or services; 43 | loss of use, data, or profits; or business interruption) however caused and on 44 | any theory of liability, whether in contract, strict liability, or tort 45 | (including negligence or otherwise) arising in any way out of the use of this 46 | software, even if advised of the possibility of such damage. 47 | 48 | 49 | 50 | GNU General Public License 51 | -------------------------- 52 | 53 | GPL licenses are very very long, so instead of including them here we offer 54 | you URLs with full text: 55 | 56 | - [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html) 57 | - [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html) 58 | -------------------------------------------------------------------------------- /src/Kdyby/Github/Api/CurlClient.php: -------------------------------------------------------------------------------- 1 | 27 | * 28 | * @method onRequest(Request $request, $options) 29 | * @method onError(Github\Exception $e, Response $response) 30 | * @method onSuccess(Response $response) 31 | */ 32 | class CurlClient extends Nette\Object implements Github\HttpClient 33 | { 34 | 35 | /** 36 | * Default options for curl. 37 | * @var array 38 | */ 39 | public static $defaultCurlOptions = array( 40 | CURLOPT_CONNECTTIMEOUT => 10, 41 | CURLOPT_RETURNTRANSFER => TRUE, 42 | CURLOPT_TIMEOUT => 20, 43 | CURLOPT_USERAGENT => 'kdyby-github-php', 44 | CURLOPT_HTTPHEADER => array( 45 | 'Accept' => 'application/vnd.github.v3+json', 46 | ), 47 | CURLINFO_HEADER_OUT => TRUE, 48 | CURLOPT_HEADER => TRUE, 49 | CURLOPT_AUTOREFERER => TRUE, 50 | ); 51 | 52 | /** 53 | * Options for curl. 54 | * @var array 55 | */ 56 | public $curlOptions = array(); 57 | 58 | /** 59 | * @var array of function(Request $request, $options) 60 | */ 61 | public $onRequest = array(); 62 | 63 | /** 64 | * @var array of function(Github\Exception $e, Response $response) 65 | */ 66 | public $onError = array(); 67 | 68 | /** 69 | * @var array of function(Response $response) 70 | */ 71 | public $onSuccess = array(); 72 | 73 | /** 74 | * @var array 75 | */ 76 | private $memoryCache = array(); 77 | 78 | 79 | 80 | public function __construct() 81 | { 82 | $this->curlOptions = self::$defaultCurlOptions; 83 | } 84 | 85 | 86 | 87 | /** 88 | * Makes an HTTP request. This method can be overridden by subclasses if 89 | * developers want to do fancier things or use something other than curl to 90 | * make the request. 91 | * 92 | * @param Request $request 93 | * @throws Github\ApiException 94 | * @return Response 95 | */ 96 | public function makeRequest(Request $request) 97 | { 98 | if (isset($this->memoryCache[$cacheKey = md5(serialize($request))])) { 99 | return $this->memoryCache[$cacheKey]; 100 | } 101 | 102 | $ch = $this->buildCurlResource($request); 103 | $result = curl_exec($ch); 104 | 105 | // provide certificate if needed 106 | if (curl_errno($ch) == CURLE_SSL_CACERT || curl_errno($ch) === CURLE_SSL_CACERT_BADFILE) { 107 | Debugger::log('Invalid or no certificate authority found, using bundled information', 'github'); 108 | $this->curlOptions[CURLOPT_CAINFO] = CertificateHelper::getCaInfoFile(); 109 | curl_setopt($ch, CURLOPT_CAINFO, CertificateHelper::getCaInfoFile()); 110 | $result = curl_exec($ch); 111 | } 112 | 113 | // With dual stacked DNS responses, it's possible for a server to 114 | // have IPv6 enabled but not have IPv6 connectivity. If this is 115 | // the case, curl will try IPv4 first and if that fails, then it will 116 | // fall back to IPv6 and the error EHOSTUNREACH is returned by the operating system. 117 | if ($result === FALSE && empty($opts[CURLOPT_IPRESOLVE])) { 118 | $matches = array(); 119 | if (preg_match('/Failed to connect to ([^:].*): Network is unreachable/', curl_error($ch), $matches)) { 120 | if (strlen(@inet_pton($matches[1])) === 16) { 121 | Debugger::log('Invalid IPv6 configuration on server, Please disable or get native IPv6 on your server.', 'github'); 122 | $this->curlOptions[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; 123 | curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); 124 | $result = curl_exec($ch); 125 | } 126 | } 127 | } 128 | 129 | $info = curl_getinfo($ch); 130 | $info['http_code'] = (int) $info['http_code']; 131 | if (isset($info['request_header'])) { 132 | list($info['request_header']) = self::parseHeaders($info['request_header']); 133 | } 134 | $info['method'] = isset($post['method']) ? $post['method']: 'GET'; 135 | $info['headers'] = self::parseHeaders(substr($result, 0, $info['header_size'])); 136 | $info['error'] = $result === FALSE ? array('message' => curl_error($ch), 'code' => curl_errno($ch)) : array(); 137 | 138 | $request->setHeaders($info['request_header']); 139 | $response = new Response($request, substr($result, $info['header_size']), $info['http_code'], end($info['headers']), $info); 140 | 141 | if (!$response->isOk()) { 142 | $e = $response->toException(); 143 | curl_close($ch); 144 | $this->onError($e, $response); 145 | throw $e; 146 | } 147 | 148 | $this->onSuccess($response); 149 | curl_close($ch); 150 | 151 | return $this->memoryCache[$cacheKey] = $response; 152 | } 153 | 154 | 155 | 156 | /** 157 | * @param Request $request 158 | * @return resource 159 | */ 160 | protected function buildCurlResource(Request $request) 161 | { 162 | $ch = curl_init((string) $request->getUrl()); 163 | $options = $this->curlOptions; 164 | $options[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); 165 | 166 | // configuring a POST request 167 | if ($request->getPost()) { 168 | $options[CURLOPT_POSTFIELDS] = $request->getPost(); 169 | } 170 | 171 | if ($request->isHead()) { 172 | $options[CURLOPT_NOBODY] = TRUE; 173 | 174 | } elseif ($request->isGet()) { 175 | $options[CURLOPT_HTTPGET] = TRUE; 176 | } 177 | 178 | // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait 179 | // for 2 seconds if the server does not support this header. 180 | $options[CURLOPT_HTTPHEADER]['Expect'] = ''; 181 | $tmp = array(); 182 | foreach ($request->getHeaders() + $options[CURLOPT_HTTPHEADER] as $name => $value) { 183 | $tmp[] = trim("$name: $value"); 184 | } 185 | $options[CURLOPT_HTTPHEADER] = $tmp; 186 | 187 | // execute request 188 | curl_setopt_array($ch, $options); 189 | $this->onRequest($request, $options); 190 | 191 | return $ch; 192 | } 193 | 194 | 195 | 196 | private static function parseHeaders($raw) 197 | { 198 | $headers = array(); 199 | 200 | // Split the string on every "double" new line. 201 | foreach (explode("\r\n\r\n", $raw) as $index => $block) { 202 | 203 | // Loop of response headers. The "count() -1" is to 204 | //avoid an empty row for the extra line break before the body of the response. 205 | foreach (Strings::split(trim($block), '~[\r\n]+~') as $i => $line) { 206 | if (preg_match('~^([a-z-]+\\:)(.*)$~is', $line)) { 207 | list($key, $val) = explode(': ', $line, 2); 208 | $headers[$index][$key] = $val; 209 | 210 | } elseif (!empty($line)) { 211 | $headers[$index][] = $line; 212 | } 213 | } 214 | } 215 | 216 | return $headers; 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /src/Kdyby/Github/Api/Request.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class Request extends Nette\Object 23 | { 24 | 25 | const GET = 'GET'; 26 | const HEAD = 'HEAD'; 27 | const POST = 'POST'; 28 | const PATCH = 'PATCH'; 29 | const PUT = 'PUT'; 30 | const DELETE = 'DELETE'; 31 | 32 | /** 33 | * @var \Nette\Http\Url 34 | */ 35 | private $url; 36 | 37 | /** 38 | * @var string 39 | */ 40 | private $method; 41 | 42 | /** 43 | * @var array|string 44 | */ 45 | private $post; 46 | 47 | /** 48 | * @var array 49 | */ 50 | private $headers; 51 | 52 | 53 | 54 | public function __construct(Nette\Http\Url $url, $method = self::GET, $post = array(), array $headers = array()) 55 | { 56 | $this->url = $url; 57 | $this->method = strtoupper($method); 58 | $this->headers = $headers; 59 | 60 | if (!is_array($post)) { 61 | $this->post = $post; 62 | 63 | } elseif ($post) { 64 | $this->post = array_map(function ($value) { 65 | if ($value instanceof Nette\Http\UrlScript) { 66 | return (string) $value; 67 | 68 | } elseif ($value instanceof \CURLFile) { 69 | return $value; 70 | } 71 | 72 | return !is_string($value) ? Json::encode($value) : $value; 73 | }, $post); 74 | } 75 | } 76 | 77 | 78 | 79 | /** 80 | * @return Nette\Http\Url 81 | */ 82 | public function getUrl() 83 | { 84 | return clone $this->url; 85 | } 86 | 87 | 88 | 89 | /** 90 | * @return array 91 | */ 92 | public function getParameters() 93 | { 94 | parse_str($this->url->getQuery(), $params); 95 | return $params; 96 | } 97 | 98 | 99 | 100 | /** 101 | * @return bool 102 | */ 103 | public function isPaginated() 104 | { 105 | $params = $this->getParameters(); 106 | return $this->isGet() && (isset($params['per_page']) || isset($params['page'])); 107 | } 108 | 109 | 110 | 111 | /** 112 | * @return string 113 | */ 114 | public function getMethod() 115 | { 116 | return $this->method; 117 | } 118 | 119 | 120 | 121 | /** 122 | * @return bool 123 | */ 124 | public function isGet() 125 | { 126 | return $this->method === self::GET; 127 | } 128 | 129 | 130 | 131 | /** 132 | * @return bool 133 | */ 134 | public function isHead() 135 | { 136 | return $this->method === self::HEAD; 137 | } 138 | 139 | 140 | 141 | /** 142 | * @return bool 143 | */ 144 | public function isPost() 145 | { 146 | return $this->method === self::POST; 147 | } 148 | 149 | 150 | 151 | /** 152 | * @return bool 153 | */ 154 | public function isPut() 155 | { 156 | return $this->method === self::PUT; 157 | } 158 | 159 | 160 | 161 | /** 162 | * @return bool 163 | */ 164 | public function isPatch() 165 | { 166 | return $this->method === self::PATCH; 167 | } 168 | 169 | 170 | 171 | /** 172 | * @return bool 173 | */ 174 | public function isDelete() 175 | { 176 | return $this->method === self::DELETE; 177 | } 178 | 179 | 180 | 181 | /** 182 | * @return array|string|NULL 183 | */ 184 | public function getPost() 185 | { 186 | return $this->post; 187 | } 188 | 189 | 190 | 191 | /** 192 | * @return array 193 | */ 194 | public function getHeaders() 195 | { 196 | return $this->headers; 197 | } 198 | 199 | 200 | 201 | /** 202 | * @param array|string $post 203 | * @return Request 204 | */ 205 | public function setPost($post) 206 | { 207 | $this->post = $post; 208 | return $this; 209 | } 210 | 211 | 212 | 213 | /** 214 | * @param array $headers 215 | * @return Request 216 | */ 217 | public function setHeaders(array $headers) 218 | { 219 | $this->headers = $headers; 220 | return $this; 221 | } 222 | 223 | 224 | 225 | /** 226 | * @param string $header 227 | * @param string $value 228 | * @return Request 229 | */ 230 | public function setHeader($header, $value) 231 | { 232 | $this->headers[$header] = $value; 233 | return $this; 234 | } 235 | 236 | 237 | 238 | /** 239 | * @param $url 240 | * @return Request 241 | */ 242 | public function copyWithUrl($url) 243 | { 244 | $headers = $this->headers; 245 | array_shift($headers); // drop info about HTTP version 246 | return new static(new Nette\Http\Url($url), $this->getMethod(), $this->post, $headers); 247 | } 248 | 249 | } 250 | -------------------------------------------------------------------------------- /src/Kdyby/Github/Api/Response.php: -------------------------------------------------------------------------------- 1 | 22 | * 23 | * @property-read Request $request 24 | * @property-read string $content 25 | * @property-read int $httpCode 26 | * @property-read array $headers 27 | * @property-read array $debugInfo 28 | */ 29 | class Response extends Nette\Object 30 | { 31 | 32 | /** 33 | * @var Request 34 | */ 35 | private $request; 36 | 37 | /** 38 | * @var string|array 39 | */ 40 | private $content; 41 | 42 | /** 43 | * @var string|array 44 | */ 45 | private $arrayContent; 46 | 47 | /** 48 | * @var int 49 | */ 50 | private $httpCode; 51 | 52 | /** 53 | * @var array 54 | */ 55 | private $headers; 56 | 57 | /** 58 | * @var array 59 | */ 60 | private $info; 61 | 62 | 63 | 64 | public function __construct(Request $request, $content, $httpCode, $headers = array(), $info = array()) 65 | { 66 | $this->request = $request; 67 | $this->content = $content; 68 | $this->httpCode = (int) $httpCode; 69 | $this->headers = $headers; 70 | $this->info = $info; 71 | } 72 | 73 | 74 | 75 | /** 76 | * @return Request 77 | */ 78 | public function getRequest() 79 | { 80 | return $this->request; 81 | } 82 | 83 | 84 | 85 | /** 86 | * @return array|string 87 | */ 88 | public function getContent() 89 | { 90 | return $this->content; 91 | } 92 | 93 | 94 | 95 | /** 96 | * @return bool 97 | */ 98 | public function isJson() 99 | { 100 | return isset($this->headers['Content-Type']) 101 | && preg_match('~^application/json;.*~is', $this->headers['Content-Type']); 102 | } 103 | 104 | 105 | 106 | /** 107 | * @throws Github\ApiException 108 | * @return array 109 | */ 110 | public function toArray() 111 | { 112 | if ($this->arrayContent !== NULL) { 113 | return $this->arrayContent; 114 | } 115 | 116 | if (!$this->isJson()) { 117 | return NULL; 118 | } 119 | 120 | try { 121 | return $this->arrayContent = Json::decode($this->content, Json::FORCE_ARRAY); 122 | 123 | } catch (Nette\Utils\JsonException $jsonException) { 124 | $e = new Github\ApiException($jsonException->getMessage() . ($this->content ? "\n\n" . $this->content : ''), $this->httpCode, $jsonException); 125 | $e->bindResponse($this->request, $this); 126 | throw $e; 127 | } 128 | } 129 | 130 | 131 | 132 | /** 133 | * @return bool 134 | */ 135 | public function isPaginated() 136 | { 137 | return $this->request->isPaginated() || ($this->request->isGet() && isset($this->headers['Link'])); 138 | } 139 | 140 | 141 | 142 | /** 143 | * @return int 144 | */ 145 | public function getHttpCode() 146 | { 147 | return $this->httpCode; 148 | } 149 | 150 | 151 | 152 | /** 153 | * @return array 154 | */ 155 | public function getHeaders() 156 | { 157 | return $this->headers; 158 | } 159 | 160 | 161 | 162 | /** 163 | * @return bool 164 | */ 165 | public function isOk() 166 | { 167 | return $this->httpCode >= 200 && $this->httpCode < 300; 168 | } 169 | 170 | 171 | 172 | /** 173 | * @return Github\ApiException|static 174 | */ 175 | public function toException() 176 | { 177 | if ($this->httpCode < 300 && $this->content !== FALSE) { 178 | return NULL; 179 | } 180 | 181 | $error = isset($this->info['error']) ? $this->info['error'] : NULL; 182 | $e = new Github\RequestFailedException( 183 | $error ? $error['message'] : '', 184 | $error ? (int) $error['code'] : 0 185 | ); 186 | 187 | if (!$this->hasRemainingRateLimit()) { 188 | $e = new Github\ApiLimitExceedException("The API rate limit of {$this->getRateLimit()} has been exceeded. See https://developer.github.com/v3/#rate-limiting for details.", $e); 189 | 190 | } elseif ($this->content && $this->isJson()) { 191 | $response = $this->toArray(); 192 | 193 | if ($this->httpCode === 400) { 194 | $e = new Github\BadRequestException(isset($response['message']) ? $response['message'] : $this->content, $this->httpCode, $e); 195 | 196 | } elseif ($this->httpCode === 422 && isset($response['errors'])) { 197 | $e = new Github\ValidationFailedException('Validation Failed: ' . self::parseErrors($response), $this->httpCode, $e); 198 | 199 | } elseif ($this->httpCode === 404 && isset($response['message'])) { 200 | $e = new Github\UnknownResourceException($response['message'] . ': ' . $this->request->getUrl(), $this->httpCode, $e); 201 | 202 | } elseif (isset($response['message'])) { 203 | $e = new Github\ApiException($response['message'], $this->httpCode, $e); 204 | } 205 | } 206 | 207 | return $e->bindResponse($this->request, $this); 208 | } 209 | 210 | 211 | 212 | /** 213 | * @return bool 214 | */ 215 | public function hasRemainingRateLimit() 216 | { 217 | return isset($this->headers['X-RateLimit-Remaining']) ? $this->headers['X-RateLimit-Remaining'] > 0 : TRUE; 218 | } 219 | 220 | 221 | 222 | /** 223 | * @return int 224 | */ 225 | public function getRateLimit() 226 | { 227 | return isset($this->headers['X-RateLimit-Limit']) ? $this->headers['X-RateLimit-Limit'] : 5000; 228 | } 229 | 230 | 231 | 232 | /** 233 | * @see https://developer.github.com/guides/traversing-with-pagination/#navigating-through-the-pages 234 | * @param string $rel 235 | * @return array 236 | */ 237 | public function getPaginationLink($rel = 'next') 238 | { 239 | if (!isset($this->headers['Link']) || !preg_match('~<(?P[^>]+)>;\s*rel="' . preg_quote($rel) . '"~i', $this->headers['Link'], $m)) { 240 | return NULL; 241 | } 242 | 243 | return new Nette\Http\UrlScript($m['link']); 244 | } 245 | 246 | 247 | 248 | /** 249 | * @internal 250 | * @return array 251 | */ 252 | public function getDebugInfo() 253 | { 254 | return $this->info; 255 | } 256 | 257 | 258 | 259 | private static function parseErrors(array $response) 260 | { 261 | $errors = array(); 262 | foreach ($response['errors'] as $error) { 263 | switch ($error['code']) { 264 | case 'missing': 265 | $errors[] = sprintf('The %s %s does not exist, for resource "%s"', $error['field'], $error['value'], $error['resource']); 266 | break; 267 | 268 | case 'missing_field': 269 | $errors[] = sprintf('Field "%s" is missing, for resource "%s"', $error['field'], $error['resource']); 270 | break; 271 | 272 | case 'invalid': 273 | $errors[] = sprintf('Field "%s" is invalid, for resource "%s"', $error['field'], $error['resource']); 274 | break; 275 | 276 | case 'already_exists': 277 | $errors[] = sprintf('Field "%s" already exists, for resource "%s"', $error['field'], $error['resource']); 278 | break; 279 | 280 | default: 281 | $errors[] = $error['message']; 282 | break; 283 | } 284 | } 285 | 286 | return implode(', ', $errors); 287 | } 288 | 289 | } 290 | -------------------------------------------------------------------------------- /src/Kdyby/Github/Client.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class Client extends Nette\Object 26 | { 27 | 28 | const AUTH_URL_TOKEN = 'url_token'; 29 | const AUTH_URL_CLIENT_ID = 'url_client_id'; 30 | const AUTH_HTTP_PASSWORD = 'http_password'; 31 | 32 | /** 33 | * @var Api\CurlClient 34 | */ 35 | private $httpClient; 36 | 37 | /** 38 | * @var Configuration 39 | */ 40 | private $config; 41 | 42 | /** 43 | * @var \Nette\Http\IRequest 44 | */ 45 | private $httpRequest; 46 | 47 | /** 48 | * @var SessionStorage 49 | */ 50 | private $session; 51 | 52 | /** 53 | * The ID of the Github user, or 0 if the user is logged out. 54 | * @var integer 55 | */ 56 | protected $user; 57 | 58 | /** 59 | * The OAuth access token received in exchange for a valid authorization code. 60 | * null means the access token has yet to be determined. 61 | * @var string 62 | */ 63 | protected $accessToken; 64 | 65 | /** 66 | * Indicates the authorization method for next api call. 67 | * @var string 68 | */ 69 | protected $authorizeBy; 70 | 71 | 72 | 73 | /** 74 | * @param Configuration $config 75 | * @param Nette\Http\IRequest $httpRequest 76 | * @param SessionStorage $session 77 | * @param Api\CurlClient $httpClient 78 | */ 79 | public function __construct(Configuration $config, Nette\Http\IRequest $httpRequest, SessionStorage $session, HttpClient $httpClient) 80 | { 81 | $this->config = $config; 82 | $this->httpRequest = $httpRequest; 83 | $this->session = $session; 84 | $this->httpClient = $httpClient; 85 | } 86 | 87 | 88 | 89 | /** 90 | * @return Configuration 91 | */ 92 | public function getConfig() 93 | { 94 | return $this->config; 95 | } 96 | 97 | 98 | 99 | /** 100 | * @return Nette\Http\UrlScript 101 | */ 102 | public function getCurrentUrl() 103 | { 104 | return clone $this->httpRequest->getUrl(); 105 | } 106 | 107 | 108 | 109 | /** 110 | * @return SessionStorage 111 | */ 112 | public function getSession() 113 | { 114 | return $this->session; 115 | } 116 | 117 | 118 | 119 | /** 120 | * Get the UID of the connected user, or 0 if the Github user is not connected. 121 | * 122 | * @return string the UID if available. 123 | */ 124 | public function getUser() 125 | { 126 | if ($this->user === NULL) { 127 | $this->user = $this->getUserFromAvailableData(); 128 | } 129 | 130 | return $this->user; 131 | } 132 | 133 | 134 | 135 | /** 136 | * @param int|string $profileId 137 | * @return Profile 138 | */ 139 | public function getProfile($profileId = NULL) 140 | { 141 | return new Profile($this, $profileId); 142 | } 143 | 144 | 145 | 146 | /** 147 | * @param string $path 148 | * @param array $params 149 | * @param array $headers 150 | * @throws ApiException 151 | * @return ArrayHash|string|Paginator|ArrayHash[] 152 | */ 153 | public function get($path, array $params = array(), array $headers = array()) 154 | { 155 | return $this->api($path, Api\Request::GET, $params, array(), $headers); 156 | } 157 | 158 | 159 | 160 | /** 161 | * @param string $path 162 | * @param array $params 163 | * @param array $headers 164 | * @throws ApiException 165 | * @return ArrayHash|string|Paginator|ArrayHash[] 166 | */ 167 | public function head($path, array $params = array(), array $headers = array()) 168 | { 169 | return $this->api($path, Api\Request::HEAD, $params, array(), $headers); 170 | } 171 | 172 | 173 | 174 | /** 175 | * @param string $path 176 | * @param array $params 177 | * @param array|string $post 178 | * @param array $headers 179 | * @throws ApiException 180 | * @return ArrayHash|string|Paginator|ArrayHash[] 181 | */ 182 | public function post($path, array $params = array(), $post = array(), array $headers = array()) 183 | { 184 | return $this->api($path, Api\Request::POST, $params, $post, $headers); 185 | } 186 | 187 | 188 | 189 | /** 190 | * @param string $path 191 | * @param array $params 192 | * @param array|string $post 193 | * @param array $headers 194 | * @throws ApiException 195 | * @return ArrayHash|string|Paginator|ArrayHash[] 196 | */ 197 | public function patch($path, array $params = array(), $post = array(), array $headers = array()) 198 | { 199 | return $this->api($path, Api\Request::PATCH, $params, $post, $headers); 200 | } 201 | 202 | 203 | 204 | /** 205 | * @param string $path 206 | * @param array $params 207 | * @param array|string $post 208 | * @param array $headers 209 | * @throws ApiException 210 | * @return ArrayHash|string|Paginator|ArrayHash[] 211 | */ 212 | public function put($path, array $params = array(), $post = array(), array $headers = array()) 213 | { 214 | return $this->api($path, Api\Request::PUT, $params, $post, $headers); 215 | } 216 | 217 | 218 | 219 | /** 220 | * @param string $path 221 | * @param array $params 222 | * @param array $headers 223 | * @throws ApiException 224 | * @return ArrayHash|string|Paginator|ArrayHash[] 225 | */ 226 | public function delete($path, array $params = array(), array $headers = array()) 227 | { 228 | return $this->api($path, Api\Request::DELETE, $params, array(), $headers); 229 | } 230 | 231 | 232 | 233 | /** 234 | * Simply pass anything starting with a slash and it will call the Api, for example 235 | * 236 | * $details = $github->api('/user'); 237 | * 238 | * 239 | * @param string $path 240 | * @param string $method The argument is optional 241 | * @param array $params Query parameters 242 | * @param array|string $post Post request parameters or body to send 243 | * @param array $headers Http request headers 244 | * @throws ApiException 245 | * @return ArrayHash|string|Paginator|ArrayHash[] 246 | */ 247 | public function api($path, $method = Api\Request::GET, array $params = array(), $post = array(), array $headers = array()) 248 | { 249 | if (is_array($method)) { 250 | $headers = $post; 251 | $post = $params; 252 | $params = $method; 253 | $method = Api\Request::GET; 254 | } 255 | 256 | list($params, $headers) = $this->authorizeRequest($params, $headers); 257 | $response = $this->httpClient->makeRequest( 258 | new Api\Request($this->buildRequestUrl($path, $params), $method, $post, $headers) 259 | ); 260 | 261 | if ($response->isPaginated()) { 262 | return new Paginator($this, $response); 263 | } 264 | 265 | return $response->isJson() ? ArrayHash::from($response->toArray()) : $response->getContent(); 266 | } 267 | 268 | 269 | 270 | protected function authorizeRequest($params, $headers) 271 | { 272 | if (isset($this->authorizeBy[self::AUTH_URL_CLIENT_ID])) { 273 | $params['client_id'] = $this->config->appId; 274 | $params['client_secret'] = $this->config->appSecret; 275 | 276 | } elseif (isset($this->authorizeBy[self::AUTH_HTTP_PASSWORD])) { 277 | list($login, $password) = $this->authorizeBy[self::AUTH_HTTP_PASSWORD]; 278 | $headers['Authorization'] = sprintf('Basic %s', base64_encode($login . ':' . $password)); 279 | 280 | } elseif (isset($this->authorizeBy[self::AUTH_URL_TOKEN])) { 281 | $params['access_token'] = $this->getAccessToken(); 282 | 283 | } elseif ($token = $this->getAccessToken()) { // automatically sign by user's token if he's authorized 284 | $headers['Authorization'] = 'token ' . $token; 285 | } 286 | 287 | $this->authorizeBy = array(); 288 | 289 | return array($params, $headers); 290 | } 291 | 292 | 293 | 294 | /** 295 | * Allows you to write less code, because you won't have the get the parameters and pass them. 296 | * 297 | * 298 | * $response = $client->get('/applications/:client_id/tokens/:access_token'); 299 | * 300 | * 301 | * @param $path 302 | * @param $params 303 | * @return Nette\Http\UrlScript 304 | * @throws \Nette\Utils\RegexpException 305 | */ 306 | protected function buildRequestUrl($path, $params) 307 | { 308 | if (($q = stripos($path, '?')) !== FALSE) { 309 | $query = substr($path, $q + 1); 310 | parse_str($query, $params); 311 | $path = substr($path, 0, $q); 312 | } 313 | 314 | $url = $this->config->createUrl('api', $path, $params); 315 | if (substr_count($url->path, ':') === 0) { // no parameters 316 | return $url; 317 | } 318 | 319 | $client = $this; 320 | $url->setPath(Nette\Utils\Strings::replace($url->getPath(), '~(?<=\\/|^)\\:(\w+)(?=\\/|\\z)~i', function ($m) use ($client) { 321 | if ($m[1] === 'client_id') { 322 | return $client->config->appId; 323 | 324 | } elseif ($m[1] === 'client_secret') { 325 | return $client->config->appSecret; 326 | 327 | } elseif ($m[1] === 'user_id') { 328 | return $client->getUser(); 329 | 330 | } elseif ($m[1] === 'access_token') { 331 | return $client->getAccessToken(); 332 | 333 | } elseif ($m[1] === 'login' && $client->getUser()) { 334 | try { 335 | $user = $client->get('/user'); // the repeated call is cached on http client level 336 | return $user->login; 337 | 338 | } catch (ApiException $e) { 339 | return $m[0]; 340 | } 341 | } 342 | 343 | return $m[0]; 344 | })); 345 | 346 | return $url; 347 | } 348 | 349 | 350 | 351 | /** 352 | * The next request will contain parameter `access_token` in the url. 353 | * 354 | * 355 | * $response = $client->authByUrlToken()->get('/users/whatever'); 356 | * // will generate https://api.github.com/users/whatever?access_token=xxx 357 | * 358 | * 359 | * Expects, that the user is authorized or that you've provided the `access_token` using 360 | * 361 | * 362 | * $client->setAccessToken($token); 363 | * 364 | * 365 | * @return Client 366 | */ 367 | public function authByUrlToken() 368 | { 369 | $this->authorizeBy = array(self::AUTH_URL_TOKEN => TRUE); 370 | return $this; 371 | } 372 | 373 | 374 | 375 | /** 376 | * The next request will contain `client_id` and `client_secret` parameters, 377 | * that will be automatically fetched from your config. 378 | * 379 | * 380 | * $response = $client->authByClientIdParameter()->get('/users/whatever'); 381 | * // will generate https://api.github.com/users/whatever?client_id=xxx&client_secret=yyy 382 | * 383 | * 384 | * @return Client 385 | */ 386 | public function authByClientIdParameter() 387 | { 388 | $this->authorizeBy = array(self::AUTH_URL_CLIENT_ID => TRUE); 389 | return $this; 390 | } 391 | 392 | 393 | 394 | /** 395 | * The next request will be authorized by provided username and password. 396 | * 397 | * 398 | * $response = $client->authByPassword('user', 'password')->get('/users/whatever'); 399 | * // will generate https://user:password@api.github.com/users/whatever 400 | * 401 | * 402 | * @return Client 403 | */ 404 | public function authByPassword($login, $password) 405 | { 406 | $this->authorizeBy = array(self::AUTH_HTTP_PASSWORD => array($login, $password)); 407 | return $this; 408 | } 409 | 410 | 411 | 412 | /** 413 | * Sets the access token for api calls. Use this if you get 414 | * your access token by other means and just want the SDK 415 | * to use it. 416 | * 417 | * @param string $accessToken an access token. 418 | * @return Client 419 | */ 420 | public function setAccessToken($accessToken) 421 | { 422 | $this->accessToken = $accessToken; 423 | return $this; 424 | } 425 | 426 | 427 | 428 | /** 429 | * Determines the access token that should be used for API calls. 430 | * The first time this is called, $this->accessToken is set equal 431 | * to either a valid user access token, or it's set to the application 432 | * access token if a valid user access token wasn't available. Subsequent 433 | * calls return whatever the first call returned. 434 | * 435 | * @return string The access token 436 | */ 437 | public function getAccessToken() 438 | { 439 | if ($this->accessToken !== NULL) { 440 | return $this->accessToken; // we've done this already and cached it. Just return. 441 | } 442 | 443 | if ($accessToken = $this->getUserAccessToken()) { 444 | $this->setAccessToken($accessToken); 445 | } 446 | 447 | return $this->accessToken; 448 | } 449 | 450 | 451 | 452 | /** 453 | * @internal 454 | * @return Api\CurlClient 455 | */ 456 | public function getHttpClient() 457 | { 458 | return $this->httpClient; 459 | } 460 | 461 | 462 | 463 | /** 464 | * Determines and returns the user access token, first using 465 | * the signed request if present, and then falling back on 466 | * the authorization code if present. The intent is to 467 | * return a valid user access token, or false if one is determined 468 | * to not be available. 469 | * 470 | * @return string A valid user access token, or false if one could not be determined. 471 | */ 472 | protected function getUserAccessToken() 473 | { 474 | if (($code = $this->getCode()) && $code != $this->session->code) { 475 | if ($accessToken = $this->getAccessTokenFromCode($code)) { 476 | $this->session->code = $code; 477 | return $this->session->access_token = $accessToken; 478 | } 479 | 480 | // code was bogus, so everything based on it should be invalidated. 481 | $this->session->clearAll(); 482 | return FALSE; 483 | } 484 | 485 | // as a fallback, just return whatever is in the persistent 486 | // store, knowing nothing explicit (signed request, authorization 487 | // code, etc.) was present to shadow it (or we saw a code in $_REQUEST, 488 | // but it's the same as what's in the persistent store) 489 | return $this->session->access_token; 490 | } 491 | 492 | 493 | 494 | /** 495 | * Determines the connected user by first examining any signed 496 | * requests, then considering an authorization code, and then 497 | * falling back to any persistent store storing the user. 498 | * 499 | * @return integer The id of the connected Github user, or 0 if no such user exists. 500 | */ 501 | protected function getUserFromAvailableData() 502 | { 503 | $user = $this->session->get('user_id', 0); 504 | 505 | // use access_token to fetch user id if we have a user access_token, or if 506 | // the cached access token has changed. 507 | if (($accessToken = $this->getAccessToken()) && !($user && $this->session->access_token === $accessToken)) { 508 | if (!$user = $this->getUserFromAccessToken()) { 509 | $this->session->clearAll(); 510 | 511 | } else { 512 | $this->session->user_id = $user; 513 | } 514 | } 515 | 516 | return $user; 517 | } 518 | 519 | 520 | 521 | /** 522 | * Get the authorization code from the query parameters, if it exists, 523 | * and otherwise return false to signal no authorization code was 524 | * discoverable. 525 | * 526 | * @return mixed The authorization code, or false if the authorization code could not be determined. 527 | */ 528 | protected function getCode() 529 | { 530 | $state = $this->getRequest('state'); 531 | if (($code = $this->getRequest('code')) && $state && $this->session->state === $state) { 532 | $this->session->state = NULL; // CSRF state has done its job, so clear it 533 | return $code; 534 | } 535 | 536 | return FALSE; 537 | } 538 | 539 | 540 | 541 | /** 542 | * Retrieves the UID with the understanding that $this->accessToken has already been set and is seemingly legitimate. 543 | * It relies on Github's API to retrieve user information and then extract the user ID. 544 | * 545 | * @return integer Returns the UID of the Github user, or 0 if the Github user could not be determined. 546 | */ 547 | protected function getUserFromAccessToken() 548 | { 549 | try { 550 | $user = $this->get('/user'); 551 | 552 | return isset($user['id']) ? $user['id'] : 0; 553 | } catch (\Exception $e) { } 554 | 555 | return 0; 556 | } 557 | 558 | 559 | 560 | /** 561 | * Retrieves an access token for the given authorization code 562 | * (previously generated from www.github.com on behalf of a specific user). 563 | * The authorization code is sent to api.github.com/oauth 564 | * and a legitimate access token is generated provided the access token 565 | * and the user for which it was generated all match, and the user is 566 | * either logged in to Github or has granted an offline access permission. 567 | * 568 | * @param string $code An authorization code. 569 | * @param null $redirectUri 570 | * @return mixed An access token exchanged for the authorization code, or false if an access token could not be generated. 571 | */ 572 | protected function getAccessTokenFromCode($code, $redirectUri = NULL) 573 | { 574 | if (empty($code)) { 575 | return FALSE; 576 | } 577 | 578 | if ($redirectUri === NULL) { 579 | $redirectUri = $this->getCurrentUrl(); 580 | parse_str($redirectUri->getQuery(), $query); 581 | unset($query['code'], $query['state']); 582 | $redirectUri->setQuery($query); 583 | } 584 | 585 | try { 586 | $url = $this->config->createUrl('oauth', 'access_token', array( 587 | 'client_id' => $this->config->appId, 588 | 'client_secret' => $this->config->appSecret, 589 | 'code' => $code, 590 | 'redirect_uri' => $redirectUri, 591 | )); 592 | 593 | $response = $this->httpClient->makeRequest(new Api\Request($url, Api\Request::POST, array(), array('Accept' => 'application/json'))); 594 | if (!$response->isOk() || !$response->isJson()) { 595 | return FALSE; 596 | } 597 | 598 | $token = $response->toArray(); 599 | 600 | } catch (\Exception $e) { 601 | // most likely that user very recently revoked authorization. 602 | // In any event, we don't have an access token, so say so. 603 | return FALSE; 604 | } 605 | 606 | return isset($token['access_token']) ? $token['access_token'] : FALSE; 607 | } 608 | 609 | 610 | 611 | /** 612 | * Destroy the current session 613 | */ 614 | public function destroySession() 615 | { 616 | $this->accessToken = NULL; 617 | $this->user = NULL; 618 | $this->session->clearAll(); 619 | } 620 | 621 | 622 | 623 | /** 624 | * @param string $key 625 | * @param mixed $default 626 | * @return mixed|null 627 | */ 628 | protected function getRequest($key, $default = NULL) 629 | { 630 | if ($value = $this->httpRequest->getPost($key)) { 631 | return $value; 632 | } 633 | 634 | if ($value = $this->httpRequest->getQuery($key)) { 635 | return $value; 636 | } 637 | 638 | return $default; 639 | } 640 | 641 | } 642 | -------------------------------------------------------------------------------- /src/Kdyby/Github/Configuration.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Configuration extends Nette\Object 25 | { 26 | 27 | /** 28 | * @var string 29 | */ 30 | public $appId; 31 | 32 | /** 33 | * @var string 34 | */ 35 | public $appSecret; 36 | 37 | /** 38 | * @var array 39 | */ 40 | public $permissions = array(); 41 | 42 | /** 43 | * @var array 44 | */ 45 | public $domains = array( 46 | 'oauth' => 'https://github.com/login/oauth/', 47 | 'api' => 'https://api.github.com/', 48 | ); 49 | 50 | 51 | 52 | public function __construct($appId, $appSecret) 53 | { 54 | $this->appId = $appId; 55 | $this->appSecret = $appSecret; 56 | } 57 | 58 | 59 | 60 | /** 61 | * Build the URL for given domain alias, path and parameters. 62 | * 63 | * @param string $name The name of the domain 64 | * @param string $path Optional path (without a leading slash) 65 | * @param array $params Optional query parameters 66 | * 67 | * @return UrlScript The URL for the given parameters 68 | */ 69 | public function createUrl($name, $path = NULL, $params = array()) 70 | { 71 | if (preg_match('~^https?://([^.]+\\.)?github\\.com/~', trim($path))) { 72 | $url = new UrlScript($path); 73 | 74 | } else { 75 | $url = new UrlScript($this->domains[$name]); 76 | $url->path .= ltrim($path, '/'); 77 | } 78 | 79 | $url->appendQuery(array_map(function ($param) { 80 | return $param instanceof UrlScript ? (string) $param : $param; 81 | }, $params)); 82 | 83 | return $url; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/Kdyby/Github/DI/GithubExtension.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class GithubExtension extends Nette\DI\CompilerExtension 24 | { 25 | 26 | /** 27 | * @var array 28 | */ 29 | public $defaults = array( 30 | 'appId' => NULL, 31 | 'appSecret' => NULL, 32 | 'permissions' => array(), 33 | 'clearAllWithLogout' => TRUE, 34 | 'curlOptions' => array(), 35 | 'debugger' => '%debugMode%', 36 | ); 37 | 38 | 39 | 40 | public function __construct() 41 | { 42 | $this->defaults['curlOptions'] = Kdyby\Github\Api\CurlClient::$defaultCurlOptions; 43 | } 44 | 45 | 46 | 47 | public function loadConfiguration() 48 | { 49 | $builder = $this->getContainerBuilder(); 50 | 51 | $config = $this->getConfig($this->defaults); 52 | Validators::assert($config['appId'], 'string', 'Application ID'); 53 | Validators::assert($config['appSecret'], 'string:40', 'Application secret'); 54 | 55 | $builder->addDefinition($this->prefix('client')) 56 | ->setClass('Kdyby\Github\Client'); 57 | 58 | $builder->addDefinition($this->prefix('config')) 59 | ->setClass('Kdyby\Github\Configuration', array( 60 | $config['appId'], 61 | $config['appSecret'], 62 | )) 63 | ->addSetup('$permissions', array($config['permissions'])); 64 | 65 | foreach ($config['curlOptions'] as $option => $value) { 66 | if (defined($option)) { 67 | unset($config['curlOptions'][$option]); 68 | $config['curlOptions'][constant($option)] = $value; 69 | } 70 | } 71 | 72 | $httpClient = $builder->addDefinition($this->prefix('httpClient')) 73 | ->setClass('Kdyby\Github\Api\CurlClient') 74 | ->addSetup('$service->curlOptions = ?;', array($config['curlOptions'])); 75 | 76 | $builder->addDefinition($this->prefix('session')) 77 | ->setClass('Kdyby\Github\SessionStorage'); 78 | 79 | if ($config['debugger']) { 80 | $builder->addDefinition($this->prefix('panel')) 81 | ->setClass('Kdyby\Github\Diagnostics\Panel'); 82 | 83 | $httpClient->addSetup($this->prefix('@panel') . '::register', array('@self')); 84 | } 85 | 86 | if ($config['clearAllWithLogout']) { 87 | $builder->getDefinition('user') 88 | ->addSetup('$sl = ?; ?->onLoggedOut[] = function () use ($sl) { $sl->getService(?)->clearAll(); }', array( 89 | '@container', '@self', $this->prefix('session') 90 | )); 91 | } 92 | } 93 | 94 | 95 | 96 | public static function register(Nette\Configurator $configurator) 97 | { 98 | $configurator->onCompile[] = function ($config, Nette\DI\Compiler $compiler) { 99 | $compiler->addExtension('github', new GithubExtension()); 100 | }; 101 | } 102 | 103 | } 104 | 105 | -------------------------------------------------------------------------------- /src/Kdyby/Github/Diagnostics/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kdyby/Github/55978e274b338cf712661f2f429f1a487e869f52/src/Kdyby/Github/Diagnostics/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /src/Kdyby/Github/Diagnostics/Panel.php: -------------------------------------------------------------------------------- 1 | 27 | * 28 | * @property callable $begin 29 | * @property callable $failure 30 | * @property callable $success 31 | */ 32 | class Panel extends Nette\Object implements IBarPanel 33 | { 34 | 35 | /** 36 | * @var int logged time 37 | */ 38 | private $totalTime = 0; 39 | 40 | /** 41 | * @var array 42 | */ 43 | private $calls = array(); 44 | 45 | 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function getTab() 51 | { 52 | $img = Html::el('img', array('height' => '16px')) 53 | ->src('data:image/png;base64,' . base64_encode(file_get_contents(__DIR__ . '/GitHub-Mark-32px.png'))); 54 | $tab = Html::el('span')->title('Github')->add($img); 55 | $title = Html::el()->setText('Github'); 56 | if ($this->calls) { 57 | $title->setText( 58 | count($this->calls) . ' call' . (count($this->calls) > 1 ? 's' : '') . 59 | ' / ' . sprintf('%0.2f', $this->totalTime) . ' s' 60 | ); 61 | } 62 | return (string)$tab->add($title); 63 | } 64 | 65 | 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getPanel() 71 | { 72 | if (!$this->calls) { 73 | return NULL; 74 | } 75 | 76 | ob_start(); 77 | $esc = function ($s) { 78 | return htmlSpecialChars($s, ENT_QUOTES, 'UTF-8'); 79 | }; 80 | $click = class_exists('\Tracy\Dumper') 81 | ? function ($o, $c = FALSE) { return \Tracy\Dumper::toHtml($o, array('collapse' => $c)); } 82 | : Callback::closure('\Tracy\Helpers::clickableDump'); 83 | $totalTime = $this->totalTime ? sprintf('%0.3f', $this->totalTime * 1000) . ' ms' : 'none'; 84 | 85 | require __DIR__ . '/panel.phtml'; 86 | return ob_get_clean(); 87 | } 88 | 89 | 90 | 91 | /** 92 | * @param Api\Request $request 93 | * @param array $options 94 | */ 95 | public function begin(Api\Request $request, $options = array()) 96 | { 97 | $url = $request->getUrl(); 98 | $url->setQuery(''); 99 | 100 | $this->calls[spl_object_hash($request)] = (object) array( 101 | 'url' => (string) $url, 102 | 'params' => $request->getParameters(), 103 | 'options' => self::toConstantNames($options), 104 | 'result' => NULL, 105 | 'exception' => NULL, 106 | 'info' => array(), 107 | 'time' => 0, 108 | ); 109 | } 110 | 111 | 112 | 113 | /** 114 | * @param Api\Response $response 115 | */ 116 | public function success(Api\Response $response) 117 | { 118 | if (!isset($this->calls[$oid = spl_object_hash($response->getRequest())])) { 119 | return; 120 | } 121 | 122 | $debugInfo = $response->debugInfo; 123 | 124 | $current = $this->calls[$oid]; 125 | $this->totalTime += $current->time = $debugInfo['total_time']; 126 | unset($debugInfo['total_time']); 127 | $current->info = $debugInfo; 128 | $current->info['method'] = $response->getRequest()->getMethod(); 129 | $current->result = $response->toArray() ?: $response->getContent(); 130 | } 131 | 132 | 133 | 134 | /** 135 | * @param \Exception|\Throwable $exception 136 | * @param \Kdyby\Github\Api\Response $response 137 | */ 138 | public function failure($exception, Api\Response $response) 139 | { 140 | if (!isset($this->calls[$oid = spl_object_hash($response->getRequest())])) { 141 | return; 142 | } 143 | 144 | $debugInfo = $response->debugInfo; 145 | 146 | $current = $this->calls[$oid]; 147 | $this->totalTime += $current->time = $debugInfo['total_time']; 148 | unset($debugInfo['total_time']); 149 | $current->info = $debugInfo; 150 | $current->info['method'] = $response->getRequest()->getMethod(); 151 | $current->exception = $exception; 152 | } 153 | 154 | 155 | 156 | /** 157 | * @param Api\CurlClient $client 158 | */ 159 | public function register(Api\CurlClient $client) 160 | { 161 | $client->onRequest[] = $this->begin; 162 | $client->onError[] = $this->failure; 163 | $client->onSuccess[] = $this->success; 164 | 165 | self::getDebuggerBar()->addPanel($this); 166 | self::getDebuggerBlueScreen()->addPanel(array($this, 'renderException')); 167 | } 168 | 169 | 170 | 171 | public function renderException($e = NULL) 172 | { 173 | if (!$e instanceof ApiException || !$e->response) { 174 | return NULL; 175 | } 176 | 177 | $h = 'htmlSpecialChars'; 178 | $serializeHeaders = function ($headers) use ($h) { 179 | $s = ''; 180 | foreach ($headers as $header => $value) { 181 | if (!empty($header)) { 182 | $s .= $h($header) . ': '; 183 | } 184 | $s .= $h($value) . "
"; 185 | } 186 | return $s; 187 | }; 188 | 189 | $panel = ''; 190 | 191 | $panel .= '

Request

';
192 | 		$panel .= $serializeHeaders($e->request->getHeaders());
193 | 		if (!in_array($e->request->getMethod(), array('GET', 'HEAD'))) {
194 | 			$panel .= '
' . $h(is_array($e->request->getPost()) ? json_encode($e->request->getPost()) : $e->request->getPost()); 195 | } 196 | $panel .= '
'; 197 | 198 | $panel .= '

Response

';
199 | 		$panel .= $serializeHeaders($e->response->getHeaders());
200 | 		if ($e->response->getContent()) {
201 | 			$panel .= '
' . $h($e->response->toArray() ?: $e->response->getContent()); 202 | } 203 | $panel .= '
'; 204 | 205 | return array( 206 | 'tab' => 'Github', 207 | 'panel' => $panel, 208 | ); 209 | } 210 | 211 | 212 | 213 | /** 214 | * @param array $options 215 | */ 216 | private function toConstantNames(array $options) 217 | { 218 | static $map; 219 | if (!$map) { 220 | $map = array(); 221 | foreach (get_defined_constants() as $name => $value) { 222 | if (substr($name, 0, 8) !== 'CURLOPT_') { 223 | continue; 224 | } 225 | 226 | $map[$value] = $name; 227 | } 228 | } 229 | 230 | $renamed = array(); 231 | foreach ($options as $int => $value) { 232 | $renamed[isset($map[$int]) ? $map[$int] : $int] = $value; 233 | } 234 | 235 | return $renamed; 236 | } 237 | 238 | 239 | 240 | /** 241 | * @return Bar 242 | */ 243 | private static function getDebuggerBar() 244 | { 245 | return method_exists('Tracy\Debugger', 'getBar') ? Debugger::getBar() : Debugger::$bar; 246 | } 247 | 248 | 249 | 250 | /** 251 | * @return BlueScreen 252 | */ 253 | private static function getDebuggerBlueScreen() 254 | { 255 | return method_exists('Tracy\Debugger', 'getBlueScreen') ? Debugger::getBlueScreen() : Debugger::$blueScreen; 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /src/Kdyby/Github/Diagnostics/panel.phtml: -------------------------------------------------------------------------------- 1 | 11 | 12 |

Queries: calls)); ?>, time:

13 |
14 | calls as $call): ?> 15 |

info['method'])); ?> url); ?>

16 | 17 | 18 | 19 | 20 | 21 | 22 |
Timetime * 1000, 2, '.', ' ')) ?> ms
Queryparams, TRUE); ?>
Responseresult ?: $call->exception, TRUE); ?>
Optionsoptions, TRUE); ?>
Infoinfo, TRUE); ?>
23 | 24 |
25 | -------------------------------------------------------------------------------- /src/Kdyby/Github/HttpClient.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | interface HttpClient 22 | { 23 | 24 | /** 25 | * @param Api\Request $request 26 | * @throws ApiException 27 | * @return Api\Response 28 | */ 29 | function makeRequest(Api\Request $request); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/Kdyby/Github/Paginator.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class Paginator extends Nette\Object implements \Iterator 23 | { 24 | 25 | const PER_PAGE_MAX = 100; 26 | 27 | /** 28 | * @var Api\CurlClient 29 | */ 30 | private $httpClient; 31 | 32 | /** 33 | * @var int 34 | */ 35 | private $firstPage; 36 | 37 | /** 38 | * @var int 39 | */ 40 | private $perPage; 41 | 42 | /** 43 | * @var int|NULL 44 | */ 45 | private $maxResults; 46 | 47 | /** 48 | * @var array 49 | */ 50 | private $resources = array(); 51 | 52 | /** 53 | * @var Api\Response[] 54 | */ 55 | private $responses = array(); 56 | 57 | /** 58 | * @var int 59 | */ 60 | private $itemCursor; 61 | 62 | /** 63 | * @var int 64 | */ 65 | private $pageCursor; 66 | 67 | 68 | 69 | public function __construct(Client $client, Api\Response $response) 70 | { 71 | $this->httpClient = $client->getHttpClient(); 72 | $resource = $response->toArray(); 73 | 74 | $params = $response->request->getParameters(); 75 | $this->firstPage = isset($params['page']) ? (int) max($params['page'], 1) : 1; 76 | $this->perPage = isset($params['per_page']) ? (int) $params['per_page'] : count($resource); 77 | 78 | $this->responses[$this->firstPage] = $response; 79 | $this->resources[$this->firstPage] = $resource; 80 | } 81 | 82 | 83 | 84 | /** 85 | * If you setup maximum number of results, the pagination will stop after fetching the desired number. 86 | * If you have per_page=50 and wan't to fetch 200 results, it will make 4 requests in total. 87 | * 88 | * @param int $maxResults 89 | * @return Paginator 90 | */ 91 | public function limitResults($maxResults) 92 | { 93 | $this->maxResults = (int)$maxResults; 94 | return $this; 95 | } 96 | 97 | 98 | 99 | public function rewind() 100 | { 101 | $this->itemCursor = 0; 102 | $this->pageCursor = $this->firstPage; 103 | } 104 | 105 | 106 | 107 | public function valid() 108 | { 109 | return isset($this->resources[$this->pageCursor][$this->itemCursor]) 110 | && ! $this->loadedMaxResults(); 111 | } 112 | 113 | 114 | 115 | /** 116 | * @return bool 117 | */ 118 | public function loadedMaxResults() 119 | { 120 | if ($this->maxResults === NULL) { 121 | return FALSE; 122 | } 123 | 124 | return $this->maxResults <= ($this->itemCursor + ($this->pageCursor - $this->firstPage) * $this->perPage); 125 | } 126 | 127 | 128 | 129 | public function current() 130 | { 131 | if (!$this->valid()) { 132 | return NULL; 133 | } 134 | 135 | return Nette\Utils\ArrayHash::from($this->resources[$this->pageCursor][$this->itemCursor]); 136 | } 137 | 138 | 139 | 140 | public function next() 141 | { 142 | $this->itemCursor++; 143 | 144 | // if cursor points at result of next page, try to load it 145 | if ($this->itemCursor < $this->perPage || $this->itemCursor % $this->perPage !== 0) { 146 | return; 147 | } 148 | 149 | if (isset($this->resources[$this->pageCursor + 1])) { // already loaded 150 | $this->itemCursor = 0; 151 | $this->pageCursor++; 152 | return; 153 | } 154 | 155 | if ($this->loadedMaxResults()) { 156 | return; 157 | } 158 | 159 | if (!$nextPage = $this->responses[$this->pageCursor]->getPaginationLink('next')) { 160 | return; // end 161 | } 162 | 163 | try { 164 | $prevRequest = $this->responses[$this->pageCursor]->getRequest(); 165 | $response = $this->httpClient->makeRequest($prevRequest->copyWithUrl($nextPage)); 166 | 167 | $this->itemCursor = 0; 168 | $this->pageCursor++; 169 | $this->responses[$this->pageCursor] = $response; 170 | $this->resources[$this->pageCursor] = $response->toArray(); 171 | 172 | } catch (\Exception $e) { 173 | $this->itemCursor--; // revert back so the user can continue if needed 174 | } 175 | } 176 | 177 | 178 | 179 | public function key() 180 | { 181 | return $this->itemCursor + ($this->pageCursor - 1) * $this->perPage; 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /src/Kdyby/Github/Profile.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Profile extends Nette\Object 25 | { 26 | 27 | 28 | /** 29 | * @var Client 30 | */ 31 | private $github; 32 | 33 | /** 34 | * @var string 35 | */ 36 | private $profileId; 37 | 38 | /** 39 | * @var ArrayHash 40 | */ 41 | private $details; 42 | 43 | /** 44 | * @var string 45 | */ 46 | private $primaryEmail; 47 | 48 | 49 | 50 | /** 51 | * @param Client $github 52 | * @param string $profileId 53 | */ 54 | public function __construct(Client $github, $profileId = NULL) 55 | { 56 | $this->github = $github; 57 | 58 | if (is_numeric($profileId)) { 59 | throw new InvalidArgumentException("ProfileId must be a username of the account you're trying to read or NULL, which means actually logged in user."); 60 | } 61 | 62 | $this->profileId = $profileId; 63 | } 64 | 65 | 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getId() 71 | { 72 | if ($this->profileId === NULL) { 73 | return $this->github->getUser(); 74 | } 75 | 76 | return $this->profileId; 77 | } 78 | 79 | 80 | 81 | /** 82 | * @param string $key 83 | * @return ArrayHash|NULL 84 | */ 85 | public function getDetails($key = NULL) 86 | { 87 | if ($this->details === NULL) { 88 | try { 89 | 90 | if ($this->profileId !== NULL) { 91 | $this->details = $this->github->api('/users/' . rawurlencode($this->profileId)); 92 | 93 | } elseif ($this->github->getUser()) { 94 | $this->details = $this->github->api('/user'); 95 | 96 | } else { 97 | $this->details = array(); 98 | } 99 | 100 | } catch (\Exception $e) { 101 | // todo: log? 102 | } 103 | } 104 | 105 | if ($key !== NULL) { 106 | if (empty($this->details[$key])) { 107 | if ($key === 'email') { 108 | $this->details['email'] = $this->getPrimaryEmail(); 109 | } 110 | } 111 | 112 | return isset($this->details[$key]) ? $this->details[$key] : NULL; 113 | } 114 | 115 | return $this->details; 116 | } 117 | 118 | 119 | 120 | /** 121 | * @return string|NULL 122 | */ 123 | public function getPrimaryEmail() 124 | { 125 | if ($this->primaryEmail !== NULL) { 126 | return $this->primaryEmail; 127 | } 128 | 129 | if ($this->profileId !== NULL) { 130 | $this->getDetails(); 131 | return $this->details['email']; 132 | } 133 | 134 | try { 135 | $emails = (array) $this->github->api('/user/emails'); 136 | 137 | } catch (\Exception $e) { 138 | return NULL; // todo: log? 139 | } 140 | 141 | if (!count($emails)) { 142 | return NULL; 143 | } 144 | 145 | usort($emails, function ($a, $b) { 146 | $primary = $b->primary - $a->primary; 147 | return $primary !== 0 ? $primary : ($b->verified - $a->verified); 148 | }); 149 | 150 | return $this->primaryEmail = reset($emails)->email; 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/Kdyby/Github/SessionStorage.php: -------------------------------------------------------------------------------- 1 | 21 | * 22 | * @property string $state A CSRF state variable to assist in the defense against CSRF attacks. 23 | * @property string $code 24 | * @property string $access_token 25 | * @property string $user_id 26 | */ 27 | class SessionStorage extends Nette\Object 28 | { 29 | 30 | /** 31 | * @var \Nette\Http\SessionSection 32 | */ 33 | protected $session; 34 | 35 | 36 | 37 | /** 38 | * @param \Nette\Http\Session $session 39 | * @param Configuration $config 40 | */ 41 | public function __construct(Nette\Http\Session $session, Configuration $config) 42 | { 43 | $this->session = $session->getSection('Github/' . $config->appId); 44 | } 45 | 46 | 47 | 48 | /** 49 | * Lays down a CSRF state token for this process. 50 | * 51 | * @return void 52 | */ 53 | public function establishCSRFTokenState() 54 | { 55 | if (!$this->state) { 56 | $this->state = md5(uniqid(mt_rand(), TRUE)); 57 | } 58 | } 59 | 60 | 61 | 62 | /** 63 | * Stores the given ($key, $value) pair, so that future calls to 64 | * getPersistentData($key) return $value. This call may be in another request. 65 | * 66 | * Provides the implementations of the inherited abstract 67 | * methods. The implementation uses PHP sessions to maintain 68 | * a store for authorization codes, user ids, CSRF states, and 69 | * access tokens. 70 | */ 71 | public function set($key, $value) 72 | { 73 | $this->session->$key = $value; 74 | } 75 | 76 | 77 | 78 | /** 79 | * @param string $key The key of the data to retrieve 80 | * @param mixed $default The default value to return if $key is not found 81 | * 82 | * @return mixed 83 | */ 84 | public function get($key, $default = FALSE) 85 | { 86 | return isset($this->session->$key) ? $this->session->$key : $default; 87 | } 88 | 89 | 90 | 91 | /** 92 | * Clear the data with $key from the persistent storage 93 | * 94 | * @param string $key 95 | * @return void 96 | */ 97 | public function clear($key) 98 | { 99 | unset($this->session->$key); 100 | } 101 | 102 | 103 | 104 | /** 105 | * Clear all data from the persistent storage 106 | * 107 | * @return void 108 | */ 109 | public function clearAll() 110 | { 111 | $this->session->remove(); 112 | } 113 | 114 | 115 | 116 | /** 117 | * @param string $name 118 | * @return mixed 119 | */ 120 | public function &__get($name) 121 | { 122 | $value = $this->get($name); 123 | return $value; 124 | } 125 | 126 | 127 | 128 | /** 129 | * @param string $name 130 | * @param mixed $value 131 | */ 132 | public function __set($name, $value) 133 | { 134 | $this->set($name, $value); 135 | } 136 | 137 | 138 | 139 | /** 140 | * @param string $name 141 | * @return bool 142 | */ 143 | public function __isset($name) 144 | { 145 | return isset($this->session->{$name}); 146 | } 147 | 148 | 149 | 150 | /** 151 | * @param string $name 152 | */ 153 | public function __unset($name) 154 | { 155 | $this->clear($name); 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/Kdyby/Github/UI/LoginDialog.php: -------------------------------------------------------------------------------- 1 | 25 | * 26 | * @method onResponse(LoginDialog $dialog) 27 | */ 28 | class LoginDialog extends Nette\Application\UI\Control 29 | { 30 | 31 | /** 32 | * @var array of function(LoginDialog $dialog) 33 | */ 34 | public $onResponse = array(); 35 | 36 | /** 37 | * @var Github\Client 38 | */ 39 | protected $client; 40 | 41 | /** 42 | * @var Github\Configuration 43 | */ 44 | protected $config; 45 | 46 | /** 47 | * @var Github\SessionStorage 48 | */ 49 | protected $session; 50 | 51 | /** 52 | * @var UrlScript 53 | */ 54 | protected $currentUrl; 55 | 56 | /** 57 | * @var string 58 | */ 59 | protected $scope; 60 | 61 | 62 | 63 | /** 64 | * @param Github\Client $github 65 | */ 66 | public function __construct(Github\Client $github) 67 | { 68 | $this->client = $github; 69 | $this->config = $github->getConfig(); 70 | $this->session = $github->getSession(); 71 | $this->currentUrl = $github->getCurrentUrl(); 72 | 73 | parent::__construct(); 74 | $this->monitor('Nette\Application\IPresenter'); 75 | } 76 | 77 | 78 | 79 | /** 80 | * @return Github\Client 81 | */ 82 | public function getClient() 83 | { 84 | return $this->client; 85 | } 86 | 87 | 88 | 89 | /** 90 | * @param \Nette\ComponentModel\Container $obj 91 | */ 92 | protected function attached($obj) 93 | { 94 | parent::attached($obj); 95 | 96 | if ($obj instanceof Nette\Application\IPresenter) { 97 | $this->currentUrl = new UrlScript($this->link('//response!')); 98 | } 99 | } 100 | 101 | 102 | 103 | /** 104 | * @param string|array $scope 105 | */ 106 | public function setScope($scope) 107 | { 108 | $this->scope = implode(',', (array) $scope); 109 | } 110 | 111 | 112 | 113 | public function handleCallback() 114 | { 115 | 116 | } 117 | 118 | 119 | 120 | /** 121 | * Checks, if there is a user in storage and if not, it redirects to login dialog. 122 | * If the user is already in session storage, it will behave, as if were redirected from github right now, 123 | * this means, it will directly call onResponse event. 124 | * 125 | * @throws \Nette\Application\AbortException 126 | */ 127 | public function handleOpen() 128 | { 129 | if (!$this->client->getUser()) { // no user 130 | $this->open(); 131 | } 132 | 133 | $this->onResponse($this); 134 | $this->presenter->redirect('this'); 135 | } 136 | 137 | 138 | 139 | /** 140 | * @throws \Nette\Application\AbortException 141 | */ 142 | public function open() 143 | { 144 | $this->presenter->redirectUrl($this->getUrl()); 145 | } 146 | 147 | 148 | 149 | /** 150 | * @return array 151 | */ 152 | public function getQueryParams() 153 | { 154 | // CSRF 155 | $this->client->getSession()->establishCSRFTokenState(); 156 | 157 | $params = array( 158 | 'client_id' => $this->config->appId, 159 | 'redirect_uri' => (string) $this->currentUrl, 160 | 'state' => $this->session->state, 161 | 'scope' => $this->scope ?: implode(',', (array) $this->config->permissions), 162 | ); 163 | 164 | return $params; 165 | } 166 | 167 | 168 | 169 | /** 170 | * @return string 171 | */ 172 | public function getUrl() 173 | { 174 | return (string) $this->config->createUrl('oauth', 'authorize', $this->getQueryParams()); 175 | } 176 | 177 | 178 | 179 | public function handleResponse() 180 | { 181 | $this->client->getUser(); // check the received parameters and save user 182 | $this->onResponse($this); 183 | $this->presenter->redirect('this', array('state' => NULL, 'code' => NULL)); 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /src/Kdyby/Github/exceptions.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | interface Exception 20 | { 21 | 22 | } 23 | 24 | 25 | 26 | /** 27 | * @author Filip Procházka 28 | */ 29 | class InvalidArgumentException extends \InvalidArgumentException implements Exception 30 | { 31 | 32 | } 33 | 34 | 35 | 36 | /** 37 | * @author Filip Procházka 38 | */ 39 | class InvalidStateException extends \RuntimeException implements Exception 40 | { 41 | 42 | } 43 | 44 | 45 | 46 | /** 47 | * @author Filip Procházka 48 | */ 49 | class NotSupportedException extends \LogicException implements Exception 50 | { 51 | 52 | } 53 | 54 | 55 | 56 | /** 57 | * @author Filip Procházka 58 | */ 59 | class ApiException extends \RuntimeException implements Exception 60 | { 61 | 62 | /** 63 | * @var Api\Request|NULL 64 | */ 65 | public $request; 66 | 67 | /** 68 | * @var Api\Response|NULL 69 | */ 70 | public $response; 71 | 72 | 73 | 74 | /** 75 | * @return ApiException|static 76 | */ 77 | public function bindResponse(Api\Request $request, Api\Response $response = NULL) 78 | { 79 | $this->request = $request; 80 | $this->response = $response; 81 | return $this; 82 | } 83 | 84 | } 85 | 86 | 87 | 88 | /** 89 | * @author Filip Procházka 90 | */ 91 | class ApiLimitExceedException extends ApiException 92 | { 93 | 94 | } 95 | 96 | 97 | 98 | /** 99 | * @author Filip Procházka 100 | */ 101 | class RequestFailedException extends ApiException 102 | { 103 | 104 | } 105 | 106 | 107 | 108 | /** 109 | * @author Filip Procházka 110 | */ 111 | class ValidationFailedException extends ApiException 112 | { 113 | 114 | } 115 | 116 | 117 | 118 | /** 119 | * @author Filip Procházka 120 | */ 121 | class BadRequestException extends ApiException 122 | { 123 | 124 | } 125 | 126 | 127 | 128 | /** 129 | * @author Filip Procházka 130 | */ 131 | class UnknownResourceException extends ApiException 132 | { 133 | 134 | } 135 | --------------------------------------------------------------------------------