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: