From a0e107f17f27bc1dfc49de2a4493a461213e782e Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Mon, 30 Sep 2013 17:13:03 +0200 Subject: [PATCH] Implemented WebFinger and replaced our XRD with PEAR XML_XRD New plugins: * LRDD LRDD implements client-side RFC6415 and RFC7033 resource descriptor discovery procedures. I.e. LRDD, host-meta and WebFinger stuff. OStatus and OpenID now depend on the LRDD plugin (XML_XRD). * WebFinger This plugin implements the server-side of RFC6415 and RFC7033. Note: WebFinger technically doesn't handle XRD, but we serve both that and JRD (JSON Resource Descriptor), depending on Accept header and one ugly hack to check for old StatusNet installations. WebFinger depends on LRDD. We might make this even prettier by using Net_WebFinger, but it is not currently RFC7033 compliant (no /.well-known/webfinger resource GETs). Disabling the WebFinger plugin would effectively render your site non- federated (which might be desired on a private site). Disabling the LRDD plugin would make your site unable to do modern web URI lookups (making life just a little bit harder). --- EVENTS.txt | 22 - actions/hostmeta.php | 69 --- actions/userxrd.php | 67 --- classes/Profile.php | 16 +- classes/User.php | 19 +- index.php | 2 +- lib/accountmover.php | 17 +- lib/discovery.php | 447 ------------------ lib/httpclient.php | 4 +- lib/invalidurlexception.php | 52 ++ lib/plugin.php | 6 +- lib/router.php | 4 - lib/siteprofile.php | 4 + lib/xrd.php | 188 -------- lib/xrdaction.php | 162 ------- .../AccountManager/AccountManagerPlugin.php | 4 +- plugins/LRDD/EVENTS.txt | 6 + plugins/LRDD/LRDDPlugin.php | 65 +++ plugins/LRDD/extlib/XML/XRD.php | 258 ++++++++++ plugins/LRDD/extlib/XML/XRD/Element/Link.php | 120 +++++ .../LRDD/extlib/XML/XRD/Element/Property.php | 55 +++ plugins/LRDD/extlib/XML/XRD/Exception.php | 30 ++ plugins/LRDD/extlib/XML/XRD/Loader.php | 156 ++++++ .../LRDD/extlib/XML/XRD/Loader/Exception.php | 59 +++ plugins/LRDD/extlib/XML/XRD/Loader/JSON.php | 187 ++++++++ plugins/LRDD/extlib/XML/XRD/Loader/XML.php | 218 +++++++++ .../LRDD/extlib/XML/XRD/LogicException.php | 30 ++ .../LRDD/extlib/XML/XRD/PropertyAccess.php | 133 ++++++ plugins/LRDD/extlib/XML/XRD/Serializer.php | 79 ++++ .../extlib/XML/XRD/Serializer/Exception.php | 29 ++ .../LRDD/extlib/XML/XRD/Serializer/JSON.php | 94 ++++ .../LRDD/extlib/XML/XRD/Serializer/XML.php | 137 ++++++ plugins/LRDD/lib/discovery.php | 202 ++++++++ {lib => plugins/LRDD/lib}/linkheader.php | 0 plugins/LRDD/lib/lrddmethod.php | 55 +++ plugins/LRDD/lib/lrddmethod/hostmeta.php | 60 +++ plugins/LRDD/lib/lrddmethod/linkheader.php | 50 ++ plugins/LRDD/lib/lrddmethod/linkhtml.php | 79 ++++ plugins/LRDD/lib/lrddmethod/webfinger.php | 37 ++ plugins/OMB/OMBPlugin.php | 2 + plugins/OStatus/OStatusPlugin.php | 64 +-- plugins/OStatus/actions/ostatusinit.php | 20 +- plugins/OStatus/actions/ostatustag.php | 20 +- plugins/OStatus/actions/xrd.php | 126 ----- plugins/OStatus/classes/Ostatus_profile.php | 12 +- plugins/OStatus/lib/discoveryhints.php | 18 +- plugins/OStatus/lib/magicenvelope.php | 25 +- .../scripts/update_ostatus_profiles.php | 10 +- plugins/OpenID/OpenIDPlugin.php | 21 +- plugins/WebFinger/EVENTS.txt | 29 ++ plugins/WebFinger/WebFingerPlugin.php | 97 ++++ plugins/WebFinger/actions/hostmeta.php | 41 ++ .../actions/ownerxrd.php | 54 +-- plugins/WebFinger/actions/webfinger.php | 122 +++++ plugins/WebFinger/lib/webfinger.php | 100 ++++ .../lib/webfingerreconstructionexception.php | 55 +++ plugins/WebFinger/lib/xrdaction.php | 150 ++++++ scripts/command.php | 5 +- socialfy-your-domain/dot-well-known/host-meta | 2 +- 59 files changed, 2942 insertions(+), 1253 deletions(-) delete mode 100644 actions/hostmeta.php delete mode 100644 actions/userxrd.php delete mode 100644 lib/discovery.php create mode 100644 lib/invalidurlexception.php delete mode 100644 lib/xrd.php delete mode 100644 lib/xrdaction.php create mode 100644 plugins/LRDD/EVENTS.txt create mode 100644 plugins/LRDD/LRDDPlugin.php create mode 100644 plugins/LRDD/extlib/XML/XRD.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Element/Link.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Element/Property.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Exception.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Loader.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Loader/Exception.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Loader/JSON.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Loader/XML.php create mode 100644 plugins/LRDD/extlib/XML/XRD/LogicException.php create mode 100644 plugins/LRDD/extlib/XML/XRD/PropertyAccess.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Serializer.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Serializer/Exception.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Serializer/JSON.php create mode 100644 plugins/LRDD/extlib/XML/XRD/Serializer/XML.php create mode 100644 plugins/LRDD/lib/discovery.php rename {lib => plugins/LRDD/lib}/linkheader.php (100%) create mode 100644 plugins/LRDD/lib/lrddmethod.php create mode 100644 plugins/LRDD/lib/lrddmethod/hostmeta.php create mode 100644 plugins/LRDD/lib/lrddmethod/linkheader.php create mode 100644 plugins/LRDD/lib/lrddmethod/linkhtml.php create mode 100644 plugins/LRDD/lib/lrddmethod/webfinger.php delete mode 100644 plugins/OStatus/actions/xrd.php create mode 100644 plugins/WebFinger/EVENTS.txt create mode 100644 plugins/WebFinger/WebFingerPlugin.php create mode 100644 plugins/WebFinger/actions/hostmeta.php rename plugins/{OStatus => WebFinger}/actions/ownerxrd.php (54%) create mode 100644 plugins/WebFinger/actions/webfinger.php create mode 100644 plugins/WebFinger/lib/webfinger.php create mode 100644 plugins/WebFinger/lib/webfingerreconstructionexception.php create mode 100644 plugins/WebFinger/lib/xrdaction.php 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