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 */ 19require_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 */ 25define('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 */ 31define('SERVICES_YADIS_MATCH_ANY', 102); 32 33/** 34 * The priority value used for service elements with no priority 35 * specified. 36 */ 37define('SERVICES_YADIS_MAX_PRIORITY', pow(2, 30)); 38 39/** 40 * XRD XML namespace 41 */ 42define('Auth_Yadis_XMLNS_XRD_2_0', 'xri://$xrd*($v*2.0)'); 43 44/** 45 * XRDS XML namespace 46 */ 47define('Auth_Yadis_XMLNS_XRDS', 'xri://$xrds'); 48 49function 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 */ 58function 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 */ 83class 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 */ 214function 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 */ 252class 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