├── 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 | [](https://travis-ci.org/Kdyby/Github)
5 | [](https://packagist.org/packages/kdyby/github)
6 | [](https://packagist.org/packages/kdyby/github)
7 | [](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 .= '
Response
';
199 | $panel .= $serializeHeaders($e->response->getHeaders());
200 | if ($e->response->getContent()) {
201 | $panel .= '
' . $h($e->response->toArray() ?: $e->response->getContent());
202 | }
203 | $panel .= '
Time | time * 1000, 2, '.', ' ')) ?> ms |
---|---|
Query | params, TRUE); ?> |
Response | result ?: $call->exception, TRUE); ?> |
Options | options, TRUE); ?> |
Info | info, TRUE); ?> |