aboutsummaryrefslogtreecommitdiff
path: root/src/phinde
diff options
context:
space:
mode:
Diffstat (limited to 'src/phinde')
-rw-r--r--src/phinde/HttpRequest.php4
-rw-r--r--src/phinde/HubUrlExtractor.php226
-rw-r--r--src/phinde/Subscriptions.php153
3 files changed, 378 insertions, 5 deletions
diff --git a/src/phinde/HttpRequest.php b/src/phinde/HttpRequest.php
index e68bd84..4635973 100644
--- a/src/phinde/HttpRequest.php
+++ b/src/phinde/HttpRequest.php
@@ -3,9 +3,9 @@ namespace phinde;
class HttpRequest extends \HTTP_Request2
{
- public function __construct($url)
+ public function __construct($url = null, $method = 'GET')
{
- parent::__construct($url);
+ parent::__construct($url, $method);
$this->setConfig('follow_redirects', true);
$this->setConfig('connect_timeout', 5);
$this->setConfig('timeout', 10);
diff --git a/src/phinde/HubUrlExtractor.php b/src/phinde/HubUrlExtractor.php
new file mode 100644
index 0000000..e2d328a
--- /dev/null
+++ b/src/phinde/HubUrlExtractor.php
@@ -0,0 +1,226 @@
+<?php
+namespace phinde;
+
+class HubUrlExtractor
+{
+ /**
+ * HTTP request object that's used to do the requests
+ *
+ * @var \HTTP_Request2
+ */
+ protected $request;
+
+ /**
+ * Get the hub and self/canonical URL of a given topic URL.
+ * Uses link headers and parses HTML link rels.
+ *
+ * @param string $url Topic URL
+ *
+ * @return array Array of URLs with keys: hub, self
+ */
+ public function getUrls($url)
+ {
+ //at first, try a HEAD request that does not transfer so much data
+ $req = $this->getRequest();
+ $req->setUrl($url);
+ $req->setMethod(\HTTP_Request2::METHOD_HEAD);
+ $res = $req->send();
+
+ if (intval($res->getStatus() / 100) >= 4
+ && $res->getStatus() != 405 //method not supported/allowed
+ ) {
+ return null;
+ }
+
+ $url = $res->getEffectiveUrl();
+ $base = new \Net_URL2($url);
+
+ $urls = $this->extractHeader($res);
+ if (count($urls) === 2) {
+ return $this->absolutifyUrls($urls, $base);
+ }
+
+ list($type) = explode(';', $res->getHeader('Content-type'));
+ if ($type != 'text/html' && $type != 'text/xml'
+ && $type != 'application/xhtml+xml'
+ //FIXME: atom, rss
+ && $res->getStatus() != 405//HEAD method not allowed
+ ) {
+ //we will not be able to extract links from the content
+ return $urls;
+ }
+
+ //HEAD failed, do a normal GET
+ $req->setMethod(\HTTP_Request2::METHOD_GET);
+ $res = $req->send();
+ if (intval($res->getStatus() / 100) >= 4) {
+ return $urls;
+ }
+
+ //yes, maybe the server does return this header now
+ // e.g. PHP's Phar::webPhar() does not work with HEAD
+ // https://bugs.php.net/bug.php?id=51918
+ $urls = array_merge($this->extractHeader($res), $urls);
+ if (count($urls) === 2) {
+ return $this->absolutifyUrls($urls, $base);
+ }
+
+ //FIXME: atom/rss
+ $body = $res->getBody();
+ $doc = $this->loadHtml($body, $res);
+
+ $xpath = new \DOMXPath($doc);
+ $xpath->registerNamespace('h', 'http://www.w3.org/1999/xhtml');
+
+ $nodeList = $xpath->query(
+ '/*[self::html or self::h:html]'
+ . '/*[self::head or self::h:head]'
+ . '/*[(self::link or self::h:link)'
+ . ' and'
+ . ' ('
+ . ' contains(concat(" ", normalize-space(@rel), " "), " hub ")'
+ . ' or'
+ . ' contains(concat(" ", normalize-space(@rel), " "), " canonical ")'
+ . ' or'
+ . ' contains(concat(" ", normalize-space(@rel), " "), " self ")'
+ . ' )'
+ . ']'
+ );
+
+ if ($nodeList->length == 0) {
+ //topic has no links
+ return $urls;
+ }
+
+ foreach ($nodeList as $link) {
+ $uri = $link->attributes->getNamedItem('href')->nodeValue;
+ $types = explode(
+ ' ', $link->attributes->getNamedItem('rel')->nodeValue
+ );
+ foreach ($types as $type) {
+ if ($type == 'canonical') {
+ $type = 'self';
+ }
+ if ($type == 'hub' || $type == 'self'
+ && !isset($urls[$type])
+ ) {
+ $urls[$type] = $uri;
+ }
+ }
+ }
+
+ //FIXME: base href
+ return $this->absolutifyUrls($urls, $base);
+ }
+
+ /**
+ * Extract hub url from the HTTP response headers.
+ *
+ * @param object $res HTTP response
+ *
+ * @return array Array with maximal two keys: hub and self
+ */
+ protected function extractHeader(\HTTP_Request2_Response $res)
+ {
+ $http = new \HTTP2();
+
+ $urls = array();
+ $links = $http->parseLinks($res->getHeader('Link'));
+ foreach ($links as $link) {
+ if (isset($link['_uri']) && isset($link['rel'])) {
+ if (!isset($urls['hub'])
+ && array_search('hub', $link['rel']) !== false
+ ) {
+ $urls['hub'] = $link['_uri'];
+ }
+ if (!isset($urls['self'])
+ && array_search('self', $link['rel']) !== false
+ ) {
+ $urls['self'] = $link['_uri'];
+ }
+ }
+ }
+ return $urls;
+ }
+
+ /**
+ * Load a DOMDocument from the given HTML or XML
+ *
+ * @param string $sourceBody Content of $source URI
+ * @param object $res HTTP response from fetching $source
+ *
+ * @return \DOMDocument DOM document object with HTML/XML loaded
+ */
+ protected static function loadHtml($sourceBody, \HTTP_Request2_Response $res)
+ {
+ $doc = new \DOMDocument();
+
+ libxml_clear_errors();
+ $old = libxml_use_internal_errors(true);
+
+ $typeParts = explode(';', $res->getHeader('content-type'));
+ $type = $typeParts[0];
+ if ($type == 'application/xhtml+xml'
+ || $type == 'application/xml'
+ || $type == 'text/xml'
+ ) {
+ $doc->loadXML($sourceBody);
+ } else {
+ $doc->loadHTML($sourceBody);
+ }
+
+ libxml_clear_errors();
+ libxml_use_internal_errors($old);
+
+ return $doc;
+ }
+
+ /**
+ * Returns the HTTP request object clone that can be used
+ * for one HTTP request.
+ *
+ * @return HTTP_Request2 Clone of the setRequest() object
+ */
+ public function getRequest()
+ {
+ if ($this->request === null) {
+ $request = new \HTTP_Request2();
+ $request->setConfig('follow_redirects', true);
+ $this->setRequestTemplate($request);
+ }
+
+ //we need to clone because previous requests could have
+ //set internal variables like POST data that we don't want now
+ return clone $this->request;
+ }
+
+ /**
+ * Sets a custom HTTP request object that will be used to do HTTP requests
+ *
+ * @param object $request Request object
+ *
+ * @return self
+ */
+ public function setRequestTemplate(\HTTP_Request2 $request)
+ {
+ $this->request = $request;
+ return $this;
+ }
+
+ /**
+ * Make the list of urls absolute
+ *
+ * @param array $urls Array of maybe relative URLs
+ * @param object $base Base URL to resolve the relatives against
+ *
+ * @return array List of absolute URLs
+ */
+ protected function absolutifyUrls($urls, \Net_URL2 $base)
+ {
+ foreach ($urls as $key => $url) {
+ $urls[$key] = (string) $base->resolve($url);
+ }
+ return $urls;
+ }
+}
+?>
diff --git a/src/phinde/Subscriptions.php b/src/phinde/Subscriptions.php
index 9db4b16..4d00ab8 100644
--- a/src/phinde/Subscriptions.php
+++ b/src/phinde/Subscriptions.php
@@ -1,12 +1,159 @@
<?php
namespace phinde;
+/**
+ * Database table containing information about Pubsubhubbub subscriptions
+ */
class Subscriptions
{
- public function get($url)
+ protected $db;
+
+ public function __construct()
+ {
+ $this->db = new \PDO(
+ $GLOBALS['phinde']['db_dsn'],
+ $GLOBALS['phinde']['db_user'],
+ $GLOBALS['phinde']['db_pass']
+ );
+ $this->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ }
+
+ /**
+ * Fetch a topic
+ *
+ * @param string $topic Topic URL
+ *
+ * @return false|object False if the row does not exist
+ */
+ public function get($topic)
+ {
+ $stmt = $this->db->prepare(
+ 'SELECT * FROM subscriptions'
+ . ' WHERE sub_topic = :topic'
+ );
+ $stmt->execute([':topic' => $topic]);
+
+ //fetchObject() itself returns FALSE on failure
+ return $stmt->fetchObject();
+ }
+
+ /**
+ * Create a new subscription entry in database.
+ * Automatically generates secret, capkey and lease seconds.
+ *
+ * This method does NOT:
+ * - check for duplicates (do it yourself)
+ * - return the object (fetch it yourself)
+ * - send subscription requests to the hub
+ *
+ * @param string $topic URL to subscribe to
+ *
+ * @return void
+ */
+ public function create($topic)
+ {
+ $stmt = $this->db->prepare(
+ 'INSERT INTO subscriptions'
+ . ' (sub_topic, sub_status, sub_lease_seconds, sub_expires'
+ . ', sub_secret, sub_capkey, sub_created, sub_updated'
+ . ', sub_pings, sub_lastping, sub_statusmessage)'
+ . ' VALUES '
+ . ' (:topic, "subscribing", :lease_seconds, "0000-00-00 00:00:00"'
+ . ', :secret, :capkey, NOW(), NOW()'
+ . ', 0, "0000-00-00 00:00:00", "")'
+ );
+ $stmt->execute(
+ [
+ ':topic' => $topic,
+ ':lease_seconds' => 86400 * 30,
+ ':secret' => bin2hex(openssl_random_pseudo_bytes(16)),
+ ':capkey' => bin2hex(openssl_random_pseudo_bytes(16)),
+ ]
+ );
+ }
+
+ /**
+ * A subscription has been confirmed by the hub - mark it as active.
+ *
+ * @param integer $subId Subscription ID
+ * @param integer $leaseSeconds Number of seconds until subscription expires
+ *
+ * @return void
+ */
+ public function subscribed($subId, $leaseSeconds)
+ {
+ $this->db->prepare(
+ 'UPDATE subscriptions'
+ . ' SET sub_status = "active"'
+ . ' , sub_lease_seconds = :leaseSeconds'
+ . ' , sub_expires = :expires'
+ . ' , sub_updated = NOW()'
+ . ' WHERE sub_id = :id'
+ )->execute(
+ [
+ ':leaseSeconds' => $leaseSeconds,
+ ':expires' => gmdate('Y-m-d H:i:s', time() + $leaseSeconds),
+ ':id' => $subId,
+ ]
+ );
+ }
+
+ /**
+ * Mark a subscription as "unsubscribed"
+ *
+ * @param integer $subId Subscription ID
+ *
+ * @return void
+ */
+ public function unsubscribed($subId)
+ {
+ $this->db->prepare(
+ 'UPDATE subscriptions'
+ . ' SET sub_status = "unsubscribed"'
+ . ' , sub_updated = NOW()'
+ . ' WHERE sub_id = :id'
+ )->execute([':id' => $subId]);
+ }
+
+ public function denied($subId, $reason)
+ {
+ $this->db->prepare(
+ 'UPDATE subscriptions'
+ . ' SET sub_status = "denied"'
+ . ' , sub_statusmessage = :reason'
+ . ' , sub_updated = NOW()'
+ . ' WHERE sub_id = :id'
+ )->execute([':id' => $subId, ':reason' => $reason]);
+ }
+
+ public function pinged($subId)
+ {
+ $this->db->prepare(
+ 'UPDATE subscriptions'
+ . ' SET sub_pings = sub_pings + 1'
+ . ' , sub_lastping = NOW()'
+ . ' , sub_updated = NOW()'
+ . ' WHERE sub_id = :id'
+ )->execute([':id' => $subId]);
+ }
+
+ /**
+ * Detect the hub for the given topic URL
+ *
+ * @param string $url Topic URL
+ *
+ * @return array Topic URL and hub URL. Hub URL is NULL if there is none.
+ */
+ public function detectHub($url)
{
- //FIXME
- return false;
+ $hue = new HubUrlExtractor();
+ $hue->setRequestTemplate(new HttpRequest());
+ $urls = $hue->getUrls($url);
+ //we violate the spec by not requiring a self URL
+ $topicUrl = isset($urls['self']) ? $urls['self'] : $url;
+ $hubUrl = isset($urls['hub']) ? $urls['hub'] : null;
+
+ return array($topicUrl, $hubUrl);
}
}
?>