diff --git a/EVENTS.txt b/EVENTS.txt index 1d5b610b27..a113bb56a1 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -585,12 +585,6 @@ EndPublicXRDS: End XRDS output (right before the closing XRDS tag) - $action: the current action - &$xrdsoutputter - XRDSOutputter object to write to -StartHostMetaLinks: Start /.well-known/host-meta links -- &links: array containing the links elements to be written - -EndHostMetaLinks: End /.well-known/host-meta links -- &links: array containing the links elements to be written - StartCheckPassword: Check a username/password - $nickname: The nickname to check - $password: The password to check @@ -987,22 +981,6 @@ EndAtomPubNewActivity: When a new activity comes in through Atom Pub API - $user: user publishing the entry - $notice: notice that was created -StartXrdActionAliases: About to set aliases for the XRD object for a user -- &$xrd: XRD object being shown -- $user: User being shown - -EndXrdActionAliases: Done with aliases for the XRD object for a user -- &$xrd: XRD object being shown -- $user: User being shown - -StartXrdActionLinks: About to set links for the XRD object for a user -- &$xrd: XRD object being shown -- $user: User being shown - -EndXrdActionLinks: Done with links for the XRD object for a user -- &$xrd: XRD object being shown -- $user: User being shown - AdminPanelCheck: When checking whether the current user can access a given admin panel - $name: Name of the admin panel - &$isOK: Boolean whether the user is allowed to use the panel diff --git a/actions/hostmeta.php b/actions/hostmeta.php deleted file mode 100644 index 3d541543a2..0000000000 --- a/actions/hostmeta.php +++ /dev/null @@ -1,69 +0,0 @@ -. - */ - -/** - * @category Action - * @package StatusNet - * @maintainer James Walker - * @author Craig Andrews - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -// @todo XXX: Add documentation. -class HostMetaAction extends Action -{ - /** - * Is read only? - * - * @return boolean true - */ - function isReadOnly() - { - return true; - } - - function handle() - { - parent::handle(); - - $xrd = new XRD(); - $xrd->host = strtolower($_SERVER['SERVER_NAME']); - - if(Event::handle('StartHostMetaLinks', array(&$xrd->links))) { - $url = common_local_url('userxrd'); - $url.= '?uri={uri}'; - $xrd->links[] = array('rel' => Discovery::LRDD_REL, - 'template' => $url, - 'title' => array('Resource Descriptor')); - Event::handle('EndHostMetaLinks', array(&$xrd->links)); - } - - // Output Cross-Origin Resource Sharing (CORS) header - if (common_config('discovery', 'cors')) { - header('Access-Control-Allow-Origin: *'); - } - - header('Content-type: application/xrd+xml'); - - print $xrd->toXML(); - } -} diff --git a/actions/userxrd.php b/actions/userxrd.php deleted file mode 100644 index 98195d1e33..0000000000 --- a/actions/userxrd.php +++ /dev/null @@ -1,67 +0,0 @@ -. - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -/** - * @package OStatusPlugin - * @maintainer James Walker - */ -class UserxrdAction extends XrdAction -{ - function prepare($args) - { - parent::prepare($args); - global $config; - - $this->uri = $this->trimmed('uri'); - $this->uri = self::normalize($this->uri); - - if (self::isWebfinger($this->uri)) { - $parts = explode('@', substr(urldecode($this->uri), 5)); - if (count($parts) == 2) { - list($nick, $domain) = $parts; - // @fixme confirm the domain too - // @fixme if domain checking is added, ensure that it will not - // cause problems with sites that have changed domains! - $nick = common_canonical_nickname($nick); - $this->user = User::getKV('nickname', $nick); - } - } else { - $this->user = User::getKV('uri', $this->uri); - if (empty($this->user)) { - // try and get it by profile url - $profile = Profile::getKV('profileurl', $this->uri); - if (!empty($profile)) { - $this->user = User::getKV('id', $profile->id); - } - } - } - - if (!$this->user) { - // TRANS: Client error displayed when user not found for an action. - $this->clientError(_('No such user.'), 404); - return false; - } - - return true; - } -} diff --git a/classes/Profile.php b/classes/Profile.php index 8a7f7c1ff8..79eb00d22a 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1327,12 +1327,26 @@ class Profile extends Managed_DataObject return $noun->asString('activity:' . $element); } + /** + * Returns the profile's canonical url, not necessarily a uri/unique id + * + * @return string $profileurl + */ + public function getUrl() + { + if (empty($this->profileurl) || + !filter_var($this->profileurl, FILTER_VALIDATE_URL)) { + throw new InvalidUrlException($this->profileurl); + } + return $this->profileurl; + } + /** * Returns the best URI for a profile. Plugins may override. * * @return string $uri */ - function getUri() + public function getUri() { $uri = null; diff --git a/classes/User.php b/classes/User.php index 14ff66825a..82545d4bb5 100644 --- a/classes/User.php +++ b/classes/User.php @@ -907,6 +907,10 @@ class User extends Managed_DataObject self::cacheSet('user:site_owner', $owner); } + if (!($owner instanceof User)) { + throw new ServerException(_('No site owner configured.')); + } + return $owner; } @@ -936,14 +940,13 @@ class User extends Managed_DataObject // try the site owner. if (empty($user)) { - $user = User::siteOwner(); - } - - if (!empty($user)) { - return $user; - } else { - // TRANS: Server exception. - throw new ServerException(_('No single user defined for single-user mode.')); + try { + $user = User::siteOwner(); + return $user; + } catch (ServerException $e) { + // TRANS: Server exception. + throw new ServerException(_('No single user defined for single-user mode.')); + } } } else { // TRANS: Server exception. diff --git a/index.php b/index.php index 98ad54a1fc..2b27845e59 100644 --- a/index.php +++ b/index.php @@ -203,7 +203,7 @@ function setupRW() function isLoginAction($action) { - static $loginActions = array('login', 'recoverpassword', 'api', 'doc', 'register', 'publicxrds', 'otp', 'opensearch', 'rsd', 'hostmeta'); + static $loginActions = array('login', 'recoverpassword', 'api', 'doc', 'register', 'publicxrds', 'otp', 'opensearch', 'rsd'); $login = null; diff --git a/lib/accountmover.php b/lib/accountmover.php index 3e9228994a..429b6c0465 100644 --- a/lib/accountmover.php +++ b/lib/accountmover.php @@ -109,18 +109,11 @@ class AccountMover extends QueueHandler $svcDocUrl = null; $username = null; - foreach ($xrd->links as $link) { - if ($link['rel'] == 'http://apinamespace.org/atom' && - $link['type'] == 'application/atomsvc+xml') { - $svcDocUrl = $link['href']; - if (!empty($link['property'])) { - foreach ($link['property'] as $property) { - if ($property['type'] == 'http://apinamespace.org/atom/username') { - $username = $property['value']; - break; - } - } - } + $link = $xrd->links->get('http://apinamespace.org/atom', 'application/atomsvc+xml'); + if (!is_null($link)) { + $svcDocUrl = $link->href; + if (isset($link['http://apinamespace.org/atom/username'])) { + $username = $link['http://apinamespace.org/atom/username']; break; } } diff --git a/lib/discovery.php b/lib/discovery.php deleted file mode 100644 index 1430227c5f..0000000000 --- a/lib/discovery.php +++ /dev/null @@ -1,447 +0,0 @@ -. - * - * @category Discovery - * @package StatusNet - * @author James Walker - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -/** - * This class implements LRDD-based service discovery based on the "Hammer Draft" - * (including webfinger) - * - * @category Discovery - * @package StatusNet - * @author James Walker - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - * - * @see http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf - */ -class Discovery -{ - const LRDD_REL = 'lrdd'; - 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(); - - /** - * Constructor for a discovery object - * - * Registers different discovery methods. - * - * @return Discovery this - */ - - public function __construct() - { - $this->registerMethod('Discovery_LRDD_Host_Meta'); - $this->registerMethod('Discovery_LRDD_Link_Header'); - $this->registerMethod('Discovery_LRDD_Link_HTML'); - } - - /** - * Register a discovery class - * - * @param string $class Class name - * - * @return void - */ - 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. - * - * @param string $user_id User ID to normalize - * - * @return string normalized acct: or http(s)?: URI - */ - public static function normalize($user_id) - { - if (substr($user_id, 0, 5) == 'http:' || - substr($user_id, 0, 6) == 'https:' || - substr($user_id, 0, 5) == 'acct:') { - return $user_id; - } - - if (strpos($user_id, '@') !== false) { - return 'acct:' . $user_id; - } - - return 'http://' . $user_id; - } - - /** - * Determine if a string is a Webfinger ID - * - * Webfinger IDs look like foo@example.com or acct:foo@example.com - * - * @param string $user_id ID to check - * - * @return boolean true if $user_id is a Webfinger, else false - */ - public static function isWebfinger($user_id) - { - $uri = Discovery::normalize($user_id); - - return (substr($uri, 0, 5) == 'acct:'); - } - - /** - * Given a user ID, return the first available XRD - * - * @param string $id User ID URI - * - * @return XRD XRD object for the user - */ - public function lookup($id) - { - // Normalize the incoming $id to make sure we have a uri - $uri = $this->normalize($id); - - foreach ($this->methods as $class) { - $links = call_user_func(array($class, 'discover'), $uri); - if ($link = Discovery::getService($links, Discovery::LRDD_REL)) { - // Load the LRDD XRD - if (!empty($link['template'])) { - $xrd_uri = Discovery::applyTemplate($link['template'], $uri); - } else { - $xrd_uri = $link['href']; - } - - $xrd = $this->fetchXrd($xrd_uri); - if ($xrd) { - return $xrd; - } - } - } - - // TRANS: Exception. %s is an ID. - throw new Exception(sprintf(_('Unable to find services for %s.'), $id)); - } - - /** - * Given an array of links, returns the matching service - * - * @param array $links Links to check - * @param string $service Service to find - * - * @return array $link assoc array representing the link - */ - public static function getService($links, $service) - { - if (!is_array($links)) { - return false; - } - - foreach ($links as $link) { - if ($link['rel'] == $service) { - return $link; - } - } - } - - /** - * Apply a template using an ID - * - * Replaces {uri} in template string with the ID given. - * - * @param string $template Template to match - * @param string $id User ID to replace with - * - * @return string replaced values - */ - public static function applyTemplate($template, $id) - { - $template = str_replace('{uri}', urlencode($id), $template); - - return $template; - } - - /** - * Fetch an XRD file and parse - * - * @param string $url URL of the XRD - * - * @return XRD object representing the XRD file - */ - public static function fetchXrd($url) - { - try { - $client = new HTTPClient(); - $response = $client->get($url); - } catch (HTTP_Request2_Exception $e) { - return false; - } - - if ($response->getStatus() != 200) { - return false; - } - - return XRD::parse($response->getBody()); - } -} - -/** - * Abstract interface for discovery - * - * Objects that implement this interface can retrieve an array of - * XRD links for the URI. - * - * @category Discovery - * @package StatusNet - * @author James Walker - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ -interface Discovery_LRDD -{ - /** - * Discover interesting info about the URI - * - * @param string $uri URI to inquire about - * - * @return array Links in the XRD file - */ - public function discover($uri); -} - -/** - * Implementation of discovery using host-meta file - * - * Discovers XRD file for a user by going to the organization's - * host-meta file and trying to find a template for LRDD. - * - * @category Discovery - * @package StatusNet - * @author James Walker - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ -class Discovery_LRDD_Host_Meta implements Discovery_LRDD -{ - /** - * Discovery core method - * - * For Webfinger and HTTP URIs, fetch the host-meta file - * and look for LRDD templates - * - * @param string $uri URI to inquire about - * - * @return array Links in the XRD file - */ - public function discover($uri) - { - 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); - } - - $url = 'http://'. $domain .'/.well-known/host-meta'; - - $xrd = Discovery::fetchXrd($url); - - if ($xrd) { - return $xrd->links; - } - } -} - -/** - * Implementation of discovery using HTTP Link header - * - * Discovers XRD file for a user by fetching the URL and reading any - * Link: headers in the HTTP response. - * - * @category Discovery - * @package StatusNet - * @author James Walker - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ -class Discovery_LRDD_Link_Header implements Discovery_LRDD -{ - /** - * Discovery core method - * - * For HTTP IDs fetch the URL and look for Link headers. - * - * @param string $uri URI to inquire about - * - * @return array Links in the XRD file - * - * @todo fail out of Webfinger URIs faster - */ - public function discover($uri) - { - try { - $client = new HTTPClient(); - $response = $client->get($uri); - } catch (HTTP_Request2_Exception $e) { - return false; - } - - if ($response->getStatus() != 200) { - return false; - } - - $link_header = $response->getHeader('Link'); - if (!$link_header) { - // return false; - } - - return array(Discovery_LRDD_Link_Header::parseHeader($link_header)); - } - - /** - * Given a string or array of headers, returns XRD-like assoc array - * - * @param string|array $header string or array of strings for headers - * - * @return array Link header in XRD-like format - */ - protected static function parseHeader($header) - { - $lh = new LinkHeader($header); - - return array('href' => $lh->href, - 'rel' => $lh->rel, - 'type' => $lh->type); - } -} - -/** - * Implementation of discovery using HTML element - * - * Discovers XRD file for a user by fetching the URL and reading any - * elements in the HTML response. - * - * @category Discovery - * @package StatusNet - * @author James Walker - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ -class Discovery_LRDD_Link_HTML implements Discovery_LRDD -{ - /** - * Discovery core method - * - * For HTTP IDs, fetch the URL and look for elements - * in the HTML response. - * - * @param string $uri URI to inquire about - * - * @return array Links in XRD-ish assoc array - * - * @todo fail out of Webfinger URIs faster - */ - public function discover($uri) - { - try { - $client = new HTTPClient(); - $response = $client->get($uri); - } catch (HTTP_Request2_Exception $e) { - return false; - } - - if ($response->getStatus() != 200) { - return false; - } - - return Discovery_LRDD_Link_HTML::parse($response->getBody()); - } - - /** - * Parse HTML and return elements - * - * Given an HTML string, scans the string for elements - * - * @param string $html HTML to scan - * - * @return array array of associative arrays in XRD-ish format - */ - public function parse($html) - { - $links = array(); - - preg_match('/]*)?>(.*?)<\/head>/is', $html, $head_matches); - $head_html = $head_matches[2]; - - preg_match_all('/]*>/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/lib/httpclient.php b/lib/httpclient.php index e157951211..0dd73e422e 100644 --- a/lib/httpclient.php +++ b/lib/httpclient.php @@ -27,7 +27,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET')) { +if (!defined('GNUSOCIAL')) { exit(1); } @@ -242,7 +242,7 @@ class HTTPClient extends HTTP_Request2 } /** - * Pulls up StatusNet's customized user-agent string, so services + * Pulls up GNU Social's customized user-agent string, so services * we hit can track down the responsible software. * * @return string diff --git a/lib/invalidurlexception.php b/lib/invalidurlexception.php new file mode 100644 index 0000000000..531b79698a --- /dev/null +++ b/lib/invalidurlexception.php @@ -0,0 +1,52 @@ +. + * + * @category Exception + * @package StatusNet + * @author Mikael Nordfeldth + * @copyright 2013 Free Software Foundation, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Class for an exception when a URL is invalid + * + * @category Exception + * @package GNUSocial + * @author Mikael Nordfeldth + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class InvalidUrlException extends ServerException +{ + public $url = null; + + public function __construct($url) + { + $this->url = $url; + // We could log an entry here with the search parameters + parent::__construct(_('Invalid URL.')); + } +} diff --git a/lib/plugin.php b/lib/plugin.php index f97a07fe5a..80a3e96207 100644 --- a/lib/plugin.php +++ b/lib/plugin.php @@ -105,11 +105,15 @@ class Plugin if (preg_match('/^(\w+)(Action|Form)$/', $cls, $type)) { $type = array_map('strtolower', $type); $file = "$basedir/{$type[2]}s/{$type[1]}.php"; - } else { + } + if (!file_exists($file)) { $file = "$basedir/classes/{$cls}.php"; + // library files can be put into subdirs ('_'->'/' conversion) + // such as LRDDMethod_WebFinger -> lib/lrddmethod/webfinger.php if (!file_exists($file)) { $type = strtolower($cls); + $type = str_replace('_', '/', $type); $file = "$basedir/lib/{$type}.php"; } } diff --git a/lib/router.php b/lib/router.php index 25c436ac92..e6a45a5956 100644 --- a/lib/router.php +++ b/lib/router.php @@ -173,10 +173,6 @@ class Router $m->connect('main/xrds', array('action' => 'publicxrds')); - $m->connect('.well-known/host-meta', - array('action' => 'hostmeta')); - $m->connect('main/xrd', - array('action' => 'userxrd')); // settings diff --git a/lib/siteprofile.php b/lib/siteprofile.php index 16237f1bc3..dbb74a81b2 100644 --- a/lib/siteprofile.php +++ b/lib/siteprofile.php @@ -83,6 +83,7 @@ abstract class SiteProfileSettings 'Bookmark' => null, 'Event' => null, 'OpenID' => null, + 'LRDD' => null, 'Poll' => null, 'QnA' => null, 'SearchSub' => null, @@ -120,6 +121,7 @@ class PublicSite extends SiteProfileSettings 'ExtendedProfile' => null, 'Geonames' => null, 'OStatus' => null, + 'WebFinger' => null, )) ), 'discovery' => array('cors' => true) // Allow Cross-Origin Resource Sharing for service discovery (host-meta, XRD, etc.) @@ -208,6 +210,7 @@ class CommunitySite extends SiteProfileSettings 'Directory' => null, 'Geonames' => null, 'OStatus' => null, + 'WebFinger' => null, )) ), 'discovery' => array('cors' => true) // Allow Cross-Origin Resource Sharing for service discovery (host-meta, XRD, etc.) @@ -246,6 +249,7 @@ class SingleuserSite extends SiteProfileSettings 'OStatus' => null, 'TwitterBridge' => null, 'FacebookBridge' => null, + 'WebFinger' => null, )) ), 'discovery' => array('cors' => true) // Allow Cross-Origin Resource Sharing for service discovery (host-meta, XRD, etc.) diff --git a/lib/xrd.php b/lib/xrd.php deleted file mode 100644 index 43cb2ec73c..0000000000 --- a/lib/xrd.php +++ /dev/null @@ -1,188 +0,0 @@ -. - * - * @package StatusNet - * @author James Walker - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ -class XRD -{ - const XML_NS = 'http://www.w3.org/2000/xmlns/'; - - const XRD_NS = 'http://docs.oasis-open.org/ns/xri/xrd-1.0'; - - const HOST_META_NS = 'http://host-meta.net/xrd/1.0'; - - public $expires; - - public $subject; - - public $host; - - public $alias = array(); - - public $types = array(); - - public $links = array(); - - public static function parse($xml) - { - $xrd = new XRD(); - - $dom = new DOMDocument(); - - // Don't spew XML warnings to output - $old = error_reporting(); - error_reporting($old & ~E_WARNING); - $ok = $dom->loadXML($xml); - error_reporting($old); - - if (!$ok) { - // TRANS: Exception. - throw new Exception(_('Invalid XML.')); - } - $xrd_element = $dom->getElementsByTagName('XRD')->item(0); - if (!$xrd_element) { - // TRANS: Exception. - throw new Exception(_('Invalid XML, missing XRD root.')); - } - - // Check for host-meta host - $host = $xrd_element->getElementsByTagName('Host')->item(0); - if ($host) { - $xrd->host = $host->nodeValue; - } - - // Loop through other elements - foreach ($xrd_element->childNodes as $node) { - if (!($node instanceof DOMElement)) { - continue; - } - switch ($node->tagName) { - case 'Expires': - $xrd->expires = $node->nodeValue; - break; - case 'Subject': - $xrd->subject = $node->nodeValue; - break; - - case 'Alias': - $xrd->alias[] = $node->nodeValue; - break; - - case 'Link': - $xrd->links[] = $xrd->parseLink($node); - break; - - case 'Type': - $xrd->types[] = $xrd->parseType($node); - break; - - } - } - return $xrd; - } - - public function toXML() - { - $xs = new XMLStringer(); - - $xs->startXML(); - $xs->elementStart('XRD', array('xmlns' => XRD::XRD_NS)); - - if ($this->host) { - $xs->element('hm:Host', array('xmlns:hm' => XRD::HOST_META_NS), $this->host); - } - - if ($this->expires) { - $xs->element('Expires', null, $this->expires); - } - - if ($this->subject) { - $xs->element('Subject', null, $this->subject); - } - - foreach ($this->alias as $alias) { - $xs->element('Alias', null, $alias); - } - - foreach ($this->links as $link) { - $titles = array(); - $properties = array(); - if (isset($link['title'])) { - $titles = $link['title']; - unset($link['title']); - } - if (isset($link['property'])) { - $properties = $link['property']; - unset($link['property']); - } - $xs->elementStart('Link', $link); - foreach ($titles as $title) { - $xs->element('Title', null, $title); - } - foreach ($properties as $property) { - $xs->element('Property', - array('type' => $property['type']), - $property['value']); - } - $xs->elementEnd('Link'); - } - - $xs->elementEnd('XRD'); - - return $xs->getString(); - } - - function parseType($element) - { - return array(); - } - - function parseLink($element) - { - $link = array(); - $link['rel'] = $element->getAttribute('rel'); - $link['type'] = $element->getAttribute('type'); - $link['href'] = $element->getAttribute('href'); - $link['template'] = $element->getAttribute('template'); - foreach ($element->childNodes as $node) { - if ($node instanceof DOMElement) { - switch($node->tagName) { - case 'Title': - $link['title'][] = $node->nodeValue; - break; - case 'Property': - $link['property'][] = array('type' => $node->getAttribute('type'), - 'value' => $node->nodeValue); - break; - default: - common_log(LOG_NOTICE, "Unexpected tag name {$node->tagName} found in XRD file."); - } - } - } - - return $link; - } -} diff --git a/lib/xrdaction.php b/lib/xrdaction.php deleted file mode 100644 index 6cca80d93e..0000000000 --- a/lib/xrdaction.php +++ /dev/null @@ -1,162 +0,0 @@ -. - */ - -/** - * @package OStatusPlugin - * @maintainer James Walker - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -class XrdAction extends Action -{ - 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 $uri; - - public $user; - - public $xrd; - - function handle() - { - $nick = $this->user->nickname; - $profile = $this->user->getProfile(); - - if (empty($this->xrd)) { - $xrd = new XRD(); - } else { - $xrd = $this->xrd; - } - - if (empty($xrd->subject)) { - $xrd->subject = self::normalize($this->uri); - } - - if (Event::handle('StartXrdActionAliases', array(&$xrd, $this->user))) { - - // Possible aliases for the user - - $uris = array($this->user->uri, $profile->profileurl); - - // FIXME: Webfinger generation code should live somewhere on its own - - $path = common_config('site', 'path'); - - if (empty($path)) { - $uris[] = sprintf('acct:%s@%s', $nick, common_config('site', 'server')); - } - - foreach ($uris as $uri) { - if ($uri != $xrd->subject) { - $xrd->alias[] = $uri; - } - } - - Event::handle('EndXrdActionAliases', array(&$xrd, $this->user)); - } - - if (Event::handle('StartXrdActionLinks', array(&$xrd, $this->user))) { - - $xrd->links[] = array('rel' => self::PROFILEPAGE, - 'type' => 'text/html', - 'href' => $profile->profileurl); - - // XFN - $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', - 'type' => 'text/html', - 'href' => $profile->profileurl); - // FOAF - $xrd->links[] = array('rel' => 'describedby', - 'type' => 'application/rdf+xml', - 'href' => common_local_url('foaf', - array('nickname' => $nick))); - - $xrd->links[] = array('rel' => 'http://apinamespace.org/atom', - 'type' => 'application/atomsvc+xml', - 'href' => common_local_url('ApiAtomService', array('id' => $nick)), - 'property' => array(array('type' => 'http://apinamespace.org/atom/username', - 'value' => $nick))); - - if (common_config('site', 'fancy')) { - $apiRoot = common_path('api/', true); - } else { - $apiRoot = common_path('index.php/api/', true); - } - - $xrd->links[] = array('rel' => 'http://apinamespace.org/twitter', - 'href' => $apiRoot, - 'property' => array(array('type' => 'http://apinamespace.org/twitter/username', - 'value' => $nick))); - - Event::handle('EndXrdActionLinks', array(&$xrd, $this->user)); - } - - if (common_config('discovery', 'cors')) { - header('Access-Control-Allow-Origin: *'); - } - - header('Content-type: application/xrd+xml'); - - print $xrd->toXML(); - } - - /** - * Given a "user id" make sure it's normalized to either a webfinger - * acct: uri or a profile HTTP URL. - */ - - public static function normalize($user_id) - { - if (substr($user_id, 0, 5) == 'http:' || - substr($user_id, 0, 6) == 'https:' || - substr($user_id, 0, 5) == 'acct:') { - return $user_id; - } - - if (strpos($user_id, '@') !== FALSE) { - return 'acct:' . $user_id; - } - - return 'http://' . $user_id; - } - - public static function isWebfinger($user_id) - { - $uri = self::normalize($user_id); - - return (substr($uri, 0, 5) == 'acct:'); - } - - /** - * Is this action read-only? - * - * @param array $args other arguments - * - * @return boolean is read only action? - */ - function isReadOnly($args) - { - return true; - } -} diff --git a/plugins/AccountManager/AccountManagerPlugin.php b/plugins/AccountManager/AccountManagerPlugin.php index a4ddc747ca..3cf4bf440b 100644 --- a/plugins/AccountManager/AccountManagerPlugin.php +++ b/plugins/AccountManager/AccountManagerPlugin.php @@ -57,8 +57,8 @@ class AccountManagerPlugin extends Plugin } function onStartHostMetaLinks(&$links) { - $links[] = array('rel' => AccountManagerPlugin::AM_REL, - 'href' => common_local_url('AccountManagementControlDocument')); + $links[] = new XML_XRD_Element_Link(AccountManagerPlugin::AM_REL, + common_local_url('AccountManagementControlDocument')); } function onStartShowHTML($action) diff --git a/plugins/LRDD/EVENTS.txt b/plugins/LRDD/EVENTS.txt new file mode 100644 index 0000000000..f959808bf6 --- /dev/null +++ b/plugins/LRDD/EVENTS.txt @@ -0,0 +1,6 @@ +StartDiscoveryMethodRegistration +- $disco: Discovery object that accepts the registrations + +EndDiscoveryMethodRegistration: Register remote URI discovery methods +- $disco: Discovery object that accepts the registrations + diff --git a/plugins/LRDD/LRDDPlugin.php b/plugins/LRDD/LRDDPlugin.php new file mode 100644 index 0000000000..afe119c648 --- /dev/null +++ b/plugins/LRDD/LRDDPlugin.php @@ -0,0 +1,65 @@ +. + */ + +/** + * Implements Link-based Resource Descriptor Discovery based on RFC6415, + * Web Host Metadata, i.e. the predecessor to WebFinger resource discovery. + * + * @package GNUSocial + * @author Mikael Nordfeldth + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__ . '/extlib/'); + +class LRDDPlugin extends Plugin +{ + public function onAutoload($cls) + { + switch ($cls) { + case 'XML_XRD': + require_once __DIR__ . '/extlib/XML/XRD.php'; + return false; + } + + return parent::onAutoload($cls); + } + public function onStartDiscoveryMethodRegistration(Discovery $disco) { + $disco->registerMethod('LRDDMethod_WebFinger'); + } + + public function onEndDiscoveryMethodRegistration(Discovery $disco) { + $disco->registerMethod('LRDDMethod_HostMeta'); + $disco->registerMethod('LRDDMethod_LinkHeader'); + $disco->registerMethod('LRDDMethod_LinkHTML'); + } + + public function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'LRDD', + 'version' => STATUSNET_VERSION, + 'author' => 'Mikael Nordfeldth', + 'homepage' => 'http://www.gnu.org/software/social/', + // TRANS: Plugin description. + 'rawdescription' => _m('Implements LRDD support for GNU Social.')); + + return true; + } +} diff --git a/plugins/LRDD/extlib/XML/XRD.php b/plugins/LRDD/extlib/XML/XRD.php new file mode 100644 index 0000000000..207d0ae3c5 --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD.php @@ -0,0 +1,258 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +require_once 'XML/XRD/PropertyAccess.php'; +require_once 'XML/XRD/Element/Link.php'; +require_once 'XML/XRD/Loader.php'; +require_once 'XML/XRD/Serializer.php'; + +/** + * Main class used to load XRD documents from string or file. + * + * After loading the file, access to links is possible with get() and getAll(), + * as well as foreach-iterating over the XML_XRD object. + * + * Property access is possible with getProperties() and array access (foreach) + * on the XML_XRD object. + * + * Verification that the subject/aliases match the requested URL can be done with + * describes(). + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD extends XML_XRD_PropertyAccess implements IteratorAggregate +{ + /** + * XRD file/string loading dispatcher + * + * @var XML_XRD_Loader + */ + public $loader; + + /** + * XRD serializing dispatcher + * + * @var XML_XRD_Serializer + */ + public $serializer; + + /** + * XRD subject + * + * @var string + */ + public $subject; + + /** + * Array of subject alias strings + * + * @var array + */ + public $aliases = array(); + + /** + * Array of link objects + * + * @var array + */ + public $links = array(); + + /** + * Unix timestamp when the document expires. + * NULL when no expiry date set. + * + * @var integer|null + */ + public $expires; + + /** + * xml:id of the XRD document + * + * @var string|null + */ + public $id; + + + + /** + * Loads the contents of the given file. + * + * Note: Only use file type auto-detection for local files. + * Do not use it on remote files as the file gets requested several times. + * + * @param string $file Path to an XRD file + * @param string $type File type: xml or json, NULL for auto-detection + * + * @return void + * + * @throws XML_XRD_Loader_Exception When the file is invalid or cannot be + * loaded + */ + public function loadFile($file, $type = null) + { + if (!isset($this->loader)) { + $this->loader = new XML_XRD_Loader($this); + } + return $this->loader->loadFile($file, $type); + } + + /** + * Loads the contents of the given string + * + * @param string $str XRD string + * @param string $type File type: xml or json, NULL for auto-detection + * + * @return void + * + * @throws XML_XRD_Loader_Exception When the string is invalid or cannot be + * loaded + */ + public function loadString($str, $type = null) + { + if (!isset($this->loader)) { + $this->loader = new XML_XRD_Loader($this); + } + return $this->loader->loadString($str, $type); + } + + /** + * Checks if the XRD document describes the given URI. + * + * This should always be used to make sure the XRD file + * is the correct one for e.g. the given host, and not a copycat. + * + * Checks against the subject and aliases + * + * @param string $uri An URI that the document is expected to describe + * + * @return boolean True or false + */ + public function describes($uri) + { + if ($this->subject == $uri) { + return true; + } + foreach ($this->aliases as $alias) { + if ($alias == $uri) { + return true; + } + } + + return false; + } + + /** + * Get the link with highest priority for the given relation and type. + * + * @param string $rel Relation name + * @param string $type MIME Type + * @param boolean $typeFallback When true and no link with the given type + * could be found, the best link without a + * type will be returned + * + * @return XML_XRD_Element_Link Link object or NULL if none found + */ + public function get($rel, $type = null, $typeFallback = true) + { + $links = $this->getAll($rel, $type, $typeFallback); + if (count($links) == 0) { + return null; + } + + return $links[0]; + } + + + /** + * Get all links with the given relation and type, highest priority first. + * + * @param string $rel Relation name + * @param string $type MIME Type + * @param boolean $typeFallback When true and no link with the given type + * could be found, the best link without a + * type will be returned + * + * @return array Array of XML_XRD_Element_Link objects + */ + public function getAll($rel, $type = null, $typeFallback = true) + { + $links = array(); + $exactType = false; + foreach ($this->links as $link) { + if ($link->rel == $rel + && ($type === null || $link->type == $type + || $typeFallback && $link->type === null) + ) { + $links[] = $link; + $exactType |= $typeFallback && $type !== null + && $link->type == $type; + } + } + if ($exactType) { + //remove all links without type + $exactlinks = array(); + foreach ($links as $link) { + if ($link->type !== null) { + $exactlinks[] = $link; + } + } + $links = $exactlinks; + } + return $links; + } + + /** + * Return the iterator object to loop over the links + * + * Part of the IteratorAggregate interface + * + * @return Traversable Iterator for the links + */ + public function getIterator() + { + return new ArrayIterator($this->links); + } + + /** + * Converts this XRD object to XML or JSON. + * + * @param string $type Serialization type: xml or json + * + * @return string Generated content + */ + public function to($type) + { + if (!isset($this->serializer)) { + $this->serializer = new XML_XRD_Serializer($this); + } + return $this->serializer->to($type); + } + + /** + * Converts this XRD object to XML. + * + * @return string Generated XML + * + * @deprecated use to('xml') + */ + public function toXML() + { + return $this->to('xml'); + } +} +?> \ No newline at end of file diff --git a/plugins/LRDD/extlib/XML/XRD/Element/Link.php b/plugins/LRDD/extlib/XML/XRD/Element/Link.php new file mode 100644 index 0000000000..31b59b0c2e --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Element/Link.php @@ -0,0 +1,120 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +require_once 'XML/XRD/PropertyAccess.php'; + +/** + * Link element in a XRD file. Attribute access via object properties. + * + * Retrieving the title of a link is possible with the getTitle() convenience + * method. + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD_Element_Link extends XML_XRD_PropertyAccess +{ + /** + * Link relation + * + * @var string + */ + public $rel; + + /** + * Link type (MIME type) + * + * @var string + */ + public $type; + + /** + * Link URL + * + * @var string + */ + public $href; + + /** + * Link URL template + * + * @var string + */ + public $template; + + /** + * Array of key-value pairs: Key is the language, value the title + * + * @var array + */ + public $titles = array(); + + + + /** + * Create a new instance and load data from the XML element + * + * @param string $relOrXml string with the relation name/URL + * @param string $href HREF value + * @param string $type Type value + * @param boolean $isTemplate When set to true, the $href is + * used as template + */ + public function __construct( + $rel = null, $href = null, $type = null, $isTemplate = false + ) { + $this->rel = $rel; + if ($isTemplate) { + $this->template = $href; + } else { + $this->href = $href; + } + $this->type = $type; + } + + /** + * Returns the title of the link in the given language. + * If the language is not available, the first title without the language + * is returned. If no such one exists, the first title is returned. + * + * @param string $lang 2-letter language name + * + * @return string|null Link title + */ + public function getTitle($lang = null) + { + if (count($this->titles) == 0) { + return null; + } + + if ($lang == null) { + return reset($this->titles); + } + + if (isset($this->titles[$lang])) { + return $this->titles[$lang]; + } + if (isset($this->titles[''])) { + return $this->titles['']; + } + + //return first + return reset($this->titles); + } +} + +?> \ No newline at end of file diff --git a/plugins/LRDD/extlib/XML/XRD/Element/Property.php b/plugins/LRDD/extlib/XML/XRD/Element/Property.php new file mode 100644 index 0000000000..b82265bab7 --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Element/Property.php @@ -0,0 +1,55 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +/** + * Property element in a XRD document. + * + * The root element as well as tags may have children. + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD_Element_Property +{ + /** + * Value of the property. + * + * @var string|null + */ + public $value; + + /** + * Type of the propery. + * + * @var string + */ + public $type; + + /** + * Create a new instance + * + * @param string $type String representing the property type + * @param string $value Value of the property, may be NULL + */ + public function __construct($type = null, $value = null) + { + $this->type = $type; + $this->value = $value; + } +} + +?> diff --git a/plugins/LRDD/extlib/XML/XRD/Exception.php b/plugins/LRDD/extlib/XML/XRD/Exception.php new file mode 100644 index 0000000000..766ae63653 --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Exception.php @@ -0,0 +1,30 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +/** + * Base exception interface for all XML_XRD related exceptions. + * With that interface, it is possible to catch all XML_XRD exceptions + * with a single catch statement. + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +interface XML_XRD_Exception +{ +} + +?> \ No newline at end of file diff --git a/plugins/LRDD/extlib/XML/XRD/Loader.php b/plugins/LRDD/extlib/XML/XRD/Loader.php new file mode 100644 index 0000000000..7d497ad028 --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Loader.php @@ -0,0 +1,156 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +require_once 'XML/XRD/Loader/Exception.php'; + +/** + * File/string loading dispatcher. + * Loads the correct loader for the type of XRD file (XML or JSON). + * Also provides type auto-detection. + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD_Loader +{ + public function __construct(XML_XRD $xrd) + { + $this->xrd = $xrd; + } + + /** + * Loads the contents of the given file. + * + * Note: Only use file type auto-detection for local files. + * Do not use it on remote files as the file gets requested several times. + * + * @param string $file Path to an XRD file + * @param string $type File type: xml or json, NULL for auto-detection + * + * @return void + * + * @throws XML_XRD_Loader_Exception When the file is invalid or cannot be + * loaded + */ + public function loadFile($file, $type = null) + { + if ($type === null) { + $type = $this->detectTypeFromFile($file); + } + $loader = $this->getLoader($type); + $loader->loadFile($file); + } + + /** + * Loads the contents of the given string + * + * @param string $str XRD string + * @param string $type File type: xml or json, NULL for auto-detection + * + * @return void + * + * @throws XML_XRD_Loader_Exception When the string is invalid or cannot be + * loaded + */ + public function loadString($str, $type = null) + { + if ($type === null) { + $type = $this->detectTypeFromString($str); + } + $loader = $this->getLoader($type); + $loader->loadString($str); + } + + /** + * Creates a XRD loader object for the given type + * + * @param string $type File type: xml or json + * + * @return XML_XRD_Loader + */ + protected function getLoader($type) + { + $class = 'XML_XRD_Loader_' . strtoupper($type); + $file = str_replace('_', '/', $class) . '.php'; + include_once $file; + if (class_exists($class)) { + return new $class($this->xrd); + } + + throw new XML_XRD_Loader_Exception( + 'No loader for XRD type "' . $type . '"', + XML_XRD_Loader_Exception::NO_LOADER + ); + } + + /** + * Tries to detect the file type (xml or json) from the file content + * + * @param string $file File name to check + * + * @return string File type ('xml' or 'json') + * + * @throws XML_XRD_Loader_Exception When opening the file fails. + */ + public function detectTypeFromFile($file) + { + if (!file_exists($file)) { + throw new XML_XRD_Loader_Exception( + 'Error loading XRD file: File does not exist', + XML_XRD_Loader_Exception::OPEN_FILE + ); + } + $handle = fopen($file, 'r'); + if (!$handle) { + throw new XML_XRD_Loader_Exception( + 'Cannot open file to determine type', + XML_XRD_Loader_Exception::OPEN_FILE + ); + } + + $str = (string)fgets($handle, 10); + fclose($handle); + return $this->detectTypeFromString($str); + } + + /** + * Tries to detect the file type from the content of the file + * + * @param string $str Content of XRD file + * + * @return string File type ('xml' or 'json') + * + * @throws XML_XRD_Loader_Exception When the type cannot be detected + */ + public function detectTypeFromString($str) + { + if (substr($str, 0, 1) == '{') { + return 'json'; + } else if (substr($str, 0, 5) == ' diff --git a/plugins/LRDD/extlib/XML/XRD/Loader/Exception.php b/plugins/LRDD/extlib/XML/XRD/Loader/Exception.php new file mode 100644 index 0000000000..43b6bc8362 --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Loader/Exception.php @@ -0,0 +1,59 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +require_once 'XML/XRD/Exception.php'; + +/** + * XML_XRD exception that's thrown when loading the XRD fails. + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD_Loader_Exception extends Exception implements XML_XRD_Exception +{ + /** + * The document namespace is not the XRD 1.0 namespace + */ + const DOC_NS = 10; + + /** + * The document root element is not XRD + */ + const DOC_ROOT = 11; + + /** + * Error loading the XML|JSON file|string + */ + const LOAD = 12; + + /** + * Unsupported XRD file/string type (no loader) + */ + const NO_LOADER = 13; + + /** + * Error opening file + */ + const OPEN_FILE = 14; + + /** + * Detecting the file type failed + */ + const DETECT_TYPE = 20; +} + +?> \ No newline at end of file diff --git a/plugins/LRDD/extlib/XML/XRD/Loader/JSON.php b/plugins/LRDD/extlib/XML/XRD/Loader/JSON.php new file mode 100644 index 0000000000..48ed8cae7b --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Loader/JSON.php @@ -0,0 +1,187 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +/** + * Loads XRD data from a JSON file + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD_Loader_JSON +{ + /** + * Data storage the XML data get loaded into + * + * @var XML_XRD + */ + protected $xrd; + + + + /** + * Init object with xrd object + * + * @param XML_XRD $xrd Data storage the JSON data get loaded into + */ + public function __construct(XML_XRD $xrd) + { + $this->xrd = $xrd; + } + + /** + * Loads the contents of the given file + * + * @param string $file Path to an JRD file + * + * @return void + * + * @throws XML_XRD_Loader_Exception When the JSON is invalid or cannot be + * loaded + */ + public function loadFile($file) + { + $json = file_get_contents($file); + if ($json === false) { + throw new XML_XRD_Loader_Exception( + 'Error loading JRD file: ' . $file, + XML_XRD_Loader_Exception::LOAD + ); + } + return $this->loadString($json); + } + + /** + * Loads the contents of the given string + * + * @param string $json JSON string + * + * @return void + * + * @throws XML_XRD_Loader_Exception When the JSON is invalid or cannot be + * loaded + */ + public function loadString($json) + { + if ($json == '') { + throw new XML_XRD_Loader_Exception( + 'Error loading JRD: string empty', + XML_XRD_Loader_Exception::LOAD + ); + } + + $obj = json_decode($json); + if ($obj !== null) { + return $this->load($obj); + } + + $constants = get_defined_constants(true); + $json_errors = array(); + foreach ($constants['json'] as $name => $value) { + if (!strncmp($name, 'JSON_ERROR_', 11)) { + $json_errors[$value] = $name; + } + } + throw new XML_XRD_Loader_Exception( + 'Error loading JRD: ' . $json_errors[json_last_error()], + XML_XRD_Loader_Exception::LOAD + ); + } + + /** + * Loads the JSON object into the classes' data structures + * + * @param object $j JSON object containing the whole JSON document + * + * @return void + */ + public function load(stdClass $j) + { + if (isset($j->subject)) { + $this->xrd->subject = (string)$j->subject; + } + if (isset($j->aliases)) { + foreach ($j->aliases as $jAlias) { + $this->xrd->aliases[] = (string)$jAlias; + } + } + + if (isset($j->links)) { + foreach ($j->links as $jLink) { + $this->xrd->links[] = $this->loadLink($jLink); + } + } + + $this->loadProperties($this->xrd, $j); + + if (isset($j->expires)) { + $this->xrd->expires = strtotime($j->expires); + } + } + + /** + * Loads the Property elements from XML + * + * @param object $store Data store where the properties get stored + * @param object $j JSON element with "properties" variable + * + * @return boolean True when all went well + */ + protected function loadProperties( + XML_XRD_PropertyAccess $store, stdClass $j + ) { + if (!isset($j->properties)) { + return true; + } + + foreach ($j->properties as $type => $jProp) { + $store->properties[] = new XML_XRD_Element_Property( + $type, (string)$jProp + ); + } + + return true; + } + + /** + * Create a link element object from XML element + * + * @param object $j JSON link object + * + * @return XML_XRD_Element_Link Created link object + */ + protected function loadLink(stdClass $j) + { + $link = new XML_XRD_Element_Link(); + foreach (array('rel', 'type', 'href', 'template') as $var) { + if (isset($j->$var)) { + $link->$var = (string)$j->$var; + } + } + + if (isset($j->titles)) { + foreach ($j->titles as $lang => $jTitle) { + if (!isset($link->titles[$lang])) { + $link->titles[$lang] = (string)$jTitle; + } + } + } + $this->loadProperties($link, $j); + + return $link; + } +} +?> diff --git a/plugins/LRDD/extlib/XML/XRD/Loader/XML.php b/plugins/LRDD/extlib/XML/XRD/Loader/XML.php new file mode 100644 index 0000000000..30b3255bab --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Loader/XML.php @@ -0,0 +1,218 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +/** + * Loads XRD data from an XML file + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD_Loader_XML +{ + /** + * Data storage the XML data get loaded into + * + * @var XML_XRD + */ + protected $xrd; + + /** + * XRD 1.0 namespace + */ + const NS_XRD = 'http://docs.oasis-open.org/ns/xri/xrd-1.0'; + + + + /** + * Init object with xrd object + * + * @param XML_XRD $xrd Data storage the XML data get loaded into + */ + public function __construct(XML_XRD $xrd) + { + $this->xrd = $xrd; + } + + /** + * Loads the contents of the given file + * + * @param string $file Path to an XRD file + * + * @return void + * + * @throws XML_XRD_Loader_Exception When the XML is invalid or cannot be + * loaded + */ + public function loadFile($file) + { + $old = libxml_use_internal_errors(true); + $x = simplexml_load_file($file); + libxml_use_internal_errors($old); + if ($x === false) { + throw new XML_XRD_Loader_Exception( + 'Error loading XML file: ' . libxml_get_last_error()->message, + XML_XRD_Loader_Exception::LOAD + ); + } + return $this->load($x); + } + + /** + * Loads the contents of the given string + * + * @param string $xml XML string + * + * @return void + * + * @throws XML_XRD_Loader_Exception When the XML is invalid or cannot be + * loaded + */ + public function loadString($xml) + { + if ($xml == '') { + throw new XML_XRD_Loader_Exception( + 'Error loading XML string: string empty', + XML_XRD_Loader_Exception::LOAD + ); + } + $old = libxml_use_internal_errors(true); + $x = simplexml_load_string($xml); + libxml_use_internal_errors($old); + if ($x === false) { + throw new XML_XRD_Loader_Exception( + 'Error loading XML string: ' . libxml_get_last_error()->message, + XML_XRD_Loader_Exception::LOAD + ); + } + return $this->load($x); + } + + /** + * Loads the XML element into the classes' data structures + * + * @param object $x XML element containing the whole XRD document + * + * @return void + * + * @throws XML_XRD_Loader_Exception When the XML is invalid + */ + public function load(SimpleXMLElement $x) + { + $ns = $x->getDocNamespaces(); + if ($ns[''] !== self::NS_XRD) { + throw new XML_XRD_Loader_Exception( + 'Wrong document namespace', XML_XRD_Loader_Exception::DOC_NS + ); + } + if ($x->getName() != 'XRD') { + throw new XML_XRD_Loader_Exception( + 'XML root element is not "XRD"', XML_XRD_Loader_Exception::DOC_ROOT + ); + } + + if (isset($x->Subject)) { + $this->xrd->subject = (string)$x->Subject; + } + foreach ($x->Alias as $xAlias) { + $this->xrd->aliases[] = (string)$xAlias; + } + + foreach ($x->Link as $xLink) { + $this->xrd->links[] = $this->loadLink($xLink); + } + + $this->loadProperties($this->xrd, $x); + + if (isset($x->Expires)) { + $this->xrd->expires = strtotime($x->Expires); + } + + $xmlAttrs = $x->attributes('http://www.w3.org/XML/1998/namespace'); + if (isset($xmlAttrs['id'])) { + $this->xrd->id = (string)$xmlAttrs['id']; + } + } + + /** + * Loads the Property elements from XML + * + * @param object $store Data store where the properties get stored + * @param object $x XML element + * + * @return boolean True when all went well + */ + protected function loadProperties( + XML_XRD_PropertyAccess $store, SimpleXMLElement $x + ) { + foreach ($x->Property as $xProp) { + $store->properties[] = $this->loadProperty($xProp); + } + } + + /** + * Create a link element object from XML element + * + * @param object $x XML link element + * + * @return XML_XRD_Element_Link Created link object + */ + protected function loadLink(SimpleXMLElement $x) + { + $link = new XML_XRD_Element_Link(); + foreach (array('rel', 'type', 'href', 'template') as $var) { + if (isset($x[$var])) { + $link->$var = (string)$x[$var]; + } + } + + foreach ($x->Title as $xTitle) { + $xmlAttrs = $xTitle->attributes('http://www.w3.org/XML/1998/namespace'); + $lang = ''; + if (isset($xmlAttrs['lang'])) { + $lang = (string)$xmlAttrs['lang']; + } + if (!isset($link->titles[$lang])) { + $link->titles[$lang] = (string)$xTitle; + } + } + $this->loadProperties($link, $x); + + return $link; + } + + /** + * Create a property element object from XML element + * + * @param object $x XML property element + * + * @return XML_XRD_Element_Property Created link object + */ + protected function loadProperty(SimpleXMLElement $x) + { + $prop = new XML_XRD_Element_Property(); + if (isset($x['type'])) { + $prop->type = (string)$x['type']; + } + $s = (string)$x; + if ($s != '') { + $prop->value = $s; + } + + return $prop; + } +} +?> diff --git a/plugins/LRDD/extlib/XML/XRD/LogicException.php b/plugins/LRDD/extlib/XML/XRD/LogicException.php new file mode 100644 index 0000000000..6729b70d3a --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/LogicException.php @@ -0,0 +1,30 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +require_once 'XML/XRD/Exception.php'; + +/** + * XML_XRD exception that's thrown when something is not supported + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD_LogicException extends LogicException implements XML_XRD_Exception +{ +} + +?> \ No newline at end of file diff --git a/plugins/LRDD/extlib/XML/XRD/PropertyAccess.php b/plugins/LRDD/extlib/XML/XRD/PropertyAccess.php new file mode 100644 index 0000000000..ccfea7f09f --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/PropertyAccess.php @@ -0,0 +1,133 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +require_once 'XML/XRD/LogicException.php'; +require_once 'XML/XRD/Element/Property.php'; + +/** + * Provides ArrayAccess to extending classes (XML_XRD and XML_XRD_Element_Link). + * + * By extending PropertyAccess, access to properties is possible with + * "$object['propertyType']" array access notation. + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +abstract class XML_XRD_PropertyAccess implements ArrayAccess +{ + + /** + * Array of property objects + * + * @var array + */ + public $properties = array(); + + + /** + * Check if the property with the given type exists + * + * Part of the ArrayAccess interface + * + * @param string $type Property type to check for + * + * @return boolean True if it exists + */ + public function offsetExists($type) + { + foreach ($this->properties as $prop) { + if ($prop->type == $type) { + return true; + } + } + return false; + } + + /** + * Return the highest ranked property with the given type + * + * Part of the ArrayAccess interface + * + * @param string $type Property type to check for + * + * @return string Property value or NULL if empty + */ + public function offsetGet($type) + { + foreach ($this->properties as $prop) { + if ($prop->type == $type) { + return $prop->value; + } + } + return null; + } + + /** + * Not implemented. + * + * Part of the ArrayAccess interface + * + * @param string $type Property type to check for + * @param string $value New property value + * + * @return void + * + * @throws XML_XRD_LogicException Always + */ + public function offsetSet($type, $value) + { + throw new XML_XRD_LogicException('Changing properties not implemented'); + } + + /** + * Not implemented. + * + * Part of the ArrayAccess interface + * + * @param string $type Property type to check for + * + * @return void + * + * @throws XML_XRD_LogicException Always + */ + public function offsetUnset($type) + { + throw new XML_XRD_LogicException('Changing properties not implemented'); + } + + /** + * Get all properties with the given type + * + * @param string $type Property type to filter by + * + * @return array Array of XML_XRD_Element_Property objects + */ + public function getProperties($type = null) + { + if ($type === null) { + return $this->properties; + } + $properties = array(); + foreach ($this->properties as $prop) { + if ($prop->type == $type) { + $properties[] = $prop; + } + } + return $properties; + } +} +?> \ No newline at end of file diff --git a/plugins/LRDD/extlib/XML/XRD/Serializer.php b/plugins/LRDD/extlib/XML/XRD/Serializer.php new file mode 100644 index 0000000000..ebabcdfc71 --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Serializer.php @@ -0,0 +1,79 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +require_once 'XML/XRD/Serializer/Exception.php'; + +/** + * Serialization dispatcher - loads the correct serializer for saving XRD data. + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD_Serializer +{ + /** + * XRD data storage + * + * @var XML_XRD + */ + protected $xrd; + + /** + * Init object with xrd object + * + * @param XML_XRD $xrd Data storage the data are fetched from + */ + public function __construct(XML_XRD $xrd) + { + $this->xrd = $xrd; + } + + /** + * Convert the XRD data into a string of the given type + * + * @param string $type File type: xml or json + * + * @return string Serialized data + */ + public function to($type) + { + return (string)$this->getSerializer($type); + } + + /** + * Creates a XRD loader object for the given type + * + * @param string $type File type: xml or json + * + * @return XML_XRD_Loader + */ + protected function getSerializer($type) + { + $class = 'XML_XRD_Serializer_' . strtoupper($type); + $file = str_replace('_', '/', $class) . '.php'; + include_once $file; + if (class_exists($class)) { + return new $class($this->xrd); + } + + throw new XML_XRD_Serializer_Exception( + 'No serializer for type "' . $type . '"', + XML_XRD_Loader_Exception::NO_LOADER + ); + } +} +?> diff --git a/plugins/LRDD/extlib/XML/XRD/Serializer/Exception.php b/plugins/LRDD/extlib/XML/XRD/Serializer/Exception.php new file mode 100644 index 0000000000..fadbee7df2 --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Serializer/Exception.php @@ -0,0 +1,29 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +require_once 'XML/XRD/Exception.php'; + +/** + * XML_XRD exception that's thrown when saving an XRD file fails. + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD_Serializer_Exception extends Exception implements XML_XRD_Exception +{ +} +?> diff --git a/plugins/LRDD/extlib/XML/XRD/Serializer/JSON.php b/plugins/LRDD/extlib/XML/XRD/Serializer/JSON.php new file mode 100644 index 0000000000..3dfe7d5f4e --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Serializer/JSON.php @@ -0,0 +1,94 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +/** + * Generate JSON from a XML_XRD object (for JRD files). + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + * @link http://tools.ietf.org/html/rfc6415#appendix-A + */ +class XML_XRD_Serializer_JSON +{ + protected $xrd; + + /** + * Create new instance + * + * @param XML_XRD $xrd XRD instance to convert to JSON + */ + public function __construct(XML_XRD $xrd) + { + $this->xrd = $xrd; + } + + /** + * Generate JSON. + * + * @return string JSON code + */ + public function __toString() + { + $o = new stdClass(); + if ($this->xrd->expires !== null) { + $o->expires = gmdate('Y-m-d\TH:i:s\Z', $this->xrd->expires); + } + if ($this->xrd->subject !== null) { + $o->subject = $this->xrd->subject; + } + foreach ($this->xrd->aliases as $alias) { + $o->aliases[] = $alias; + } + foreach ($this->xrd->properties as $property) { + $o->properties[$property->type] = $property->value; + } + $o->links = array(); + foreach ($this->xrd->links as $link) { + $lid = count($o->links); + $o->links[$lid] = new stdClass(); + if ($link->rel) { + $o->links[$lid]->rel = $link->rel; + } + if ($link->type) { + $o->links[$lid]->type = $link->type; + } + if ($link->href) { + $o->links[$lid]->href = $link->href; + } + if ($link->template !== null && $link->href === null) { + $o->links[$lid]->template = $link->template; + } + + foreach ($link->titles as $lang => $value) { + if ($lang == null) { + $lang = 'default'; + } + $o->links[$lid]->titles[$lang] = $value; + } + foreach ($link->properties as $property) { + $o->links[$lid]->properties[$property->type] = $property->value; + } + } + if (count($o->links) == 0) { + unset($o->links); + } + + return json_encode($o); + } +} + +?> diff --git a/plugins/LRDD/extlib/XML/XRD/Serializer/XML.php b/plugins/LRDD/extlib/XML/XRD/Serializer/XML.php new file mode 100644 index 0000000000..c8cfa61912 --- /dev/null +++ b/plugins/LRDD/extlib/XML/XRD/Serializer/XML.php @@ -0,0 +1,137 @@ + + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @link http://pear.php.net/package/XML_XRD + */ + +/** + * Generate XML from a XML_XRD object. + * + * @category XML + * @package XML_XRD + * @author Christian Weiske + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version Release: @package_version@ + * @link http://pear.php.net/package/XML_XRD + */ +class XML_XRD_Serializer_XML +{ + protected $xrd; + + /** + * Create new instance + * + * @param XML_XRD $xrd XRD instance to convert to XML + */ + public function __construct(XML_XRD $xrd) + { + $this->xrd = $xrd; + } + + /** + * Generate XML. + * + * @return string Full XML code + */ + public function __toString() + { + $hasXsi = false; + $x = new XMLWriter(); + $x->openMemory(); + //no encoding means UTF-8 + //http://www.w3.org/TR/2008/REC-xml-20081126/#sec-guessing-no-ext-info + $x->startDocument('1.0', 'UTF-8'); + $x->setIndent(true); + $x->startElement('XRD'); + $x->writeAttribute('xmlns', 'http://docs.oasis-open.org/ns/xri/xrd-1.0'); + $x->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + if ($this->xrd->id) { + $x->writeAttribute('xml:id', $this->xrd->id); + } + + if ($this->xrd->expires !== null) { + $x->writeElement( + 'Expires', gmdate('Y-m-d\TH:i:s\Z', $this->xrd->expires) + ); + } + if ($this->xrd->subject !== null) { + $x->writeElement('Subject', $this->xrd->subject); + } + foreach ($this->xrd->aliases as $alias) { + $x->writeElement('Alias', $alias); + } + foreach ($this->xrd->properties as $property) { + $this->writeProperty($x, $property, $hasXsi); + } + + foreach ($this->xrd->links as $link) { + $x->startElement('Link'); + $x->writeAttribute('rel', $link->rel); + if ($link->type !== null) { + $x->writeAttribute('type', $link->type); + } + if ($link->href !== null) { + $x->writeAttribute('href', $link->href); + } + //template only when no href + if ($link->template !== null && $link->href === null) { + $x->writeAttribute('template', $link->template); + } + + foreach ($link->titles as $lang => $value) { + $x->startElement('Title'); + if ($lang) { + $x->writeAttribute('xml:lang', $lang); + } + $x->text($value); + $x->endElement(); + } + foreach ($link->properties as $property) { + $this->writeProperty($x, $property, $hasXsi); + } + $x->endElement(); + } + + $x->endElement(); + $x->endDocument(); + $s = $x->flush(); + if (!$hasXsi) { + $s = str_replace( + ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"', '', $s + ); + } + return $s; + } + + /** + * Write a property in the XMLWriter stream output + * + * @param XMLWriter $x Writer object to write to + * @param XML_XRD_Element_Property $property Property to write + * @param boolean &$hasXsi If an xsi: attribute is used + * + * @return void + */ + protected function writeProperty( + XMLWriter $x, XML_XRD_Element_Property $property, &$hasXsi + ) { + $x->startElement('Property'); + $x->writeAttribute('type', $property->type); + if ($property->value === null) { + $x->writeAttribute('xsi:nil', 'true'); + $hasXsi = true; + } else { + $x->text($property->value); + } + $x->endElement(); + } +} + +?> \ No newline at end of file diff --git a/plugins/LRDD/lib/discovery.php b/plugins/LRDD/lib/discovery.php new file mode 100644 index 0000000000..f9b8d2930f --- /dev/null +++ b/plugins/LRDD/lib/discovery.php @@ -0,0 +1,202 @@ +. + * + * @category Discovery + * @package GNUSocial + * @author James Walker + * @author Mikael Nordfeldth + * @copyright 2010 StatusNet, Inc. + * @copyright 2013 Free Software Foundation, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://www.gnu.org/software/social/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +class Discovery +{ + const LRDD_REL = 'lrdd'; + const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from'; + const HCARD = 'http://microformats.org/profile/hcard'; + + const JRD_MIMETYPE_OLD = 'application/json'; // RFC6415 uses this + const JRD_MIMETYPE = 'application/jrd+json'; + const XRD_MIMETYPE = 'application/xrd+xml'; + + public $methods = array(); + + /** + * Constructor for a discovery object + * + * Registers different discovery methods. + * + * @return Discovery this + */ + + public function __construct() + { + if (Event::handle('StartDiscoveryMethodRegistration', array($this))) { + Event::handle('EndDiscoveryMethodRegistration', array($this)); + } + } + + public static function supportedMimeTypes() + { + return array('json'=>self::JRD_MIMETYPE, + 'jsonold'=>self::JRD_MIMETYPE_OLD, + 'xml'=>self::XRD_MIMETYPE); + } + + /** + * Register a discovery class + * + * @param string $class Class name + * + * @return void + */ + public function registerMethod($class) + { + $this->methods[] = $class; + } + + /** + * Given a user ID, return the first available resource descriptor + * + * @param string $id User ID URI + * + * @return XML_XRD object for the resource descriptor of the id + */ + public function lookup($id) + { + // Normalize the incoming $id to make sure we have a uri + $uri = self::normalize($id); + + foreach ($this->methods as $class) { + try { + $xrd = new XML_XRD(); + + common_debug("LRDD discovery method for '$uri': {$class}"); + $lrdd = new $class; + $links = call_user_func(array($lrdd, 'discover'), $uri); + $link = Discovery::getService($links, Discovery::LRDD_REL); + + // Load the LRDD XRD + if (!empty($link->template)) { + $xrd_uri = Discovery::applyTemplate($link->template, $uri); + } elseif (!empty($link->href)) { + $xrd_uri = $link->href; + } else { + throw new Exception('No resource descriptor URI in link.'); + } + + $client = new HTTPClient(); + $headers = array(); + if (!is_null($link->type)) { + $headers[] = "Accept: {$link->type}"; + } + + $response = $client->get($xrd_uri, $headers); + if ($response->getStatus() != 200) { + throw new Exception('Unexpected HTTP status code.'); + } + + $xrd->loadString($response->getBody()); + return $xrd; + } catch (Exception $e) { + continue; + } + } + + // TRANS: Exception. %s is an ID. + throw new Exception(sprintf(_('Unable to find services for %s.'), $id)); + } + + /** + * Given an array of links, returns the matching service + * + * @param array $links Links to check (as instances of XML_XRD_Element_Link) + * @param string $service Service to find + * + * @return array $link assoc array representing the link + */ + public static function getService(array $links, $service) + { + foreach ($links as $link) { + if ($link->rel === $service) { + return $link; + } + common_debug('LINK: rel '.$link->rel.' !== '.$service); + } + + throw new Exception('No service link found'); + } + + /** + * Given a "user id" make sure it's normalized to an acct: uri + * + * @param string $user_id User ID to normalize + * + * @return string normalized acct: URI + */ + public static function normalize($uri) + { + if (is_null($uri) || $uri==='') { + throw new Exception(_('No resource given.')); + } + + $parts = parse_url($uri); + // If we don't have a scheme, but the path implies user@host, + // though this is far from a perfect matching procedure... + if (!isset($parts['scheme']) && isset($parts['path']) + && preg_match('/[\w@\w]/u', $parts['path'])) { + return 'acct:' . $uri; + } + + return $uri; + } + + public static function isAcct($uri) + { + return (mb_strtolower(mb_substr($uri, 0, 5)) == 'acct:'); + } + + /** + * Apply a template using an ID + * + * Replaces {uri} in template string with the ID given. + * + * @param string $template Template to match + * @param string $uri URI to replace with + * + * @return string replaced values + */ + public static function applyTemplate($template, $uri) + { + $template = str_replace('{uri}', urlencode($uri), $template); + + return $template; + } +} + + diff --git a/lib/linkheader.php b/plugins/LRDD/lib/linkheader.php similarity index 100% rename from lib/linkheader.php rename to plugins/LRDD/lib/linkheader.php diff --git a/plugins/LRDD/lib/lrddmethod.php b/plugins/LRDD/lib/lrddmethod.php new file mode 100644 index 0000000000..ee9a24a5da --- /dev/null +++ b/plugins/LRDD/lib/lrddmethod.php @@ -0,0 +1,55 @@ + + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +abstract class LRDDMethod +{ + protected $xrd = null; + + public function __construct() { + $this->xrd = new XML_XRD(); + } + + /** + * Discover interesting info about the URI + * + * @param string $uri URI to inquire about + * + * @return array of XML_XRD_Element_Link elements to discovered resource descriptors + */ + abstract public function discover($uri); + + protected function fetchUrl($url, $method=HTTPClient::METHOD_GET) + { + $client = new HTTPClient(); + + // GAAHHH, this method sucks! How about we make a better HTTPClient interface? + switch ($method) { + case HTTPClient::METHOD_GET: + $response = $client->get($url); + break; + case HTTPClient::METHOD_HEAD: + $response = $client->head($url); + break; + default: + throw new Exception('Bad HTTP method.'); + } + + if ($response->getStatus() != 200) { + throw new Exception('Unexpected HTTP status code.'); + } + + return $response; + } +} diff --git a/plugins/LRDD/lib/lrddmethod/hostmeta.php b/plugins/LRDD/lib/lrddmethod/hostmeta.php new file mode 100644 index 0000000000..0bd9117964 --- /dev/null +++ b/plugins/LRDD/lib/lrddmethod/hostmeta.php @@ -0,0 +1,60 @@ + + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class LRDDMethod_HostMeta extends LRDDMethod +{ + /** + * For RFC6415 and HTTP URIs, fetch the host-meta file + * and look for LRDD templates + */ + public function discover($uri) + { + // This is allowed for RFC6415 but not the 'WebFinger' RFC7033. + $try_schemes = array('https', 'http'); + + $scheme = mb_strtolower(parse_url($uri, PHP_URL_SCHEME)); + switch ($scheme) { + case 'acct': + if (!Discovery::isAcct($uri)) { + throw new Exception('Bad resource URI: '.$uri); + } + // We can't use parse_url data for this, since the 'host' + // entry is only set if the scheme has '://' after it. + list($user, $domain) = explode('@', parse_url($uri, PHP_URL_PATH)); + break; + case 'http': + case 'https': + $domain = mb_strtolower(parse_url($uri, PHP_URL_HOST)); + $try_schemes = array($scheme); + break; + default: + throw new Exception('Unable to discover resource descriptor endpoint.'); + } + + foreach ($try_schemes as $scheme) { + $url = $scheme . '://' . $domain . '/.well-known/host-meta'; + + try { + $response = self::fetchUrl($url); + $this->xrd->loadString($response->getBody()); + } catch (Exception $e) { + common_debug('LRDD could not load resource descriptor: '.$url.' ('.$e->getMessage().')'); + continue; + } + return $this->xrd->links; + } + + throw new Exception('Unable to retrieve resource descriptor links.'); + } +} diff --git a/plugins/LRDD/lib/lrddmethod/linkheader.php b/plugins/LRDD/lib/lrddmethod/linkheader.php new file mode 100644 index 0000000000..1ffc704eaa --- /dev/null +++ b/plugins/LRDD/lib/lrddmethod/linkheader.php @@ -0,0 +1,50 @@ + + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class LRDDMethod_LinkHeader extends LRDDMethod +{ + /** + * For HTTP IDs fetch the URL and look for Link headers. + * + * @todo fail out of WebFinger URIs faster + */ + public function discover($uri) + { + $response = self::fetchUrl($uri, HTTPClient::METHOD_HEAD); + + $link_header = $response->getHeader('Link'); + if (empty($link_header)) { + throw new Exception('No Link header found'); + } + common_debug('LRDD LinkHeader found: '.var_export($link_header,true)); + + return self::parseHeader($link_header); + } + + /** + * Given a string or array of headers, returns JRD-like assoc array + * + * @param string|array $header string or array of strings for headers + * + * @return array of associative arrays in JRD-like array format + */ + protected static function parseHeader($header) + { + $lh = new LinkHeader($header); + + $link = new XML_XRD_Element_Link($lh->rel, $lh->href, $lh->type); + + return array($link); + } +} diff --git a/plugins/LRDD/lib/lrddmethod/linkhtml.php b/plugins/LRDD/lib/lrddmethod/linkhtml.php new file mode 100644 index 0000000000..0d8ff5775a --- /dev/null +++ b/plugins/LRDD/lib/lrddmethod/linkhtml.php @@ -0,0 +1,79 @@ + element + * + * Discovers XRD file for a user by fetching the URL and reading any + * elements in the HTML response. + * + * @category Discovery + * @package StatusNet + * @author James Walker + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class LRDDMethod_LinkHTML extends LRDDMethod +{ + /** + * For HTTP IDs, fetch the URL and look for elements + * in the HTML response. + * + * @todo fail out of WebFinger URIs faster + */ + public function discover($uri) + { + $response = self::fetchUrl($uri); + + return self::parse($response->getBody()); + } + + /** + * Parse HTML and return elements + * + * Given an HTML string, scans the string for elements + * + * @param string $html HTML to scan + * + * @return array array of associative arrays in JRD-ish array format + */ + public function parse($html) + { + $links = array(); + + preg_match('/]*)?>(.*?)<\/head>/is', $html, $head_matches); + $head_html = $head_matches[2]; + + preg_match_all('/]*>/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[] = new XML_XRD_Element_Link($link_rel, $link_uri, $link_type); + } + + return $links; + } +} diff --git a/plugins/LRDD/lib/lrddmethod/webfinger.php b/plugins/LRDD/lib/lrddmethod/webfinger.php new file mode 100644 index 0000000000..a38e025f23 --- /dev/null +++ b/plugins/LRDD/lib/lrddmethod/webfinger.php @@ -0,0 +1,37 @@ + + * @copyright 2013 Free Software Foundation, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class LRDDMethod_WebFinger extends LRDDMethod +{ + /** + * Simply returns the WebFinger URL over HTTPS at the uri's domain: + * https://{domain}/.well-known/webfinger?resource={uri} + */ + public function discover($uri) + { + if (!Discovery::isAcct($uri)) { + throw new Exception('Bad resource URI: '.$uri); + } + list($user, $domain) = explode('@', parse_url($uri, PHP_URL_PATH)); + if (!filter_var($domain, FILTER_VALIDATE_IP) + && !filter_var(gethostbyname($domain), FILTER_VALIDATE_IP)) { + throw new Exception('Bad resource host.'); + } + + $link = new XML_XRD_Element_Link( + Discovery::LRDD_REL, + 'https://' . $domain . '/.well-known/webfinger?resource={uri}', + Discovery::JRD_MIMETYPE, + true); //isTemplate + + return array($link); + } +} diff --git a/plugins/OMB/OMBPlugin.php b/plugins/OMB/OMBPlugin.php index cd319d24c2..27c35a07de 100644 --- a/plugins/OMB/OMBPlugin.php +++ b/plugins/OMB/OMBPlugin.php @@ -45,6 +45,8 @@ if (!defined('STATUSNET')) { /** * OMB plugin main class * + * Depends on: WebFinger plugin + * * @category Integration * @package StatusNet * @author Zach Copley diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index be9be1838e..ab72547893 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -18,13 +18,15 @@ */ /** + * OStatusPlugin implementation for GNU Social + * + * Depends on: WebFinger plugin + * * @package OStatusPlugin * @maintainer Brion Vibber */ -if (!defined('STATUSNET')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/'); @@ -52,8 +54,6 @@ class OStatusPlugin extends Plugin function onRouterInitialized($m) { // Discovery actions - $m->connect('main/ownerxrd', - array('action' => 'ownerxrd')); $m->connect('main/ostatustag', array('action' => 'ostatustag')); $m->connect('main/ostatustag?nickname=:nickname', @@ -137,20 +137,6 @@ class OStatusPlugin extends Plugin return true; } - /** - * Add a link header for LRDD Discovery - */ - function onStartShowHTML($action) - { - if ($action instanceof ShowstreamAction) { - $acct = 'acct:'. $action->profile->nickname .'@'. common_config('site', 'server'); - $url = common_local_url('userxrd'); - $url.= '?uri='. $acct; - - header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="application/xrd+xml"'); - } - } - /** * Set up a PuSH hub link to our internal link for canonical timeline * Atom feeds for users and groups. @@ -1328,42 +1314,38 @@ class OStatusPlugin extends Plugin return true; } - function onEndXrdActionLinks(&$xrd, $user) + function onEndXrdActionLinks(XML_XRD $xrd, Profile $target) { - $xrd->links[] = array('rel' => Discovery::UPDATESFROM, - 'href' => common_local_url('ApiTimelineUser', - array('id' => $user->id, - 'format' => 'atom')), - 'type' => 'application/atom+xml'); + $xrd->links[] = new XML_XRD_Element_Link(Discovery::UPDATESFROM, + common_local_url('ApiTimelineUser', + array('id' => $target->id, 'format' => 'atom')), + 'application/atom+xml'); // Salmon $salmon_url = common_local_url('usersalmon', - array('id' => $user->id)); + array('id' => $target->id)); - $xrd->links[] = array('rel' => Salmon::REL_SALMON, - 'href' => $salmon_url); + $xrd->links[] = new XML_XRD_Element_Link(Salmon::REL_SALMON, $salmon_url); // XXX : Deprecated - to be removed. - $xrd->links[] = array('rel' => Salmon::NS_REPLIES, - 'href' => $salmon_url); - - $xrd->links[] = array('rel' => Salmon::NS_MENTIONS, - 'href' => $salmon_url); + $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_REPLIES, $salmon_url); + $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_MENTIONS, $salmon_url); // Get this user's keypair - $magickey = Magicsig::getKV('user_id', $user->id); - if (!$magickey) { + $magickey = Magicsig::getKV('user_id', $target->id); + if (!($magickey instanceof Magicsig)) { // No keypair yet, let's generate one. $magickey = new Magicsig(); - $magickey->generate($user->id); + $magickey->generate($target->id); } - $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, - 'href' => 'data:application/magic-public-key,'. $magickey->toString(false)); + $xrd->links[] = new XML_XRD_Element_Link(Magicsig::PUBLICKEYREL, + '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}'; - $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', - 'template' => $url ); + $xrd->links[] = new XML_XRD_Element_Link('http://ostatus.org/schema/1.0/subscribe', + common_local_url('ostatussub') . '?profile={uri}', + null, // type not set + true); // isTemplate return true; } diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php index f559eec3e3..d8b3fdec51 100644 --- a/plugins/OStatus/actions/ostatusinit.php +++ b/plugins/OStatus/actions/ostatusinit.php @@ -175,20 +175,14 @@ class OStatusInitAction extends Action $target_profile = $this->targetProfile(); $disco = new Discovery; - $result = $disco->lookup($acct); - if (!$result) { - // TRANS: Client error. - $this->clientError(_m('Could not look up OStatus account profile.')); - } - - foreach ($result->links as $link) { - if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') { - // We found a URL - let's redirect! - $url = Discovery::applyTemplate($link['template'], $target_profile); - common_log(LOG_INFO, "Sending remote subscriber $acct to $url"); - common_redirect($url, 303); - } + $xrd = $disco->lookup($acct); + $link = $xrd->get('http://ostatus.org/schema/1.0/subscribe'); + if (!is_null($link)) { + // We found a URL - let's redirect! + $url = Discovery::applyTemplate($link['template'], $target_profile); + common_log(LOG_INFO, "Sending remote subscriber $acct to $url"); + common_redirect($url, 303); } // TRANS: Client error. $this->clientError(_m('Could not confirm remote profile address.')); diff --git a/plugins/OStatus/actions/ostatustag.php b/plugins/OStatus/actions/ostatustag.php index b94dec12eb..7a3be739c7 100644 --- a/plugins/OStatus/actions/ostatustag.php +++ b/plugins/OStatus/actions/ostatustag.php @@ -87,20 +87,14 @@ class OStatusTagAction extends OStatusInitAction $target_profile = $this->targetProfile(); $disco = new Discovery; - $result = $disco->lookup($acct); - if (!$result) { - // TRANS: Client error displayed when remote profile could not be looked up. - $this->clientError(_m('Could not look up OStatus account profile.')); - } - - foreach ($result->links as $link) { - if ($link['rel'] == 'http://ostatus.org/schema/1.0/tag') { - // We found a URL - let's redirect! - $url = Discovery::applyTemplate($link['template'], $target_profile); - common_log(LOG_INFO, "Sending remote subscriber $acct to $url"); - common_redirect($url, 303); - } + $xrd = $disco->lookup($acct); + $link = $xrd->get('http://ostatus.org/schema/1.0/tag'); + if (!is_null($link)) { + // We found a URL - let's redirect! + $url = Discovery::applyTemplate($link->template, $target_profile); + common_log(LOG_INFO, "Sending remote subscriber $acct to $url"); + common_redirect($url, 303); } // TRANS: Client error displayed when remote profile address could not be confirmed. $this->clientError(_m('Could not confirm remote profile address.')); diff --git a/plugins/OStatus/actions/xrd.php b/plugins/OStatus/actions/xrd.php deleted file mode 100644 index 779ce4d515..0000000000 --- a/plugins/OStatus/actions/xrd.php +++ /dev/null @@ -1,126 +0,0 @@ -. - */ - -/** - * @package OStatusPlugin - * @maintainer James Walker - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -class XrdAction extends Action -{ - public $uri; - - public $user; - - public $xrd; - - function handle() - { - $nick = $this->user->nickname; - $profile = $this->user->getProfile(); - - if (empty($this->xrd)) { - $xrd = new XRD(); - } else { - $xrd = $this->xrd; - } - - if (empty($xrd->subject)) { - $xrd->subject = Discovery::normalize($this->uri); - } - - // Possible aliases for the user - - $uris = array($this->user->uri, $profile->profileurl); - - // FIXME: Webfinger generation code should live somewhere on its own - - $path = common_config('site', 'path'); - - if (empty($path)) { - $uris[] = sprintf('acct:%s@%s', $nick, common_config('site', 'server')); - } - - foreach ($uris as $uri) { - if ($uri != $xrd->subject) { - $xrd->alias[] = $uri; - } - } - - $xrd->links[] = array('rel' => Discovery::PROFILEPAGE, - 'type' => 'text/html', - 'href' => $profile->profileurl); - - $xrd->links[] = array('rel' => Discovery::UPDATESFROM, - 'href' => common_local_url('ApiTimelineUser', - array('id' => $this->user->id, - 'format' => 'atom')), - 'type' => 'application/atom+xml'); - - // XFN - $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', - 'type' => 'text/html', - 'href' => $profile->profileurl); - // FOAF - $xrd->links[] = array('rel' => 'describedby', - 'type' => 'application/rdf+xml', - 'href' => common_local_url('foaf', - array('nickname' => $nick))); - - // Salmon - $salmon_url = common_local_url('usersalmon', - array('id' => $this->user->id)); - - $xrd->links[] = array('rel' => Salmon::REL_SALMON, - 'href' => $salmon_url); - // XXX : Deprecated - to be removed. - $xrd->links[] = array('rel' => Salmon::NS_REPLIES, - 'href' => $salmon_url); - - $xrd->links[] = array('rel' => Salmon::NS_MENTIONS, - 'href' => $salmon_url); - - // Get this user's keypair - $magickey = Magicsig::getKV('user_id', $this->user->id); - if (!$magickey) { - // No keypair yet, let's generate one. - $magickey = new Magicsig(); - $magickey->generate($this->user->id); - } - - $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, - '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}'; - $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', - 'template' => $url ); - - $url = common_local_url('tagprofile') . '?uri={uri}'; - $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/tag', - 'template' => $url ); - - header('Content-type: application/xrd+xml'); - print $xrd->toXML(); - } -} diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index ba3d4f3eb4..89c88d9449 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -1011,14 +1011,14 @@ class Ostatus_profile extends Managed_DataObject // Check if they've got an LRDD header - $lrdd = LinkHeader::getLink($response, 'lrdd', 'application/xrd+xml'); - - if (!empty($lrdd)) { - - $xrd = Discovery::fetchXrd($lrdd); + $lrdd = LinkHeader::getLink($response, 'lrdd'); + try { + $xrd = new XML_XRD(); + $xrd->loadFile($lrdd); $xrdHints = DiscoveryHints::fromXRD($xrd); - $hints = array_merge($hints, $xrdHints); + } catch (Exception $e) { + // No hints available from XRD } // If discovery found a feedurl (probably from LRDD), use it. diff --git a/plugins/OStatus/lib/discoveryhints.php b/plugins/OStatus/lib/discoveryhints.php index 0a86a1bf04..ab0586dac0 100644 --- a/plugins/OStatus/lib/discoveryhints.php +++ b/plugins/OStatus/lib/discoveryhints.php @@ -20,26 +20,26 @@ */ class DiscoveryHints { - static function fromXRD($xrd) + static function fromXRD(XML_XRD $xrd) { $hints = array(); - foreach ($xrd->links as $link) { - switch ($link['rel']) { - case Discovery::PROFILEPAGE: - $hints['profileurl'] = $link['href']; + foreach ($xrd->getAll() as $link) { + switch ($link->rel) { + case WebFinger::PROFILEPAGE: + $hints['profileurl'] = $link->href; break; case Salmon::NS_MENTIONS: case Salmon::NS_REPLIES: - $hints['salmon'] = $link['href']; + $hints['salmon'] = $link->href; break; case Discovery::UPDATESFROM: - if (empty($link['type']) || $link['type'] == 'application/atom+xml') { - $hints['feedurl'] = $link['href']; + if (empty($link->type) || $link->type == 'application/atom+xml') { + $hints['feedurl'] = $link->href; } break; case Discovery::HCARD: - $hints['hcardurl'] = $link['href']; + $hints['hcardurl'] = $link->href; break; default: break; diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index 487435927d..a6a60bfa37 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -56,23 +56,22 @@ class MagicEnvelope } catch (Exception $e) { return false; } - if ($xrd->links) { - if ($link = Discovery::getService($xrd->links, Magicsig::PUBLICKEYREL)) { - $keypair = false; - $parts = explode(',', $link['href']); + $link = $xrd->get(Magicsig::PUBLICKEYREL); + if (!is_null($link)) { + $keypair = false; + $parts = explode(',', $link['href']); + if (count($parts) == 2) { + $keypair = $parts[1]; + } else { + // Backwards compatibility check for separator bug in 0.9.0 + $parts = explode(';', $link['href']); if (count($parts) == 2) { $keypair = $parts[1]; - } else { - // Backwards compatibility check for separator bug in 0.9.0 - $parts = explode(';', $link['href']); - if (count($parts) == 2) { - $keypair = $parts[1]; - } } + } - if ($keypair) { - return $keypair; - } + if ($keypair) { + return $keypair; } } // TRANS: Exception. diff --git a/plugins/OStatus/scripts/update_ostatus_profiles.php b/plugins/OStatus/scripts/update_ostatus_profiles.php index f0bc3e12dc..6e68ca7f91 100644 --- a/plugins/OStatus/scripts/update_ostatus_profiles.php +++ b/plugins/OStatus/scripts/update_ostatus_profiles.php @@ -157,13 +157,13 @@ class LooseOstatusProfile extends Ostatus_profile // Check if they've got an LRDD header $lrdd = LinkHeader::getLink($response, 'lrdd', 'application/xrd+xml'); - - if (!empty($lrdd)) { - - $xrd = Discovery::fetchXrd($lrdd); + try { + $xrd = new XML_XRD(); + $xrd->loadFile($lrdd); $xrdHints = DiscoveryHints::fromXRD($xrd); - $hints = array_merge($hints, $xrdHints); + } catch (Exception $e) { + // No hints available from XRD } // If discovery found a feedurl (probably from LRDD), use it. diff --git a/plugins/OpenID/OpenIDPlugin.php b/plugins/OpenID/OpenIDPlugin.php index f4a9e061a1..0e05983806 100644 --- a/plugins/OpenID/OpenIDPlugin.php +++ b/plugins/OpenID/OpenIDPlugin.php @@ -37,6 +37,8 @@ if (!defined('STATUSNET')) { * This class enables consumer support for OpenID, the distributed authentication * and identity system. * + * Depends on: WebFinger plugin for HostMeta-lookup (user@host format) + * * @category Plugin * @package StatusNet * @author Evan Prodromou @@ -408,8 +410,8 @@ class OpenIDPlugin extends Plugin } /** - * We include a element linking to the userxrds page, for OpenID - * client-side authentication. + * We include a element linking to the webfinger resource page, + * for OpenID client-side authentication. * * @param Action $action Action being shown * @@ -765,20 +767,17 @@ class OpenIDPlugin extends Plugin * Webfinger identity to services that support it. See * http://webfinger.org/login for an example. * - * @param XRD &$xrd Currently-displaying XRD object - * @param User $user The user that it's for + * @param XML_XRD $xrd Currently-displaying resource descriptor + * @param Profile $target The profile that it's for * * @return boolean hook value (always true) */ - function onEndXrdActionLinks(&$xrd, $user) + function onEndXrdActionLinks(XML_XRD $xrd, Profile $target) { - $profile = $user->getProfile(); - - if (!empty($profile)) { - $xrd->links[] = array('rel' => 'http://specs.openid.net/auth/2.0/provider', - 'href' => $profile->profileurl); - } + $xrd->links[] = new XML_XRD_Element_Link( + 'http://specs.openid.net/auth/2.0/provider', + $target->profileurl); return true; } diff --git a/plugins/WebFinger/EVENTS.txt b/plugins/WebFinger/EVENTS.txt new file mode 100644 index 0000000000..81641e9906 --- /dev/null +++ b/plugins/WebFinger/EVENTS.txt @@ -0,0 +1,29 @@ +StartHostMetaLinks: Start /.well-known/host-meta links +- &links: array containing the links elements to be written + +EndHostMetaLinks: End /.well-known/host-meta links +- &links: array containing the links elements to be written + +StartWebFingerReconstruction: +- $profile: Profile object for which we want a WebFinger ID +- &$acct: String reference where reconstructed ID is stored + +EndWebFingerReconstruction: +- $profile: Profile object for which we want a WebFinger ID +- &$acct: String reference where reconstructed ID is stored + +StartXrdActionAliases: About to set aliases for the XRD for a user +- $xrd: XML_XRD object being shown +- $target: Profile being shown + +EndXrdActionAliases: Done with aliases for the XRD for a user +- $xrd: XML_XRD object being shown +- $target: Profile being shown + +StartXrdActionLinks: About to set links for the XRD for a profile +- $xrd: XML_XRD object being shown +- $target: Profile being shown + +EndXrdActionLinks: Done with links for the XRD for a profile +- $xrd: XML_XRD object being shown +- $target: Profile being shown diff --git a/plugins/WebFinger/WebFingerPlugin.php b/plugins/WebFinger/WebFingerPlugin.php new file mode 100644 index 0000000000..3c7231a090 --- /dev/null +++ b/plugins/WebFinger/WebFingerPlugin.php @@ -0,0 +1,97 @@ +. + */ + +/** + * Implements WebFinger for GNU Social, as well as support for the + * '.well-known/host-meta' resource. + * + * Depends on: LRDD plugin + * + * @package GNUSocial + * @author Mikael Nordfeldth + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +class WebFingerPlugin extends Plugin +{ + public function onRouterInitialized($m) + { + $m->connect('.well-known/host-meta', array('action' => 'hostmeta')); + $m->connect('.well-known/host-meta.:format', + array('action' => 'hostmeta', + 'format' => '(xml|json)')); + // the resource GET parameter can be anywhere, so don't mention it here + $m->connect('.well-known/webfinger', array('action' => 'webfinger')); + $m->connect('.well-known/webfinger.:format', + array('action' => 'webfinger', + 'format' => '(xml|json)')); + $m->connect('main/ownerxrd', array('action' => 'ownerxrd')); + return true; + } + + public function onLoginAction($action, &$login) + { + switch ($action) { + case 'hostmeta': + case 'webfinger': + $login = true; + return false; + } + + return true; + } + + public function onStartHostMetaLinks(array &$links) + { + foreach (Discovery::supportedMimeTypes() as $type) { + $links[] = new XML_XRD_Element_Link(Discovery::LRDD_REL, + common_local_url('webfinger') . '?resource={uri}', + $type, + true); // isTemplate + } + } + + /** + * Add a link header for LRDD Discovery + */ + public function onStartShowHTML($action) + { + if ($action instanceof ShowstreamAction) { + $acct = 'acct:'. $action->profile->nickname .'@'. common_config('site', 'server'); + $url = common_local_url('webfinger') . '?resource='.$acct; + + foreach (array(Discovery::JRD_MIMETYPE, Discovery::XRD_MIMETYPE) as $type) { + header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="'.$type.'"'); + } + } + } + + public function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'WebFinger', + 'version' => STATUSNET_VERSION, + 'author' => 'Mikael Nordfeldth', + 'homepage' => 'http://www.gnu.org/software/social/', + // TRANS: Plugin description. + 'rawdescription' => _m('Adds WebFinger lookup to GNU Social')); + + return true; + } +} diff --git a/plugins/WebFinger/actions/hostmeta.php b/plugins/WebFinger/actions/hostmeta.php new file mode 100644 index 0000000000..ac07c485f7 --- /dev/null +++ b/plugins/WebFinger/actions/hostmeta.php @@ -0,0 +1,41 @@ +. + */ + +/** + * @category Action + * @package StatusNet + * @author James Walker + * @author Craig Andrews + * @author Mikael Nordfeldth + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +// @todo XXX: Add documentation. +class HostMetaAction extends XrdAction +{ + protected $defaultformat = 'xml'; + + protected function setXRD() + { + if(Event::handle('StartHostMetaLinks', array(&$this->xrd->links))) { + Event::handle('EndHostMetaLinks', array(&$this->xrd->links)); + } + } +} diff --git a/plugins/OStatus/actions/ownerxrd.php b/plugins/WebFinger/actions/ownerxrd.php similarity index 54% rename from plugins/OStatus/actions/ownerxrd.php rename to plugins/WebFinger/actions/ownerxrd.php index 48f1e24870..6f04c1c7d8 100644 --- a/plugins/OStatus/actions/ownerxrd.php +++ b/plugins/WebFinger/actions/ownerxrd.php @@ -18,42 +18,42 @@ */ /** - * @package OStatusPlugin - * @maintainer James Walker + * @package WebFingerPlugin + * @author James Walker + * @author Mikael Nordfeldth */ -if (!defined('STATUSNET')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } -class OwnerxrdAction extends XrdAction +class OwnerxrdAction extends WebfingerAction { + protected $defaultformat = 'xml'; - public $uri; - - function prepare($args) + protected function prepare(array $args=array()) { - $this->user = User::siteOwner(); + $user = User::siteOwner(); - if (!$this->user) { - // TRANS: Client error displayed when referring to a non-existing user. - $this->clientError(_m('No such user.'), 404); - return false; - } + $nick = common_canonical_nickname($user->nickname); + $args['resource'] = 'acct:' . $nick . '@' . common_config('site', 'server'); - $nick = common_canonical_nickname($this->user->nickname); - $acct = 'acct:' . $nick . '@' . common_config('site', 'server'); - - $this->xrd = new XRD(); - - // Check to see if a $config['webfinger']['owner'] has been set - if ($owner = common_config('webfinger', 'owner')) { - $this->xrd->subject = Discovery::normalize($owner); - $this->xrd->alias[] = $acct; - } else { - $this->xrd->subject = $acct; - } + // We have now set $args['resource'] to the configured value, since + // only this local site configuration knows who the owner is! + parent::prepare($args); return true; } + + protected function setXRD() + { + parent::setXRD(); + + // Check to see if a $config['webfinger']['owner'] has been set + // and then make sure 'subject' is set to that primary identity. + if ($owner = common_config('webfinger', 'owner')) { + $this->xrd->aliases[] = $this->xrd->subject; + $this->xrd->subject = Discovery::normalize($owner); + } else { + $this->xrd->subject = $this->resource; + } + } } diff --git a/plugins/WebFinger/actions/webfinger.php b/plugins/WebFinger/actions/webfinger.php new file mode 100644 index 0000000000..ff717de400 --- /dev/null +++ b/plugins/WebFinger/actions/webfinger.php @@ -0,0 +1,122 @@ +. + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * @package WebFingerPlugin + * @author James Walker + * @author Mikael Nordfeldth + */ +class WebfingerAction extends XrdAction +{ + protected function prepare(array $args=array()) + { + parent::prepare($args); + + // throws exception if resource is empty + $this->resource = Discovery::normalize($this->trimmed('resource')); + + if (Discovery::isAcct($this->resource)) { + $parts = explode('@', substr(urldecode($this->resource), 5)); + if (count($parts) == 2) { + list($nick, $domain) = $parts; + if ($domain === common_config('site', 'server')) { + $nick = common_canonical_nickname($nick); + $user = User::getKV('nickname', $nick); + if (!($user instanceof User)) { + throw new NoSuchUserException(array('nickname'=>$nick)); + } + $this->target = $user->getProfile(); + } else { + throw new Exception(_('Remote profiles not supported via WebFinger yet.')); + } + } + } else { + $user = User::getKV('uri', $this->resource); + if ($user instanceof User) { + $this->target = $user->getProfile(); + } else { + // try and get it by profile url + $this->target = Profile::getKV('profileurl', $this->resource); + } + } + + if (!($this->target instanceof Profile)) { + // TRANS: Client error displayed when user not found for an action. + $this->clientError(_('No such user: ') . var_export($this->resource,true), 404); + } + + return true; + } + + protected function setXRD() + { + if (empty($this->target)) { + throw new Exception(_('Target not set for resource descriptor')); + } + + // $this->target set in a _child_ class prepare() + $nick = $this->target->nickname; + + $this->xrd->subject = $this->resource; + + if (Event::handle('StartXrdActionAliases', array($this->xrd, $this->target))) { + $uris = WebFinger::getIdentities($this->target); + foreach ($uris as $uri) { + if ($uri != $this->xrd->subject && !in_array($uri, $this->xrd->aliases)) { + $this->xrd->aliases[] = $uri; + } + } + Event::handle('EndXrdActionAliases', array($this->xrd, $this->target)); + } + + if (Event::handle('StartXrdActionLinks', array($this->xrd, $this->target))) { + + $this->xrd->links[] = new XML_XRD_Element_Link(WebFinger::PROFILEPAGE, + $this->target->getUrl(), 'text/html'); + + // XFN + $this->xrd->links[] = new XML_XRD_Element_Link('http://gmpg.org/xfn/11', + $this->target->getUrl(), 'text/html'); + // FOAF + $this->xrd->links[] = new XML_XRD_Element_Link('describedby', + common_local_url('foaf', array('nickname' => $nick)), + 'application/rdf+xml'); + + $link = new XML_XRD_Element_Link('http://apinamespace.org/atom', + common_local_url('ApiAtomService', array('id' => $nick)), + 'application/atomsvc+xml'); +// XML_XRD must implement changing properties first $link['http://apinamespace.org/atom/username'] = $nick; + $this->xrd->links[] = clone $link; + + if (common_config('site', 'fancy')) { + $apiRoot = common_path('api/', true); + } else { + $apiRoot = common_path('index.php/api/', true); + } + + $link = new XML_XRD_Element_Link('http://apinamespace.org/twitter', $apiRoot); +// XML_XRD must implement changing properties first $link['http://apinamespace.org/twitter/username'] = $nick; + $this->xrd->links[] = clone $link; + + Event::handle('EndXrdActionLinks', array($this->xrd, $this->target)); + } + } +} diff --git a/plugins/WebFinger/lib/webfinger.php b/plugins/WebFinger/lib/webfinger.php new file mode 100644 index 0000000000..bd758917fd --- /dev/null +++ b/plugins/WebFinger/lib/webfinger.php @@ -0,0 +1,100 @@ +. + * + * @package GNUSocial + * @author Mikael Nordfeldth + * @copyright 2013 Free Software Foundation, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class WebFinger +{ + const PROFILEPAGE = 'http://webfinger.net/rel/profile-page'; + + /* + * Reconstructs a WebFinger ID from data we know about the profile. + * + * @param Profile $profile The profile we want a WebFinger ID for + * + * @return string $acct acct:user@example.com URI + */ + public static function reconstruct(Profile $profile) + { + $acct = null; + + if (Event::handle('StartWebFingerReconstruction', array($profile, &$acct))) { + // TODO: getUri may not always give us the correct host on remote users? + $host = parse_url($profile->getUri(), PHP_URL_HOST); + if (empty($profile->nickname) || empty($host)) { + throw new WebFingerReconstructionException($profile); + } + $acct = sprintf('acct:%s@%s', $profile->nickname, $host); + + Event::handle('EndWebFingerReconstruction', array($profile, &$acct)); + } + + return $acct; + } + + /* + * Gets all URI aliases for a Profile + * + * @param Profile $profile The profile we want aliases for + * + * @return array $aliases All the Profile's alternative URLs + */ + public static function getAliases(Profile $profile) + { + $aliases = array(); + $aliases[] = $profile->getUri(); + try { + $aliases[] = $profile->getUrl(); + } catch (InvalidUrlException $e) { + common_debug('Profile id='.$profile->id.' has invalid profileurl: ' . + var_export($profile->profileurl, true)); + } + return $aliases; + } + + /* + * Gets all identities for a Profile, includes WebFinger acct: if + * available, as well as alias URLs. + * + * @param Profile $profile The profile we want aliases for + * + * @return array $uris WebFinger acct: URI and alias URLs + */ + public static function getIdentities(Profile $profile) + { + $uris = array(); + try { + $uris[] = self::reconstruct($profile); + } catch (WebFingerReconstructionException $e) { + common_debug('WebFinger reconstruction for Profile failed, ' . + ' (id='.$profile->id.')'); + } + $uris = array_merge($uris, self::getAliases($profile)); + + return $uris; + } +} diff --git a/plugins/WebFinger/lib/webfingerreconstructionexception.php b/plugins/WebFinger/lib/webfingerreconstructionexception.php new file mode 100644 index 0000000000..d6a1afe869 --- /dev/null +++ b/plugins/WebFinger/lib/webfingerreconstructionexception.php @@ -0,0 +1,55 @@ +. + * + * @category Exception + * @package StatusNet + * @author Mikael Nordfeldth + * @copyright 2013 Free Software Foundation, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Class for an exception when a WebFinger acct: URI can not be constructed + * using the data we have in a Profile. + * + * @category Exception + * @package StatusNet + * @author Mikael Nordfeldth + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class WebFingerReconstructionException extends ServerException +{ + public $target = null; + + public function __construct(Profile $target) + { + $this->target = $target; + + // We could log an entry here with the search parameters + parent::__construct(_('WebFinger URI generation failed.')); + } +} diff --git a/plugins/WebFinger/lib/xrdaction.php b/plugins/WebFinger/lib/xrdaction.php new file mode 100644 index 0000000000..5089ad44e0 --- /dev/null +++ b/plugins/WebFinger/lib/xrdaction.php @@ -0,0 +1,150 @@ +. + */ + +/** + * @package WebFingerPlugin + * @author James Walker + * @author Mikael Nordfeldth + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +abstract class XrdAction extends Action +{ + // json or xml for now, this may still be overriden because of + // our back-compatibility with StatusNet <=1.1.1 + protected $defaultformat = null; + + protected $resource = null; + protected $target = null; + protected $xrd = null; + + public function isReadOnly($args) + { + return true; + } + + /* + * Configures $this->xrd which will later be printed. Must be + * implemented by child classes. + */ + abstract protected function setXRD(); + + protected function prepare(array $args=array()) + { + if (!isset($args['format'])) { + $args['format'] = $this->defaultformat; + } + + parent::prepare($args); + + $this->xrd = new XML_XRD(); + + return true; + } + + protected function handle() + { + parent::handle(); + + $this->setXRD(); + + if (common_config('discovery', 'cors')) { + header('Access-Control-Allow-Origin: *'); + } + + $this->showPage(); + } + + public function mimeType() + { + try { + return $this->checkAccept(); + } catch (Exception $e) { + $supported = Discovery::supportedMimeTypes(); + $docformat = $this->arg('format'); + + if (!empty($docformat) && isset($supported[$docformat])) { + return $supported[$docformat]; + } + } + + /* + * "A WebFinger resource MUST return a JRD as the representation + * for the resource if the client requests no other supported + * format explicitly via the HTTP "Accept" header. [...] + * The WebFinger resource MUST silently ignore any requested + * representations that it does not understand and support." + * -- RFC 7033 (WebFinger) + * http://tools.ietf.org/html/rfc7033 + */ + return Discovery::JRD_MIMETYPE; + } + + public function showPage() + { + $mimeType = $this->mimeType(); + header("Content-type: {$mimeType}"); + + switch ($mimeType) { + case Discovery::XRD_MIMETYPE: + print $this->xrd->toXML(); + break; + case Discovery::JRD_MIMETYPE: + case Discovery::JRD_MIMETYPE_OLD: + print $this->xrd->to('json'); + break; + default: + throw new Exception(_('No supported MIME type in Accept header.')); + } + } + + protected function checkAccept() + { + $type = null; + $httpaccept = isset($_SERVER['HTTP_ACCEPT']) + ? $_SERVER['HTTP_ACCEPT'] : null; + $useragent = isset($_SERVER['HTTP_USER_AGENT']) + ? $_SERVER['HTTP_USER_AGENT'] : null; + + if ($httpaccept !== null && $httpaccept != '*/*') { + $can_serve = implode(',', Discovery::supportedMimeTypes()); + $type = common_negotiate_type(common_accept_to_prefs($httpaccept), + common_accept_to_prefs($can_serve)); + } else { + /* + * HACK: for StatusNet to work against us, we must always serve an + * XRD to at least versions <1.1.1 (at time of writing) since they + * don't send Accept headers (in their 'Discovery::fetchXrd' calls) + */ + $matches = array(); + preg_match('/(StatusNet)\/(\d+\.\d+(\.\d+)?)/', $useragent, $browser); + if (count($browser)>2 && $browser[1] === 'StatusNet' + && version_compare($browser[2], '1.1.1') < 1) { + return Discovery::XRD_MIMETYPE; + } + } + + if (empty($type)) { + throw new Exception(_('No specified MIME type in Accept header.')); + } + + return $type; + } +} diff --git a/scripts/command.php b/scripts/command.php index 8cf44ecd2d..256743f535 100755 --- a/scripts/command.php +++ b/scripts/command.php @@ -64,8 +64,9 @@ if (have_option('i', 'id')) { exit(1); } } else if (have_option('o', 'owner')) { - $user = User::siteOwner(); - if (empty($user)) { + try { + $user = User::siteOwner(); + } catch (ServerException $e) { print "Site has no owner.\n"; exit(1); } diff --git a/socialfy-your-domain/dot-well-known/host-meta b/socialfy-your-domain/dot-well-known/host-meta index e44591a4eb..a8d9fe0284 100644 --- a/socialfy-your-domain/dot-well-known/host-meta +++ b/socialfy-your-domain/dot-well-known/host-meta @@ -1 +1 @@ -example.comResource Descriptor \ No newline at end of file +example.comWebFinger resource descriptor