first work on remote fork notifications with linkback (webmention/pingback)
authorChristian Weiske <cweiske@cweiske.de>
Fri, 22 Nov 2013 20:32:06 +0000 (21:32 +0100)
committerChristian Weiske <cweiske@cweiske.de>
Fri, 22 Nov 2013 20:32:06 +0000 (21:32 +0100)
14 files changed:
data/config.default.php
data/templates/display-sidebar-fork.htm
src/phorkie/ForkRemote.php
src/phorkie/HtmlParser.php [new file with mode: 0644]
src/phorkie/Notificator.php
src/phorkie/Notificator/Linkback.php [new file with mode: 0644]
src/phorkie/Notificator/Webhook.php [new file with mode: 0644]
src/phorkie/Repository.php
src/phorkie/Repository/ConnectionInfo.php
src/phorkie/Repository/LinkbackReceiver.php [new file with mode: 0644]
src/phorkie/Repository/Remote.php
www/.htaccess
www/display.php
www/linkback.php [new file with mode: 0644]

index 85713cd..45adf92 100644 (file)
@@ -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,
index d3793e1..9ed5626 100644 (file)
@@ -1,7 +1,7 @@
 {% set conns = repo.getConnectionInfo() %}
 {% if conns.isFork() %}
  {% set origin = conns.getOrigin() %}
- <h4>Fork of</h4>
+ <h4 id="parent">Fork of</h4>
  <p>
   {% set webpage = origin.getWebURL() %}
   {% if webpage %}
@@ -9,6 +9,26 @@
   {% else %}
    {{origin.getTitle()}}
   {% endif %}
-  (<a href="{{origin.getCloneUrl()}}">clone URL</a>)
+  <a href="{{origin.getCloneUrl()}}" title="Clone URL"><i class="icon-globe"></i></a>
  </p>
+{% endif %}
+{% if conns.hasForks() %}
+ <h4 id="forks">Forks</h4>
+ <ul>
+  {% for remote in conns.getForks %}
+  <li>
+   {% set webpage = remote.getWebURL() %}
+   {% if webpage %}
+    <a href="{{webpage}}">{{remote.getTitle()}}</a>
+   {% else %}
+    {{remote.getTitle()}}
+   {% endif %}
+
+   {% set cloneUrl = remote.getCloneUrl() %}
+   {% if cloneUrl %}
+   <a href="{{cloneUrl}}" title="Clone URL"><i class="icon-globe"></i></a>
+   {% endif %}
+  </li>
+  {% endfor %}
+ </ul>
 {% endif %}
\ No newline at end of file
index 31b4839..f8e319a 100644 (file)
@@ -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'])) {
-                //<link href=".." rel="vcs-git" title="title" />
-                $title = (string)$elem['title'];
-            } else if ($str != '') {
-                //<a href=".." rel="vcs-git">title</a>
-                $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 (file)
index 0000000..f751074
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+namespace phorkie;
+
+class HtmlParser
+{
+    /**
+     * Contains error message when parse() failed
+     */
+    public $error;
+
+    /**
+     * Array with keys (URL title) and values (arrays of urls)
+     * Only supported URLs are included.
+     *
+     * @var array
+     */
+    protected $arGitUrls;
+
+
+
+    /**
+     * Extract git URLs from the given URL, eventually fetching
+     * HTML and extracting URLs from there.
+     *
+     * Sets $error and $arGitUrls class variables
+     *
+     * @param string $url  Git or HTTP URL
+     * @param string $html HTML content of $url
+     *
+     * @return boolean True when all went well, false in case of an error
+     * @uses   $error
+     * @uses   $arGitUrls
+     */
+    public function extractGitUrls($url, $html = null)
+    {
+        if ($url == '') {
+            $this->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'])) {
+                //<link href=".." rel="vcs-git" title="title" />
+                $title = (string)$elem['title'];
+            } else if ($str != '') {
+                //<a href=".." rel="vcs-git">title</a>
+                $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';
+    }
+
+}
+?>
index 3ef5c81..dc6a6af 100644 (file)
@@ -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 (file)
index 0000000..4c1abfb
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+namespace phorkie;
+
+/**
+ * Send out linkbacks for the remote paste URL when it gets forked here
+ */
+class Notificator_Linkback
+{
+    protected $config;
+
+    public function __construct($config)
+    {
+        $this->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 (file)
index 0000000..5737243
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+namespace phorkie;
+
+/**
+ * Send out webhook callbacks when something happens
+ */
+class Notificator_Webhook
+{
+    protected $config;
+
+    public function __construct($config)
+    {
+        $this->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
+            }
+        }
+    }
+}
+?>
+
index 7428c8a..2683ad0 100644 (file)
@@ -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');
         }
index 3815856..ce96c3e 100644 (file)
@@ -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 (file)
index 0000000..bce7643
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+namespace phorkie;
+
+class Repository_LinkbackReceiver
+    implements \PEAR2\Services\Linkback\Server\Callback\IStorage
+{
+    protected $repo;
+
+    public function __construct($repo)
+    {
+        $this->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
+    }
+}
+?>
index 4f5034c..3bb153f 100644 (file)
@@ -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);
             }
         }
 
index b853d35..c076e6c 100644 (file)
@@ -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
index ae29b87..a41f4a8 100644 (file)
@@ -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 (file)
index 0000000..ce6f2de
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+namespace phorkie;
+/**
+ * Receive linkback
+ */
+$reqWritePermissions = false;
+require_once 'www-header.php';
+
+$repo = new Repository();
+$repo->loadFromRequest();
+
+$s = new \PEAR2\Services\Linkback\Server();
+$s->addCallback(new Repository_LinkbackReceiver($repo));
+$s->run();
+?>
\ No newline at end of file