GitHost.io will be shut down on June 1, 2019. At that point this instance will be unreachable and all data will be irrevocably deleted. More details at https://about.gitlab.com/gitlab-hosted/#githost-is-shutting-down-on-june-1st-2019

Commit a0e107f1 authored by mmn's avatar mmn

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).
parent 44f7ad61
......@@ -585,12 +585,6 @@ EndPublicXRDS: End XRDS output (right before the closing XRDS tag)
- $action: the current action
- &$xrdsoutputter - XRDSOutputter object to write to
StartHostMetaLinks: Start /.well-known/host-meta links
- &links: array containing the links elements to be written
EndHostMetaLinks: End /.well-known/host-meta links
- &links: array containing the links elements to be written
StartCheckPassword: Check a username/password
- $nickname: The nickname to check
- $password: The password to check
......@@ -987,22 +981,6 @@ EndAtomPubNewActivity: When a new activity comes in through Atom Pub API
- $user: user publishing the entry
- $notice: notice that was created
StartXrdActionAliases: About to set aliases for the XRD object for a user
- &$xrd: XRD object being shown
- $user: User being shown
EndXrdActionAliases: Done with aliases for the XRD object for a user
- &$xrd: XRD object being shown
- $user: User being shown
StartXrdActionLinks: About to set links for the XRD object for a user
- &$xrd: XRD object being shown
- $user: User being shown
EndXrdActionLinks: Done with links for the XRD object for a user
- &$xrd: XRD object being shown
- $user: User being shown
AdminPanelCheck: When checking whether the current user can access a given admin panel
- $name: Name of the admin panel
- &$isOK: Boolean whether the user is allowed to use the panel
......
......@@ -1327,12 +1327,26 @@ class Profile extends Managed_DataObject
return $noun->asString('activity:' . $element);
}
/**
* Returns the profile's canonical url, not necessarily a uri/unique id
*
* @return string $profileurl
*/
public function getUrl()
{
if (empty($this->profileurl) ||
!filter_var($this->profileurl, FILTER_VALIDATE_URL)) {
throw new InvalidUrlException($this->profileurl);
}
return $this->profileurl;
}
/**
* Returns the best URI for a profile. Plugins may override.
*
* @return string $uri
*/
function getUri()
public function getUri()
{
$uri = null;
......
......@@ -907,6 +907,10 @@ class User extends Managed_DataObject
self::cacheSet('user:site_owner', $owner);
}
if (!($owner instanceof User)) {
throw new ServerException(_('No site owner configured.'));
}
return $owner;
}
......@@ -936,14 +940,13 @@ class User extends Managed_DataObject
// try the site owner.
if (empty($user)) {
$user = User::siteOwner();
}
if (!empty($user)) {
return $user;
} else {
// TRANS: Server exception.
throw new ServerException(_('No single user defined for single-user mode.'));
try {
$user = User::siteOwner();
return $user;
} catch (ServerException $e) {
// TRANS: Server exception.
throw new ServerException(_('No single user defined for single-user mode.'));
}
}
} else {
// TRANS: Server exception.
......
......@@ -203,7 +203,7 @@ function setupRW()
function isLoginAction($action)
{
static $loginActions = array('login', 'recoverpassword', 'api', 'doc', 'register', 'publicxrds', 'otp', 'opensearch', 'rsd', 'hostmeta');
static $loginActions = array('login', 'recoverpassword', 'api', 'doc', 'register', 'publicxrds', 'otp', 'opensearch', 'rsd');
$login = null;
......
......@@ -109,18 +109,11 @@ class AccountMover extends QueueHandler
$svcDocUrl = null;
$username = null;
foreach ($xrd->links as $link) {
if ($link['rel'] == 'http://apinamespace.org/atom' &&
$link['type'] == 'application/atomsvc+xml') {
$svcDocUrl = $link['href'];
if (!empty($link['property'])) {
foreach ($link['property'] as $property) {
if ($property['type'] == 'http://apinamespace.org/atom/username') {
$username = $property['value'];
break;
}
}
}
$link = $xrd->links->get('http://apinamespace.org/atom', 'application/atomsvc+xml');
if (!is_null($link)) {
$svcDocUrl = $link->href;
if (isset($link['http://apinamespace.org/atom/username'])) {
$username = $link['http://apinamespace.org/atom/username'];
break;
}
}
......
This diff is collapsed.
......@@ -27,7 +27,7 @@
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
if (!defined('GNUSOCIAL')) {
exit(1);
}
......@@ -242,7 +242,7 @@ class HTTPClient extends HTTP_Request2
}
/**
* Pulls up StatusNet's customized user-agent string, so services
* Pulls up GNU Social's customized user-agent string, so services
* we hit can track down the responsible software.
*
* @return string
......
<?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.'));
}
}
......@@ -105,11 +105,15 @@ class Plugin
if (preg_match('/^(\w+)(Action|Form)$/', $cls, $type)) {
$type = array_map('strtolower', $type);
$file = "$basedir/{$type[2]}s/{$type[1]}.php";
} else {
}
if (!file_exists($file)) {
$file = "$basedir/classes/{$cls}.php";
// library files can be put into subdirs ('_'->'/' conversion)
// such as LRDDMethod_WebFinger -> lib/lrddmethod/webfinger.php
if (!file_exists($file)) {
$type = strtolower($cls);
$type = str_replace('_', '/', $type);
$file = "$basedir/lib/{$type}.php";
}
}
......
......@@ -173,10 +173,6 @@ class Router
$m->connect('main/xrds',
array('action' => 'publicxrds'));
$m->connect('.well-known/host-meta',
array('action' => 'hostmeta'));
$m->connect('main/xrd',
array('action' => 'userxrd'));
// settings
......
......@@ -83,6 +83,7 @@ abstract class SiteProfileSettings
'Bookmark' => null,
'Event' => null,
'OpenID' => null,
'LRDD' => null,
'Poll' => null,
'QnA' => null,
'SearchSub' => null,
......@@ -120,6 +121,7 @@ class PublicSite extends SiteProfileSettings
'ExtendedProfile' => null,
'Geonames' => null,
'OStatus' => null,
'WebFinger' => null,
))
),
'discovery' => array('cors' => true) // Allow Cross-Origin Resource Sharing for service discovery (host-meta, XRD, etc.)
......@@ -208,6 +210,7 @@ class CommunitySite extends SiteProfileSettings
'Directory' => null,
'Geonames' => null,
'OStatus' => null,
'WebFinger' => null,
))
),
'discovery' => array('cors' => true) // Allow Cross-Origin Resource Sharing for service discovery (host-meta, XRD, etc.)
......@@ -246,6 +249,7 @@ class SingleuserSite extends SiteProfileSettings
'OStatus' => null,
'TwitterBridge' => null,
'FacebookBridge' => null,
'WebFinger' => null,
))
),
'discovery' => array('cors' => true) // Allow Cross-Origin Resource Sharing for service discovery (host-meta, XRD, etc.)
......
<?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;
}
}
<?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;
}
}
......@@ -57,8 +57,8 @@ class AccountManagerPlugin extends Plugin
}
function onStartHostMetaLinks(&$links) {
$links[] = array('rel' => AccountManagerPlugin::AM_REL,
'href' => common_local_url('AccountManagementControlDocument'));
$links[] = new XML_XRD_Element_Link(AccountManagerPlugin::AM_REL,
common_local_url('AccountManagementControlDocument'));
}
function onStartShowHTML($action)
......
StartDiscoveryMethodRegistration
- $disco: Discovery object that accepts the registrations
EndDiscoveryMethodRegistration: Register remote URI discovery methods
- $disco: Discovery object that accepts the registrations
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
* 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
......@@ -17,51 +17,49 @@
* 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>
* 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>
*/
class UserxrdAction extends XrdAction
{
function prepare($args)
{
parent::prepare($args);
global $config;
$this->uri = $this->trimmed('uri');
$this->uri = self::normalize($this->uri);
if (!defined('GNUSOCIAL')) { exit(1); }
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);
}
}
}
set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__ . '/extlib/');
if (!$this->user) {
// TRANS: Client error displayed when user not found for an action.
$this->clientError(_('No such user.'), 404);
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',