3 * StatusNet - the distributed open-source microblogging tool
4 * Copyright (C) 2010, StatusNet, Inc.
6 * Use Hammer discovery stack to find out interesting things about an URI
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU Affero General Public License as published by
12 * the Free Software Foundation, either version 3 of the License, or
13 * (at your option) any later version.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU Affero General Public License for more details.
20 * You should have received a copy of the GNU Affero General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 * @author James Walker <james@status.net>
26 * @copyright 2010 StatusNet, Inc.
27 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
28 * @link http://status.net/
31 if (!defined('STATUSNET')) {
36 * This class implements LRDD-based service discovery based on the "Hammer Draft"
37 * (including webfinger)
41 * @author James Walker <james@status.net>
42 * @copyright 2010 StatusNet, Inc.
43 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
44 * @link http://status.net/
46 * @see http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf
50 const LRDD_REL = 'lrdd';
51 const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
52 const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
53 const HCARD = 'http://microformats.org/profile/hcard';
55 public $methods = array();
58 * Constructor for a discovery object
60 * Registers different discovery methods.
62 * @return Discovery this
65 public function __construct()
67 $this->registerMethod('Discovery_LRDD_Host_Meta');
68 $this->registerMethod('Discovery_LRDD_Link_Header');
69 $this->registerMethod('Discovery_LRDD_Link_HTML');
73 * Register a discovery class
75 * @param string $class Class name
79 public function registerMethod($class)
81 $this->methods[] = $class;
85 * Given a "user id" make sure it's normalized to either a webfinger
86 * acct: uri or a profile HTTP URL.
88 * @param string $user_id User ID to normalize
90 * @return string normalized acct: or http(s)?: URI
92 public static function normalize($user_id)
94 if (substr($user_id, 0, 5) == 'http:' ||
95 substr($user_id, 0, 6) == 'https:' ||
96 substr($user_id, 0, 5) == 'acct:') {
100 if (strpos($user_id, '@') !== false) {
101 return 'acct:' . $user_id;
104 return 'http://' . $user_id;
108 * Determine if a string is a Webfinger ID
110 * Webfinger IDs look like foo@example.com or acct:foo@example.com
112 * @param string $user_id ID to check
114 * @return boolean true if $user_id is a Webfinger, else false
116 public static function isWebfinger($user_id)
118 $uri = Discovery::normalize($user_id);
120 return (substr($uri, 0, 5) == 'acct:');
124 * Given a user ID, return the first available XRD
126 * @param string $id User ID URI
128 * @return XRD XRD object for the user
130 public function lookup($id)
132 // Normalize the incoming $id to make sure we have a uri
133 $uri = $this->normalize($id);
135 foreach ($this->methods as $class) {
136 $links = call_user_func(array($class, 'discover'), $uri);
137 if ($link = Discovery::getService($links, Discovery::LRDD_REL)) {
139 if (!empty($link['template'])) {
140 $xrd_uri = Discovery::applyTemplate($link['template'], $uri);
142 $xrd_uri = $link['href'];
145 $xrd = $this->fetchXrd($xrd_uri);
152 // TRANS: Exception. %s is an ID.
153 throw new Exception(sprintf(_('Unable to find services for %s.'), $id));
157 * Given an array of links, returns the matching service
159 * @param array $links Links to check
160 * @param string $service Service to find
162 * @return array $link assoc array representing the link
164 public static function getService($links, $service)
166 if (!is_array($links)) {
170 foreach ($links as $link) {
171 if ($link['rel'] == $service) {
178 * Apply a template using an ID
180 * Replaces {uri} in template string with the ID given.
182 * @param string $template Template to match
183 * @param string $id User ID to replace with
185 * @return string replaced values
187 public static function applyTemplate($template, $id)
189 $template = str_replace('{uri}', urlencode($id), $template);
195 * Fetch an XRD file and parse
197 * @param string $url URL of the XRD
199 * @return XRD object representing the XRD file
201 public static function fetchXrd($url)
204 $client = new HTTPClient();
205 $response = $client->get($url);
206 } catch (HTTP_Request2_Exception $e) {
210 if ($response->getStatus() != 200) {
214 return XRD::parse($response->getBody());
219 * Abstract interface for discovery
221 * Objects that implement this interface can retrieve an array of
222 * XRD links for the URI.
224 * @category Discovery
226 * @author James Walker <james@status.net>
227 * @copyright 2010 StatusNet, Inc.
228 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
229 * @link http://status.net/
231 interface Discovery_LRDD
234 * Discover interesting info about the URI
236 * @param string $uri URI to inquire about
238 * @return array Links in the XRD file
240 public function discover($uri);
244 * Implementation of discovery using host-meta file
246 * Discovers XRD file for a user by going to the organization's
247 * host-meta file and trying to find a template for LRDD.
249 * @category Discovery
251 * @author James Walker <james@status.net>
252 * @copyright 2010 StatusNet, Inc.
253 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
254 * @link http://status.net/
256 class Discovery_LRDD_Host_Meta implements Discovery_LRDD
259 * Discovery core method
261 * For Webfinger and HTTP URIs, fetch the host-meta file
262 * and look for LRDD templates
264 * @param string $uri URI to inquire about
266 * @return array Links in the XRD file
268 public function discover($uri)
270 if (Discovery::isWebfinger($uri)) {
271 // We have a webfinger acct: - start with host-meta
272 list($name, $domain) = explode('@', $uri);
274 $domain = parse_url($uri, PHP_URL_HOST);
277 $url = 'http://'. $domain .'/.well-known/host-meta';
279 $xrd = Discovery::fetchXrd($url);
282 if ($xrd->host != $domain) {
292 * Implementation of discovery using HTTP Link header
294 * Discovers XRD file for a user by fetching the URL and reading any
295 * Link: headers in the HTTP response.
297 * @category Discovery
299 * @author James Walker <james@status.net>
300 * @copyright 2010 StatusNet, Inc.
301 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
302 * @link http://status.net/
304 class Discovery_LRDD_Link_Header implements Discovery_LRDD
307 * Discovery core method
309 * For HTTP IDs fetch the URL and look for Link headers.
311 * @param string $uri URI to inquire about
313 * @return array Links in the XRD file
315 * @todo fail out of Webfinger URIs faster
317 public function discover($uri)
320 $client = new HTTPClient();
321 $response = $client->get($uri);
322 } catch (HTTP_Request2_Exception $e) {
326 if ($response->getStatus() != 200) {
330 $link_header = $response->getHeader('Link');
335 return array(Discovery_LRDD_Link_Header::parseHeader($link_header));
339 * Given a string or array of headers, returns XRD-like assoc array
341 * @param string|array $header string or array of strings for headers
343 * @return array Link header in XRD-like format
345 protected static function parseHeader($header)
347 $lh = new LinkHeader($header);
349 return array('href' => $lh->href,
351 'type' => $lh->type);
356 * Implementation of discovery using HTML <link> element
358 * Discovers XRD file for a user by fetching the URL and reading any
359 * <link> elements in the HTML response.
361 * @category Discovery
363 * @author James Walker <james@status.net>
364 * @copyright 2010 StatusNet, Inc.
365 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
366 * @link http://status.net/
368 class Discovery_LRDD_Link_HTML implements Discovery_LRDD
371 * Discovery core method
373 * For HTTP IDs, fetch the URL and look for <link> elements
374 * in the HTML response.
376 * @param string $uri URI to inquire about
378 * @return array Links in XRD-ish assoc array
380 * @todo fail out of Webfinger URIs faster
382 public function discover($uri)
385 $client = new HTTPClient();
386 $response = $client->get($uri);
387 } catch (HTTP_Request2_Exception $e) {
391 if ($response->getStatus() != 200) {
395 return Discovery_LRDD_Link_HTML::parse($response->getBody());
399 * Parse HTML and return <link> elements
401 * Given an HTML string, scans the string for <link> elements
403 * @param string $html HTML to scan
405 * @return array array of associative arrays in XRD-ish format
407 public function parse($html)
411 preg_match('/<head(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches);
412 $head_html = $head_matches[2];
414 preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches);
416 foreach ($link_matches[0] as $link_html) {
421 preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches);
422 if ( isset($rel_matches[3]) ) {
423 $link_rel = $rel_matches[3];
424 } else if ( isset($rel_matches[1]) ) {
425 $link_rel = $rel_matches[1];
428 preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches);
429 if ( isset($href_matches[3]) ) {
430 $link_uri = $href_matches[3];
431 } else if ( isset($href_matches[1]) ) {
432 $link_uri = $href_matches[1];
435 preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches);
436 if ( isset($type_matches[3]) ) {
437 $link_type = $type_matches[3];
438 } else if ( isset($type_matches[1]) ) {
439 $link_type = $type_matches[1];
445 'type' => $link_type,