diff options
Diffstat (limited to 'plugins/OStatus/lib')
-rw-r--r-- | plugins/OStatus/lib/discovery.php | 91 | ||||
-rw-r--r-- | plugins/OStatus/lib/discoveryhints.php | 252 | ||||
-rw-r--r-- | plugins/OStatus/lib/feeddiscovery.php | 33 | ||||
-rw-r--r-- | plugins/OStatus/lib/linkheader.php | 63 | ||||
-rw-r--r-- | plugins/OStatus/lib/magicenvelope.php | 21 | ||||
-rw-r--r-- | plugins/OStatus/lib/safecrypt_rsa.php | 18 | ||||
-rw-r--r-- | plugins/OStatus/lib/safemath_biginteger.php | 20 | ||||
-rw-r--r-- | plugins/OStatus/lib/xrdaction.php | 8 |
8 files changed, 423 insertions, 83 deletions
diff --git a/plugins/OStatus/lib/discovery.php b/plugins/OStatus/lib/discovery.php index f8449b309..7187c1f3e 100644 --- a/plugins/OStatus/lib/discovery.php +++ b/plugins/OStatus/lib/discovery.php @@ -40,7 +40,7 @@ class Discovery const PROFILEPAGE = 'http://webfinger.net/rel/profile-page'; const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from'; const HCARD = 'http://microformats.org/profile/hcard'; - + public $methods = array(); public function __construct() @@ -50,12 +50,11 @@ class Discovery $this->registerMethod('Discovery_LRDD_Link_HTML'); } - public function registerMethod($class) { $this->methods[] = $class; } - + /** * Given a "user id" make sure it's normalized to either a webfinger * acct: uri or a profile HTTP URL. @@ -78,7 +77,7 @@ class Discovery public static function isWebfinger($user_id) { $uri = Discovery::normalize($user_id); - + return (substr($uri, 0, 5) == 'acct:'); } @@ -99,7 +98,7 @@ class Discovery } else { $xrd_uri = $link['href']; } - + $xrd = $this->fetchXrd($xrd_uri); if ($xrd) { return $xrd; @@ -114,14 +113,13 @@ class Discovery if (!is_array($links)) { return false; } - + foreach ($links as $link) { if ($link['rel'] == $service) { return $link; } } } - public static function applyTemplate($template, $id) { @@ -130,7 +128,6 @@ class Discovery return $template; } - public static function fetchXrd($url) { try { @@ -157,12 +154,13 @@ class Discovery_LRDD_Host_Meta implements Discovery_LRDD { public function discover($uri) { - if (!Discovery::isWebfinger($uri)) { - return false; + if (Discovery::isWebfinger($uri)) { + // We have a webfinger acct: - start with host-meta + list($name, $domain) = explode('@', $uri); + } else { + $domain = parse_url($uri, PHP_URL_HOST); } - - // We have a webfinger acct: - start with host-meta - list($name, $domain) = explode('@', $uri); + $url = 'http://'. $domain .'/.well-known/host-meta'; $xrd = Discovery::fetchXrd($url); @@ -171,7 +169,7 @@ class Discovery_LRDD_Host_Meta implements Discovery_LRDD if ($xrd->host != $domain) { return false; } - + return $xrd->links; } } @@ -187,7 +185,7 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD } catch (HTTP_Request2_Exception $e) { return false; } - + if ($response->getStatus() != 200) { return false; } @@ -196,51 +194,17 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD if (!$link_header) { // return false; } - - return Discovery_LRDD_Link_Header::parseHeader($link_header); + + return array(Discovery_LRDD_Link_Header::parseHeader($link_header)); } protected static function parseHeader($header) { - preg_match('/^<[^>]+>/', $header, $uri_reference); - //if (empty($uri_reference)) return; - - $links = array(); - - $link_uri = trim($uri_reference[0], '<>'); - $link_rel = array(); - $link_type = null; - - // remove uri-reference from header - $header = substr($header, strlen($uri_reference[0])); - - // parse link-params - $params = explode(';', $header); - - foreach ($params as $param) { - if (empty($param)) continue; - list($param_name, $param_value) = explode('=', $param, 2); - $param_name = trim($param_name); - $param_value = preg_replace('(^"|"$)', '', trim($param_value)); - - // for now we only care about 'rel' and 'type' link params - // TODO do something with the other links-params - switch ($param_name) { - case 'rel': - $link_rel = trim($param_value); - break; - - case 'type': - $link_type = trim($param_value); - } - } - - $links[] = array( - 'href' => $link_uri, - 'rel' => $link_rel, - 'type' => $link_type); + $lh = new LinkHeader($header); - return $links; + return array('href' => $lh->href, + 'rel' => $lh->rel, + 'type' => $lh->type); } } @@ -262,49 +226,48 @@ class Discovery_LRDD_Link_HTML implements Discovery_LRDD return Discovery_LRDD_Link_HTML::parse($response->getBody()); } - public function parse($html) { $links = array(); - + preg_match('/<head(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches); $head_html = $head_matches[2]; - + preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches); - + foreach ($link_matches[0] as $link_html) { $link_url = null; $link_rel = null; $link_type = null; - + preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches); if ( isset($rel_matches[3]) ) { $link_rel = $rel_matches[3]; } else if ( isset($rel_matches[1]) ) { $link_rel = $rel_matches[1]; } - + preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches); if ( isset($href_matches[3]) ) { $link_uri = $href_matches[3]; } else if ( isset($href_matches[1]) ) { $link_uri = $href_matches[1]; } - + preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches); if ( isset($type_matches[3]) ) { $link_type = $type_matches[3]; } else if ( isset($type_matches[1]) ) { $link_type = $type_matches[1]; } - + $links[] = array( 'href' => $link_url, 'rel' => $link_rel, 'type' => $link_type, ); } - + return $links; } } diff --git a/plugins/OStatus/lib/discoveryhints.php b/plugins/OStatus/lib/discoveryhints.php new file mode 100644 index 000000000..80cfbbf15 --- /dev/null +++ b/plugins/OStatus/lib/discoveryhints.php @@ -0,0 +1,252 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * Some utilities for generating hint data + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +class DiscoveryHints { + + static function fromXRD($xrd) + { + $hints = array(); + + foreach ($xrd->links as $link) { + switch ($link['rel']) { + case Discovery::PROFILEPAGE: + $hints['profileurl'] = $link['href']; + break; + case Salmon::NS_REPLIES: + $hints['salmon'] = $link['href']; + break; + case Discovery::UPDATESFROM: + $hints['feedurl'] = $link['href']; + break; + case Discovery::HCARD: + $hints['hcardurl'] = $link['href']; + break; + default: + break; + } + } + + return $hints; + } + + static function fromHcardUrl($url) + { + $client = new HTTPClient(); + $client->setHeader('Accept', 'text/html,application/xhtml+xml'); + $response = $client->get($url); + + if (!$response->isOk()) { + return null; + } + + return self::hcardHints($response->getBody(), + $response->getUrl()); + } + + static function hcardHints($body, $url) + { + $hcard = self::_hcard($body, $url); + + if (empty($hcard)) { + return array(); + } + + $hints = array(); + + // XXX: don't copy stuff into an array and then copy it again + + if (array_key_exists('nickname', $hcard)) { + $hints['nickname'] = $hcard['nickname']; + } + + if (array_key_exists('fn', $hcard)) { + $hints['fullname'] = $hcard['fn']; + } else if (array_key_exists('n', $hcard)) { + $hints['fullname'] = implode(' ', $hcard['n']); + } + + if (array_key_exists('photo', $hcard)) { + $hints['avatar'] = $hcard['photo'][0]; + } + + if (array_key_exists('note', $hcard)) { + $hints['bio'] = $hcard['note']; + } + + if (array_key_exists('adr', $hcard)) { + if (is_string($hcard['adr'])) { + $hints['location'] = $hcard['adr']; + } else if (is_array($hcard['adr'])) { + $hints['location'] = implode(' ', $hcard['adr']); + } + } + + if (array_key_exists('url', $hcard)) { + if (is_string($hcard['url'])) { + $hints['homepage'] = $hcard['url']; + } else if (is_array($hcard['url']) && !empty($hcard['url'])) { + // HACK get the last one; that's how our hcards look + $hints['homepage'] = $hcard['url'][count($hcard['url'])-1]; + } + } + + return $hints; + } + + static function _hcard($body, $url) + { + // DOMDocument::loadHTML may throw warnings on unrecognized elements. + + $old = error_reporting(error_reporting() & ~E_WARNING); + + $doc = new DOMDocument(); + $doc->loadHTML($body); + + error_reporting($old); + + $xp = new DOMXPath($doc); + + $hcardNodes = self::_getChildrenByClass($doc->documentElement, 'vcard', $xp); + + $hcards = array(); + + for ($i = 0; $i < $hcardNodes->length; $i++) { + + $hcardNode = $hcardNodes->item($i); + + $hcard = self::_hcardFromNode($hcardNode, $xp, $url); + + $hcards[] = $hcard; + } + + $repr = null; + + foreach ($hcards as $hcard) { + if (in_array($url, $hcard['url'])) { + $repr = $hcard; + break; + } + } + + if (!is_null($repr)) { + return $repr; + } else if (count($hcards) > 0) { + return $hcards[0]; + } else { + return null; + } + } + + function _getChildrenByClass($el, $cls, $xp) + { + // borrowed from hkit. Thanks dudes! + + $qry = ".//*[contains(concat(' ',normalize-space(@class),' '),' $cls ')]"; + + $nodes = $xp->query($qry, $el); + + return $nodes; + } + + function _hcardFromNode($hcardNode, $xp, $base) + { + $hcard = array(); + + $hcard['url'] = array(); + + $urlNodes = self::_getChildrenByClass($hcardNode, 'url', $xp); + + for ($j = 0; $j < $urlNodes->length; $j++) { + + $urlNode = $urlNodes->item($j); + + if ($urlNode->hasAttribute('href')) { + $url = $urlNode->getAttribute('href'); + } else { + $url = $urlNode->textContent; + } + + $hcard['url'][] = self::_rel2abs($url, $base); + } + + $hcard['photo'] = array(); + + $photoNodes = self::_getChildrenByClass($hcardNode, 'photo', $xp); + + for ($j = 0; $j < $photoNodes->length; $j++) { + $photoNode = $photoNodes->item($j); + if ($photoNode->hasAttribute('src')) { + $url = $photoNode->getAttribute('src'); + } else if ($photoNode->hasAttribute('href')) { + $url = $photoNode->getAttribute('href'); + } else { + $url = $photoNode->textContent; + } + $hcard['photo'][] = self::_rel2abs($url, $base); + } + + $singles = array('nickname', 'note', 'fn', 'n', 'adr'); + + foreach ($singles as $single) { + + $nodes = self::_getChildrenByClass($hcardNode, $single, $xp); + + if ($nodes->length > 0) { + $node = $nodes->item(0); + $hcard[$single] = $node->textContent; + } + } + + return $hcard; + } + + // XXX: this is a first pass; we probably need + // to handle things like ../ and ./ and so on + + static function _rel2abs($rel, $wrt) + { + $parts = parse_url($rel); + + if ($parts === false) { + return false; + } + + // If it's got a scheme, use it + + if (!empty($parts['scheme'])) { + return $rel; + } + + $w = parse_url($wrt); + + $base = $w['scheme'].'://'.$w['host']; + + if ($rel[0] == '/') { + return $base.$rel; + } + + $wp = explode('/', $w['path']); + + array_pop($wp); + + return $base.implode('/', $wp).'/'.$rel; + } +} diff --git a/plugins/OStatus/lib/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php index 7afb71bdc..4809f9d35 100644 --- a/plugins/OStatus/lib/feeddiscovery.php +++ b/plugins/OStatus/lib/feeddiscovery.php @@ -73,6 +73,7 @@ class FeedDiscovery public $uri; public $type; public $feed; + public $root; /** Post-initialize query helper... */ public function getLink($rel, $type=null) @@ -83,7 +84,7 @@ class FeedDiscovery public function getAtomLink($rel, $type=null) { - return ActivityUtils::getLink($this->feed->documentElement, $rel, $type); + return ActivityUtils::getLink($this->root, $rel, $type); } /** @@ -117,7 +118,7 @@ class FeedDiscovery return $this->discoverFromURL($target, false); } } - + return $this->initFromResponse($response); } @@ -129,7 +130,7 @@ class FeedDiscovery function initFromResponse($response) { if (!$response->isOk()) { - throw new FeedSubBadResponseException($response->getCode()); + throw new FeedSubBadResponseException($response->getStatus()); } $sourceurl = $response->getUrl(); @@ -154,9 +155,27 @@ class FeedDiscovery $this->uri = $sourceurl; $this->type = $type; $this->feed = $feed; + + $el = $this->feed->documentElement; + + // Looking for the "root" element: RSS channel or Atom feed + + if ($el->tagName == 'rss') { + $channels = $el->getElementsByTagName('channel'); + if ($channels->length > 0) { + $this->root = $channels->item(0); + } else { + throw new FeedSubBadXmlException($sourceurl); + } + } else if ($el->tagName == 'feed') { + $this->root = $el; + } else { + throw new FeedSubBadXmlException($sourceurl); + } + return $this->uri; } else { - throw new FeedSubBadXmlException($url); + throw new FeedSubBadXmlException($sourceurl); } } @@ -202,7 +221,7 @@ class FeedDiscovery 'application/atom+xml' => false, 'application/rss+xml' => false, ); - + $nodes = $dom->getElementsByTagName('link'); for ($i = 0; $i < $nodes->length; $i++) { $node = $nodes->item($i); @@ -211,11 +230,11 @@ class FeedDiscovery $type = $node->attributes->getNamedItem('type'); $href = $node->attributes->getNamedItem('href'); if ($rel && $type && $href) { - $rel = trim($rel->value); + $rel = array_filter(explode(" ", $rel->value)); $type = trim($type->value); $href = trim($href->value); - if (trim($rel) == 'alternate' && array_key_exists($type, $feeds) && empty($feeds[$type])) { + if (in_array('alternate', $rel) && array_key_exists($type, $feeds) && empty($feeds[$type])) { // Save the first feed found of each type... $feeds[$type] = $this->resolveURI($href, $base); } diff --git a/plugins/OStatus/lib/linkheader.php b/plugins/OStatus/lib/linkheader.php new file mode 100644 index 000000000..cd78d31ce --- /dev/null +++ b/plugins/OStatus/lib/linkheader.php @@ -0,0 +1,63 @@ +<?php + +class LinkHeader +{ + var $href; + var $rel; + var $type; + + function __construct($str) + { + preg_match('/^<[^>]+>/', $str, $uri_reference); + //if (empty($uri_reference)) return; + + $this->href = trim($uri_reference[0], '<>'); + $this->rel = array(); + $this->type = null; + + // remove uri-reference from header + $str = substr($str, strlen($uri_reference[0])); + + // parse link-params + $params = explode(';', $str); + + foreach ($params as $param) { + if (empty($param)) continue; + list($param_name, $param_value) = explode('=', $param, 2); + $param_name = trim($param_name); + $param_value = preg_replace('(^"|"$)', '', trim($param_value)); + + // for now we only care about 'rel' and 'type' link params + // TODO do something with the other links-params + switch ($param_name) { + case 'rel': + $this->rel = trim($param_value); + break; + + case 'type': + $this->type = trim($param_value); + } + } + } + + static function getLink($response, $rel=null, $type=null) + { + $headers = $response->getHeader('Link'); + if ($headers) { + // Can get an array or string, so try to simplify the path + if (!is_array($headers)) { + $headers = array($headers); + } + + foreach ($headers as $header) { + $lh = new LinkHeader($header); + + if ((is_null($rel) || $lh->rel == $rel) && + (is_null($type) || $lh->type == $type)) { + return $lh->href; + } + } + } + return null; + } +} diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index fb8c57c71..9266cab5c 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -59,7 +59,11 @@ class MagicEnvelope } if ($xrd->links) { if ($link = Discovery::getService($xrd->links, Magicsig::PUBLICKEYREL)) { - list($type, $keypair) = explode(';', $link['href']); + list($type, $keypair) = explode(',', $link['href']); + if (empty($keypair)) { + // Backwards compatibility check for separator bug in 0.9.0 + list($type, $keypair) = explode(';', $link['href']); + } return $keypair; } } @@ -70,7 +74,7 @@ class MagicEnvelope public function signMessage($text, $mimetype, $keypair) { $signature_alg = Magicsig::fromString($keypair); - $armored_text = base64_encode($text); + $armored_text = base64_url_encode($text); return array( 'data' => $armored_text, @@ -108,7 +112,7 @@ class MagicEnvelope public function unfold($env) { $dom = new DOMDocument(); - $dom->loadXML(base64_decode($env['data'])); + $dom->loadXML(base64_url_decode($env['data'])); if ($dom->documentElement->tagName != 'entry') { return false; @@ -165,7 +169,7 @@ class MagicEnvelope return false; } - $text = base64_decode($env['data']); + $text = base64_url_decode($env['data']); $signer_uri = $this->getAuthor($text); try { @@ -193,11 +197,12 @@ class MagicEnvelope public function fromDom($dom) { - if ($dom->documentElement->tagName == 'entry') { + $env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'env')->item(0); + if (!$env_element) { $env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'provenance')->item(0); - } else if ($dom->documentElement->tagName == 'me:env') { - $env_element = $dom->documentElement; - } else { + } + + if (!$env_element) { return false; } diff --git a/plugins/OStatus/lib/safecrypt_rsa.php b/plugins/OStatus/lib/safecrypt_rsa.php new file mode 100644 index 000000000..f3aa2c928 --- /dev/null +++ b/plugins/OStatus/lib/safecrypt_rsa.php @@ -0,0 +1,18 @@ +<?php + +require_once 'Crypt/RSA.php'; + +/** + * Crypt_RSA stores a Math_BigInteger with value 0, which triggers a bug + * in Math_BigInteger's wakeup function which spews notices to log or output. + * This wrapper replaces it with a version that survives serialization. + */ +class SafeCrypt_RSA extends Crypt_RSA +{ + function __construct() + { + parent::__construct(); + $this->zero = new SafeMath_BigInteger(); + } +} + diff --git a/plugins/OStatus/lib/safemath_biginteger.php b/plugins/OStatus/lib/safemath_biginteger.php new file mode 100644 index 000000000..c05e24d1e --- /dev/null +++ b/plugins/OStatus/lib/safemath_biginteger.php @@ -0,0 +1,20 @@ +<?php + +require_once 'Math/BigInteger.php'; + +/** + * Crypt_RSA stores a Math_BigInteger with value 0, which triggers a bug + * in Math_BigInteger's wakeup function which spews notices to log or output. + * This wrapper replaces it with a version that survives serialization. + */ +class SafeMath_BigInteger extends Math_BigInteger +{ + function __wakeup() + { + if ($this->hex == '') { + $this->hex = '0'; + } + parent::__wakeup(); + } +} + diff --git a/plugins/OStatus/lib/xrdaction.php b/plugins/OStatus/lib/xrdaction.php index 6881292ad..f1a56e0a8 100644 --- a/plugins/OStatus/lib/xrdaction.php +++ b/plugins/OStatus/lib/xrdaction.php @@ -46,10 +46,10 @@ class XrdAction extends Action if (empty($xrd->subject)) { $xrd->subject = Discovery::normalize($this->uri); } - $xrd->alias[] = common_profile_url($nick); + $xrd->alias[] = $this->user->uri; $xrd->links[] = array('rel' => Discovery::PROFILEPAGE, 'type' => 'text/html', - 'href' => common_profile_url($nick)); + 'href' => $this->user->uri); $xrd->links[] = array('rel' => Discovery::UPDATESFROM, 'href' => common_local_url('ApiTimelineUser', @@ -65,7 +65,7 @@ class XrdAction extends Action // XFN $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', 'type' => 'text/html', - 'href' => common_profile_url($nick)); + 'href' => $this->user->uri); // FOAF $xrd->links[] = array('rel' => 'describedby', 'type' => 'application/rdf+xml', @@ -91,7 +91,7 @@ class XrdAction extends Action } $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, - 'href' => 'data:application/magic-public-key;'. $magickey->toString(false)); + 'href' => 'data:application/magic-public-key,'. $magickey->toString(false)); // TODO - finalize where the redirect should go on the publisher $url = common_local_url('ostatussub') . '?profile={uri}'; |