From: Christian Weiske Date: Fri, 22 Nov 2013 20:32:06 +0000 (+0100) Subject: first work on remote fork notifications with linkback (webmention/pingback) X-Git-Tag: v0.4.0~62 X-Git-Url: https://git.cweiske.de/phorkie.git/commitdiff_plain/f47ddf0758f120dfb26f03fb36be5cd897a10f23 first work on remote fork notifications with linkback (webmention/pingback) --- diff --git a/data/config.default.php b/data/config.default.php index 85713cd..45adf92 100644 --- a/data/config.default.php +++ b/data/config.default.php @@ -14,10 +14,14 @@ $GLOBALS['phorkie']['cfg'] = array( 'geshi' => 'MediaWiki/geshi/geshi/geshi.php', 'index' => 'new',//"new" or "list" 'perPage' => 10, - 'webhooks' => array( - /* array of urls that get called when - a paste is created, edited or deleted */ - ) + 'notificator' => array( + /* send out pingback/webmentions when a remote paste is forked */ + 'linkback' => true, + 'webhook' => array( + /* array of urls that get called when + a paste is created, edited or deleted */ + ) + ), ); $GLOBALS['phorkie']['auth'] = array( // 0 = public, no authentication, 1 = protect adds/edits/deletes, diff --git a/data/templates/display-sidebar-fork.htm b/data/templates/display-sidebar-fork.htm index d3793e1..9ed5626 100644 --- a/data/templates/display-sidebar-fork.htm +++ b/data/templates/display-sidebar-fork.htm @@ -1,7 +1,7 @@ {% set conns = repo.getConnectionInfo() %} {% if conns.isFork() %} {% set origin = conns.getOrigin() %} -

Fork of

+

Fork of

{% set webpage = origin.getWebURL() %} {% if webpage %} @@ -9,6 +9,26 @@ {% else %} {{origin.getTitle()}} {% endif %} - (clone URL) +

+{% endif %} +{% if conns.hasForks() %} +

Forks

+ {% endif %} \ No newline at end of file diff --git a/src/phorkie/ForkRemote.php b/src/phorkie/ForkRemote.php index 31b4839..f8e319a 100644 --- a/src/phorkie/ForkRemote.php +++ b/src/phorkie/ForkRemote.php @@ -27,84 +27,12 @@ class ForkRemote public function parse() { - if ($this->url == '') { - $this->error = 'Empty fork URL'; - return false; - } - - $arUrl = parse_url($this->url); - $scheme = isset($arUrl['scheme']) ? $arUrl['scheme'] : ''; - - if ($scheme == 'https' && isset($arUrl['host']) - && $arUrl['host'] == 'gist.github.com' - ) { - $this->arGitUrls[][] = 'git://gist.github.com/' - . ltrim($arUrl['path'], '/') . '.git'; - return true; - } - - switch ($scheme) { - case 'git': - //clearly a git url - $this->arGitUrls = array(array($this->url)); - return true; - - case 'ssh': - //FIXME: maybe loosen this when we know how to skip the - //"do you trust this server" question of ssh - $this->error = 'ssh:// URLs are not supported'; - return false; - - case 'http': - case 'https': - return $this->extractUrlsFromHtml($this->url); - } - - $this->error = 'Unknown URLs scheme: ' . $scheme; - return false; - } - - protected function extractUrlsFromHtml($url) - { - //HTML is not necessarily well-formed, and Gitorious has many problems - // in this regard - //$sx = simplexml_load_file($url); - libxml_use_internal_errors(true); - $sx = simplexml_import_dom(\DomDocument::loadHtmlFile($url)); - $elems = $sx->xpath('//*[@rel="vcs-git"]'); - $titles = $sx->xpath('/html/head/title'); - $pageTitle = $this->cleanPageTitle((string) reset($titles)); - - $count = $anonymous = 0; - foreach ($elems as $elem) { - if (!isset($elem['href'])) { - continue; - } - $str = (string)$elem; - if (isset($elem['title'])) { - // - $title = (string)$elem['title']; - } else if ($str != '') { - //title - $title = $str; - } else if ($pageTitle != '') { - $title = $pageTitle; - } else { - $title = 'Unnamed repository #' . ++$anonymous; - } - $url = (string)$elem['href']; - if ($this->isSupported($url)) { - ++$count; - $this->arGitUrls[$title][] = $url; - } - } - - if ($count > 0) { - return true; - } + $hp = new HtmlParser(); + $ret = $hp->extractGitUrls($this->url); + $this->arGitUrls = $hp->getGitUrls(); + $this->error = $hp->error; - $this->error = 'No git:// clone URL found'; - return false; + return $ret; } /** @@ -149,30 +77,5 @@ class ForkRemote { $this->url = $url; } - - public function isSupported($url) - { - $scheme = parse_url($url, PHP_URL_SCHEME); - return $scheme == 'git' - || $scheme == 'http' || $scheme == 'https'; - } - - /** - * Remove application names from HTML page titles - * - * @param string $title HTML page title - * - * @return string Cleaned HTML page title - */ - protected function cleanPageTitle($title) - { - $title = trim($title); - if (substr($title, -9) == '- phorkie') { - $title = trim(substr($title, 0, -9)); - } - - return $title; - } } - ?> diff --git a/src/phorkie/HtmlParser.php b/src/phorkie/HtmlParser.php new file mode 100644 index 0000000..f751074 --- /dev/null +++ b/src/phorkie/HtmlParser.php @@ -0,0 +1,153 @@ +error = 'Empty fork URL'; + return false; + } + + $arUrl = parse_url($url); + $scheme = isset($arUrl['scheme']) ? $arUrl['scheme'] : ''; + + if ($scheme == 'https' && isset($arUrl['host']) + && $arUrl['host'] == 'gist.github.com' + ) { + //FIXME: title + $this->arGitUrls[][] = 'git://gist.github.com/' + . ltrim($arUrl['path'], '/') . '.git'; + return true; + } + + switch ($scheme) { + case 'git': + //clearly a git url + $this->arGitUrls = array(array($url)); + return true; + + case 'ssh': + //FIXME: maybe loosen this when we know how to skip the + //"do you trust this server" question of ssh + $this->error = 'ssh:// URLs are not supported'; + return false; + + case 'http': + case 'https': + return $this->extractUrlsFromHtml($url, $html); + } + + $this->error = 'Unknown URLs scheme: ' . $scheme; + return false; + } + + protected function extractUrlsFromHtml($url, $html = null) + { + //HTML is not necessarily well-formed, and Gitorious has many problems + // in this regard + //$sx = simplexml_load_file($url); + + libxml_use_internal_errors(true); + if ($html === null) { + $sx = simplexml_import_dom(\DOMDocument::loadHTMLFile($url)); + } else { + $sx = simplexml_import_dom(\DOMDocument::loadHTML($html)); + } + + $elems = $sx->xpath('//*[@rel="vcs-git"]'); + $titles = $sx->xpath('/html/head/title'); + $pageTitle = $this->cleanPageTitle((string) reset($titles)); + + $count = $anonymous = 0; + foreach ($elems as $elem) { + if (!isset($elem['href'])) { + continue; + } + $str = (string)$elem; + if (isset($elem['title'])) { + // + $title = (string)$elem['title']; + } else if ($str != '') { + //title + $title = $str; + } else if ($pageTitle != '') { + $title = $pageTitle; + } else { + $title = 'Unnamed repository #' . ++$anonymous; + } + $url = (string)$elem['href']; + if ($this->isSupported($url)) { + ++$count; + $this->arGitUrls[$title][] = $url; + } + } + + if ($count > 0) { + return true; + } + + $this->error = 'No git:// clone URL found'; + return false; + } + + public function getGitUrls() + { + return $this->arGitUrls; + } + + /** + * Remove application names from HTML page titles + * + * @param string $title HTML page title + * + * @return string Cleaned HTML page title + */ + protected function cleanPageTitle($title) + { + $title = trim($title); + if (substr($title, -9) == '- phorkie') { + $title = trim(substr($title, 0, -9)); + } + + return $title; + } + + public function isSupported($url) + { + $scheme = parse_url($url, PHP_URL_SCHEME); + return $scheme == 'git' + || $scheme == 'http' || $scheme == 'https'; + } + +} +?> diff --git a/src/phorkie/Notificator.php b/src/phorkie/Notificator.php index 3ef5c81..dc6a6af 100644 --- a/src/phorkie/Notificator.php +++ b/src/phorkie/Notificator.php @@ -6,6 +6,21 @@ namespace phorkie; */ class Notificator { + protected $notificators = array(); + + public function __construct() + { + $this->loadNotificators(); + } + + protected function loadNotificators() + { + foreach ($GLOBALS['phorkie']['cfg']['notificator'] as $type => $config) { + $class = '\\phorkie\\Notificator_' . ucfirst($type); + $this->notificators[] = new $class($config); + } + } + /** * A repository has been created */ @@ -31,40 +46,12 @@ class Notificator } /** - * Call webhook URLs with our payload + * Call all notificator plugins */ protected function send($event, Repository $repo) { - if (count($GLOBALS['phorkie']['cfg']['webhooks']) == 0) { - return; - } - - /* slightly inspired by - https://help.github.com/articles/post-receive-hooks */ - $payload = (object) array( - 'event' => $event, - 'author' => array( - 'name' => $_SESSION['name'], - 'email' => $_SESSION['email'] - ), - 'repository' => array( - 'name' => $repo->getTitle(), - 'url' => $repo->getLink('display', null, true), - 'description' => $repo->getDescription(), - 'owner' => $repo->getOwner() - ) - ); - foreach ($GLOBALS['phorkie']['cfg']['webhooks'] as $url) { - $req = new \HTTP_Request2($url); - $req->setMethod(\HTTP_Request2::METHOD_POST) - ->setHeader('Content-Type: application/vnd.phorkie.webhook+json') - ->setBody(json_encode($payload)); - try { - $response = $req->send(); - //FIXME log response codes != 200 - } catch (HTTP_Request2_Exception $e) { - //FIXME log exceptions - } + foreach ($this->notificators as $notificator) { + $notificator->send($event, $repo); } } } diff --git a/src/phorkie/Notificator/Linkback.php b/src/phorkie/Notificator/Linkback.php new file mode 100644 index 0000000..4c1abfb --- /dev/null +++ b/src/phorkie/Notificator/Linkback.php @@ -0,0 +1,59 @@ +config = $config; + } + + /** + * Send linkback on "create" events to remote repositories + */ + public function send($event, Repository $repo) + { + if ($this->config === false) { + return; + } + + if ($event != 'create') { + return; + } + + $origin = $repo->getConnectionInfo()->getOrigin(); + if ($origin === null) { + return; + } + $originWebUrl = $origin->getWebUrl(true); + if ($originWebUrl === null) { + return; + } + + + $this->pbc = new \PEAR2\Services\Linkback\Client(); + $req = $this->pbc->getRequest(); + $req->setConfig( + array( + 'ssl_verify_peer' => false, + 'ssl_verify_host' => false + ) + ); + $this->pbc->setRequestTemplate($req); + $req->setHeader('user-agent', 'phorkie'); + try { + $res = $this->pbc->send( + $repo->getLink('display', null, true), + $originWebUrl + ); + } catch (\Exception $e) { + //FIXME: log errors + } + } +} +?> diff --git a/src/phorkie/Notificator/Webhook.php b/src/phorkie/Notificator/Webhook.php new file mode 100644 index 0000000..5737243 --- /dev/null +++ b/src/phorkie/Notificator/Webhook.php @@ -0,0 +1,55 @@ +config = $config; + } + + /** + * Call webhook URLs with our payload + */ + public function send($event, Repository $repo) + { + if (count($this->config) == 0) { + return; + } + + /* slightly inspired by + https://help.github.com/articles/post-receive-hooks */ + $payload = (object) array( + 'event' => $event, + 'author' => array( + 'name' => $_SESSION['name'], + 'email' => $_SESSION['email'] + ), + 'repository' => array( + 'name' => $repo->getTitle(), + 'url' => $repo->getLink('display', null, true), + 'description' => $repo->getDescription(), + 'owner' => $repo->getOwner() + ) + ); + foreach ($this->config as $url) { + $req = new \HTTP_Request2($url); + $req->setMethod(\HTTP_Request2::METHOD_POST) + ->setHeader('Content-Type: application/vnd.phorkie.webhook+json') + ->setBody(json_encode($payload)); + try { + $response = $req->send(); + //FIXME log response codes != 200 + } catch (HTTP_Request2_Exception $e) { + //FIXME log exceptions + } + } + } +} +?> + diff --git a/src/phorkie/Repository.php b/src/phorkie/Repository.php index 7428c8a..2683ad0 100644 --- a/src/phorkie/Repository.php +++ b/src/phorkie/Repository.php @@ -144,7 +144,7 @@ class Repository /** * Loads the list of files in this repository * - * @return File[] Array of files + * @return File[] Array of file objects */ public function getFiles() { @@ -186,6 +186,11 @@ class Repository return $name; } + /** + * Return array with all file paths in this repository + * + * @return array + */ protected function getFilePaths() { if ($this->hash === null) { @@ -328,6 +333,8 @@ class Repository $link = $this->id . '/delete/confirm'; } else if ($type == 'revision') { $link = $this->id . '/rev/' . $option; + } else if ($type == 'linkback') { + $link = $this->id . '/linkback'; } else { throw new Exception('Unknown link type'); } diff --git a/src/phorkie/Repository/ConnectionInfo.php b/src/phorkie/Repository/ConnectionInfo.php index 3815856..ce96c3e 100644 --- a/src/phorkie/Repository/ConnectionInfo.php +++ b/src/phorkie/Repository/ConnectionInfo.php @@ -18,6 +18,11 @@ class Repository_ConnectionInfo return $this->getOrigin() !== null; } + public function hasForks() + { + return count($this->getForks()) > 0; + } + public function getOrigin() { @@ -36,7 +41,18 @@ class Repository_ConnectionInfo return new Repository_Remote($name, $this->arConfig['remote ' . $name]); } + public function getForks() + { + $arForks = array(); + foreach ($this->arConfig as $name => $data) { + if (substr($name, 0, 12) != 'remote fork-') { + continue; + } + $arForks[substr($name, 7)] = new Repository_Remote( + substr($name, 7), $data + ); + } + return $arForks; + } } - - ?> diff --git a/src/phorkie/Repository/LinkbackReceiver.php b/src/phorkie/Repository/LinkbackReceiver.php new file mode 100644 index 0000000..bce7643 --- /dev/null +++ b/src/phorkie/Repository/LinkbackReceiver.php @@ -0,0 +1,88 @@ +repo = $repo; + } + + public function storeLinkback( + $target, $source, $sourceBody, \HTTP_Request2_Response $res + ) { + //FIXME: deleted + //FIXME: updated + //FIXME: cleanuptask + + $hp = new HtmlParser(); + $ok = $hp->extractGitUrls($source, $sourceBody); + if ($ok === false) { + //failed to extract git URL from linkback source + //FIXME: send exception + //$hp->error + return; + } + + $ci = $this->repo->getConnectionInfo(); + $forks = $ci->getForks(); + + $remoteCloneUrl = $remoteTitle = null; + $arRemoteCloneUrls = array(); + $arGitUrls = $hp->getGitUrls(); + foreach ($arGitUrls as $remoteTitle => $arUrls) { + foreach ($arUrls as $remoteCloneUrl) { + $arRemoteCloneUrls[$remoteCloneUrl] = $remoteTitle; + } + } + + $remoteid = 'fork-' . uniqid(); + //check if we already know this remote + foreach ($forks as $remote) { + if (isset($arRemoteCloneUrls[$remote->getCloneUrl()]) + || $source == $remote->getWebURL(true) + ) { + $remoteid = $remote->getName(); + break; + } + } + + if ($this->isLocalWebUrl($source)) { + //convert both web and clone url to local urls + } + + $vc = $this->repo->getVc(); + $vc->getCommand('config') + ->addArgument('remote.' . $remoteid . '.homepage') + ->addArgument($source) + ->execute(); + if ($remoteTitle !== null) { + $vc->getCommand('config') + ->addArgument('remote.' . $remoteid . '.title') + ->addArgument($remoteTitle) + ->execute(); + } + if ($remoteCloneUrl !== null) { + $vc->getCommand('config') + ->addArgument('remote.' . $remoteid . '.url') + ->addArgument($remoteCloneUrl) + ->execute(); + } + } + + protected function isLocalWebUrl($url) + { + $base = Tools::fullUrl(); + if (substr($url, 0, strlen($base)) != $base) { + //base does not match + return false; + } + + $remainder = substr($url, strlen($base)); + //FIXME: check if it exists + } +} +?> diff --git a/src/phorkie/Repository/Remote.php b/src/phorkie/Repository/Remote.php index 4f5034c..3bb153f 100644 --- a/src/phorkie/Repository/Remote.php +++ b/src/phorkie/Repository/Remote.php @@ -13,6 +13,11 @@ class Repository_Remote } + public function getName() + { + return $this->name; + } + public function getTitle() { if (isset($this->arConfig['title'])) { @@ -38,10 +43,13 @@ class Repository_Remote } } - return $this->arConfig['url']; + if (isset($this->arConfig['url'])) { + return $this->arConfig['url']; + } + return null; } - public function getWebURL() + public function getWebURL($full = false) { if (isset($this->arConfig['homepage'])) { return $this->arConfig['homepage']; @@ -50,7 +58,7 @@ class Repository_Remote if ($this->isLocal()) { $local = $this->getLocalRepository(); if ($local !== null) { - return $local->getLink('display'); + return $local->getLink('display', null, $full); } } diff --git a/www/.htaccess b/www/.htaccess index b853d35..c076e6c 100644 --- a/www/.htaccess +++ b/www/.htaccess @@ -11,6 +11,7 @@ RewriteRule ^([0-9]+)/delete/confirm$ delete.php?id=$1&confirm=1 RewriteRule ^([0-9]+)/doap$ doap.php?id=$1 RewriteRule ^([0-9]+)/edit$ edit.php?id=$1 RewriteRule ^([0-9]+)/fork$ fork.php?id=$1 +RewriteRule ^([0-9]+)/linkback$ linkback.php?id=$1 RewriteRule ^([0-9]+)/raw/(.+)$ raw.php?id=$1&file=$2 RewriteRule ^([0-9]+)/rev/(.+)$ revision.php?id=$1&rev=$2 RewriteRule ^([0-9]+)/rev-raw/(.+)/(.+)$ raw.php?id=$1&rev=$2&file=$3 diff --git a/www/display.php b/www/display.php index ae29b87..a41f4a8 100644 --- a/www/display.php +++ b/www/display.php @@ -9,6 +9,12 @@ require_once 'www-header.php'; $repo = new Repository(); $repo->loadFromRequest(); +header('X-Pingback: ' . $repo->getLink('linkback', null, true)); +header( + 'Link: <' . $repo->getLink('linkback', null, true) . '>;' + . 'rel="http://webmention.org/"' +); + render( 'display', array( diff --git a/www/linkback.php b/www/linkback.php new file mode 100644 index 0000000..ce6f2de --- /dev/null +++ b/www/linkback.php @@ -0,0 +1,15 @@ +loadFromRequest(); + +$s = new \PEAR2\Services\Linkback\Server(); +$s->addCallback(new Repository_LinkbackReceiver($repo)); +$s->run(); +?> \ No newline at end of file