1: <?php
  2: 
  3: /**
  4:  * This module contains the XRDS parsing code.
  5:  *
  6:  * PHP versions 4 and 5
  7:  *
  8:  * LICENSE: See the COPYING file included in this distribution.
  9:  *
 10:  * @package OpenID
 11:  * @author JanRain, Inc. <openid@janrain.com>
 12:  * @copyright 2005-2008 Janrain, Inc.
 13:  * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
 14:  */
 15: 
 16: /**
 17:  * Require the XPath implementation.
 18:  */
 19: require_once 'Auth/Yadis/XML.php';
 20: 
 21: /**
 22:  * This match mode means a given service must match ALL filters passed
 23:  * to the Auth_Yadis_XRDS::services() call.
 24:  */
 25: define('SERVICES_YADIS_MATCH_ALL', 101);
 26: 
 27: /**
 28:  * This match mode means a given service must match ANY filters (at
 29:  * least one) passed to the Auth_Yadis_XRDS::services() call.
 30:  */
 31: define('SERVICES_YADIS_MATCH_ANY', 102);
 32: 
 33: /**
 34:  * The priority value used for service elements with no priority
 35:  * specified.
 36:  */
 37: define('SERVICES_YADIS_MAX_PRIORITY', pow(2, 30));
 38: 
 39: /**
 40:  * XRD XML namespace
 41:  */
 42: define('Auth_Yadis_XMLNS_XRD_2_0', 'xri://$xrd*($v*2.0)');
 43: 
 44: /**
 45:  * XRDS XML namespace
 46:  */
 47: define('Auth_Yadis_XMLNS_XRDS', 'xri://$xrds');
 48: 
 49: function Auth_Yadis_getNSMap()
 50: {
 51:     return array('xrds' => Auth_Yadis_XMLNS_XRDS,
 52:                  'xrd' => Auth_Yadis_XMLNS_XRD_2_0);
 53: }
 54: 
 55: /**
 56:  * @access private
 57:  */
 58: function Auth_Yadis_array_scramble($arr)
 59: {
 60:     $result = array();
 61: 
 62:     while (count($arr)) {
 63:         $index = array_rand($arr, 1);
 64:         $result[] = $arr[$index];
 65:         unset($arr[$index]);
 66:     }
 67: 
 68:     return $result;
 69: }
 70: 
 71: /**
 72:  * This class represents a <Service> element in an XRDS document.
 73:  * Objects of this type are returned by
 74:  * Auth_Yadis_XRDS::services() and
 75:  * Auth_Yadis_Yadis::services().  Each object corresponds directly
 76:  * to a <Service> element in the XRDS and supplies a
 77:  * getElements($name) method which you should use to inspect the
 78:  * element's contents.  See {@link Auth_Yadis_Yadis} for more
 79:  * information on the role this class plays in Yadis discovery.
 80:  *
 81:  * @package OpenID
 82:  */
 83: class Auth_Yadis_Service {
 84: 
 85:     /**
 86:      * Creates an empty service object.
 87:      */
 88:     function Auth_Yadis_Service()
 89:     {
 90:         $this->element = null;
 91:         $this->parser = null;
 92:     }
 93: 
 94:     /**
 95:      * Return the URIs in the "Type" elements, if any, of this Service
 96:      * element.
 97:      *
 98:      * @return array $type_uris An array of Type URI strings.
 99:      */
100:     function getTypes()
101:     {
102:         $t = array();
103:         foreach ($this->getElements('xrd:Type') as $elem) {
104:             $c = $this->parser->content($elem);
105:             if ($c) {
106:                 $t[] = $c;
107:             }
108:         }
109:         return $t;
110:     }
111: 
112:     function matchTypes($type_uris)
113:     {
114:         $result = array();
115: 
116:         foreach ($this->getTypes() as $typ) {
117:             if (in_array($typ, $type_uris)) {
118:                 $result[] = $typ;
119:             }
120:         }
121: 
122:         return $result;
123:     }
124: 
125:     /**
126:      * Return the URIs in the "URI" elements, if any, of this Service
127:      * element.  The URIs are returned sorted in priority order.
128:      *
129:      * @return array $uris An array of URI strings.
130:      */
131:     function getURIs()
132:     {
133:         $uris = array();
134:         $last = array();
135: 
136:         foreach ($this->getElements('xrd:URI') as $elem) {
137:             $uri_string = $this->parser->content($elem);
138:             $attrs = $this->parser->attributes($elem);
139:             if ($attrs &&
140:                 array_key_exists('priority', $attrs)) {
141:                 $priority = intval($attrs['priority']);
142:                 if (!array_key_exists($priority, $uris)) {
143:                     $uris[$priority] = array();
144:                 }
145: 
146:                 $uris[$priority][] = $uri_string;
147:             } else {
148:                 $last[] = $uri_string;
149:             }
150:         }
151: 
152:         $keys = array_keys($uris);
153:         sort($keys);
154: 
155:         // Rebuild array of URIs.
156:         $result = array();
157:         foreach ($keys as $k) {
158:             $new_uris = Auth_Yadis_array_scramble($uris[$k]);
159:             $result = array_merge($result, $new_uris);
160:         }
161: 
162:         $result = array_merge($result,
163:                               Auth_Yadis_array_scramble($last));
164: 
165:         return $result;
166:     }
167: 
168:     /**
169:      * Returns the "priority" attribute value of this <Service>
170:      * element, if the attribute is present.  Returns null if not.
171:      *
172:      * @return mixed $result Null or integer, depending on whether
173:      * this Service element has a 'priority' attribute.
174:      */
175:     function getPriority()
176:     {
177:         $attributes = $this->parser->attributes($this->element);
178: 
179:         if (array_key_exists('priority', $attributes)) {
180:             return intval($attributes['priority']);
181:         }
182: 
183:         return null;
184:     }
185: 
186:     /**
187:      * Used to get XML elements from this object's <Service> element.
188:      *
189:      * This is what you should use to get all custom information out
190:      * of this element. This is used by service filter functions to
191:      * determine whether a service element contains specific tags,
192:      * etc.  NOTE: this only considers elements which are direct
193:      * children of the <Service> element for this object.
194:      *
195:      * @param string $name The name of the element to look for
196:      * @return array $list An array of elements with the specified
197:      * name which are direct children of the <Service> element.  The
198:      * nodes returned by this function can be passed to $this->parser
199:      * methods (see {@link Auth_Yadis_XMLParser}).
200:      */
201:     function getElements($name)
202:     {
203:         return $this->parser->evalXPath($name, $this->element);
204:     }
205: }
206: 
207: /*
208:  * Return the expiration date of this XRD element, or None if no
209:  * expiration was specified.
210:  *
211:  * @param $default The value to use as the expiration if no expiration
212:  * was specified in the XRD.
213:  */
214: function Auth_Yadis_getXRDExpiration($xrd_element, $default=null)
215: {
216:     $expires_element = $xrd_element->parser->evalXPath('/xrd:Expires');
217:     if ($expires_element === null) {
218:         return $default;
219:     } else {
220:         $expires_string = $expires_element->text;
221: 
222:         // Will raise ValueError if the string is not the expected
223:         // format
224:         $t = strptime($expires_string, "%Y-%m-%dT%H:%M:%SZ");
225: 
226:         if ($t === false) {
227:             return false;
228:         }
229: 
230:         // [int $hour [, int $minute [, int $second [,
231:         //  int $month [, int $day [, int $year ]]]]]]
232:         return mktime($t['tm_hour'], $t['tm_min'], $t['tm_sec'],
233:                       $t['tm_mon'], $t['tm_day'], $t['tm_year']);
234:     }
235: }
236: 
237: /**
238:  * This class performs parsing of XRDS documents.
239:  *
240:  * You should not instantiate this class directly; rather, call
241:  * parseXRDS statically:
242:  *
243:  * <pre>  $xrds = Auth_Yadis_XRDS::parseXRDS($xml_string);</pre>
244:  *
245:  * If the XRDS can be parsed and is valid, an instance of
246:  * Auth_Yadis_XRDS will be returned.  Otherwise, null will be
247:  * returned.  This class is used by the Auth_Yadis_Yadis::discover
248:  * method.
249:  *
250:  * @package OpenID
251:  */
252: class Auth_Yadis_XRDS {
253: 
254:     /**
255:      * Instantiate a Auth_Yadis_XRDS object.  Requires an XPath
256:      * instance which has been used to parse a valid XRDS document.
257:      */
258:     function Auth_Yadis_XRDS($xmlParser, $xrdNodes)
259:     {
260:         $this->parser = $xmlParser;
261:         $this->xrdNode = $xrdNodes[count($xrdNodes) - 1];
262:         $this->allXrdNodes = $xrdNodes;
263:         $this->serviceList = array();
264:         $this->_parse();
265:     }
266: 
267:     /**
268:      * Parse an XML string (XRDS document) and return either a
269:      * Auth_Yadis_XRDS object or null, depending on whether the
270:      * XRDS XML is valid.
271:      *
272:      * @param string $xml_string An XRDS XML string.
273:      * @return mixed $xrds An instance of Auth_Yadis_XRDS or null,
274:      * depending on the validity of $xml_string
275:      */
276:     static function parseXRDS($xml_string, $extra_ns_map = null)
277:     {
278:         $_null = null;
279: 
280:         if (!$xml_string) {
281:             return $_null;
282:         }
283: 
284:         $parser = Auth_Yadis_getXMLParser();
285: 
286:         $ns_map = Auth_Yadis_getNSMap();
287: 
288:         if ($extra_ns_map && is_array($extra_ns_map)) {
289:             $ns_map = array_merge($ns_map, $extra_ns_map);
290:         }
291: 
292:         if (!($parser && $parser->init($xml_string, $ns_map))) {
293:             return $_null;
294:         }
295: 
296:         // Try to get root element.
297:         $root = $parser->evalXPath('/xrds:XRDS[1]');
298:         if (!$root) {
299:             return $_null;
300:         }
301: 
302:         if (is_array($root)) {
303:             $root = $root[0];
304:         }
305: 
306:         $attrs = $parser->attributes($root);
307: 
308:         if (array_key_exists('xmlns:xrd', $attrs) &&
309:             $attrs['xmlns:xrd'] != Auth_Yadis_XMLNS_XRDS) {
310:             return $_null;
311:         } else if (array_key_exists('xmlns', $attrs) &&
312:                    preg_match('/xri/', $attrs['xmlns']) &&
313:                    $attrs['xmlns'] != Auth_Yadis_XMLNS_XRD_2_0) {
314:             return $_null;
315:         }
316: 
317:         // Get the last XRD node.
318:         $xrd_nodes = $parser->evalXPath('/xrds:XRDS[1]/xrd:XRD');
319: 
320:         if (!$xrd_nodes) {
321:             return $_null;
322:         }
323: 
324:         $xrds = new Auth_Yadis_XRDS($parser, $xrd_nodes);
325:         return $xrds;
326:     }
327: 
328:     /**
329:      * @access private
330:      */
331:     function _addService($priority, $service)
332:     {
333:         $priority = intval($priority);
334: 
335:         if (!array_key_exists($priority, $this->serviceList)) {
336:             $this->serviceList[$priority] = array();
337:         }
338: 
339:         $this->serviceList[$priority][] = $service;
340:     }
341: 
342:     /**
343:      * Creates the service list using nodes from the XRDS XML
344:      * document.
345:      *
346:      * @access private
347:      */
348:     function _parse()
349:     {
350:         $this->serviceList = array();
351: 
352:         $services = $this->parser->evalXPath('xrd:Service', $this->xrdNode);
353: 
354:         foreach ($services as $node) {
355:             $s = new Auth_Yadis_Service();
356:             $s->element = $node;
357:             $s->parser = $this->parser;
358: 
359:             $priority = $s->getPriority();
360: 
361:             if ($priority === null) {
362:                 $priority = SERVICES_YADIS_MAX_PRIORITY;
363:             }
364: 
365:             $this->_addService($priority, $s);
366:         }
367:     }
368: 
369:     /**
370:      * Returns a list of service objects which correspond to <Service>
371:      * elements in the XRDS XML document for this object.
372:      *
373:      * Optionally, an array of filter callbacks may be given to limit
374:      * the list of returned service objects.  Furthermore, the default
375:      * mode is to return all service objects which match ANY of the
376:      * specified filters, but $filter_mode may be
377:      * SERVICES_YADIS_MATCH_ALL if you want to be sure that the
378:      * returned services match all the given filters.  See {@link
379:      * Auth_Yadis_Yadis} for detailed usage information on filter
380:      * functions.
381:      *
382:      * @param mixed $filters An array of callbacks to filter the
383:      * returned services, or null if all services are to be returned.
384:      * @param integer $filter_mode SERVICES_YADIS_MATCH_ALL or
385:      * SERVICES_YADIS_MATCH_ANY, depending on whether the returned
386:      * services should match ALL or ANY of the specified filters,
387:      * respectively.
388:      * @return mixed $services An array of {@link
389:      * Auth_Yadis_Service} objects if $filter_mode is a valid
390:      * mode; null if $filter_mode is an invalid mode (i.e., not
391:      * SERVICES_YADIS_MATCH_ANY or SERVICES_YADIS_MATCH_ALL).
392:      */
393:     function services($filters = null,
394:                       $filter_mode = SERVICES_YADIS_MATCH_ANY)
395:     {
396: 
397:         $pri_keys = array_keys($this->serviceList);
398:         sort($pri_keys, SORT_NUMERIC);
399: 
400:         // If no filters are specified, return the entire service
401:         // list, ordered by priority.
402:         if (!$filters ||
403:             (!is_array($filters))) {
404: 
405:             $result = array();
406:             foreach ($pri_keys as $pri) {
407:                 $result = array_merge($result, $this->serviceList[$pri]);
408:             }
409: 
410:             return $result;
411:         }
412: 
413:         // If a bad filter mode is specified, return null.
414:         if (!in_array($filter_mode, array(SERVICES_YADIS_MATCH_ANY,
415:                                           SERVICES_YADIS_MATCH_ALL))) {
416:             return null;
417:         }
418: 
419:         // Otherwise, use the callbacks in the filter list to
420:         // determine which services are returned.
421:         $filtered = array();
422: 
423:         foreach ($pri_keys as $priority_value) {
424:             $service_obj_list = $this->serviceList[$priority_value];
425: 
426:             foreach ($service_obj_list as $service) {
427: 
428:                 $matches = 0;
429: 
430:                 foreach ($filters as $filter) {
431: 
432:                     if (call_user_func_array($filter, array(&$service))) {
433:                         $matches++;
434: 
435:                         if ($filter_mode == SERVICES_YADIS_MATCH_ANY) {
436:                             $pri = $service->getPriority();
437:                             if ($pri === null) {
438:                                 $pri = SERVICES_YADIS_MAX_PRIORITY;
439:                             }
440: 
441:                             if (!array_key_exists($pri, $filtered)) {
442:                                 $filtered[$pri] = array();
443:                             }
444: 
445:                             $filtered[$pri][] = $service;
446:                             break;
447:                         }
448:                     }
449:                 }
450: 
451:                 if (($filter_mode == SERVICES_YADIS_MATCH_ALL) &&
452:                     ($matches == count($filters))) {
453: 
454:                     $pri = $service->getPriority();
455:                     if ($pri === null) {
456:                         $pri = SERVICES_YADIS_MAX_PRIORITY;
457:                     }
458: 
459:                     if (!array_key_exists($pri, $filtered)) {
460:                         $filtered[$pri] = array();
461:                     }
462:                     $filtered[$pri][] = $service;
463:                 }
464:             }
465:         }
466: 
467:         $pri_keys = array_keys($filtered);
468:         sort($pri_keys, SORT_NUMERIC);
469: 
470:         $result = array();
471:         foreach ($pri_keys as $pri) {
472:             $result = array_merge($result, $filtered[$pri]);
473:         }
474: 
475:         return $result;
476:     }
477: }
478: 
479: