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 [ 52 'xrds' => Auth_Yadis_XMLNS_XRDS, 53 'xrd' => Auth_Yadis_XMLNS_XRD_2_0, 54 ]; 55} 56 57/** 58 * @access private 59 * @param array $arr 60 * @return array 61 */ 62function Auth_Yadis_array_scramble($arr) 63{ 64 $result = []; 65 66 while (count($arr)) { 67 $index = array_rand($arr, 1); 68 $result[] = $arr[$index]; 69 unset($arr[$index]); 70 } 71 72 return $result; 73} 74 75/** 76 * This class represents a <Service> element in an XRDS document. 77 * Objects of this type are returned by 78 * Auth_Yadis_XRDS::services() and 79 * Auth_Yadis_Yadis::services(). Each object corresponds directly 80 * to a <Service> element in the XRDS and supplies a 81 * getElements($name) method which you should use to inspect the 82 * element's contents. See {@link Auth_Yadis_Yadis} for more 83 * information on the role this class plays in Yadis discovery. 84 * 85 * @package OpenID 86 */ 87class Auth_Yadis_Service { 88 89 public $element = null; 90 91 /** @var Auth_Yadis_XMLParser */ 92 public $parser = null; 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 = []; 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 = []; 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 = []; 134 $last = []; 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] = []; 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 = []; 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 /** @var Auth_Yadis_XMLParser */ 255 public $parser; 256 257 public $xrdNode; 258 259 public $allXrdNodes; 260 261 /** @var Auth_Yadis_Service[][] */ 262 public $serviceList; 263 264 /** 265 * Instantiate a Auth_Yadis_XRDS object. Requires an XPath 266 * instance which has been used to parse a valid XRDS document. 267 * 268 * @param Auth_Yadis_XMLParser $xmlParser 269 * @param array $xrdNodes 270 */ 271 function __construct($xmlParser, $xrdNodes) 272 { 273 $this->parser = $xmlParser; 274 $this->xrdNode = $xrdNodes[count($xrdNodes) - 1]; 275 $this->allXrdNodes = $xrdNodes; 276 $this->serviceList = []; 277 $this->_parse(); 278 } 279 280 /** 281 * Parse an XML string (XRDS document) and return either a 282 * Auth_Yadis_XRDS object or null, depending on whether the 283 * XRDS XML is valid. 284 * 285 * @param string $xml_string An XRDS XML string. 286 * @param array|null $extra_ns_map 287 * @return mixed $xrds An instance of Auth_Yadis_XRDS or null, 288 * depending on the validity of $xml_string 289 */ 290 static function parseXRDS($xml_string, $extra_ns_map = null) 291 { 292 $_null = null; 293 294 if (!$xml_string) { 295 return $_null; 296 } 297 298 $parser = Auth_Yadis_getXMLParser(); 299 300 $ns_map = Auth_Yadis_getNSMap(); 301 302 if ($extra_ns_map && is_array($extra_ns_map)) { 303 $ns_map = array_merge($ns_map, $extra_ns_map); 304 } 305 306 if (!($parser && $parser->init($xml_string, $ns_map))) { 307 return $_null; 308 } 309 310 // Try to get root element. 311 $root = $parser->evalXPath('/xrds:XRDS[1]'); 312 if (!$root) { 313 return $_null; 314 } 315 316 if (is_array($root)) { 317 $root = $root[0]; 318 } 319 320 $attrs = $parser->attributes($root); 321 322 if (array_key_exists('xmlns:xrd', $attrs) && 323 $attrs['xmlns:xrd'] != Auth_Yadis_XMLNS_XRDS) { 324 return $_null; 325 } else if (array_key_exists('xmlns', $attrs) && 326 preg_match('/xri/', $attrs['xmlns']) && 327 $attrs['xmlns'] != Auth_Yadis_XMLNS_XRD_2_0) { 328 return $_null; 329 } 330 331 // Get the last XRD node. 332 $xrd_nodes = $parser->evalXPath('/xrds:XRDS[1]/xrd:XRD'); 333 334 if (!$xrd_nodes) { 335 return $_null; 336 } 337 338 return new Auth_Yadis_XRDS($parser, $xrd_nodes); 339 } 340 341 /** 342 * @access private 343 * @param int $priority 344 * @param string $service 345 */ 346 function _addService($priority, $service) 347 { 348 $priority = intval($priority); 349 350 if (!array_key_exists($priority, $this->serviceList)) { 351 $this->serviceList[$priority] = []; 352 } 353 354 $this->serviceList[$priority][] = $service; 355 } 356 357 /** 358 * Creates the service list using nodes from the XRDS XML 359 * document. 360 * 361 * @access private 362 */ 363 function _parse() 364 { 365 $this->serviceList = []; 366 367 $services = $this->parser->evalXPath('xrd:Service', $this->xrdNode); 368 369 foreach ($services as $node) { 370 $s = new Auth_Yadis_Service(); 371 $s->element = $node; 372 $s->parser = $this->parser; 373 374 $priority = $s->getPriority(); 375 376 if ($priority === null) { 377 $priority = SERVICES_YADIS_MAX_PRIORITY; 378 } 379 380 $this->_addService($priority, $s); 381 } 382 } 383 384 /** 385 * Returns a list of service objects which correspond to <Service> 386 * elements in the XRDS XML document for this object. 387 * 388 * Optionally, an array of filter callbacks may be given to limit 389 * the list of returned service objects. Furthermore, the default 390 * mode is to return all service objects which match ANY of the 391 * specified filters, but $filter_mode may be 392 * SERVICES_YADIS_MATCH_ALL if you want to be sure that the 393 * returned services match all the given filters. See {@link 394 * Auth_Yadis_Yadis} for detailed usage information on filter 395 * functions. 396 * 397 * @param mixed $filters An array of callbacks to filter the 398 * returned services, or null if all services are to be returned. 399 * @param integer $filter_mode SERVICES_YADIS_MATCH_ALL or 400 * SERVICES_YADIS_MATCH_ANY, depending on whether the returned 401 * services should match ALL or ANY of the specified filters, 402 * respectively. 403 * @return mixed $services An array of {@link 404 * Auth_Yadis_Service} objects if $filter_mode is a valid 405 * mode; null if $filter_mode is an invalid mode (i.e., not 406 * SERVICES_YADIS_MATCH_ANY or SERVICES_YADIS_MATCH_ALL). 407 */ 408 function services($filters = null, 409 $filter_mode = SERVICES_YADIS_MATCH_ANY) 410 { 411 412 $pri_keys = array_keys($this->serviceList); 413 sort($pri_keys, SORT_NUMERIC); 414 415 // If no filters are specified, return the entire service 416 // list, ordered by priority. 417 if (!$filters || 418 (!is_array($filters))) { 419 420 $result = []; 421 foreach ($pri_keys as $pri) { 422 $result = array_merge($result, $this->serviceList[$pri]); 423 } 424 425 return $result; 426 } 427 428 // If a bad filter mode is specified, return null. 429 if (!in_array($filter_mode, [ 430 SERVICES_YADIS_MATCH_ANY, 431 SERVICES_YADIS_MATCH_ALL, 432 ])) { 433 return null; 434 } 435 436 // Otherwise, use the callbacks in the filter list to 437 // determine which services are returned. 438 $filtered = []; 439 440 foreach ($pri_keys as $priority_value) { 441 $service_obj_list = $this->serviceList[$priority_value]; 442 443 foreach ($service_obj_list as $service) { 444 445 $matches = 0; 446 447 foreach ($filters as $filter) { 448 449 if (call_user_func_array($filter, [$service])) { 450 $matches++; 451 452 if ($filter_mode == SERVICES_YADIS_MATCH_ANY) { 453 $pri = $service->getPriority(); 454 if ($pri === null) { 455 $pri = SERVICES_YADIS_MAX_PRIORITY; 456 } 457 458 if (!array_key_exists($pri, $filtered)) { 459 $filtered[$pri] = []; 460 } 461 462 $filtered[$pri][] = $service; 463 break; 464 } 465 } 466 } 467 468 if (($filter_mode == SERVICES_YADIS_MATCH_ALL) && 469 ($matches == count($filters))) { 470 471 $pri = $service->getPriority(); 472 if ($pri === null) { 473 $pri = SERVICES_YADIS_MAX_PRIORITY; 474 } 475 476 if (!array_key_exists($pri, $filtered)) { 477 $filtered[$pri] = []; 478 } 479 $filtered[$pri][] = $service; 480 } 481 } 482 } 483 484 $pri_keys = array_keys($filtered); 485 sort($pri_keys, SORT_NUMERIC); 486 487 $result = []; 488 foreach ($pri_keys as $pri) { 489 $result = array_merge($result, $filtered[$pri]); 490 } 491 492 return $result; 493 } 494} 495 496