]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Implemented WebFinger and replaced our XRD with PEAR XML_XRD
authorMikael Nordfeldth <mmn@hethane.se>
Mon, 30 Sep 2013 15:13:03 +0000 (17:13 +0200)
committerMikael Nordfeldth <mmn@hethane.se>
Mon, 30 Sep 2013 20:04:52 +0000 (22:04 +0200)
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).

61 files changed:
EVENTS.txt
actions/hostmeta.php [deleted file]
actions/userxrd.php [deleted file]
classes/Profile.php
classes/User.php
index.php
lib/accountmover.php
lib/discovery.php [deleted file]
lib/httpclient.php
lib/invalidurlexception.php [new file with mode: 0644]
lib/linkheader.php [deleted file]
lib/plugin.php
lib/router.php
lib/siteprofile.php
lib/xrd.php [deleted file]
lib/xrdaction.php [deleted file]
plugins/AccountManager/AccountManagerPlugin.php
plugins/LRDD/EVENTS.txt [new file with mode: 0644]
plugins/LRDD/LRDDPlugin.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Element/Link.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Element/Property.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Exception.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Loader.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Loader/Exception.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Loader/JSON.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Loader/XML.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/LogicException.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/PropertyAccess.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Serializer.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Serializer/Exception.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Serializer/JSON.php [new file with mode: 0644]
plugins/LRDD/extlib/XML/XRD/Serializer/XML.php [new file with mode: 0644]
plugins/LRDD/lib/discovery.php [new file with mode: 0644]
plugins/LRDD/lib/linkheader.php [new file with mode: 0644]
plugins/LRDD/lib/lrddmethod.php [new file with mode: 0644]
plugins/LRDD/lib/lrddmethod/hostmeta.php [new file with mode: 0644]
plugins/LRDD/lib/lrddmethod/linkheader.php [new file with mode: 0644]
plugins/LRDD/lib/lrddmethod/linkhtml.php [new file with mode: 0644]
plugins/LRDD/lib/lrddmethod/webfinger.php [new file with mode: 0644]
plugins/OMB/OMBPlugin.php
plugins/OStatus/OStatusPlugin.php
plugins/OStatus/actions/ostatusinit.php
plugins/OStatus/actions/ostatustag.php
plugins/OStatus/actions/ownerxrd.php [deleted file]
plugins/OStatus/actions/xrd.php [deleted file]
plugins/OStatus/classes/Ostatus_profile.php
plugins/OStatus/lib/discoveryhints.php
plugins/OStatus/lib/magicenvelope.php
plugins/OStatus/scripts/update_ostatus_profiles.php
plugins/OpenID/OpenIDPlugin.php
plugins/WebFinger/EVENTS.txt [new file with mode: 0644]
plugins/WebFinger/WebFingerPlugin.php [new file with mode: 0644]
plugins/WebFinger/actions/hostmeta.php [new file with mode: 0644]
plugins/WebFinger/actions/ownerxrd.php [new file with mode: 0644]
plugins/WebFinger/actions/webfinger.php [new file with mode: 0644]
plugins/WebFinger/lib/webfinger.php [new file with mode: 0644]
plugins/WebFinger/lib/webfingerreconstructionexception.php [new file with mode: 0644]
plugins/WebFinger/lib/xrdaction.php [new file with mode: 0644]
scripts/command.php
socialfy-your-domain/dot-well-known/host-meta

index 1d5b610b2798d1602814981dbfe36d0154c2c2b1..a113bb56a1040b046b075e4a620cf12a364274a3 100644 (file)
@@ -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 (file)
index 3d54154..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @category Action
- * @package  StatusNet
- * @maintainer James Walker <james@status.net>
- * @author   Craig Andrews <candrews@integralblue.com>
- */
-
-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 (file)
index 98195d1..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-if (!defined('STATUSNET')) {
-    exit(1);
-}
-
-/**
- * @package OStatusPlugin
- * @maintainer James Walker <james@status.net>
- */
-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;
-    }
-}
index 8a7f7c1ff80aa7ef371c0e2ff62905a907891536..79eb00d22a48187e02ac0512f4540aabd71438aa 100644 (file)
@@ -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;
 
index 14ff66825aff2b5919c472009cd715730d0e5bdd..82545d4bb50ed04bb80bf22273fca7148ec56151 100644 (file)
@@ -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.
index 98ad54a1fc9b7512b2528ebc23454b630930d2eb..2b27845e59dcbce9ed64e69bda2ba9c028a8a2d3 100644 (file)
--- 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;
 
index 3e9228994a3f16315a72db96a3eef132e46bc233..429b6c04656f1b00845613148146bc1f89bf792b 100644 (file)
@@ -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 (file)
index 1430227..0000000
+++ /dev/null
@@ -1,447 +0,0 @@
-<?php
-/**
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * Use Hammer discovery stack to find out interesting things about an URI
- *
- * PHP version 5
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @category  Discovery
- * @package   StatusNet
- * @author    James Walker <james@status.net>
- * @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 <james@status.net>
- * @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 <james@status.net>
- * @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 <james@status.net>
- * @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 <james@status.net>
- * @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 <link> element
- *
- * Discovers XRD file for a user by fetching the URL and reading any
- * <link> elements in the HTML response.
- *
- * @category  Discovery
- * @package   StatusNet
- * @author    James Walker <james@status.net>
- * @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 <link> 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 <link> elements
-     *
-     * Given an HTML string, scans the string for <link> 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(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches);
-        $head_html = $head_matches[2];
-
-        preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches);
-
-        foreach ($link_matches[0] as $link_html) {
-            $link_url  = null;
-            $link_rel  = null;
-            $link_type = null;
-
-            preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches);
-            if ( isset($rel_matches[3]) ) {
-                $link_rel = $rel_matches[3];
-            } else if ( isset($rel_matches[1]) ) {
-                $link_rel = $rel_matches[1];
-            }
-
-            preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches);
-            if ( isset($href_matches[3]) ) {
-                $link_uri = $href_matches[3];
-            } else if ( isset($href_matches[1]) ) {
-                $link_uri = $href_matches[1];
-            }
-
-            preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches);
-            if ( isset($type_matches[3]) ) {
-                $link_type = $type_matches[3];
-            } else if ( isset($type_matches[1]) ) {
-                $link_type = $type_matches[1];
-            }
-
-            $links[] = array(
-                'href' => $link_url,
-                'rel' => $link_rel,
-                'type' => $link_type,
-            );
-        }
-
-        return $links;
-    }
-}
index e1579512114996ef0c5ab651b6d48975dd109495..0dd73e422e83598500af933d5aec4c1f3601c758 100644 (file)
@@ -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 (file)
index 0000000..531b796
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for an exception when a URL is invalid
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Exception
+ * @package   StatusNet
+ * @author    Mikael Nordfeldth <mmn@hethane.se>
+ * @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 <mmn@hethane.se>
+ * @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/linkheader.php b/lib/linkheader.php
deleted file mode 100644 (file)
index dd8f88c..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-<?php
-/**
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * Parse HTTP response for interesting Link: headers
- *
- * PHP version 5
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @category  Discovery
- * @package   StatusNet
- * @author    James Walker <james@status.net>
- * @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);
-}
-
-/**
- * Class to represent Link: headers in an HTTP response
- *
- * Since these are a fairly important part of Hammer-stack discovery, they're
- * reified and implemented here.
- *
- * @category  Discovery
- * @package   StatusNet
- * @author    James Walker <james@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
- * @link      http://status.net/
- *
- * @see       Discovery
- */
-class LinkHeader
-{
-    var $href;
-    var $rel;
-    var $type;
-
-    /**
-     * Initialize from a string
-     *
-     * @param string $str Link: header value
-     *
-     * @return LinkHeader self
-     */
-    function __construct($str)
-    {
-        preg_match('/^<[^>]+>/', $str, $uri_reference);
-        //if (empty($uri_reference)) return;
-
-        $this->href = trim($uri_reference[0], '<>');
-        $this->rel  = array();
-        $this->type = null;
-
-        // remove uri-reference from header
-        $str = substr($str, strlen($uri_reference[0]));
-
-        // parse link-params
-        $params = explode(';', $str);
-
-        foreach ($params as $param) {
-            if (empty($param)) {
-                continue;
-            }
-            list($param_name, $param_value) = explode('=', $param, 2);
-
-            $param_name  = trim($param_name);
-            $param_value = preg_replace('(^"|"$)', '', trim($param_value));
-
-            // for now we only care about 'rel' and 'type' link params
-            // TODO do something with the other links-params
-            switch ($param_name) {
-            case 'rel':
-                $this->rel = trim($param_value);
-                break;
-
-            case 'type':
-                $this->type = trim($param_value);
-            }
-        }
-    }
-
-    /**
-     * Given an HTTP response, return the requested Link: header
-     *
-     * @param HTTP_Request2_Response $response response to check
-     * @param string                 $rel      relationship to look for
-     * @param string                 $type     media type to look for
-     *
-     * @return LinkHeader discovered header, or null on failure
-     */
-    static function getLink($response, $rel=null, $type=null)
-    {
-        $headers = $response->getHeader('Link');
-        if ($headers) {
-            // Can get an array or string, so try to simplify the path
-            if (!is_array($headers)) {
-                $headers = array($headers);
-            }
-
-            foreach ($headers as $header) {
-                $lh = new LinkHeader($header);
-
-                if ((is_null($rel) || $lh->rel == $rel) &&
-                    (is_null($type) || $lh->type == $type)) {
-                    return $lh->href;
-                }
-            }
-        }
-        return null;
-    }
-}
index f97a07fe5ae5cae34da4a9b1e56c9c7fce82bc69..80a3e96207e3001d64c652a4fdbf12c993dcf922 100644 (file)
@@ -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";
             }
         }
index 25c436ac928da2f7a3ac7e622f0a38a588b64ec5..e6a45a595633a7d41e8c9722b8b84c72771d2476 100644 (file)
@@ -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
 
index 16237f1bc3219744b490e7c2426996014e35a403..dbb74a81b2fb9816f41e562077402f953c8abdd1 100644 (file)
@@ -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 (file)
index 43cb2ec..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-<?php
-/**
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * A sample module to show best practices for StatusNet plugins
- *
- * PHP version 5
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   StatusNet
- * @author    James Walker <james@status.net>
- * @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 (file)
index 6cca80d..0000000
+++ /dev/null
@@ -1,162 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package OStatusPlugin
- * @maintainer James Walker <james@status.net>
- */
-
-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;
-    }
-}
index a4ddc747ca7ca31fa7af1d5ab51c283e1b783a18..3cf4bf440b59006218f82017f2bb773581e079ae 100644 (file)
@@ -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 (file)
index 0000000..f959808
--- /dev/null
@@ -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 (file)
index 0000000..afe119c
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+/*
+ * GNU Social - a federating social network
+ * Copyright (C) 2013, Free Software Foundation, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * 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 <mmn@hethane.se>
+ */
+
+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 (file)
index 0000000..207d0ae
--- /dev/null
@@ -0,0 +1,258 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..31b59b0
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..b82265b
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @license  http://www.gnu.org/copyleft/lesser.html LGPL
+ * @link     http://pear.php.net/package/XML_XRD
+ */
+
+/**
+ * Property element in a XRD document.
+ *
+ * The <XRD> root element as well as <Link> tags may have <Property> children.
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 (file)
index 0000000..766ae63
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..7d497ad
--- /dev/null
@@ -0,0 +1,156 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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) == '<?xml') {
+            return 'xml';
+        }
+
+        throw new XML_XRD_Loader_Exception(
+            'Detecting file type failed',
+            XML_XRD_Loader_Exception::DETECT_TYPE
+        );
+    }
+
+
+}
+
+?>
diff --git a/plugins/LRDD/extlib/XML/XRD/Loader/Exception.php b/plugins/LRDD/extlib/XML/XRD/Loader/Exception.php
new file mode 100644 (file)
index 0000000..43b6bc8
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..48ed8ca
--- /dev/null
@@ -0,0 +1,187 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..30b3255
--- /dev/null
@@ -0,0 +1,218 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..6729b70
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..ccfea7f
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..ebabcdf
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..fadbee7
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..3dfe7d5
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..c8cfa61
--- /dev/null
@@ -0,0 +1,137 @@
+<?php
+/**
+ * Part of XML_XRD
+ *
+ * PHP version 5
+ *
+ * @category XML
+ * @package  XML_XRD
+ * @author   Christian Weiske <cweiske@php.net>
+ * @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 <cweiske@php.net>
+ * @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 (file)
index 0000000..f9b8d29
--- /dev/null
@@ -0,0 +1,202 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This class performs lookups based on methods implemented in separate
+ * classes, where a resource uri is given. Examples are WebFinger (RFC7033)
+ * and the LRDD (Link-based Resource Descriptor Discovery) in RFC6415.
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Discovery
+ * @package   GNUSocial
+ * @author    James Walker <james@status.net>
+ * @author    Mikael Nordfeldth <mmn@hethane.se>
+ * @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/plugins/LRDD/lib/linkheader.php b/plugins/LRDD/lib/linkheader.php
new file mode 100644 (file)
index 0000000..dd8f88c
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Parse HTTP response for interesting Link: headers
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @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);
+}
+
+/**
+ * Class to represent Link: headers in an HTTP response
+ *
+ * Since these are a fairly important part of Hammer-stack discovery, they're
+ * reified and implemented here.
+ *
+ * @category  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ *
+ * @see       Discovery
+ */
+class LinkHeader
+{
+    var $href;
+    var $rel;
+    var $type;
+
+    /**
+     * Initialize from a string
+     *
+     * @param string $str Link: header value
+     *
+     * @return LinkHeader self
+     */
+    function __construct($str)
+    {
+        preg_match('/^<[^>]+>/', $str, $uri_reference);
+        //if (empty($uri_reference)) return;
+
+        $this->href = trim($uri_reference[0], '<>');
+        $this->rel  = array();
+        $this->type = null;
+
+        // remove uri-reference from header
+        $str = substr($str, strlen($uri_reference[0]));
+
+        // parse link-params
+        $params = explode(';', $str);
+
+        foreach ($params as $param) {
+            if (empty($param)) {
+                continue;
+            }
+            list($param_name, $param_value) = explode('=', $param, 2);
+
+            $param_name  = trim($param_name);
+            $param_value = preg_replace('(^"|"$)', '', trim($param_value));
+
+            // for now we only care about 'rel' and 'type' link params
+            // TODO do something with the other links-params
+            switch ($param_name) {
+            case 'rel':
+                $this->rel = trim($param_value);
+                break;
+
+            case 'type':
+                $this->type = trim($param_value);
+            }
+        }
+    }
+
+    /**
+     * Given an HTTP response, return the requested Link: header
+     *
+     * @param HTTP_Request2_Response $response response to check
+     * @param string                 $rel      relationship to look for
+     * @param string                 $type     media type to look for
+     *
+     * @return LinkHeader discovered header, or null on failure
+     */
+    static function getLink($response, $rel=null, $type=null)
+    {
+        $headers = $response->getHeader('Link');
+        if ($headers) {
+            // Can get an array or string, so try to simplify the path
+            if (!is_array($headers)) {
+                $headers = array($headers);
+            }
+
+            foreach ($headers as $header) {
+                $lh = new LinkHeader($header);
+
+                if ((is_null($rel) || $lh->rel == $rel) &&
+                    (is_null($type) || $lh->type == $type)) {
+                    return $lh->href;
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/plugins/LRDD/lib/lrddmethod.php b/plugins/LRDD/lib/lrddmethod.php
new file mode 100644 (file)
index 0000000..ee9a24a
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Abstract class for LRDD discovery methods
+ *
+ * Objects that extend this class can retrieve an array of
+ * resource descriptor links for the URI. The array consists
+ * of XML_XRD_Element_Link elements.
+ *
+ * @category  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @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 (file)
index 0000000..0bd9117
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Implementation of discovery using host-meta file
+ *
+ * Discovers resource descriptor 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 <james@status.net>
+ * @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 (file)
index 0000000..1ffc704
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+/**
+ * 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 <james@status.net>
+ * @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 (file)
index 0000000..0d8ff57
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Implementation of discovery using HTML <link> element
+ *
+ * Discovers XRD file for a user by fetching the URL and reading any
+ * <link> elements in the HTML response.
+ *
+ * @category  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @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 <link> 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 <link> elements
+     *
+     * Given an HTML string, scans the string for <link> 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(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches);
+        $head_html = $head_matches[2];
+
+        preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches);
+
+        foreach ($link_matches[0] as $link_html) {
+            $link_url  = null;
+            $link_rel  = null;
+            $link_type = null;
+
+            preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches);
+            if ( isset($rel_matches[3]) ) {
+                $link_rel = $rel_matches[3];
+            } else if ( isset($rel_matches[1]) ) {
+                $link_rel = $rel_matches[1];
+            }
+
+            preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches);
+            if ( isset($href_matches[3]) ) {
+                $link_uri = $href_matches[3];
+            } else if ( isset($href_matches[1]) ) {
+                $link_uri = $href_matches[1];
+            }
+
+            preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches);
+            if ( isset($type_matches[3]) ) {
+                $link_type = $type_matches[3];
+            } else if ( isset($type_matches[1]) ) {
+                $link_type = $type_matches[1];
+            }
+
+            $links[] = 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 (file)
index 0000000..a38e025
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Implementation of WebFinger resource discovery (RFC7033)
+ *
+ * @category  Discovery
+ * @package   GNUSocial
+ * @author    Mikael Nordfeldth <mmn@hethane.se>
+ * @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);
+    }
+}
index cd319d24c28725aebc41d6444127fff7cdcd1b26..27c35a07de010441f1a7a6fda4eb428b19fd9cf0 100644 (file)
@@ -45,6 +45,8 @@ if (!defined('STATUSNET')) {
 /**
  * OMB plugin main class
  *
+ * Depends on: WebFinger plugin
+ *
  * @category  Integration
  * @package   StatusNet
  * @author    Zach Copley <zach@status.net>
index be9be1838e4e0f561f80cb49e4c7256809ff9df3..ab72547893ca869c4e2bbe27eb45352304c234d8 100644 (file)
  */
 
 /**
+ * OStatusPlugin implementation for GNU Social
+ *
+ * Depends on: WebFinger plugin
+ *
  * @package OStatusPlugin
  * @maintainer Brion Vibber <brion@status.net>
  */
 
-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;
     }
index f559eec3e321f7542f957caadd6941c1b2269c6b..d8b3fdec51832d36f7fdac3f28b9b67c567ef1e1 100644 (file)
@@ -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.'));
index b94dec12ebffa14e0cd0d20e943d2939356aaeca..7a3be739c7fe295d078b1417602bda755fe86847 100644 (file)
@@ -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/ownerxrd.php b/plugins/OStatus/actions/ownerxrd.php
deleted file mode 100644 (file)
index 48f1e24..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package OStatusPlugin
- * @maintainer James Walker <james@status.net>
- */
-
-if (!defined('STATUSNET')) {
-    exit(1);
-}
-
-class OwnerxrdAction extends XrdAction
-{
-
-    public $uri;
-
-    function prepare($args)
-    {
-        $this->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($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;
-        }
-
-        return true;
-    }
-}
diff --git a/plugins/OStatus/actions/xrd.php b/plugins/OStatus/actions/xrd.php
deleted file mode 100644 (file)
index 779ce4d..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package OStatusPlugin
- * @maintainer James Walker <james@status.net>
- */
-
-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();
-    }
-}
index ba3d4f3eb4d0a40dab9dafc49de5a480a5e4b346..89c88d94491a95c0047fdc26251ab1679d3907b2 100644 (file)
@@ -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.
index 0a86a1bf0486d902f049b80868a9e83e0b9b107c..ab0586dac0808769cb5fabd55effc3b814fa41a0 100644 (file)
  */
 
 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;
index 487435927d4e8616db427db858d3dc3e43db6ef6..a6a60bfa37ddcbbea2bf7d4a70544f4a6c780d58 100644 (file)
@@ -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.
index f0bc3e12dcff684d4b932025b5a713b66dd4d86d..6e68ca7f9143f7138bc5cd6296e29560f061e1d0 100644 (file)
@@ -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.
index f4a9e061a1be6a2ff7fb0697240eefbfe9d41627..0e0598380642228354b7f93ae933d65b9334c98c 100644 (file)
@@ -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 <evan@status.net>
@@ -408,8 +410,8 @@ class OpenIDPlugin extends Plugin
     }
 
     /**
-     * We include a <meta> element linking to the userxrds page, for OpenID
-     * client-side authentication.
+     * We include a <meta> 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 (file)
index 0000000..81641e9
--- /dev/null
@@ -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 (file)
index 0000000..3c7231a
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/*
+ * GNU Social - a federating social network
+ * Copyright (C) 2013, Free Software Foundation, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * 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 <mmn@hethane.se>
+ */
+
+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 (file)
index 0000000..ac07c48
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @category Action
+ * @package  StatusNet
+ * @author   James Walker <james@status.net>
+ * @author   Craig Andrews <candrews@integralblue.com>
+ * @author   Mikael Nordfeldth <mmn@hethane.se>
+ */
+
+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/WebFinger/actions/ownerxrd.php b/plugins/WebFinger/actions/ownerxrd.php
new file mode 100644 (file)
index 0000000..6f04c1c
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package WebFingerPlugin
+ * @author James Walker <james@status.net>
+ * @author Mikael Nordfeldth <mmn@hethane.se>
+ */
+
+if (!defined('GNUSOCIAL')) { exit(1); }
+
+class OwnerxrdAction extends WebfingerAction
+{
+    protected $defaultformat = 'xml';
+
+    protected function prepare(array $args=array())
+    {
+        $user = User::siteOwner();
+
+        $nick = common_canonical_nickname($user->nickname);
+        $args['resource'] = 'acct:' . $nick . '@' . common_config('site', 'server');
+
+        // 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 (file)
index 0000000..ff717de
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('GNUSOCIAL')) { exit(1); }
+
+/**
+ * @package WebFingerPlugin
+ * @author James Walker <james@status.net>
+ * @author Mikael Nordfeldth <mmn@hethane.se>
+ */
+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 (file)
index 0000000..bd75891
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * WebFinger functions
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @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 (file)
index 0000000..d6a1afe
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for an exception when a WebFinger acct: URI can not be constructed
+ * using the data we have in a Profile.
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Exception
+ * @package   StatusNet
+ * @author    Mikael Nordfeldth <mmn@hethane.se>
+ * @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 <mmn@hethane.se>
+ * @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 (file)
index 0000000..5089ad4
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package WebFingerPlugin
+ * @author James Walker <james@status.net>
+ * @author Mikael Nordfeldth <mmn@hethane.se>
+ */
+
+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;
+    }
+}
index 8cf44ecd2d31425d3ced4b4f1efa7e75d3083d56..256743f5353bc43ff2c3a4c33957bd432d389223 100755 (executable)
@@ -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);
     }
index e44591a4eba59c0932b99cc88b9d7d781b75eb28..a8d9fe0284ffad5169ed28fa7b3262d7e09dfe37 100644 (file)
@@ -1 +1 @@
-<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0" xmlns:hm="http://host-meta.net/xrd/1.0"><hm:Host>example.com</hm:Host><Link rel="lrdd" template="http://example.com/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD>
\ No newline at end of file
+<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0" xmlns:hm="http://host-meta.net/xrd/1.0"><hm:Host>example.com</hm:Host><Link rel="lrdd" template="http://example.com/.well-known/webfinger?resource={uri}"><Title>WebFinger resource descriptor</Title></Link></XRD>