use app\Web;
use http\QueryString;
-use http\Url;
+use http\Header;
abstract class Github implements Controller
{
"title" => "Github"
]);
$this->app->getView()->registerFunction("check", [$this, "checkRepoHook"]);
+
+ if (($header = $this->app->getRequest()->getHeader("Cache-Control", Header::class))) {
+ $params = $header->getParams();
+ if (!empty($params["no-cache"])) {
+ $this->github->setMaxAge(0);
+ } elseif (!empty($params["max-age"])) {
+ $this->github->setMaxAge($params["max-age"]["value"]);
+ }
+ }
}
protected function checkToken() {
}
/**
- * Check if the pharext webhook is set for the repo and return its id
+ * Check if the pharext webhook is set for the repo and return it
* @param object $repo
* @return int hook id
*/
}
return null;
}
-
- function createLinkGenerator($links) {
- return function($which) use($links) {
- if (!isset($links[$which])) {
- if ($which !== "next" || !isset($links["last"])) {
- return null;
- } else {
- $which = "last";
- }
- }
- $url = new Url($links[$which], null, 0);
- $qry = new QueryString($url->query);
- return $qry->getInt("page", 1);
- };
- }
-
}
$this->app->getRequest()->getQuery("state"),
function($token) {
$this->github->setToken($token->access_token);
- $this->github->fetchUser($this->createUserCallback($token));
+ $this->github->readAuthUser($this->createUserCallback($token));
})->send();
if (isset($this->session->returnto)) {
$returnto = $this->session->returnto;
{
function __invoke(array $args = null) {
if ($this->checkToken()) {
- $this->github->fetchRepos(
+ $this->github->listRepos(
$this->app->getRequest()->getQuery("page"),
[$this, "reposCallback"]
)->send();
$this->app->getView()->addData(compact("repos", "links"));
foreach ($repos as $repo) {
- $this->github->fetchHooks($repo->full_name, function($hooks) use($repo) {
+ $this->github->listHooks($repo->full_name, function($hooks) use($repo) {
$repo->hooks = $hooks;
});
}
extract($args);
$this->app->getView()->addData(compact("owner", "name"));
if ($this->checkToken()) {
- $this->github->fetchRepo(
+ $this->github->readRepo(
"$owner/$name",
[$this, "repoCallback"]
)->send();
"title" => "Github: {$repo->name}"
]);
settype($repo->tags, "object");
- $this->github->fetchHooks($repo->full_name, function($hooks) use($repo) {
+ $this->github->listHooks($repo->full_name, function($hooks) use($repo) {
$repo->hooks = $hooks;
});
- $this->github->fetchTags($repo->full_name, 1, $this->createTagsCallback($repo));
- $this->github->fetchReleases($repo->full_name, 1, $this->createReleasesCallback($repo));
- $this->github->fetchContents($repo->full_name, null, $this->createContentsCallback($repo));
+ $this->github->listTags($repo->full_name, 1, $this->createTagsCallback($repo));
+ $this->github->listReleases($repo->full_name, 1, $this->createReleasesCallback($repo));
+ $this->github->readContents($repo->full_name, null, $this->createContentsCallback($repo));
}
function createReleasesCallback($repo) {
namespace app\Controller\Github;
use app\Controller\Github;
+use app\Github\API\Hooks\ListHooks;
class RepoHook extends Github
{
function addHook($owner, $repo) {
$hook_conf = $this->app->getRequest()->getForm();
- $call = $this->github->createRepoHook("$owner/$repo", $hook_conf, function($hook) use($owner, $repo, &$call) {
- $call->dropFromCache();
- $this->redirectBack("$owner/$repo");
- });
- $call->send();
+ $this->github->createRepoHook("$owner/$repo", $hook_conf, function($hook) use($owner, $repo) {
+ $call = new ListHooks($this->github, ["repo" => "$owner/$repo", "fresh" => true]);
+ $call(function($hooks, $links) use($owner, $repo, $call) {
+ $call->saveToCache([$hooks, $links]);
+ $this->redirectBack("$owner/$repo");
+ });
+ })->send();
}
function updateHook($owner, $repo) {
- $this->github->fetchRepo("$owner/$repo", function($repo) {
- $call = $this->github->fetchHooks($repo->full_name, function($hooks, $links) use($repo, &$call) {
+ $this->github->readRepo("$owner/$repo", function($repo) {
+ $call = $this->github->listHooks($repo->full_name, function($hooks, $links) use($repo, &$call) {
$repo->hooks = $hooks;
if (($hook = $this->checkRepoHook($repo))) {
$hook_conf = $this->app->getRequest()->getForm();
- $this->github->updateRepoHook($repo->full_name, $hook->id, $hook_conf, function($changed_hook) use($repo, $hook, $hooks, $links, $call) {
+ $this->github->updateRepoHook($repo->full_name, $hook->id, $hook_conf, function($changed_hook) use($repo, $hook, $hooks, $links, &$call) {
foreach ($changed_hook as $key => $val) {
$hook->$key = $val;
}
}
function delHook($owner, $repo) {
- $this->github->fetchRepo("$owner/$repo", function($repo) {
- $call = $this->github->fetchHooks($repo->full_name, function($hooks) use($repo, &$call) {
+ $this->github->readRepo("$owner/$repo", function($repo) {
+ $call = $this->github->listHooks($repo->full_name, function($hooks) use($repo, &$call) {
$repo->hooks = $hooks;
if (($hook = $this->checkRepoHook($repo))) {
- $this->github->deleteRepoHook($repo->full_name, $hook->id, function() use($repo, $call) {
+ $this->github->deleteRepoHook($repo->full_name, $hook->id, function() use($repo, &$call) {
$call->dropFromCache();
$this->redirectBack($repo->full_name);
});
use http\QueryString;
use http\Url;
+use Psr\Log\LoggerInterface;
+
class API
{
/**
* @var merry\Config
*/
private $config;
+
+ /**
+ * @var int
+ */
+ private $maxAge;
+
+ /**
+ * @var \Psr\Log\LoggerInterface;
+ */
+ private $logger;
- function __construct(Config $config, Storage $tokens = null, Storage $cache = null) {
+ function __construct(Config $config, LoggerInterface $logger, Storage $tokens = null, Storage $cache = null) {
+ $this->logger = $logger;
$this->config = $config;
$this->client = new Client;
+ $this->client->attach(new ClientObserver($logger));
$this->tokens = $tokens ?: new Storage\Session;
$this->cache = $cache;
}
+
+ /**
+ * Set maximum acceptable age of cache items
+ * @param int $seconds
+ */
+ function setMaxAge($seconds) {
+ $this->maxAge = $seconds;
+ return $this;
+ }
+
+ function getMaxAge() {
+ return $this->maxAge;
+ }
+
+ function getLogger() {
+ return $this->logger;
+ }
function getConfig() {
return $this->config;
}
function setToken($token) {
- $this->tokens->set("access_token", $token,
- $this->config->storage->token->ttl);
+ $this->tokens->set("access_token", new Storage\Item(
+ $token,
+ $this->config->storage->token->ttl
+ ));
}
function getToken() {
- if ($this->tokens->get("access_token", $token, $ttl, true)) {
- return $token;
+ if ($this->tokens->get("access_token", $token, true)) {
+ return $token->getValue();
}
- if (isset($ttl)) {
- throw new Exception\TokenExpired($ttl);
+ if (isset($token)) {
+ $this->logger->notice("Token expired", $token);
+ throw new Exception\TokenExpired($token->getLTL());
}
throw new Exception\TokenNotSet;
}
function getAuthUrl($callback_url) {
$state = base64_encode(openssl_random_pseudo_bytes(24));
- $this->tokens->set("state", $state, 5*60);
+ $this->tokens->set("state", new Storage\Item($state, 5*60));
$param = [
"state" => $state,
"client_id" => $this->config->client->id,
}
function fetchToken($code, $state, callable $callback) {
- if (!$this->tokens->get("state", $orig_state, $ttl, true)) {
- if (isset($ttl)) {
- throw new Exception\StateExpired($ttl);
+ if (!$this->tokens->get("state", $orig_state, true)) {
+ if (isset($orig_state)) {
+ $this->logger->notice("State expired", $orig_state);
+ throw new Exception\StateExpired($orig_state->getLTL());
}
throw new Exception\StateNotSet;
}
- if ($state !== $orig_state) {
- throw new Exception\StateMismatch($orig_state, $state);
+ if ($state !== $orig_state->getValue()) {
+ $this->logger->warning("State mismatch", compact("state", "orig_state"));
+ throw new Exception\StateMismatch($orig_state->getValue(), $state);
}
$call = new API\Users\ReadAuthToken($this, [
return $call($callback);
}
- function fetchUser(callable $callback) {
+ function readAuthUser(callable $callback) {
$call = new API\Users\ReadAuthUser($this);
return $call($callback);
}
- function fetchRepos($page, callable $callback) {
+ function listRepos($page, callable $callback) {
$call = new API\Repos\ListRepos($this, compact("page"));
return $call($callback);
}
- function fetchRepo($repo, callable $callback) {
+ function readRepo($repo, callable $callback) {
$call = new API\Repos\ReadRepo($this, compact("repo"));
return $call($callback);
}
- function fetchHooks($repo, callable $callback) {
+ function listHooks($repo, callable $callback) {
$call = new API\Hooks\ListHooks($this, compact("repo"));
return $call($callback);
}
- function fetchReleases($repo, $page, callable $callback) {
+ function listReleases($repo, $page, callable $callback) {
$call = new API\Releases\ListReleases($this, compact("repo", "page"));
return $call($callback);
}
- function fetchTags($repo, $page, callable $callback) {
+ function listTags($repo, $page, callable $callback) {
$call = new API\Tags\ListTags($this, compact("repo", "page"));
return $call($callback);
}
- function fetchContents($repo, $path, callable $callback) {
+ function readContents($repo, $path, callable $callback) {
$call = new API\Repos\ReadContents($this, compact("repo", "path"));
return $call($callback);
}
namespace app\Github\API;
use app\Github\API;
+use app\Github\Storage\Item;
use http\QueryString;
use http\Url;
use merry\Config;
function getCacheKey() {
$args = $this->args;
unset($args["fresh"]);
+ if (isset($args["page"]) && !strcmp($args["page"], "1")) {
+ unset($args["page"]);
+ }
ksort($args);
return sprintf("%s:%s:%s", $this->api->getToken(), $this,
new QueryString($args));
}
- function readFromCache(array &$cached = null, &$ttl = null) {
- if (empty($this->args["fresh"]) && ($cache = $this->api->getCacheStorage())) {
- $key = $this->getCacheKey();
- return $cache->get($key, $cached, $ttl);
+ function readFromCache(array &$value = null) {
+ if (!empty($this->args["fresh"])) {
+ return false;
+ }
+ if (!($cache = $this->api->getCacheStorage())) {
+ return false;
+ }
+ if (!strlen($key = $this->getCacheKey())) {
+ return false;
+ }
+ if (!$cache->get($key, $cached)) {
+ return false;
+ }
+ if (null !== $this->api->getMaxAge() && $cached->getAge() > $this->api->getMaxAge()) {
+ return false;
}
- return false;
+ $this->api->getLogger()->debug("Cache-Hit: $this", $this->args);
+ $value = $cached->getValue();
+ return true;
}
function saveToCache(array $fresh) {
}
$key = $this->getCacheKey();
- $cache->set($key, $fresh, $ttl);
+ $cache->set($key, new Item($fresh, $ttl));
}
}
if ($response->getResponseCode() >= 400) {
throw new RequestException($response);
}
- $callback($json);
+ $callback();
return true;
});
}
return true;
});
}
+
+ function getCacheKey() {
+ return null;
+ }
}
--- /dev/null
+<?php
+
+namespace app\Github;
+
+use SplObserver;
+use SplSubject;
+
+use http\Client\Request;
+
+use Psr\Log\LoggerInterface;
+
+class ClientObserver implements SplObserver
+{
+ private $logger;
+
+ function __construct(LoggerInterface $logger) {
+ $this->logger = $logger;
+ }
+
+ function update(SplSubject $client, Request $request = null, $progress = null) {
+ switch ($progress->info) {
+ case "start":
+ if (!$progress->started) {
+ $message = sprintf("API-Shot: start %s %s", $request->getRequestMethod(), $request->getRequestUrl());
+ $this->logger->debug($message);
+ }
+ break;
+ case "finished":
+ $response = $client->getResponse($request);
+ $message = sprintf("API-Shot: finished [%d] %s %s", $response->getResponseCode(), $request->getRequestMethod(), $request->getRequestUrl());
+ if ($response->getResponseCode() >= 400 || $response->getTransferInfo("error")) {
+ $this->logger->error($message, (array) $response->getTransferInfo());
+ } else {
+ $this->logger->info($message);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+}
$errors = "JSON errors:\n";
foreach ($this->errors as $error) {
- $errors .= sprintf($reasons[$error->code], $error->resource, $error->field);
+ if ($error->code === "custom") {
+ $errors .= $error->message . "\n";
+ } else {
+ $errors .= sprintf($reasons[$error->code], $error->resource, $error->field);
+ }
}
return $errors;
}
+
function __toString() {
return parent::__toString() . "\n". $this->getErrorsAsString();
}
--- /dev/null
+<?php
+
+namespace app\Github;
+
+use app\Config;
+
+class Logger extends \Monolog\Logger
+{
+ function __construct(Config $config) {
+ $channel = $config->github->log;
+ parent::__construct($channel);
+ foreach ($config->log->$channel as $logger) {
+ $reflection = new \ReflectionClass("Monolog\\Handler\\" . $logger->handler);
+ if (!empty($logger->args)) {
+ $handler = $reflection->newInstanceArgs($logger->args->toArray());
+ } else {
+ $handler = $reflection->newInstance();
+ }
+ $this->pushHandler($handler);
+ }
+ }
+}
interface Storage
{
- function set($key, $val, $ttl = null);
- function get($key, &$val = null, &$ttl = null, $update = false);
+ function set($key, Storage\Item $item);
+ function get($key, Storage\Item &$item = null, $update = false);
function del($key);
}
--- /dev/null
+<?php
+
+namespace app\Github\Storage;
+
+class Item
+{
+ private $value;
+ private $time;
+ private $ttl;
+
+ function __construct($value, $ttl = null, $time = null) {
+ $this->value = $value;
+ $this->ttl = $ttl;
+ $this->time = $time ?: time();
+ }
+
+ static function __set_state(array $state) {
+ return new static(
+ isset($state["value"]) ? $state["value"] : null,
+ isset($state["ttl"]) ? $state["ttl"] : null,
+ isset($state["time"]) ? $state["time"] : null
+ );
+ }
+
+ function toArray() {
+ return get_object_vars($this);
+ }
+
+ function getTimestamp() {
+ return $this->time;
+ }
+
+ function setTimestamp($ts = null) {
+ $this->time = $ts ?: time();
+ return $this;
+ }
+
+ function getTTL() {
+ return $this->ttl;
+ }
+
+ function setTTL($ttl = null) {
+ $this->ttl = $ttl;
+ return $this;
+ }
+
+ function getAge($from = null) {
+ return ($from ?: time()) - $this->time;
+ }
+
+ function getLTL($from = null) {
+ return $this->ttl - $this->getAge($from);
+ }
+
+ function getValue() {
+ return $this->value;
+ }
+
+ function setValue($value = null) {
+ $this->value = $value;
+ return $this;
+ }
+}
return sprintf("%s:%s", $this->ns, $key);
}
- function get($key, &$val = null, &$ltl = null, $update = false) {
+ function get($key, Item &$item = null, $update = false) {
if (!$item = $this->mc->get($this->key($key))) {
return false;
}
$ttl = $item->ttl;
$set = $item->time;
- if (!isset($ttl)) {
+ if (null === $item->getTTL()) {
return true;
}
- $now = time();
- $ltl = $ttl - ($now - $set);
- if ($ltl >= 0) {
+ if ($item->getLTL() >= 0) {
if ($update) {
- $item->time = time();
- $this->mc->set($this->key($key), $item, $ttl + 60*60*24);
+ $item->setTimestamp();
+ $this->mc->set($this->key($key), $item, $item->getTTL() + 60*60*24);
}
return true;
}
return false;
}
- function set($key, $val, $ttl = null) {
- $item = new Memcache\Item([
- "value" => $val,
- "ttl" => $ttl,
- "time" => isset($ttl) ? time() : null
- ]);
- $this->mc->set($this->key($key), $item, isset($ttl) ? $ttl + 60*60*24 : 0);
+ function set($key, Item $item) {
+ $this->mc->set($this->key($key), $item, (null !== $item->getTTL()) ? $item->getTTL() + 60*60*24 : 0);
return $this;
}
$this->mc->delete($this->key($key));
}
}
-
-namespace app\Github\Storage\Memcache;
-
-class Item
-{
- public $value;
- public $time;
- public $ttl;
-
- function __construct(array $data) {
- foreach ($data as $key => $val) {
- $this->$key = $val;
- }
- }
-}
-
return sprintf("%s:%s", $this->ns, $key);
}
- function get($key, &$val = null, &$ltl = null, $update = false) {
+ function get($key, Item &$item = null, $update = false) {
if (!$item = $this->rd->get($this->key($key))) {
- header("Cache-Item: ".serialize($item), false);
return false;
}
- $val = $item->value;
- $ttl = $item->ttl;
- $set = $item->time;
-
- if (!isset($ttl)) {
+ if (null === $item->getTTL()) {
return true;
}
- $now = time();
- $ltl = $ttl - ($now - $set);
- if ($ltl >= 0) {
+ if ($item->getLTL() >= 0) {
if ($update) {
- $item->time = time();
- $this->rd->setex($this->key($key), $ttl + 60*60*24, $item);
+ $item->setTimestamp();
+ $this->rd->setex($this->key($key), $item->getTTL() + 60*60*24, $item);
}
return true;
}
return false;
}
- function set($key, $val, $ttl = null) {
- $item = new Redis\Item([
- "value" => $val,
- "ttl" => $ttl,
- "time" => isset($ttl) ? time() : null
- ]);
- if (isset($ttl)) {
+ function set($key, Item $item) {
+ if (null === $item->getTTL()) {
$this->rd->set($this->key($key), $item);
} else {
- $this->rd->setex($this->key($key), $ttl + 60*60*24, $item);
+ $this->rd->setex($this->key($key), $item->getTTL() + 60*60*24, $item);
}
return $this;
}
$this->rd->delete($this->key($key));
}
}
-
-namespace app\Github\Storage\Redis;
-
-class Item
-{
- public $value;
- public $time;
- public $ttl;
-
- function __construct(array $data) {
- foreach ($data as $key => $val) {
- $this->$key = $val;
- }
- }
-}
-
$this->ns = $ns;
}
- function set($key, $val, $ttl = null) {
- $_SESSION[$this->ns][$key] = [$val, $ttl, isset($ttl) ? time() : null];
+ function set($key, Item $item) {
+ $_SESSION[$this->ns][$key] = $item;
return $this;
}
- function get($key, &$val = null, &$ltl = null, $update = false) {
+ function get($key, Item &$item = null, $update = false) {
if (!isset($_SESSION[$this->ns][$key])) {
return false;
}
- list($val, $ttl, $set) = $_SESSION[$this->ns][$key];
- if (!isset($ttl)) {
+ $item = $_SESSION[$this->ns][$key];
+ if (null === $item->getTTL()) {
return true;
}
- $now = time();
- $ltl = $ttl - ($now - $set);
- if ($ltl >= 0) {
+ if ($item->getLTL() >= 0) {
if ($update) {
- $_SESSION[$this->ns][$key][2] = $now;
+ $item->setTimestamp();
}
return true;
}
$config->$basic->auth->toArray(),
0);
}
+ // FIXME: configure through app.ini
return new Github\API(
$config->github
+ ,new Github\Logger($config)
,new Github\Storage\Session("gh-tokens")
#,new Github\Storage\Memcache("gh-cache")
,new Github\Storage\Redis("gh-cache")
github.storage.cache.tags.ttl = 3600
github.storage.cache.releases.ttl = 3600
+github.log = github
+
session.use_cookies = 1
session.use_only_cookies = 1
session.use_strict_mode = 1
pq.flags = 0
pq.dsn = "user=pharext host=localhost"
+log.github.streamhandler.handler = StreamHandler
+log.github.streamhandler.args[] = ../logs/github.log
+; Logger::DEBUG == 100
+log.github.streamhandler.args[] = 100
+
[localhost : production]
github.hook.url = https://pharext.ngrok.io/src/pharext.org.git/public/github/hook