Implemented WebFinger and replaced our XRD with PEAR XML_XRD

New plugins:
* LRDD
    LRDD implements client-side RFC6415 and RFC7033 resource descriptor
    discovery procedures. I.e. LRDD, host-meta and WebFinger stuff.

    OStatus and OpenID now depend on the LRDD plugin (XML_XRD).

* WebFinger
    This plugin implements the server-side of RFC6415 and RFC7033. Note:
    WebFinger technically doesn't handle XRD, but we serve both that and
    JRD (JSON Resource Descriptor), depending on Accept header and one
    ugly hack to check for old StatusNet installations.

    WebFinger depends on LRDD.

We might make this even prettier by using Net_WebFinger, but it is not
currently RFC7033 compliant (no /.well-known/webfinger resource GETs).

Disabling the WebFinger plugin would effectively render your site non-
federated (which might be desired on a private site).

Disabling the LRDD plugin would make your site unable to do modern web
URI lookups (making life just a little bit harder).
This commit is contained in:
Mikael Nordfeldth 2013-09-30 17:13:03 +02:00
parent 44f7ad612a
commit a0e107f17f
59 changed files with 2942 additions and 1253 deletions

View File

@ -585,12 +585,6 @@ EndPublicXRDS: End XRDS output (right before the closing XRDS tag)
- $action: the current action - $action: the current action
- &$xrdsoutputter - XRDSOutputter object to write to - &$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 StartCheckPassword: Check a username/password
- $nickname: The nickname to check - $nickname: The nickname to check
- $password: The password 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 - $user: user publishing the entry
- $notice: notice that was created - $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 AdminPanelCheck: When checking whether the current user can access a given admin panel
- $name: Name of the admin panel - $name: Name of the admin panel
- &$isOK: Boolean whether the user is allowed to use the panel - &$isOK: Boolean whether the user is allowed to use the panel

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -1327,12 +1327,26 @@ class Profile extends Managed_DataObject
return $noun->asString('activity:' . $element); 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. * Returns the best URI for a profile. Plugins may override.
* *
* @return string $uri * @return string $uri
*/ */
function getUri() public function getUri()
{ {
$uri = null; $uri = null;

View File

@ -907,6 +907,10 @@ class User extends Managed_DataObject
self::cacheSet('user:site_owner', $owner); self::cacheSet('user:site_owner', $owner);
} }
if (!($owner instanceof User)) {
throw new ServerException(_('No site owner configured.'));
}
return $owner; return $owner;
} }
@ -936,14 +940,13 @@ class User extends Managed_DataObject
// try the site owner. // try the site owner.
if (empty($user)) { if (empty($user)) {
$user = User::siteOwner(); try {
} $user = User::siteOwner();
return $user;
if (!empty($user)) { } catch (ServerException $e) {
return $user; // TRANS: Server exception.
} else { throw new ServerException(_('No single user defined for single-user mode.'));
// TRANS: Server exception. }
throw new ServerException(_('No single user defined for single-user mode.'));
} }
} else { } else {
// TRANS: Server exception. // TRANS: Server exception.

View File

@ -203,7 +203,7 @@ function setupRW()
function isLoginAction($action) 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; $login = null;

View File

@ -109,18 +109,11 @@ class AccountMover extends QueueHandler
$svcDocUrl = null; $svcDocUrl = null;
$username = null; $username = null;
foreach ($xrd->links as $link) { $link = $xrd->links->get('http://apinamespace.org/atom', 'application/atomsvc+xml');
if ($link['rel'] == 'http://apinamespace.org/atom' && if (!is_null($link)) {
$link['type'] == 'application/atomsvc+xml') { $svcDocUrl = $link->href;
$svcDocUrl = $link['href']; if (isset($link['http://apinamespace.org/atom/username'])) {
if (!empty($link['property'])) { $username = $link['http://apinamespace.org/atom/username'];
foreach ($link['property'] as $property) {
if ($property['type'] == 'http://apinamespace.org/atom/username') {
$username = $property['value'];
break;
}
}
}
break; break;
} }
} }

View File

@ -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;
}
}

View File

@ -27,7 +27,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) {
exit(1); 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. * we hit can track down the responsible software.
* *
* @return string * @return string

View File

@ -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.'));
}
}

View File

@ -105,11 +105,15 @@ class Plugin
if (preg_match('/^(\w+)(Action|Form)$/', $cls, $type)) { if (preg_match('/^(\w+)(Action|Form)$/', $cls, $type)) {
$type = array_map('strtolower', $type); $type = array_map('strtolower', $type);
$file = "$basedir/{$type[2]}s/{$type[1]}.php"; $file = "$basedir/{$type[2]}s/{$type[1]}.php";
} else { }
if (!file_exists($file)) {
$file = "$basedir/classes/{$cls}.php"; $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)) { if (!file_exists($file)) {
$type = strtolower($cls); $type = strtolower($cls);
$type = str_replace('_', '/', $type);
$file = "$basedir/lib/{$type}.php"; $file = "$basedir/lib/{$type}.php";
} }
} }

View File

@ -173,10 +173,6 @@ class Router
$m->connect('main/xrds', $m->connect('main/xrds',
array('action' => 'publicxrds')); array('action' => 'publicxrds'));
$m->connect('.well-known/host-meta',
array('action' => 'hostmeta'));
$m->connect('main/xrd',
array('action' => 'userxrd'));
// settings // settings

View File

@ -83,6 +83,7 @@ abstract class SiteProfileSettings
'Bookmark' => null, 'Bookmark' => null,
'Event' => null, 'Event' => null,
'OpenID' => null, 'OpenID' => null,
'LRDD' => null,
'Poll' => null, 'Poll' => null,
'QnA' => null, 'QnA' => null,
'SearchSub' => null, 'SearchSub' => null,
@ -120,6 +121,7 @@ class PublicSite extends SiteProfileSettings
'ExtendedProfile' => null, 'ExtendedProfile' => null,
'Geonames' => null, 'Geonames' => null,
'OStatus' => null, 'OStatus' => null,
'WebFinger' => null,
)) ))
), ),
'discovery' => array('cors' => true) // Allow Cross-Origin Resource Sharing for service discovery (host-meta, XRD, etc.) '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, 'Directory' => null,
'Geonames' => null, 'Geonames' => null,
'OStatus' => null, 'OStatus' => null,
'WebFinger' => null,
)) ))
), ),
'discovery' => array('cors' => true) // Allow Cross-Origin Resource Sharing for service discovery (host-meta, XRD, etc.) '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, 'OStatus' => null,
'TwitterBridge' => null, 'TwitterBridge' => null,
'FacebookBridge' => null, 'FacebookBridge' => null,
'WebFinger' => null,
)) ))
), ),
'discovery' => array('cors' => true) // Allow Cross-Origin Resource Sharing for service discovery (host-meta, XRD, etc.) 'discovery' => array('cors' => true) // Allow Cross-Origin Resource Sharing for service discovery (host-meta, XRD, etc.)

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -57,8 +57,8 @@ class AccountManagerPlugin extends Plugin
} }
function onStartHostMetaLinks(&$links) { function onStartHostMetaLinks(&$links) {
$links[] = array('rel' => AccountManagerPlugin::AM_REL, $links[] = new XML_XRD_Element_Link(AccountManagerPlugin::AM_REL,
'href' => common_local_url('AccountManagementControlDocument')); common_local_url('AccountManagementControlDocument'));
} }
function onStartShowHTML($action) function onStartShowHTML($action)

6
plugins/LRDD/EVENTS.txt Normal file
View File

@ -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

View File

@ -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;
}
}

View File

@ -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');
}
}
?>

View File

@ -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);
}
}
?>

View File

@ -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;
}
}
?>

View File

@ -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
{
}
?>

View File

@ -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
);
}
}
?>

View File

@ -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;
}
?>

View File

@ -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;
}
}
?>

View File

@ -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;
}
}
?>

View File

@ -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
{
}
?>

View File

@ -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;
}
}
?>

View File

@ -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
);
}
}
?>

View File

@ -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
{
}
?>

View File

@ -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);
}
}
?>

View File

@ -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();
}
}
?>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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.');
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -45,6 +45,8 @@ if (!defined('STATUSNET')) {
/** /**
* OMB plugin main class * OMB plugin main class
* *
* Depends on: WebFinger plugin
*
* @category Integration * @category Integration
* @package StatusNet * @package StatusNet
* @author Zach Copley <zach@status.net> * @author Zach Copley <zach@status.net>

View File

@ -18,13 +18,15 @@
*/ */
/** /**
* OStatusPlugin implementation for GNU Social
*
* Depends on: WebFinger plugin
*
* @package OStatusPlugin * @package OStatusPlugin
* @maintainer Brion Vibber <brion@status.net> * @maintainer Brion Vibber <brion@status.net>
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
exit(1);
}
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/'); set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/');
@ -52,8 +54,6 @@ class OStatusPlugin extends Plugin
function onRouterInitialized($m) function onRouterInitialized($m)
{ {
// Discovery actions // Discovery actions
$m->connect('main/ownerxrd',
array('action' => 'ownerxrd'));
$m->connect('main/ostatustag', $m->connect('main/ostatustag',
array('action' => 'ostatustag')); array('action' => 'ostatustag'));
$m->connect('main/ostatustag?nickname=:nickname', $m->connect('main/ostatustag?nickname=:nickname',
@ -137,20 +137,6 @@ class OStatusPlugin extends Plugin
return true; 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 * Set up a PuSH hub link to our internal link for canonical timeline
* Atom feeds for users and groups. * Atom feeds for users and groups.
@ -1328,42 +1314,38 @@ class OStatusPlugin extends Plugin
return true; return true;
} }
function onEndXrdActionLinks(&$xrd, $user) function onEndXrdActionLinks(XML_XRD $xrd, Profile $target)
{ {
$xrd->links[] = array('rel' => Discovery::UPDATESFROM, $xrd->links[] = new XML_XRD_Element_Link(Discovery::UPDATESFROM,
'href' => common_local_url('ApiTimelineUser', common_local_url('ApiTimelineUser',
array('id' => $user->id, array('id' => $target->id, 'format' => 'atom')),
'format' => 'atom')), 'application/atom+xml');
'type' => 'application/atom+xml');
// Salmon // Salmon
$salmon_url = common_local_url('usersalmon', $salmon_url = common_local_url('usersalmon',
array('id' => $user->id)); array('id' => $target->id));
$xrd->links[] = array('rel' => Salmon::REL_SALMON, $xrd->links[] = new XML_XRD_Element_Link(Salmon::REL_SALMON, $salmon_url);
'href' => $salmon_url);
// XXX : Deprecated - to be removed. // XXX : Deprecated - to be removed.
$xrd->links[] = array('rel' => Salmon::NS_REPLIES, $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_REPLIES, $salmon_url);
'href' => $salmon_url); $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_MENTIONS, $salmon_url);
$xrd->links[] = array('rel' => Salmon::NS_MENTIONS,
'href' => $salmon_url);
// Get this user's keypair // Get this user's keypair
$magickey = Magicsig::getKV('user_id', $user->id); $magickey = Magicsig::getKV('user_id', $target->id);
if (!$magickey) { if (!($magickey instanceof Magicsig)) {
// No keypair yet, let's generate one. // No keypair yet, let's generate one.
$magickey = new Magicsig(); $magickey = new Magicsig();
$magickey->generate($user->id); $magickey->generate($target->id);
} }
$xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, $xrd->links[] = new XML_XRD_Element_Link(Magicsig::PUBLICKEYREL,
'href' => 'data:application/magic-public-key,'. $magickey->toString(false)); 'data:application/magic-public-key,'. $magickey->toString(false));
// TODO - finalize where the redirect should go on the publisher // TODO - finalize where the redirect should go on the publisher
$url = common_local_url('ostatussub') . '?profile={uri}'; $xrd->links[] = new XML_XRD_Element_Link('http://ostatus.org/schema/1.0/subscribe',
$xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', common_local_url('ostatussub') . '?profile={uri}',
'template' => $url ); null, // type not set
true); // isTemplate
return true; return true;
} }

View File

@ -175,20 +175,14 @@ class OStatusInitAction extends Action
$target_profile = $this->targetProfile(); $target_profile = $this->targetProfile();
$disco = new Discovery; $disco = new Discovery;
$result = $disco->lookup($acct); $xrd = $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);
}
$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. // TRANS: Client error.
$this->clientError(_m('Could not confirm remote profile address.')); $this->clientError(_m('Could not confirm remote profile address.'));

View File

@ -87,20 +87,14 @@ class OStatusTagAction extends OStatusInitAction
$target_profile = $this->targetProfile(); $target_profile = $this->targetProfile();
$disco = new Discovery; $disco = new Discovery;
$result = $disco->lookup($acct); $xrd = $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);
}
$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. // TRANS: Client error displayed when remote profile address could not be confirmed.
$this->clientError(_m('Could not confirm remote profile address.')); $this->clientError(_m('Could not confirm remote profile address.'));

View File

@ -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();
}
}

View File

@ -1011,14 +1011,14 @@ class Ostatus_profile extends Managed_DataObject
// Check if they've got an LRDD header // Check if they've got an LRDD header
$lrdd = LinkHeader::getLink($response, 'lrdd', 'application/xrd+xml'); $lrdd = LinkHeader::getLink($response, 'lrdd');
try {
if (!empty($lrdd)) { $xrd = new XML_XRD();
$xrd->loadFile($lrdd);
$xrd = Discovery::fetchXrd($lrdd);
$xrdHints = DiscoveryHints::fromXRD($xrd); $xrdHints = DiscoveryHints::fromXRD($xrd);
$hints = array_merge($hints, $xrdHints); $hints = array_merge($hints, $xrdHints);
} catch (Exception $e) {
// No hints available from XRD
} }
// If discovery found a feedurl (probably from LRDD), use it. // If discovery found a feedurl (probably from LRDD), use it.

View File

@ -20,26 +20,26 @@
*/ */
class DiscoveryHints { class DiscoveryHints {
static function fromXRD($xrd) static function fromXRD(XML_XRD $xrd)
{ {
$hints = array(); $hints = array();
foreach ($xrd->links as $link) { foreach ($xrd->getAll() as $link) {
switch ($link['rel']) { switch ($link->rel) {
case Discovery::PROFILEPAGE: case WebFinger::PROFILEPAGE:
$hints['profileurl'] = $link['href']; $hints['profileurl'] = $link->href;
break; break;
case Salmon::NS_MENTIONS: case Salmon::NS_MENTIONS:
case Salmon::NS_REPLIES: case Salmon::NS_REPLIES:
$hints['salmon'] = $link['href']; $hints['salmon'] = $link->href;
break; break;
case Discovery::UPDATESFROM: case Discovery::UPDATESFROM:
if (empty($link['type']) || $link['type'] == 'application/atom+xml') { if (empty($link->type) || $link->type == 'application/atom+xml') {
$hints['feedurl'] = $link['href']; $hints['feedurl'] = $link->href;
} }
break; break;
case Discovery::HCARD: case Discovery::HCARD:
$hints['hcardurl'] = $link['href']; $hints['hcardurl'] = $link->href;
break; break;
default: default:
break; break;

View File

@ -56,23 +56,22 @@ class MagicEnvelope
} catch (Exception $e) { } catch (Exception $e) {
return false; return false;
} }
if ($xrd->links) { $link = $xrd->get(Magicsig::PUBLICKEYREL);
if ($link = Discovery::getService($xrd->links, Magicsig::PUBLICKEYREL)) { if (!is_null($link)) {
$keypair = false; $keypair = false;
$parts = explode(',', $link['href']); $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) { if (count($parts) == 2) {
$keypair = $parts[1]; $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) { if ($keypair) {
return $keypair; return $keypair;
}
} }
} }
// TRANS: Exception. // TRANS: Exception.

View File

@ -157,13 +157,13 @@ class LooseOstatusProfile extends Ostatus_profile
// Check if they've got an LRDD header // Check if they've got an LRDD header
$lrdd = LinkHeader::getLink($response, 'lrdd', 'application/xrd+xml'); $lrdd = LinkHeader::getLink($response, 'lrdd', 'application/xrd+xml');
try {
if (!empty($lrdd)) { $xrd = new XML_XRD();
$xrd->loadFile($lrdd);
$xrd = Discovery::fetchXrd($lrdd);
$xrdHints = DiscoveryHints::fromXRD($xrd); $xrdHints = DiscoveryHints::fromXRD($xrd);
$hints = array_merge($hints, $xrdHints); $hints = array_merge($hints, $xrdHints);
} catch (Exception $e) {
// No hints available from XRD
} }
// If discovery found a feedurl (probably from LRDD), use it. // If discovery found a feedurl (probably from LRDD), use it.

View File

@ -37,6 +37,8 @@ if (!defined('STATUSNET')) {
* This class enables consumer support for OpenID, the distributed authentication * This class enables consumer support for OpenID, the distributed authentication
* and identity system. * and identity system.
* *
* Depends on: WebFinger plugin for HostMeta-lookup (user@host format)
*
* @category Plugin * @category Plugin
* @package StatusNet * @package StatusNet
* @author Evan Prodromou <evan@status.net> * @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 * We include a <meta> element linking to the webfinger resource page,
* client-side authentication. * for OpenID client-side authentication.
* *
* @param Action $action Action being shown * @param Action $action Action being shown
* *
@ -765,20 +767,17 @@ class OpenIDPlugin extends Plugin
* Webfinger identity to services that support it. See * Webfinger identity to services that support it. See
* http://webfinger.org/login for an example. * http://webfinger.org/login for an example.
* *
* @param XRD &$xrd Currently-displaying XRD object * @param XML_XRD $xrd Currently-displaying resource descriptor
* @param User $user The user that it's for * @param Profile $target The profile that it's for
* *
* @return boolean hook value (always true) * @return boolean hook value (always true)
*/ */
function onEndXrdActionLinks(&$xrd, $user) function onEndXrdActionLinks(XML_XRD $xrd, Profile $target)
{ {
$profile = $user->getProfile(); $xrd->links[] = new XML_XRD_Element_Link(
'http://specs.openid.net/auth/2.0/provider',
if (!empty($profile)) { $target->profileurl);
$xrd->links[] = array('rel' => 'http://specs.openid.net/auth/2.0/provider',
'href' => $profile->profileurl);
}
return true; return true;
} }

View File

@ -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

View File

@ -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;
}
}

View File

@ -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));
}
}
}

View File

@ -18,42 +18,42 @@
*/ */
/** /**
* @package OStatusPlugin * @package WebFingerPlugin
* @maintainer James Walker <james@status.net> * @author James Walker <james@status.net>
* @author Mikael Nordfeldth <mmn@hethane.se>
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
exit(1);
}
class OwnerxrdAction extends XrdAction class OwnerxrdAction extends WebfingerAction
{ {
protected $defaultformat = 'xml';
public $uri; protected function prepare(array $args=array())
function prepare($args)
{ {
$this->user = User::siteOwner(); $user = User::siteOwner();
if (!$this->user) { $nick = common_canonical_nickname($user->nickname);
// TRANS: Client error displayed when referring to a non-existing user. $args['resource'] = 'acct:' . $nick . '@' . common_config('site', 'server');
$this->clientError(_m('No such user.'), 404);
return false;
}
$nick = common_canonical_nickname($this->user->nickname); // We have now set $args['resource'] to the configured value, since
$acct = 'acct:' . $nick . '@' . common_config('site', 'server'); // only this local site configuration knows who the owner is!
parent::prepare($args);
$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; 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;
}
}
} }

View File

@ -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));
}
}
}

View File

@ -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;
}
}

View File

@ -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.'));
}
}

View File

@ -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;
}
}

View File

@ -64,8 +64,9 @@ if (have_option('i', 'id')) {
exit(1); exit(1);
} }
} else if (have_option('o', 'owner')) { } else if (have_option('o', 'owner')) {
$user = User::siteOwner(); try {
if (empty($user)) { $user = User::siteOwner();
} catch (ServerException $e) {
print "Site has no owner.\n"; print "Site has no owner.\n";
exit(1); exit(1);
} }

View 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> <?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>