Discovery.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. <?php
  2. declare(strict_types = 1);
  3. /**
  4. * StatusNet - the distributed open-source microblogging tool
  5. * Copyright (C) 2010, StatusNet, Inc.
  6. *
  7. * This class performs lookups based on methods implemented in separate
  8. * classes, where a resource uri is given. Examples are WebFinger (RFC7033)
  9. * and the LRDD (Link-based Resource Descriptor Discovery) in RFC6415.
  10. *
  11. * PHP version 5
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU Affero General Public License as published by
  15. * the Free Software Foundation, either version 3 of the License, or
  16. * (at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU Affero General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU Affero General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. *
  26. * @category Discovery
  27. * @package GNUsocial
  28. *
  29. * @author James Walker <james@status.net>
  30. * @author Mikael Nordfeldth <mmn@hethane.se>
  31. * @copyright 2010 StatusNet, Inc.
  32. * @copyright 2013 Free Software Foundation, Inc.
  33. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
  34. *
  35. * @see http://www.gnu.org/software/social/
  36. */
  37. namespace Component\FreeNetwork\Util;
  38. use App\Core\Event;
  39. use App\Core\GSFile;
  40. use App\Core\HTTPClient;
  41. use function App\Core\I18n\_m;
  42. use App\Core\Log;
  43. use App\Util\Exception\ClientException;
  44. use Exception;
  45. use XML_XRD;
  46. use XML_XRD_Element_Link;
  47. class Discovery
  48. {
  49. public const LRDD_REL = 'lrdd';
  50. public const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
  51. public const HCARD = 'http://microformats.org/profile/hcard';
  52. public const MF2_HCARD = 'http://microformats.org/profile/h-card'; // microformats2 h-card
  53. public const JRD_MIMETYPE_OLD = 'application/json'; // RFC6415 uses this
  54. public const JRD_MIMETYPE = 'application/jrd+json';
  55. public const XRD_MIMETYPE = 'application/xrd+xml';
  56. public array $methods = [];
  57. /**
  58. * Constructor for a discovery object
  59. *
  60. * Registers different discovery methods.
  61. */
  62. public function __construct()
  63. {
  64. if (Event::handle('StartDiscoveryMethodRegistration', [$this])) {
  65. Event::handle('EndDiscoveryMethodRegistration', [$this]);
  66. }
  67. }
  68. public static function supportedMimeTypes(): array
  69. {
  70. return [
  71. 'json' => self::JRD_MIMETYPE,
  72. 'jsonold' => self::JRD_MIMETYPE_OLD,
  73. 'xml' => self::XRD_MIMETYPE,
  74. ];
  75. }
  76. /**
  77. * Register a discovery class
  78. *
  79. * @param string $class Class name
  80. */
  81. public function registerMethod($class): void
  82. {
  83. $this->methods[] = $class;
  84. }
  85. /**
  86. * Given a user ID, return the first available resource descriptor
  87. *
  88. * @param string $id User ID URI
  89. *
  90. * @return XML_XRD object for the resource descriptor of the id
  91. */
  92. public function lookup(string $id): XML_XRD
  93. {
  94. // Normalize the incoming $id to make sure we have an uri
  95. $uri = self::normalize($id);
  96. Log::debug(sprintf('Performing discovery for "%s" (normalized "%s")', $id, $uri));
  97. foreach ($this->methods as $class) {
  98. try {
  99. $xrd = new XML_XRD();
  100. Log::debug("LRDD discovery method for '{$uri}': {$class}");
  101. $lrdd = new $class;
  102. $links = $lrdd->discover($uri);
  103. $link = self::getService($links, self::LRDD_REL);
  104. // Load the LRDD XRD
  105. if (!empty($link->template)) {
  106. $xrd_uri = self::applyTemplate($link->template, $uri);
  107. } elseif (!empty($link->href)) {
  108. $xrd_uri = $link->href;
  109. } else {
  110. throw new Exception('No resource descriptor URI in link.');
  111. }
  112. $headers = [];
  113. if (!\is_null($link->type)) {
  114. $headers['Accept'] = $link->type;
  115. }
  116. $response = HTTPClient::get($xrd_uri, ['headers' => $headers]);
  117. if ($response->getStatusCode() !== 200) {
  118. throw new Exception('Unexpected HTTP status code.');
  119. }
  120. switch (GSFile::mimetypeBare($response->getHeaders()['content-type'][0])) {
  121. case self::JRD_MIMETYPE_OLD:
  122. case self::JRD_MIMETYPE:
  123. $type = 'json';
  124. break;
  125. case self::XRD_MIMETYPE:
  126. $type = 'xml';
  127. break;
  128. default:
  129. // fall back to letting XML_XRD auto-detect
  130. Log::debug('No recognized content-type header for resource descriptor body on ' . $xrd_uri);
  131. $type = null;
  132. }
  133. $xrd->loadString($response->getContent(), $type);
  134. return $xrd;
  135. } catch (ClientException $e) {
  136. if ($e->getCode() === 403) {
  137. Log::info(sprintf('%s: Aborting discovery on URL %s: %s', $class, $uri, $e->getMessage()));
  138. break;
  139. }
  140. } catch (Exception $e) {
  141. Log::info(sprintf('%s: Failed for %s: %s', $class, $uri, $e->getMessage()));
  142. continue;
  143. }
  144. }
  145. // TRANS: Exception. %s is an ID.
  146. throw new Exception(sprintf(_m('Unable to find services for %s.'), $id));
  147. }
  148. /**
  149. * Given an array of links, returns the matching service
  150. *
  151. * @param array $links Links to check (as instances of XML_XRD_Element_Link)
  152. * @param string $service Service to find
  153. *
  154. * @return XML_XRD_Element_Link $link
  155. */
  156. public static function getService(array $links, $service): XML_XRD_Element_Link
  157. {
  158. foreach ($links as $link) {
  159. if ($link->rel === $service) {
  160. return $link;
  161. }
  162. Log::debug('LINK: rel ' . $link->rel . ' !== ' . $service);
  163. }
  164. throw new Exception('No service link found');
  165. }
  166. /**
  167. * Given a "user id" make sure it's normalized to an acct: uri
  168. *
  169. * @param string $uri User ID to normalize
  170. *
  171. * @return string normalized acct: URI
  172. */
  173. public static function normalize(string $uri): string
  174. {
  175. $parts = parse_url($uri);
  176. // If we don't have a scheme, but the path implies user@host,
  177. // though this is far from a perfect matching procedure...
  178. if (!isset($parts['scheme']) && isset($parts['path'])
  179. && preg_match('/[\w@\w]/u', $parts['path'])) {
  180. return 'acct:' . $uri;
  181. }
  182. return $uri;
  183. }
  184. public static function isAcct(string $uri): bool
  185. {
  186. return mb_strtolower(mb_substr($uri, 0, 5)) == 'acct:';
  187. }
  188. /**
  189. * Apply a template using an ID
  190. *
  191. * Replaces {uri} in template string with the ID given.
  192. *
  193. * @param string $template Template to match
  194. * @param string $uri URI to replace with
  195. *
  196. * @return string replaced values
  197. */
  198. public static function applyTemplate($template, $uri): string
  199. {
  200. return str_replace('{uri}', urlencode($uri), $template);
  201. }
  202. }