--- a/lib/openid-php/Auth/Yadis/XRDS.php +++ b/lib/openid-php/Auth/Yadis/XRDS.php @@ -1,1 +1,479 @@ - + + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require the XPath implementation. + */ +require_once 'Auth/Yadis/XML.php'; + +/** + * This match mode means a given service must match ALL filters passed + * to the Auth_Yadis_XRDS::services() call. + */ +define('SERVICES_YADIS_MATCH_ALL', 101); + +/** + * This match mode means a given service must match ANY filters (at + * least one) passed to the Auth_Yadis_XRDS::services() call. + */ +define('SERVICES_YADIS_MATCH_ANY', 102); + +/** + * The priority value used for service elements with no priority + * specified. + */ +define('SERVICES_YADIS_MAX_PRIORITY', pow(2, 30)); + +/** + * XRD XML namespace + */ +define('Auth_Yadis_XMLNS_XRD_2_0', 'xri://$xrd*($v*2.0)'); + +/** + * XRDS XML namespace + */ +define('Auth_Yadis_XMLNS_XRDS', 'xri://$xrds'); + +function Auth_Yadis_getNSMap() +{ + return array('xrds' => Auth_Yadis_XMLNS_XRDS, + 'xrd' => Auth_Yadis_XMLNS_XRD_2_0); +} + +/** + * @access private + */ +function Auth_Yadis_array_scramble($arr) +{ + $result = array(); + + while (count($arr)) { + $index = array_rand($arr, 1); + $result[] = $arr[$index]; + unset($arr[$index]); + } + + return $result; +} + +/** + * This class represents a element in an XRDS document. + * Objects of this type are returned by + * Auth_Yadis_XRDS::services() and + * Auth_Yadis_Yadis::services(). Each object corresponds directly + * to a element in the XRDS and supplies a + * getElements($name) method which you should use to inspect the + * element's contents. See {@link Auth_Yadis_Yadis} for more + * information on the role this class plays in Yadis discovery. + * + * @package OpenID + */ +class Auth_Yadis_Service { + + /** + * Creates an empty service object. + */ + function Auth_Yadis_Service() + { + $this->element = null; + $this->parser = null; + } + + /** + * Return the URIs in the "Type" elements, if any, of this Service + * element. + * + * @return array $type_uris An array of Type URI strings. + */ + function getTypes() + { + $t = array(); + foreach ($this->getElements('xrd:Type') as $elem) { + $c = $this->parser->content($elem); + if ($c) { + $t[] = $c; + } + } + return $t; + } + + function matchTypes($type_uris) + { + $result = array(); + + foreach ($this->getTypes() as $typ) { + if (in_array($typ, $type_uris)) { + $result[] = $typ; + } + } + + return $result; + } + + /** + * Return the URIs in the "URI" elements, if any, of this Service + * element. The URIs are returned sorted in priority order. + * + * @return array $uris An array of URI strings. + */ + function getURIs() + { + $uris = array(); + $last = array(); + + foreach ($this->getElements('xrd:URI') as $elem) { + $uri_string = $this->parser->content($elem); + $attrs = $this->parser->attributes($elem); + if ($attrs && + array_key_exists('priority', $attrs)) { + $priority = intval($attrs['priority']); + if (!array_key_exists($priority, $uris)) { + $uris[$priority] = array(); + } + + $uris[$priority][] = $uri_string; + } else { + $last[] = $uri_string; + } + } + + $keys = array_keys($uris); + sort($keys); + + // Rebuild array of URIs. + $result = array(); + foreach ($keys as $k) { + $new_uris = Auth_Yadis_array_scramble($uris[$k]); + $result = array_merge($result, $new_uris); + } + + $result = array_merge($result, + Auth_Yadis_array_scramble($last)); + + return $result; + } + + /** + * Returns the "priority" attribute value of this + * element, if the attribute is present. Returns null if not. + * + * @return mixed $result Null or integer, depending on whether + * this Service element has a 'priority' attribute. + */ + function getPriority() + { + $attributes = $this->parser->attributes($this->element); + + if (array_key_exists('priority', $attributes)) { + return intval($attributes['priority']); + } + + return null; + } + + /** + * Used to get XML elements from this object's element. + * + * This is what you should use to get all custom information out + * of this element. This is used by service filter functions to + * determine whether a service element contains specific tags, + * etc. NOTE: this only considers elements which are direct + * children of the element for this object. + * + * @param string $name The name of the element to look for + * @return array $list An array of elements with the specified + * name which are direct children of the element. The + * nodes returned by this function can be passed to $this->parser + * methods (see {@link Auth_Yadis_XMLParser}). + */ + function getElements($name) + { + return $this->parser->evalXPath($name, $this->element); + } +} + +/* + * Return the expiration date of this XRD element, or None if no + * expiration was specified. + * + * @param $default The value to use as the expiration if no expiration + * was specified in the XRD. + */ +function Auth_Yadis_getXRDExpiration($xrd_element, $default=null) +{ + $expires_element = $xrd_element->$parser->evalXPath('/xrd:Expires'); + if ($expires_element === null) { + return $default; + } else { + $expires_string = $expires_element->text; + + // Will raise ValueError if the string is not the expected + // format + $t = strptime($expires_string, "%Y-%m-%dT%H:%M:%SZ"); + + if ($t === false) { + return false; + } + + // [int $hour [, int $minute [, int $second [, + // int $month [, int $day [, int $year ]]]]]] + return mktime($t['tm_hour'], $t['tm_min'], $t['tm_sec'], + $t['tm_mon'], $t['tm_day'], $t['tm_year']); + } +} + +/** + * This class performs parsing of XRDS documents. + * + * You should not instantiate this class directly; rather, call + * parseXRDS statically: + * + *
  $xrds = Auth_Yadis_XRDS::parseXRDS($xml_string);
+ * + * If the XRDS can be parsed and is valid, an instance of + * Auth_Yadis_XRDS will be returned. Otherwise, null will be + * returned. This class is used by the Auth_Yadis_Yadis::discover + * method. + * + * @package OpenID + */ +class Auth_Yadis_XRDS { + + /** + * Instantiate a Auth_Yadis_XRDS object. Requires an XPath + * instance which has been used to parse a valid XRDS document. + */ + function Auth_Yadis_XRDS($xmlParser, $xrdNodes) + { + $this->parser = $xmlParser; + $this->xrdNode = $xrdNodes[count($xrdNodes) - 1]; + $this->allXrdNodes = $xrdNodes; + $this->serviceList = array(); + $this->_parse(); + } + + /** + * Parse an XML string (XRDS document) and return either a + * Auth_Yadis_XRDS object or null, depending on whether the + * XRDS XML is valid. + * + * @param string $xml_string An XRDS XML string. + * @return mixed $xrds An instance of Auth_Yadis_XRDS or null, + * depending on the validity of $xml_string + */ + static function parseXRDS($xml_string, $extra_ns_map = null) + { + $_null = null; + + if (!$xml_string) { + return $_null; + } + + $parser = Auth_Yadis_getXMLParser(); + + $ns_map = Auth_Yadis_getNSMap(); + + if ($extra_ns_map && is_array($extra_ns_map)) { + $ns_map = array_merge($ns_map, $extra_ns_map); + } + + if (!($parser && $parser->init($xml_string, $ns_map))) { + return $_null; + } + + // Try to get root element. + $root = $parser->evalXPath('/xrds:XRDS[1]'); + if (!$root) { + return $_null; + } + + if (is_array($root)) { + $root = $root[0]; + } + + $attrs = $parser->attributes($root); + + if (array_key_exists('xmlns:xrd', $attrs) && + $attrs['xmlns:xrd'] != Auth_Yadis_XMLNS_XRDS) { + return $_null; + } else if (array_key_exists('xmlns', $attrs) && + preg_match('/xri/', $attrs['xmlns']) && + $attrs['xmlns'] != Auth_Yadis_XMLNS_XRD_2_0) { + return $_null; + } + + // Get the last XRD node. + $xrd_nodes = $parser->evalXPath('/xrds:XRDS[1]/xrd:XRD'); + + if (!$xrd_nodes) { + return $_null; + } + + $xrds = new Auth_Yadis_XRDS($parser, $xrd_nodes); + return $xrds; + } + + /** + * @access private + */ + function _addService($priority, $service) + { + $priority = intval($priority); + + if (!array_key_exists($priority, $this->serviceList)) { + $this->serviceList[$priority] = array(); + } + + $this->serviceList[$priority][] = $service; + } + + /** + * Creates the service list using nodes from the XRDS XML + * document. + * + * @access private + */ + function _parse() + { + $this->serviceList = array(); + + $services = $this->parser->evalXPath('xrd:Service', $this->xrdNode); + + foreach ($services as $node) { + $s = new Auth_Yadis_Service(); + $s->element = $node; + $s->parser = $this->parser; + + $priority = $s->getPriority(); + + if ($priority === null) { + $priority = SERVICES_YADIS_MAX_PRIORITY; + } + + $this->_addService($priority, $s); + } + } + + /** + * Returns a list of service objects which correspond to + * elements in the XRDS XML document for this object. + * + * Optionally, an array of filter callbacks may be given to limit + * the list of returned service objects. Furthermore, the default + * mode is to return all service objects which match ANY of the + * specified filters, but $filter_mode may be + * SERVICES_YADIS_MATCH_ALL if you want to be sure that the + * returned services match all the given filters. See {@link + * Auth_Yadis_Yadis} for detailed usage information on filter + * functions. + * + * @param mixed $filters An array of callbacks to filter the + * returned services, or null if all services are to be returned. + * @param integer $filter_mode SERVICES_YADIS_MATCH_ALL or + * SERVICES_YADIS_MATCH_ANY, depending on whether the returned + * services should match ALL or ANY of the specified filters, + * respectively. + * @return mixed $services An array of {@link + * Auth_Yadis_Service} objects if $filter_mode is a valid + * mode; null if $filter_mode is an invalid mode (i.e., not + * SERVICES_YADIS_MATCH_ANY or SERVICES_YADIS_MATCH_ALL). + */ + function services($filters = null, + $filter_mode = SERVICES_YADIS_MATCH_ANY) + { + + $pri_keys = array_keys($this->serviceList); + sort($pri_keys, SORT_NUMERIC); + + // If no filters are specified, return the entire service + // list, ordered by priority. + if (!$filters || + (!is_array($filters))) { + + $result = array(); + foreach ($pri_keys as $pri) { + $result = array_merge($result, $this->serviceList[$pri]); + } + + return $result; + } + + // If a bad filter mode is specified, return null. + if (!in_array($filter_mode, array(SERVICES_YADIS_MATCH_ANY, + SERVICES_YADIS_MATCH_ALL))) { + return null; + } + + // Otherwise, use the callbacks in the filter list to + // determine which services are returned. + $filtered = array(); + + foreach ($pri_keys as $priority_value) { + $service_obj_list = $this->serviceList[$priority_value]; + + foreach ($service_obj_list as $service) { + + $matches = 0; + + foreach ($filters as $filter) { + + if (call_user_func_array($filter, array($service))) { + $matches++; + + if ($filter_mode == SERVICES_YADIS_MATCH_ANY) { + $pri = $service->getPriority(); + if ($pri === null) { + $pri = SERVICES_YADIS_MAX_PRIORITY; + } + + if (!array_key_exists($pri, $filtered)) { + $filtered[$pri] = array(); + } + + $filtered[$pri][] = $service; + break; + } + } + } + + if (($filter_mode == SERVICES_YADIS_MATCH_ALL) && + ($matches == count($filters))) { + + $pri = $service->getPriority(); + if ($pri === null) { + $pri = SERVICES_YADIS_MAX_PRIORITY; + } + + if (!array_key_exists($pri, $filtered)) { + $filtered[$pri] = array(); + } + $filtered[$pri][] = $service; + } + } + } + + $pri_keys = array_keys($filtered); + sort($pri_keys, SORT_NUMERIC); + + $result = array(); + foreach ($pri_keys as $pri) { + $result = array_merge($result, $filtered[$pri]); + } + + return $result; + } +} + +