2021-10-18 13:22:02 +01:00
|
|
|
<?php
|
2021-10-27 04:15:07 +01:00
|
|
|
|
|
|
|
declare(strict_types = 1);
|
2021-10-18 13:22:02 +01:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*
|
|
|
|
* @see http://www.gnu.org/software/social/
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace Component\FreeNetwork\Util;
|
|
|
|
|
|
|
|
use App\Core\Event;
|
2021-10-27 04:15:07 +01:00
|
|
|
use App\Core\GSFile;
|
2021-10-18 13:22:02 +01:00
|
|
|
use App\Core\HTTPClient;
|
2021-10-27 04:15:07 +01:00
|
|
|
use function App\Core\I18n\_m;
|
2021-10-18 13:22:02 +01:00
|
|
|
use App\Core\Log;
|
|
|
|
use App\Util\Exception\ClientException;
|
2021-10-27 04:15:07 +01:00
|
|
|
use Exception;
|
2021-10-18 13:22:02 +01:00
|
|
|
use XML_XRD;
|
2021-10-27 04:15:07 +01:00
|
|
|
use XML_XRD_Element_Link;
|
2021-10-18 13:22:02 +01:00
|
|
|
|
|
|
|
class Discovery
|
|
|
|
{
|
2021-10-27 04:15:07 +01:00
|
|
|
public const LRDD_REL = 'lrdd';
|
|
|
|
public const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
|
|
|
|
public const HCARD = 'http://microformats.org/profile/hcard';
|
|
|
|
public const MF2_HCARD = 'http://microformats.org/profile/h-card'; // microformats2 h-card
|
2021-10-18 13:22:02 +01:00
|
|
|
|
2021-10-27 04:15:07 +01:00
|
|
|
public const JRD_MIMETYPE_OLD = 'application/json'; // RFC6415 uses this
|
|
|
|
public const JRD_MIMETYPE = 'application/jrd+json';
|
|
|
|
public const XRD_MIMETYPE = 'application/xrd+xml';
|
2021-10-18 13:22:02 +01:00
|
|
|
|
2021-10-27 04:15:07 +01:00
|
|
|
public array $methods = [];
|
2021-10-18 13:22:02 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor for a discovery object
|
|
|
|
*
|
|
|
|
* Registers different discovery methods.
|
|
|
|
*/
|
|
|
|
public function __construct()
|
|
|
|
{
|
|
|
|
if (Event::handle('StartDiscoveryMethodRegistration', [$this])) {
|
|
|
|
Event::handle('EndDiscoveryMethodRegistration', [$this]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-27 04:15:07 +01:00
|
|
|
public static function supportedMimeTypes(): array
|
2021-10-18 13:22:02 +01:00
|
|
|
{
|
|
|
|
return [
|
|
|
|
'json' => self::JRD_MIMETYPE,
|
|
|
|
'jsonold' => self::JRD_MIMETYPE_OLD,
|
|
|
|
'xml' => self::XRD_MIMETYPE,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register a discovery class
|
|
|
|
*
|
|
|
|
* @param string $class Class name
|
|
|
|
*/
|
2021-10-27 04:15:07 +01:00
|
|
|
public function registerMethod($class): void
|
2021-10-18 13:22:02 +01:00
|
|
|
{
|
|
|
|
$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
|
|
|
|
*/
|
2021-10-27 04:15:07 +01:00
|
|
|
public function lookup(string $id): XML_XRD
|
2021-10-18 13:22:02 +01:00
|
|
|
{
|
2021-10-27 04:15:07 +01:00
|
|
|
// Normalize the incoming $id to make sure we have an uri
|
2021-10-18 13:22:02 +01:00
|
|
|
$uri = self::normalize($id);
|
|
|
|
|
|
|
|
Log::debug(sprintf('Performing discovery for "%s" (normalized "%s")', $id, $uri));
|
|
|
|
|
|
|
|
foreach ($this->methods as $class) {
|
|
|
|
try {
|
|
|
|
$xrd = new XML_XRD();
|
|
|
|
|
|
|
|
Log::debug("LRDD discovery method for '{$uri}': {$class}");
|
|
|
|
$lrdd = new $class;
|
|
|
|
$links = $lrdd->discover($uri);
|
|
|
|
$link = self::getService($links, self::LRDD_REL);
|
|
|
|
|
|
|
|
// Load the LRDD XRD
|
|
|
|
if (!empty($link->template)) {
|
|
|
|
$xrd_uri = self::applyTemplate($link->template, $uri);
|
|
|
|
} elseif (!empty($link->href)) {
|
|
|
|
$xrd_uri = $link->href;
|
|
|
|
} else {
|
|
|
|
throw new Exception('No resource descriptor URI in link.');
|
|
|
|
}
|
|
|
|
|
|
|
|
$headers = [];
|
2021-10-27 04:15:07 +01:00
|
|
|
if (!\is_null($link->type)) {
|
|
|
|
$headers['Accept'] = $link->type;
|
2021-10-18 13:22:02 +01:00
|
|
|
}
|
|
|
|
|
2021-10-27 04:15:07 +01:00
|
|
|
$response = HTTPClient::get($xrd_uri, ['headers' => $headers]);
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
2021-10-18 13:22:02 +01:00
|
|
|
throw new Exception('Unexpected HTTP status code.');
|
|
|
|
}
|
|
|
|
|
2021-10-27 04:15:07 +01:00
|
|
|
switch (GSFile::mimetypeBare($response->getHeaders()['content-type'][0])) {
|
2021-10-18 13:22:02 +01:00
|
|
|
case self::JRD_MIMETYPE_OLD:
|
|
|
|
case self::JRD_MIMETYPE:
|
|
|
|
$type = 'json';
|
|
|
|
break;
|
|
|
|
case self::XRD_MIMETYPE:
|
|
|
|
$type = 'xml';
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// fall back to letting XML_XRD auto-detect
|
2021-10-27 04:15:07 +01:00
|
|
|
Log::debug('No recognized content-type header for resource descriptor body on ' . $xrd_uri);
|
2021-10-18 13:22:02 +01:00
|
|
|
$type = null;
|
|
|
|
}
|
2021-10-27 04:15:07 +01:00
|
|
|
$xrd->loadString($response->getContent(), $type);
|
2021-10-18 13:22:02 +01:00
|
|
|
return $xrd;
|
|
|
|
} catch (ClientException $e) {
|
|
|
|
if ($e->getCode() === 403) {
|
2021-10-27 04:15:07 +01:00
|
|
|
Log::info(sprintf('%s: Aborting discovery on URL %s: %s', $class, $uri, $e->getMessage()));
|
2021-10-18 13:22:02 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
} catch (Exception $e) {
|
2021-10-27 04:15:07 +01:00
|
|
|
Log::info(sprintf('%s: Failed for %s: %s', $class, $uri, $e->getMessage()));
|
2021-10-18 13:22:02 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TRANS: Exception. %s is an ID.
|
2021-10-27 04:15:07 +01:00
|
|
|
throw new Exception(sprintf(_m('Unable to find services for %s.'), $id));
|
2021-10-18 13:22:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*
|
2021-10-27 04:15:07 +01:00
|
|
|
* @return XML_XRD_Element_Link $link
|
2021-10-18 13:22:02 +01:00
|
|
|
*/
|
2021-10-27 04:15:07 +01:00
|
|
|
public static function getService(array $links, $service): XML_XRD_Element_Link
|
2021-10-18 13:22:02 +01:00
|
|
|
{
|
|
|
|
foreach ($links as $link) {
|
|
|
|
if ($link->rel === $service) {
|
|
|
|
return $link;
|
|
|
|
}
|
|
|
|
Log::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
|
|
|
|
*
|
2021-10-27 04:15:07 +01:00
|
|
|
* @param string $uri User ID to normalize
|
2021-10-18 13:22:02 +01:00
|
|
|
*
|
|
|
|
* @return string normalized acct: URI
|
|
|
|
*/
|
2021-10-27 04:15:07 +01:00
|
|
|
public static function normalize(string $uri): string
|
2021-10-18 13:22:02 +01:00
|
|
|
{
|
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
2021-10-27 04:15:07 +01:00
|
|
|
public static function isAcct($uri): bool
|
2021-10-18 13:22:02 +01:00
|
|
|
{
|
|
|
|
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
|
|
|
|
*/
|
2021-10-27 04:15:07 +01:00
|
|
|
public static function applyTemplate($template, $uri): string
|
2021-10-18 13:22:02 +01:00
|
|
|
{
|
2021-10-27 04:15:07 +01:00
|
|
|
return str_replace('{uri}', urlencode($uri), $template);
|
2021-10-18 13:22:02 +01:00
|
|
|
}
|
|
|
|
}
|