]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/discovery.php
PHPCS discovery.php
[quix0rs-gnu-social.git] / lib / discovery.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2010, StatusNet, Inc.
5  *
6  * Use Hammer discovery stack to find out interesting things about an URI
7  *
8  * PHP version 5
9  *
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.
14  *
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.
19  *
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/>.
22  *
23  * @category  Discovery
24  * @package   StatusNet
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/
29  */
30
31 /**
32  * This class implements LRDD-based service discovery based on the "Hammer Draft"
33  * (including webfinger)
34  *
35  * @category  Discovery
36  * @package   StatusNet
37  * @author    James Walker <james@status.net>
38  * @copyright 2010 StatusNet, Inc.
39  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
40  * @link      http://status.net/
41  *
42  * @see       http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf
43  */
44
45 class Discovery
46 {
47     const LRDD_REL    = 'lrdd';
48     const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
49     const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
50     const HCARD       = 'http://microformats.org/profile/hcard';
51
52     public $methods = array();
53
54     /**
55      * Constructor for a discovery object
56      *
57      * Registers different discovery methods.
58      *
59      * @return Discovery this
60      */
61
62     public function __construct()
63     {
64         $this->registerMethod('Discovery_LRDD_Host_Meta');
65         $this->registerMethod('Discovery_LRDD_Link_Header');
66         $this->registerMethod('Discovery_LRDD_Link_HTML');
67     }
68
69     /**
70      * Register a discovery class
71      * 
72      * @param string $class Class name
73      *
74      * @return void
75      */
76
77     public function registerMethod($class)
78     {
79         $this->methods[] = $class;
80     }
81
82     /**
83      * Given a "user id" make sure it's normalized to either a webfinger
84      * acct: uri or a profile HTTP URL.
85      *
86      * @param string $user_id User ID to normalize
87      *
88      * @return string normalized acct: or http(s)?: URI
89      */
90
91     public static function normalize($user_id)
92     {
93         if (substr($user_id, 0, 5) == 'http:' ||
94             substr($user_id, 0, 6) == 'https:' ||
95             substr($user_id, 0, 5) == 'acct:') {
96             return $user_id;
97         }
98
99         if (strpos($user_id, '@') !== false) {
100             return 'acct:' . $user_id;
101         }
102
103         return 'http://' . $user_id;
104     }
105
106     /**
107      * Determine if a string is a Webfinger ID
108      *
109      * Webfinger IDs look like foo@example.com or acct:foo@example.com
110      *
111      * @param string $user_id ID to check
112      *
113      * @return boolean true if $user_id is a Webfinger, else false
114      */
115      
116     public static function isWebfinger($user_id)
117     {
118         $uri = Discovery::normalize($user_id);
119
120         return (substr($uri, 0, 5) == 'acct:');
121     }
122
123     /**
124      * Given a user ID, return the first available XRD
125      *
126      * @param string $id User ID URI
127      *
128      * @return XRD XRD object for the user
129      */
130
131     public function lookup($id)
132     {
133         // Normalize the incoming $id to make sure we have a uri
134         $uri = $this->normalize($id);
135
136         foreach ($this->methods as $class) {
137             $links = call_user_func(array($class, 'discover'), $uri);
138             if ($link = Discovery::getService($links, Discovery::LRDD_REL)) {
139                 // Load the LRDD XRD
140                 if (!empty($link['template'])) {
141                     $xrd_uri = Discovery::applyTemplate($link['template'], $uri);
142                 } else {
143                     $xrd_uri = $link['href'];
144                 }
145
146                 $xrd = $this->fetchXrd($xrd_uri);
147                 if ($xrd) {
148                     return $xrd;
149                 }
150             }
151         }
152
153         // TRANS: Exception.
154         throw new Exception(sprintf(_('Unable to find services for %s.'), $id));
155     }
156
157     /**
158      * Given an array of links, returns the matching service
159      *
160      * @param array  $links   Links to check
161      * @param string $service Service to find
162      *
163      * @return array $link assoc array representing the link
164      */
165
166     public static function getService($links, $service)
167     {
168         if (!is_array($links)) {
169             return false;
170         }
171
172         foreach ($links as $link) {
173             if ($link['rel'] == $service) {
174                 return $link;
175             }
176         }
177     }
178
179     /**
180      * Apply a template using an ID
181      *
182      * Replaces {uri} in template string with the ID given.
183      *
184      * @param string $template Template to match
185      * @param string $id       User ID to replace with
186      *
187      * @return string replaced values
188      */
189
190     public static function applyTemplate($template, $id)
191     {
192         $template = str_replace('{uri}', urlencode($id), $template);
193
194         return $template;
195     }
196
197     /**
198      * Fetch an XRD file and parse
199      *
200      * @param string $url URL of the XRD
201      * 
202      * @return XRD object representing the XRD file
203      */
204
205     public static function fetchXrd($url)
206     {
207         try {
208             $client   = new HTTPClient();
209             $response = $client->get($url);
210         } catch (HTTP_Request2_Exception $e) {
211             return false;
212         }
213
214         if ($response->getStatus() != 200) {
215             return false;
216         }
217
218         return XRD::parse($response->getBody());
219     }
220 }
221
222 /**
223  * Abstract interface for discovery
224  *
225  * Objects that implement this interface can retrieve an array of
226  * XRD links for the URI.
227  *
228  * @category  Discovery
229  * @package   StatusNet
230  * @author    James Walker <james@status.net>
231  * @copyright 2010 StatusNet, Inc.
232  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
233  * @link      http://status.net/
234  */
235
236 interface Discovery_LRDD
237 {
238     /**
239      * Discover interesting info about the URI
240      *
241      * @param string $uri URI to inquire about
242      *
243      * @return array Links in the XRD file
244      */
245
246     public function discover($uri);
247 }
248
249 /**
250  * Implementation of discovery using host-meta file
251  *
252  * Discovers XRD file for a user by going to the organization's
253  * host-meta file and trying to find a template for LRDD.
254  *
255  * @category  Discovery
256  * @package   StatusNet
257  * @author    James Walker <james@status.net>
258  * @copyright 2010 StatusNet, Inc.
259  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
260  * @link      http://status.net/
261  */
262
263 class Discovery_LRDD_Host_Meta implements Discovery_LRDD
264 {
265     /**
266      * Discovery core method
267      *
268      * For Webfinger and HTTP URIs, fetch the host-meta file
269      * and look for LRDD templates
270      *
271      * @param string $uri URI to inquire about
272      *
273      * @return array Links in the XRD file
274      */
275
276     public function discover($uri)
277     {
278         if (Discovery::isWebfinger($uri)) {
279             // We have a webfinger acct: - start with host-meta
280             list($name, $domain) = explode('@', $uri);
281         } else {
282             $domain = parse_url($uri, PHP_URL_HOST);
283         }
284
285         $url = 'http://'. $domain .'/.well-known/host-meta';
286
287         $xrd = Discovery::fetchXrd($url);
288
289         if ($xrd) {
290             if ($xrd->host != $domain) {
291                 return false;
292             }
293
294             return $xrd->links;
295         }
296     }
297 }
298
299 /**
300  * Implementation of discovery using HTTP Link header
301  *
302  * Discovers XRD file for a user by fetching the URL and reading any
303  * Link: headers in the HTTP response.
304  *
305  * @category  Discovery
306  * @package   StatusNet
307  * @author    James Walker <james@status.net>
308  * @copyright 2010 StatusNet, Inc.
309  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
310  * @link      http://status.net/
311  */
312
313 class Discovery_LRDD_Link_Header implements Discovery_LRDD
314 {
315     /**
316      * Discovery core method
317      *
318      * For HTTP IDs fetch the URL and look for Link headers.
319      *
320      * @param string $uri URI to inquire about
321      *
322      * @return array Links in the XRD file
323      *
324      * @todo fail out of Webfinger URIs faster 
325      */
326
327     public function discover($uri)
328     {
329         try {
330             $client   = new HTTPClient();
331             $response = $client->get($uri);
332         } catch (HTTP_Request2_Exception $e) {
333             return false;
334         }
335
336         if ($response->getStatus() != 200) {
337             return false;
338         }
339
340         $link_header = $response->getHeader('Link');
341         if (!$link_header) {
342             //            return false;
343         }
344
345         return array(Discovery_LRDD_Link_Header::parseHeader($link_header));
346     }
347
348     /**
349      * Given a string or array of headers, returns XRD-like assoc array
350      *
351      * @param string|array $header string or array of strings for headers
352      * 
353      * @return array Link header in XRD-like format
354      */
355
356     protected static function parseHeader($header)
357     {
358         $lh = new LinkHeader($header);
359
360         return array('href' => $lh->href,
361                      'rel'  => $lh->rel,
362                      'type' => $lh->type);
363     }
364 }
365
366 /**
367  * Implementation of discovery using HTML <link> element
368  *
369  * Discovers XRD file for a user by fetching the URL and reading any
370  * <link> elements in the HTML response.
371  *
372  * @category  Discovery
373  * @package   StatusNet
374  * @author    James Walker <james@status.net>
375  * @copyright 2010 StatusNet, Inc.
376  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
377  * @link      http://status.net/
378  */
379
380 class Discovery_LRDD_Link_HTML implements Discovery_LRDD
381 {
382     /**
383      * Discovery core method
384      *
385      * For HTTP IDs, fetch the URL and look for <link> elements
386      * in the HTML response.
387      *
388      * @param string $uri URI to inquire about
389      *
390      * @return array Links in XRD-ish assoc array
391      *
392      * @todo fail out of Webfinger URIs faster 
393      */
394
395     public function discover($uri)
396     {
397         try {
398             $client   = new HTTPClient();
399             $response = $client->get($uri);
400         } catch (HTTP_Request2_Exception $e) {
401             return false;
402         }
403
404         if ($response->getStatus() != 200) {
405             return false;
406         }
407
408         return Discovery_LRDD_Link_HTML::parse($response->getBody());
409     }
410
411     /**
412      * Parse HTML and return <link> elements
413      *
414      * Given an HTML string, scans the string for <link> elements
415      *
416      * @param string $html HTML to scan
417      *
418      * @return array array of associative arrays in XRD-ish format
419      */
420
421     public function parse($html)
422     {
423         $links = array();
424
425         preg_match('/<head(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches);
426         $head_html = $head_matches[2];
427
428         preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches);
429
430         foreach ($link_matches[0] as $link_html) {
431             $link_url  = null;
432             $link_rel  = null;
433             $link_type = null;
434
435             preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches);
436             if ( isset($rel_matches[3]) ) {
437                 $link_rel = $rel_matches[3];
438             } else if ( isset($rel_matches[1]) ) {
439                 $link_rel = $rel_matches[1];
440             }
441
442             preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches);
443             if ( isset($href_matches[3]) ) {
444                 $link_uri = $href_matches[3];
445             } else if ( isset($href_matches[1]) ) {
446                 $link_uri = $href_matches[1];
447             }
448
449             preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches);
450             if ( isset($type_matches[3]) ) {
451                 $link_type = $type_matches[3];
452             } else if ( isset($type_matches[1]) ) {
453                 $link_type = $type_matches[1];
454             }
455
456             $links[] = array(
457                 'href' => $link_url,
458                 'rel' => $link_rel,
459                 'type' => $link_type,
460             );
461         }
462
463         return $links;
464     }
465 }