0ae7655e1f99e9606c5659950636281bb9ddeb01
[pharext/pharext.org] / app / Github / API.php
1 <?php
2
3 namespace app\Github;
4
5 use app\Github\API;
6 use app\Github\Storage;
7 use app\Github\Exception;
8 use app\Pharext;
9
10 use merry\Config;
11
12 use http\Client;
13 use http\QueryString;
14 use http\Url;
15
16 use Psr\Log\LoggerInterface;
17
18 class API
19 {
20 /**
21 * @var Client
22 */
23 private $client;
24
25 /**
26 * @var Storage
27 */
28 private $tokens;
29
30 /**
31 * @var Storage
32 */
33 private $cache;
34
35 /**
36 * @var merry\Config
37 */
38 private $config;
39
40 /**
41 * @var int
42 */
43 private $maxAge;
44
45 /**
46 * @var \Psr\Log\LoggerInterface;
47 */
48 private $logger;
49
50 /**
51 * Queued promises
52 * @var array
53 */
54 private $queue;
55
56 function __construct(Config $config, LoggerInterface $logger, Storage $tokens = null, Storage $cache = null) {
57 $this->logger = $logger;
58 $this->config = $config;
59 $this->client = new Client("curl", "github");
60 $this->client->configure($config->http->configure->toArray());
61 $this->client->attach(new ClientObserver($logger));
62 $this->tokens = $tokens ?: new Storage\Session;
63 $this->cache = $cache;
64 }
65
66 /**
67 * Set maximum acceptable age of cache items
68 * @param int $seconds
69 */
70 function setMaxAge($seconds) {
71 $this->maxAge = $seconds;
72 return $this;
73 }
74
75 function getMaxAge() {
76 return $this->maxAge;
77 }
78
79 function getLogger() {
80 return $this->logger;
81 }
82
83 function getConfig() {
84 return $this->config;
85 }
86
87 function getClient() {
88 return $this->client;
89 }
90
91 function getTokenStorage() {
92 return $this->tokens;
93 }
94
95 function getCacheStorage() {
96 return $this->cache;
97 }
98
99 function getCacheKey($ident, $page = null) {
100 return sprintf("%s:%s:%s", $this->getToken(), $ident, $page ?: 1);
101 }
102
103 function hasToken() {
104 return $this->tokens->get("access_token");
105 }
106
107 function setToken($token) {
108 $this->tokens->set("access_token", new Storage\Item(
109 $token,
110 $this->config->storage->token->ttl
111 ));
112 }
113
114 function getToken() {
115 if ($this->tokens->get("access_token", $token, true)) {
116 return $token->getValue();
117 }
118 if (isset($token)) {
119 $this->logger->notice("Token expired", $token);
120 throw new Exception\TokenExpired($token->getLTL());
121 }
122 throw new Exception\TokenNotSet;
123 }
124
125 function dropToken() {
126 $this->tokens->del("access_token");
127 }
128
129 function getAuthUrl($callback_url) {
130 $state = base64_encode(openssl_random_pseudo_bytes(24));
131 $this->tokens->set("state", new Storage\Item($state, 5*60));
132 $param = [
133 "state" => $state,
134 "client_id" => $this->config->client->id,
135 "scope" => $this->config->client->scope,
136 "redirect_uri" => $callback_url,
137 ];
138 return new Url("https://github.com/login/oauth/authorize", [
139 "query" => new QueryString($param)
140 ], 0);
141 }
142
143 function fetchToken($code, $state) {
144 if (!$this->tokens->get("state", $orig_state, true)) {
145 if (isset($orig_state)) {
146 $this->logger->notice("State expired", $orig_state);
147 throw new Exception\StateExpired($orig_state->getLTL());
148 }
149 throw new Exception\StateNotSet;
150 }
151 if ($state !== $orig_state->getValue()) {
152 $this->logger->warning("State mismatch", compact("state", "orig_state"));
153 throw new Exception\StateMismatch($orig_state->getValue(), $state);
154 }
155
156 return $this->queue(new API\Users\ReadAuthToken($this, [
157 "code" => $code,
158 "client_id" => $this->config->client->id,
159 "client_secret" => $this->config->client->secret,
160 ]));
161 }
162
163 function queue(API\Call $call) {
164 return $this->queue[] = $call();
165 }
166
167 function drain() {
168 $queue = $this->queue;
169 $this->queue = array();
170 $this->client->send();
171 return $queue;
172 }
173
174 function readAuthUser() {
175 return $this->queue(new API\Users\ReadAuthUser($this));
176 }
177
178 function listRepos($page) {
179 return $this->queue(new API\Repos\ListRepos($this, compact("page")));
180 }
181
182 function readRepo($repo) {
183 return $this->queue(new API\Repos\ReadRepo($this, compact("repo")));
184 }
185
186 /**
187 * Check if the pharext webhook is set for the repo and return it
188 * @param object $repo
189 * @return stdClass hook
190 */
191 function checkRepoHook($repo) {
192 if (!empty($repo->hooks)) {
193 foreach ($repo->hooks as $hook) {
194 if ($hook->name === "web" && $hook->config->url === $this->config->hook->url) {
195 return $hook;
196 }
197 }
198 }
199 return null;
200 }
201
202 function listHooks($repo) {
203 return $this->queue(new API\Hooks\ListHooks($this, compact("repo")));
204 }
205
206 function listReleases($repo, $page) {
207 return $this->queue(new API\Releases\ListReleases($this, compact("repo", "page")));
208 }
209
210 function listTags($repo, $page) {
211 return $this->queue(new API\Tags\ListTags($this, compact("repo", "page")));
212 }
213
214 function readContents($repo, $path = null) {
215 return $this->queue(new API\Repos\ReadContents($this, compact("repo", "path")));
216 }
217
218 function createRepoHook($repo, $conf) {
219 return $this->queue(new API\Hooks\CreateHook($this, compact("repo", "conf")));
220 }
221
222 function updateRepoHook($repo, $id, $conf) {
223 return $this->queue(new API\Hooks\UpdateHook($this, compact("repo", "id", "conf")));
224 }
225
226 function deleteRepoHook($repo, $id) {
227 return $this->queue(new API\Hooks\DeleteHook($this, compact("repo", "id")));
228 }
229
230 function createRelease($repo, $tag) {
231 return $this->queue(new API\Releases\CreateRelease($this, compact("repo", "tag")));
232 }
233
234 function publishRelease($repo, $id, $tag) {
235 return $this->queue(new API\Releases\PublishRelease($this, compact("repo", "id", "tag")));
236 }
237
238 function createReleaseAsset($url, $asset, $type) {
239 return $this->queue(new API\Releases\CreateReleaseAsset($this, compact("url", "asset", "type")));
240 }
241
242 function listReleaseAssets($repo, $id) {
243 return $this->queue(new API\Releases\ListReleaseAssets($this, compact("repo", "id")));
244 }
245
246 function uploadAssetForRelease($repo, $release, $config = null) {
247 return $this->listHooks($repo->full_name)->then(function($result) use($release, $repo, $config) {
248 list($repo->hooks) = $result;
249 $phar = new Pharext\Package(
250 $repo->clone_url,
251 $release->tag_name,
252 $repo->name,
253 $config ?: (array) $this->checkRepoHook($repo)->config
254 );
255 $name = sprintf("%s-%s.ext.phar", $repo->name, $release->tag_name);
256 $url = uri_template($release->upload_url, compact("name"));
257 $promise = $this->createReleaseAsset($url, $phar, "application/phar");
258 if ($release->draft) {
259 return $promise->then(function($result) use($release, $repo) {
260 return $this->publishRelease($repo->full_name, $release->id, $release->tag_name);
261 });
262 }
263 return $promise;
264 });
265 }
266
267 function createReleaseFromTag($repo, $tag_name, $config = null) {
268 return $this->createRelease($repo->full_name, $tag_name)->then(function($result) use($repo, $config) {
269 list($release) = $result;
270 return $this->uploadAssetForRelease($repo, $release, $config);
271 });
272 }
273
274 }