import
[web.mtrack] / inc / lib / Auth / OpenID / Discover.php
1 <?php
2
3 /**
4  * The OpenID and Yadis discovery implementation for OpenID 1.2.
5  */
6
7 require_once "Auth/OpenID.php";
8 require_once "Auth/OpenID/Parse.php";
9 require_once "Auth/OpenID/Message.php";
10 require_once "Auth/Yadis/XRIRes.php";
11 require_once "Auth/Yadis/Yadis.php";
12
13 // XML namespace value
14 define('Auth_OpenID_XMLNS_1_0', 'http://openid.net/xmlns/1.0');
15
16 // Yadis service types
17 define('Auth_OpenID_TYPE_1_2', 'http://openid.net/signon/1.2');
18 define('Auth_OpenID_TYPE_1_1', 'http://openid.net/signon/1.1');
19 define('Auth_OpenID_TYPE_1_0', 'http://openid.net/signon/1.0');
20 define('Auth_OpenID_TYPE_2_0_IDP', 'http://specs.openid.net/auth/2.0/server');
21 define('Auth_OpenID_TYPE_2_0', 'http://specs.openid.net/auth/2.0/signon');
22 define('Auth_OpenID_RP_RETURN_TO_URL_TYPE',
23        'http://specs.openid.net/auth/2.0/return_to');
24
25 function Auth_OpenID_getOpenIDTypeURIs()
26 {
27     return array(Auth_OpenID_TYPE_2_0_IDP,
28                  Auth_OpenID_TYPE_2_0,
29                  Auth_OpenID_TYPE_1_2,
30                  Auth_OpenID_TYPE_1_1,
31                  Auth_OpenID_TYPE_1_0,
32                  Auth_OpenID_RP_RETURN_TO_URL_TYPE);
33 }
34
35 /**
36  * Object representing an OpenID service endpoint.
37  */
38 class Auth_OpenID_ServiceEndpoint {
39     function Auth_OpenID_ServiceEndpoint()
40     {
41         $this->claimed_id = null;
42         $this->server_url = null;
43         $this->type_uris = array();
44         $this->local_id = null;
45         $this->canonicalID = null;
46         $this->used_yadis = false; // whether this came from an XRDS
47         $this->display_identifier = null;
48     }
49
50     function getDisplayIdentifier()
51     {
52         if ($this->display_identifier) {
53             return $this->display_identifier;
54         }
55         if (! $this->claimed_id) {
56           return $this->claimed_id;
57         }
58         $parsed = parse_url($this->claimed_id);
59         $scheme = $parsed['scheme'];
60         $host = $parsed['host'];
61         $path = $parsed['path'];
62         if (array_key_exists('query', $parsed)) {
63             $query = $parsed['query'];
64             $no_frag = "$scheme://$host$path?$query";
65         } else {
66             $no_frag = "$scheme://$host$path";
67         }
68         return $no_frag;
69     }
70
71     function usesExtension($extension_uri)
72     {
73         return in_array($extension_uri, $this->type_uris);
74     }
75
76     function preferredNamespace()
77     {
78         if (in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris) ||
79             in_array(Auth_OpenID_TYPE_2_0, $this->type_uris)) {
80             return Auth_OpenID_OPENID2_NS;
81         } else {
82             return Auth_OpenID_OPENID1_NS;
83         }
84     }
85
86     /*
87      * Query this endpoint to see if it has any of the given type
88      * URIs. This is useful for implementing other endpoint classes
89      * that e.g. need to check for the presence of multiple versions
90      * of a single protocol.
91      *
92      * @param $type_uris The URIs that you wish to check
93      *
94      * @return all types that are in both in type_uris and
95      * $this->type_uris
96      */
97     function matchTypes($type_uris)
98     {
99         $result = array();
100         foreach ($type_uris as $test_uri) {
101             if ($this->supportsType($test_uri)) {
102                 $result[] = $test_uri;
103             }
104         }
105
106         return $result;
107     }
108
109     function supportsType($type_uri)
110     {
111         // Does this endpoint support this type?
112         return ((in_array($type_uri, $this->type_uris)) ||
113                 (($type_uri == Auth_OpenID_TYPE_2_0) &&
114                  $this->isOPIdentifier()));
115     }
116
117     function compatibilityMode()
118     {
119         return $this->preferredNamespace() != Auth_OpenID_OPENID2_NS;
120     }
121
122     function isOPIdentifier()
123     {
124         return in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris);
125     }
126
127     function fromOPEndpointURL($op_endpoint_url)
128     {
129         // Construct an OP-Identifier OpenIDServiceEndpoint object for
130         // a given OP Endpoint URL
131         $obj = new Auth_OpenID_ServiceEndpoint();
132         $obj->server_url = $op_endpoint_url;
133         $obj->type_uris = array(Auth_OpenID_TYPE_2_0_IDP);
134         return $obj;
135     }
136
137     function parseService($yadis_url, $uri, $type_uris, $service_element)
138     {
139         // Set the state of this object based on the contents of the
140         // service element.  Return true if successful, false if not
141         // (if findOPLocalIdentifier returns false).
142         $this->type_uris = $type_uris;
143         $this->server_url = $uri;
144         $this->used_yadis = true;
145
146         if (!$this->isOPIdentifier()) {
147             $this->claimed_id = $yadis_url;
148             $this->local_id = Auth_OpenID_findOPLocalIdentifier(
149                                                     $service_element,
150                                                     $this->type_uris);
151             if ($this->local_id === false) {
152                 return false;
153             }
154         }
155
156         return true;
157     }
158
159     function getLocalID()
160     {
161         // Return the identifier that should be sent as the
162         // openid.identity_url parameter to the server.
163         if ($this->local_id === null && $this->canonicalID === null) {
164             return $this->claimed_id;
165         } else {
166             if ($this->local_id) {
167                 return $this->local_id;
168             } else {
169                 return $this->canonicalID;
170             }
171         }
172     }
173
174     /*
175      * Parse the given document as XRDS looking for OpenID services.
176      *
177      * @return array of Auth_OpenID_ServiceEndpoint or null if the
178      * document cannot be parsed.
179      */
180     function fromXRDS($uri, $xrds_text)
181     {
182         $xrds =& Auth_Yadis_XRDS::parseXRDS($xrds_text);
183
184         if ($xrds) {
185             $yadis_services =
186               $xrds->services(array('filter_MatchesAnyOpenIDType'));
187             return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services);
188         }
189
190         return null;
191     }
192
193     /*
194      * Create endpoints from a DiscoveryResult.
195      *
196      * @param discoveryResult Auth_Yadis_DiscoveryResult
197      * @return array of Auth_OpenID_ServiceEndpoint or null if
198      * endpoints cannot be created.
199      */
200     function fromDiscoveryResult($discoveryResult)
201     {
202         if ($discoveryResult->isXRDS()) {
203             return Auth_OpenID_ServiceEndpoint::fromXRDS(
204                                      $discoveryResult->normalized_uri,
205                                      $discoveryResult->response_text);
206         } else {
207             return Auth_OpenID_ServiceEndpoint::fromHTML(
208                                      $discoveryResult->normalized_uri,
209                                      $discoveryResult->response_text);
210         }
211     }
212
213     function fromHTML($uri, $html)
214     {
215         $discovery_types = array(
216                                  array(Auth_OpenID_TYPE_2_0,
217                                        'openid2.provider', 'openid2.local_id'),
218                                  array(Auth_OpenID_TYPE_1_1,
219                                        'openid.server', 'openid.delegate')
220                                  );
221
222         $services = array();
223
224         foreach ($discovery_types as $triple) {
225             list($type_uri, $server_rel, $delegate_rel) = $triple;
226
227             $urls = Auth_OpenID_legacy_discover($html, $server_rel,
228                                                 $delegate_rel);
229
230             if ($urls === false) {
231                 continue;
232             }
233
234             list($delegate_url, $server_url) = $urls;
235
236             $service = new Auth_OpenID_ServiceEndpoint();
237             $service->claimed_id = $uri;
238             $service->local_id = $delegate_url;
239             $service->server_url = $server_url;
240             $service->type_uris = array($type_uri);
241
242             $services[] = $service;
243         }
244
245         return $services;
246     }
247
248     function copy()
249     {
250         $x = new Auth_OpenID_ServiceEndpoint();
251
252         $x->claimed_id = $this->claimed_id;
253         $x->server_url = $this->server_url;
254         $x->type_uris = $this->type_uris;
255         $x->local_id = $this->local_id;
256         $x->canonicalID = $this->canonicalID;
257         $x->used_yadis = $this->used_yadis;
258
259         return $x;
260     }
261 }
262
263 function Auth_OpenID_findOPLocalIdentifier($service, $type_uris)
264 {
265     // Extract a openid:Delegate value from a Yadis Service element.
266     // If no delegate is found, returns null.  Returns false on
267     // discovery failure (when multiple delegate/localID tags have
268     // different values).
269
270     $service->parser->registerNamespace('openid',
271                                         Auth_OpenID_XMLNS_1_0);
272
273     $service->parser->registerNamespace('xrd',
274                                         Auth_Yadis_XMLNS_XRD_2_0);
275
276     $parser =& $service->parser;
277
278     $permitted_tags = array();
279
280     if (in_array(Auth_OpenID_TYPE_1_1, $type_uris) ||
281         in_array(Auth_OpenID_TYPE_1_0, $type_uris)) {
282         $permitted_tags[] = 'openid:Delegate';
283     }
284
285     if (in_array(Auth_OpenID_TYPE_2_0, $type_uris)) {
286         $permitted_tags[] = 'xrd:LocalID';
287     }
288
289     $local_id = null;
290
291     foreach ($permitted_tags as $tag_name) {
292         $tags = $service->getElements($tag_name);
293
294         foreach ($tags as $tag) {
295             $content = $parser->content($tag);
296
297             if ($local_id === null) {
298                 $local_id = $content;
299             } else if ($local_id != $content) {
300                 return false;
301             }
302         }
303     }
304
305     return $local_id;
306 }
307
308 function filter_MatchesAnyOpenIDType(&$service)
309 {
310     $uris = $service->getTypes();
311
312     foreach ($uris as $uri) {
313         if (in_array($uri, Auth_OpenID_getOpenIDTypeURIs())) {
314             return true;
315         }
316     }
317
318     return false;
319 }
320
321 function Auth_OpenID_bestMatchingService($service, $preferred_types)
322 {
323     // Return the index of the first matching type, or something
324     // higher if no type matches.
325     //
326     // This provides an ordering in which service elements that
327     // contain a type that comes earlier in the preferred types list
328     // come before service elements that come later. If a service
329     // element has more than one type, the most preferred one wins.
330
331     foreach ($preferred_types as $index => $typ) {
332         if (in_array($typ, $service->type_uris)) {
333             return $index;
334         }
335     }
336
337     return count($preferred_types);
338 }
339
340 function Auth_OpenID_arrangeByType($service_list, $preferred_types)
341 {
342     // Rearrange service_list in a new list so services are ordered by
343     // types listed in preferred_types.  Return the new list.
344
345     // Build a list with the service elements in tuples whose
346     // comparison will prefer the one with the best matching service
347     $prio_services = array();
348     foreach ($service_list as $index => $service) {
349         $prio_services[] = array(Auth_OpenID_bestMatchingService($service,
350                                                         $preferred_types),
351                                  $index, $service);
352     }
353
354     sort($prio_services);
355
356     // Now that the services are sorted by priority, remove the sort
357     // keys from the list.
358     foreach ($prio_services as $index => $s) {
359         $prio_services[$index] = $prio_services[$index][2];
360     }
361
362     return $prio_services;
363 }
364
365 // Extract OP Identifier services.  If none found, return the rest,
366 // sorted with most preferred first according to
367 // OpenIDServiceEndpoint.openid_type_uris.
368 //
369 // openid_services is a list of OpenIDServiceEndpoint objects.
370 //
371 // Returns a list of OpenIDServiceEndpoint objects."""
372 function Auth_OpenID_getOPOrUserServices($openid_services)
373 {
374     $op_services = Auth_OpenID_arrangeByType($openid_services,
375                                      array(Auth_OpenID_TYPE_2_0_IDP));
376
377     $openid_services = Auth_OpenID_arrangeByType($openid_services,
378                                      Auth_OpenID_getOpenIDTypeURIs());
379
380     if ($op_services) {
381         return $op_services;
382     } else {
383         return $openid_services;
384     }
385 }
386
387 function Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services)
388 {
389     $s = array();
390
391     if (!$yadis_services) {
392         return $s;
393     }
394
395     foreach ($yadis_services as $service) {
396         $type_uris = $service->getTypes();
397         $uris = $service->getURIs();
398
399         // If any Type URIs match and there is an endpoint URI
400         // specified, then this is an OpenID endpoint
401         if ($type_uris &&
402             $uris) {
403             foreach ($uris as $service_uri) {
404                 $openid_endpoint = new Auth_OpenID_ServiceEndpoint();
405                 if ($openid_endpoint->parseService($uri,
406                                                    $service_uri,
407                                                    $type_uris,
408                                                    $service)) {
409                     $s[] = $openid_endpoint;
410                 }
411             }
412         }
413     }
414
415     return $s;
416 }
417
418 function Auth_OpenID_discoverWithYadis($uri, &$fetcher,
419               $endpoint_filter='Auth_OpenID_getOPOrUserServices',
420               $discover_function=null)
421 {
422     // Discover OpenID services for a URI. Tries Yadis and falls back
423     // on old-style <link rel='...'> discovery if Yadis fails.
424
425     // Might raise a yadis.discover.DiscoveryFailure if no document
426     // came back for that URI at all.  I don't think falling back to
427     // OpenID 1.0 discovery on the same URL will help, so don't bother
428     // to catch it.
429     if ($discover_function === null) {
430         $discover_function = array('Auth_Yadis_Yadis', 'discover');
431     }
432
433     $openid_services = array();
434
435     $response = call_user_func_array($discover_function,
436                                      array($uri, &$fetcher));
437
438     $yadis_url = $response->normalized_uri;
439     $yadis_services = array();
440
441     if ($response->isFailure()) {
442         return array($uri, array());
443     }
444
445     $openid_services = Auth_OpenID_ServiceEndpoint::fromXRDS(
446                                          $yadis_url,
447                                          $response->response_text);
448
449     if (!$openid_services) {
450         if ($response->isXRDS()) {
451             return Auth_OpenID_discoverWithoutYadis($uri,
452                                                     $fetcher);
453         }
454
455         // Try to parse the response as HTML to get OpenID 1.0/1.1
456         // <link rel="...">
457         $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
458                                         $yadis_url,
459                                         $response->response_text);
460     }
461
462     $openid_services = call_user_func_array($endpoint_filter,
463                                             array(&$openid_services));
464
465     return array($yadis_url, $openid_services);
466 }
467
468 function Auth_OpenID_discoverURI($uri, &$fetcher)
469 {
470     $uri = Auth_OpenID::normalizeUrl($uri);
471     return Auth_OpenID_discoverWithYadis($uri, $fetcher);
472 }
473
474 function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher)
475 {
476     $http_resp = @$fetcher->get($uri);
477
478     if ($http_resp->status != 200 and $http_resp->status != 206) {
479         return array($uri, array());
480     }
481
482     $identity_url = $http_resp->final_url;
483
484     // Try to parse the response as HTML to get OpenID 1.0/1.1 <link
485     // rel="...">
486     $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
487                                            $identity_url,
488                                            $http_resp->body);
489
490     return array($identity_url, $openid_services);
491 }
492
493 function Auth_OpenID_discoverXRI($iname, &$fetcher)
494 {
495     $resolver = new Auth_Yadis_ProxyResolver($fetcher);
496     list($canonicalID, $yadis_services) =
497         $resolver->query($iname,
498                          Auth_OpenID_getOpenIDTypeURIs(),
499                          array('filter_MatchesAnyOpenIDType'));
500
501     $openid_services = Auth_OpenID_makeOpenIDEndpoints($iname,
502                                                        $yadis_services);
503
504     $openid_services = Auth_OpenID_getOPOrUserServices($openid_services);
505
506     for ($i = 0; $i < count($openid_services); $i++) {
507         $openid_services[$i]->canonicalID = $canonicalID;
508         $openid_services[$i]->claimed_id = $canonicalID;
509         $openid_services[$i]->display_identifier = $iname;
510     }
511
512     // FIXME: returned xri should probably be in some normal form
513     return array($iname, $openid_services);
514 }
515
516 function Auth_OpenID_discover($uri, &$fetcher)
517 {
518     // If the fetcher (i.e., PHP) doesn't support SSL, we can't do
519     // discovery on an HTTPS URL.
520     if ($fetcher->isHTTPS($uri) && !$fetcher->supportsSSL()) {
521         return array($uri, array());
522     }
523
524     if (Auth_Yadis_identifierScheme($uri) == 'XRI') {
525         $result = Auth_OpenID_discoverXRI($uri, $fetcher);
526     } else {
527         $result = Auth_OpenID_discoverURI($uri, $fetcher);
528     }
529
530     // If the fetcher doesn't support SSL, we can't interact with
531     // HTTPS server URLs; remove those endpoints from the list.
532     if (!$fetcher->supportsSSL()) {
533         $http_endpoints = array();
534         list($new_uri, $endpoints) = $result;
535
536         foreach ($endpoints as $e) {
537             if (!$fetcher->isHTTPS($e->server_url)) {
538                 $http_endpoints[] = $e;
539             }
540         }
541
542         $result = array($new_uri, $http_endpoints);
543     }
544
545     return $result;
546 }
547
548 ?>