discovery.php 7.3 KB

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