From 14c9366aac697e87499c5748b36fa7bf5e6cd320 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 22 Aug 2008 09:17:14 -0400 Subject: include external libs in a subdir to make install easier darcs-hash:20080822131714-84dde-6978424ded2ed1041a65142a25560654ac717fcd.gz --- extlib/Auth/Yadis/HTTPFetcher.php | 147 +++++++++ extlib/Auth/Yadis/Manager.php | 529 ++++++++++++++++++++++++++++++ extlib/Auth/Yadis/Misc.php | 59 ++++ extlib/Auth/Yadis/ParanoidHTTPFetcher.php | 228 +++++++++++++ extlib/Auth/Yadis/ParseHTML.php | 259 +++++++++++++++ extlib/Auth/Yadis/PlainHTTPFetcher.php | 251 ++++++++++++++ extlib/Auth/Yadis/XML.php | 374 +++++++++++++++++++++ extlib/Auth/Yadis/XRDS.php | 478 +++++++++++++++++++++++++++ extlib/Auth/Yadis/XRI.php | 234 +++++++++++++ extlib/Auth/Yadis/XRIRes.php | 72 ++++ extlib/Auth/Yadis/Yadis.php | 382 +++++++++++++++++++++ 11 files changed, 3013 insertions(+) create mode 100644 extlib/Auth/Yadis/HTTPFetcher.php create mode 100644 extlib/Auth/Yadis/Manager.php create mode 100644 extlib/Auth/Yadis/Misc.php create mode 100644 extlib/Auth/Yadis/ParanoidHTTPFetcher.php create mode 100644 extlib/Auth/Yadis/ParseHTML.php create mode 100644 extlib/Auth/Yadis/PlainHTTPFetcher.php create mode 100644 extlib/Auth/Yadis/XML.php create mode 100644 extlib/Auth/Yadis/XRDS.php create mode 100644 extlib/Auth/Yadis/XRI.php create mode 100644 extlib/Auth/Yadis/XRIRes.php create mode 100644 extlib/Auth/Yadis/Yadis.php (limited to 'extlib/Auth/Yadis') diff --git a/extlib/Auth/Yadis/HTTPFetcher.php b/extlib/Auth/Yadis/HTTPFetcher.php new file mode 100644 index 000000000..a1825403d --- /dev/null +++ b/extlib/Auth/Yadis/HTTPFetcher.php @@ -0,0 +1,147 @@ + + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require logging functionality + */ +require_once "Auth/OpenID.php"; + +define('Auth_OpenID_FETCHER_MAX_RESPONSE_KB', 1024); +define('Auth_OpenID_USER_AGENT', + 'php-openid/'.Auth_OpenID_VERSION.' (php/'.phpversion().')'); + +class Auth_Yadis_HTTPResponse { + function Auth_Yadis_HTTPResponse($final_url = null, $status = null, + $headers = null, $body = null) + { + $this->final_url = $final_url; + $this->status = $status; + $this->headers = $headers; + $this->body = $body; + } +} + +/** + * This class is the interface for HTTP fetchers the Yadis library + * uses. This interface is only important if you need to write a new + * fetcher for some reason. + * + * @access private + * @package OpenID + */ +class Auth_Yadis_HTTPFetcher { + + var $timeout = 20; // timeout in seconds. + + /** + * Return whether a URL can be fetched. Returns false if the URL + * scheme is not allowed or is not supported by this fetcher + * implementation; returns true otherwise. + * + * @return bool + */ + function canFetchURL($url) + { + if ($this->isHTTPS($url) && !$this->supportsSSL()) { + Auth_OpenID::log("HTTPS URL unsupported fetching %s", + $url); + return false; + } + + if (!$this->allowedURL($url)) { + Auth_OpenID::log("URL fetching not allowed for '%s'", + $url); + return false; + } + + return true; + } + + /** + * Return whether a URL should be allowed. Override this method to + * conform to your local policy. + * + * By default, will attempt to fetch any http or https URL. + */ + function allowedURL($url) + { + return $this->URLHasAllowedScheme($url); + } + + /** + * Does this fetcher implementation (and runtime) support fetching + * HTTPS URLs? May inspect the runtime environment. + * + * @return bool $support True if this fetcher supports HTTPS + * fetching; false if not. + */ + function supportsSSL() + { + trigger_error("not implemented", E_USER_ERROR); + } + + /** + * Is this an https URL? + * + * @access private + */ + function isHTTPS($url) + { + return (bool)preg_match('/^https:\/\//i', $url); + } + + /** + * Is this an http or https URL? + * + * @access private + */ + function URLHasAllowedScheme($url) + { + return (bool)preg_match('/^https?:\/\//i', $url); + } + + /** + * @access private + */ + function _findRedirect($headers) + { + foreach ($headers as $line) { + if (strpos(strtolower($line), "location: ") === 0) { + $parts = explode(" ", $line, 2); + return $parts[1]; + } + } + return null; + } + + /** + * Fetches the specified URL using optional extra headers and + * returns the server's response. + * + * @param string $url The URL to be fetched. + * @param array $extra_headers An array of header strings + * (e.g. "Accept: text/html"). + * @return mixed $result An array of ($code, $url, $headers, + * $body) if the URL could be fetched; null if the URL does not + * pass the URLHasAllowedScheme check or if the server's response + * is malformed. + */ + function get($url, $headers) + { + trigger_error("not implemented", E_USER_ERROR); + } +} + +?> \ No newline at end of file diff --git a/extlib/Auth/Yadis/Manager.php b/extlib/Auth/Yadis/Manager.php new file mode 100644 index 000000000..d50cf7ad6 --- /dev/null +++ b/extlib/Auth/Yadis/Manager.php @@ -0,0 +1,529 @@ +check($data) -> + * bool to implement your own session data validation. + * + * @package OpenID + */ +class Auth_Yadis_SessionLoader { + /** + * Override this. + * + * @access private + */ + function check($data) + { + return true; + } + + /** + * Given a session data value (an array), this creates an object + * (returned by $this->newObject()) whose attributes and values + * are those in $data. Returns null if $data lacks keys found in + * $this->requiredKeys(). Returns null if $this->check($data) + * evaluates to false. Returns null if $this->newObject() + * evaluates to false. + * + * @access private + */ + function fromSession($data) + { + if (!$data) { + return null; + } + + $required = $this->requiredKeys(); + + foreach ($required as $k) { + if (!array_key_exists($k, $data)) { + return null; + } + } + + if (!$this->check($data)) { + return null; + } + + $data = array_merge($data, $this->prepareForLoad($data)); + $obj = $this->newObject($data); + + if (!$obj) { + return null; + } + + foreach ($required as $k) { + $obj->$k = $data[$k]; + } + + return $obj; + } + + /** + * Prepares the data array by making any necessary changes. + * Returns an array whose keys and values will be used to update + * the original data array before calling $this->newObject($data). + * + * @access private + */ + function prepareForLoad($data) + { + return array(); + } + + /** + * Returns a new instance of this loader's class, using the + * session data to construct it if necessary. The object need + * only be created; $this->fromSession() will take care of setting + * the object's attributes. + * + * @access private + */ + function newObject($data) + { + return null; + } + + /** + * Returns an array of keys and values built from the attributes + * of $obj. If $this->prepareForSave($obj) returns an array, its keys + * and values are used to update the $data array of attributes + * from $obj. + * + * @access private + */ + function toSession($obj) + { + $data = array(); + foreach ($obj as $k => $v) { + $data[$k] = $v; + } + + $extra = $this->prepareForSave($obj); + + if ($extra && is_array($extra)) { + foreach ($extra as $k => $v) { + $data[$k] = $v; + } + } + + return $data; + } + + /** + * Override this. + * + * @access private + */ + function prepareForSave($obj) + { + return array(); + } +} + +/** + * A concrete loader implementation for Auth_OpenID_ServiceEndpoints. + * + * @package OpenID + */ +class Auth_OpenID_ServiceEndpointLoader extends Auth_Yadis_SessionLoader { + function newObject($data) + { + return new Auth_OpenID_ServiceEndpoint(); + } + + function requiredKeys() + { + $obj = new Auth_OpenID_ServiceEndpoint(); + $data = array(); + foreach ($obj as $k => $v) { + $data[] = $k; + } + return $data; + } + + function check($data) + { + return is_array($data['type_uris']); + } +} + +/** + * A concrete loader implementation for Auth_Yadis_Managers. + * + * @package OpenID + */ +class Auth_Yadis_ManagerLoader extends Auth_Yadis_SessionLoader { + function requiredKeys() + { + return array('starting_url', + 'yadis_url', + 'services', + 'session_key', + '_current', + 'stale'); + } + + function newObject($data) + { + return new Auth_Yadis_Manager($data['starting_url'], + $data['yadis_url'], + $data['services'], + $data['session_key']); + } + + function check($data) + { + return is_array($data['services']); + } + + function prepareForLoad($data) + { + $loader = new Auth_OpenID_ServiceEndpointLoader(); + $services = array(); + foreach ($data['services'] as $s) { + $services[] = $loader->fromSession($s); + } + return array('services' => $services); + } + + function prepareForSave($obj) + { + $loader = new Auth_OpenID_ServiceEndpointLoader(); + $services = array(); + foreach ($obj->services as $s) { + $services[] = $loader->toSession($s); + } + return array('services' => $services); + } +} + +/** + * The Yadis service manager which stores state in a session and + * iterates over elements in a Yadis XRDS document and lets + * a caller attempt to use each one. This is used by the Yadis + * library internally. + * + * @package OpenID + */ +class Auth_Yadis_Manager { + + /** + * Intialize a new yadis service manager. + * + * @access private + */ + function Auth_Yadis_Manager($starting_url, $yadis_url, + $services, $session_key) + { + // The URL that was used to initiate the Yadis protocol + $this->starting_url = $starting_url; + + // The URL after following redirects (the identifier) + $this->yadis_url = $yadis_url; + + // List of service elements + $this->services = $services; + + $this->session_key = $session_key; + + // Reference to the current service object + $this->_current = null; + + // Stale flag for cleanup if PHP lib has trouble. + $this->stale = false; + } + + /** + * @access private + */ + function length() + { + // How many untried services remain? + return count($this->services); + } + + /** + * Return the next service + * + * $this->current() will continue to return that service until the + * next call to this method. + */ + function nextService() + { + + if ($this->services) { + $this->_current = array_shift($this->services); + } else { + $this->_current = null; + } + + return $this->_current; + } + + /** + * @access private + */ + function current() + { + // Return the current service. + // Returns None if there are no services left. + return $this->_current; + } + + /** + * @access private + */ + function forURL($url) + { + return in_array($url, array($this->starting_url, $this->yadis_url)); + } + + /** + * @access private + */ + function started() + { + // Has the first service been returned? + return $this->_current !== null; + } +} + +/** + * State management for discovery. + * + * High-level usage pattern is to call .getNextService(discover) in + * order to find the next available service for this user for this + * session. Once a request completes, call .cleanup() to clean up the + * session state. + * + * @package OpenID + */ +class Auth_Yadis_Discovery { + + /** + * @access private + */ + var $DEFAULT_SUFFIX = 'auth'; + + /** + * @access private + */ + var $PREFIX = '_yadis_services_'; + + /** + * Initialize a discovery object. + * + * @param Auth_Yadis_PHPSession $session An object which + * implements the Auth_Yadis_PHPSession API. + * @param string $url The URL on which to attempt discovery. + * @param string $session_key_suffix The optional session key + * suffix override. + */ + function Auth_Yadis_Discovery(&$session, $url, + $session_key_suffix = null) + { + /// Initialize a discovery object + $this->session =& $session; + $this->url = $url; + if ($session_key_suffix === null) { + $session_key_suffix = $this->DEFAULT_SUFFIX; + } + + $this->session_key_suffix = $session_key_suffix; + $this->session_key = $this->PREFIX . $this->session_key_suffix; + } + + /** + * Return the next authentication service for the pair of + * user_input and session. This function handles fallback. + */ + function getNextService($discover_cb, &$fetcher) + { + $manager = $this->getManager(); + if (!$manager || (!$manager->services)) { + $this->destroyManager(); + + list($yadis_url, $services) = call_user_func($discover_cb, + $this->url, + $fetcher); + + $manager = $this->createManager($services, $yadis_url); + } + + if ($manager) { + $loader = new Auth_Yadis_ManagerLoader(); + $service = $manager->nextService(); + $this->session->set($this->session_key, + serialize($loader->toSession($manager))); + } else { + $service = null; + } + + return $service; + } + + /** + * Clean up Yadis-related services in the session and return the + * most-recently-attempted service from the manager, if one + * exists. + * + * @param $force True if the manager should be deleted regardless + * of whether it's a manager for $this->url. + */ + function cleanup($force=false) + { + $manager = $this->getManager($force); + if ($manager) { + $service = $manager->current(); + $this->destroyManager($force); + } else { + $service = null; + } + + return $service; + } + + /** + * @access private + */ + function getSessionKey() + { + // Get the session key for this starting URL and suffix + return $this->PREFIX . $this->session_key_suffix; + } + + /** + * @access private + * + * @param $force True if the manager should be returned regardless + * of whether it's a manager for $this->url. + */ + function &getManager($force=false) + { + // Extract the YadisServiceManager for this object's URL and + // suffix from the session. + + $manager_str = $this->session->get($this->getSessionKey()); + $manager = null; + + if ($manager_str !== null) { + $loader = new Auth_Yadis_ManagerLoader(); + $manager = $loader->fromSession(unserialize($manager_str)); + } + + if ($manager && ($manager->forURL($this->url) || $force)) { + return $manager; + } else { + $unused = null; + return $unused; + } + } + + /** + * @access private + */ + function &createManager($services, $yadis_url = null) + { + $key = $this->getSessionKey(); + if ($this->getManager()) { + return $this->getManager(); + } + + if ($services) { + $loader = new Auth_Yadis_ManagerLoader(); + $manager = new Auth_Yadis_Manager($this->url, $yadis_url, + $services, $key); + $this->session->set($this->session_key, + serialize($loader->toSession($manager))); + return $manager; + } else { + // Oh, PHP. + $unused = null; + return $unused; + } + } + + /** + * @access private + * + * @param $force True if the manager should be deleted regardless + * of whether it's a manager for $this->url. + */ + function destroyManager($force=false) + { + if ($this->getManager($force) !== null) { + $key = $this->getSessionKey(); + $this->session->del($key); + } + } +} + +?> \ No newline at end of file diff --git a/extlib/Auth/Yadis/Misc.php b/extlib/Auth/Yadis/Misc.php new file mode 100644 index 000000000..1134a4ff4 --- /dev/null +++ b/extlib/Auth/Yadis/Misc.php @@ -0,0 +1,59 @@ + + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +function Auth_Yadis_getUCSChars() +{ + return array( + array(0xA0, 0xD7FF), + array(0xF900, 0xFDCF), + array(0xFDF0, 0xFFEF), + array(0x10000, 0x1FFFD), + array(0x20000, 0x2FFFD), + array(0x30000, 0x3FFFD), + array(0x40000, 0x4FFFD), + array(0x50000, 0x5FFFD), + array(0x60000, 0x6FFFD), + array(0x70000, 0x7FFFD), + array(0x80000, 0x8FFFD), + array(0x90000, 0x9FFFD), + array(0xA0000, 0xAFFFD), + array(0xB0000, 0xBFFFD), + array(0xC0000, 0xCFFFD), + array(0xD0000, 0xDFFFD), + array(0xE1000, 0xEFFFD) + ); +} + +function Auth_Yadis_getIPrivateChars() +{ + return array( + array(0xE000, 0xF8FF), + array(0xF0000, 0xFFFFD), + array(0x100000, 0x10FFFD) + ); +} + +function Auth_Yadis_pct_escape_unicode($char_match) +{ + $c = $char_match[0]; + $result = ""; + for ($i = 0; $i < strlen($c); $i++) { + $result .= "%".sprintf("%X", ord($c[$i])); + } + return $result; +} + +function Auth_Yadis_startswith($s, $stuff) +{ + return strpos($s, $stuff) === 0; +} + +?> \ No newline at end of file diff --git a/extlib/Auth/Yadis/ParanoidHTTPFetcher.php b/extlib/Auth/Yadis/ParanoidHTTPFetcher.php new file mode 100644 index 000000000..8975d7f89 --- /dev/null +++ b/extlib/Auth/Yadis/ParanoidHTTPFetcher.php @@ -0,0 +1,228 @@ + + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Interface import + */ +require_once "Auth/Yadis/HTTPFetcher.php"; + +require_once "Auth/OpenID.php"; + +/** + * A paranoid {@link Auth_Yadis_HTTPFetcher} class which uses CURL + * for fetching. + * + * @package OpenID + */ +class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { + function Auth_Yadis_ParanoidHTTPFetcher() + { + $this->reset(); + } + + function reset() + { + $this->headers = array(); + $this->data = ""; + } + + /** + * @access private + */ + function _writeHeader($ch, $header) + { + array_push($this->headers, rtrim($header)); + return strlen($header); + } + + /** + * @access private + */ + function _writeData($ch, $data) + { + if (strlen($this->data) > 1024*Auth_OpenID_FETCHER_MAX_RESPONSE_KB) { + return 0; + } else { + $this->data .= $data; + return strlen($data); + } + } + + /** + * Does this fetcher support SSL URLs? + */ + function supportsSSL() + { + $v = curl_version(); + if(is_array($v)) { + return in_array('https', $v['protocols']); + } elseif (is_string($v)) { + return preg_match('/OpenSSL/i', $v); + } else { + return 0; + } + } + + function get($url, $extra_headers = null) + { + if (!$this->canFetchURL($url)) { + return null; + } + + $stop = time() + $this->timeout; + $off = $this->timeout; + + $redir = true; + + while ($redir && ($off > 0)) { + $this->reset(); + + $c = curl_init(); + + if ($c === false) { + Auth_OpenID::log( + "curl_init returned false; could not " . + "initialize for URL '%s'", $url); + return null; + } + + if (defined('CURLOPT_NOSIGNAL')) { + curl_setopt($c, CURLOPT_NOSIGNAL, true); + } + + if (!$this->allowedURL($url)) { + Auth_OpenID::log("Fetching URL not allowed: %s", + $url); + return null; + } + + curl_setopt($c, CURLOPT_WRITEFUNCTION, + array(&$this, "_writeData")); + curl_setopt($c, CURLOPT_HEADERFUNCTION, + array(&$this, "_writeHeader")); + + if ($extra_headers) { + curl_setopt($c, CURLOPT_HTTPHEADER, $extra_headers); + } + + $cv = curl_version(); + if(is_array($cv)) { + $curl_user_agent = 'curl/'.$cv['version']; + } else { + $curl_user_agent = $cv; + } + curl_setopt($c, CURLOPT_USERAGENT, + Auth_OpenID_USER_AGENT.' '.$curl_user_agent); + curl_setopt($c, CURLOPT_TIMEOUT, $off); + curl_setopt($c, CURLOPT_URL, $url); + curl_setopt($c, CURLOPT_RANGE, + "0-".(1024 * Auth_OpenID_FETCHER_MAX_RESPONSE_KB)); + + curl_exec($c); + + $code = curl_getinfo($c, CURLINFO_HTTP_CODE); + $body = $this->data; + $headers = $this->headers; + + if (!$code) { + Auth_OpenID::log("Got no response code when fetching %s", $url); + Auth_OpenID::log("CURL error (%s): %s", + curl_errno($c), curl_error($c)); + return null; + } + + if (in_array($code, array(301, 302, 303, 307))) { + $url = $this->_findRedirect($headers); + $redir = true; + } else { + $redir = false; + curl_close($c); + + $new_headers = array(); + + foreach ($headers as $header) { + if (strpos($header, ': ')) { + list($name, $value) = explode(': ', $header, 2); + $new_headers[$name] = $value; + } + } + + Auth_OpenID::log( + "Successfully fetched '%s': GET response code %s", + $url, $code); + + return new Auth_Yadis_HTTPResponse($url, $code, + $new_headers, $body); + } + + $off = $stop - time(); + } + + return null; + } + + function post($url, $body, $extra_headers = null) + { + if (!$this->canFetchURL($url)) { + return null; + } + + $this->reset(); + + $c = curl_init(); + + if (defined('CURLOPT_NOSIGNAL')) { + curl_setopt($c, CURLOPT_NOSIGNAL, true); + } + + curl_setopt($c, CURLOPT_POST, true); + curl_setopt($c, CURLOPT_POSTFIELDS, $body); + curl_setopt($c, CURLOPT_TIMEOUT, $this->timeout); + curl_setopt($c, CURLOPT_URL, $url); + curl_setopt($c, CURLOPT_WRITEFUNCTION, + array(&$this, "_writeData")); + + curl_exec($c); + + $code = curl_getinfo($c, CURLINFO_HTTP_CODE); + + if (!$code) { + Auth_OpenID::log("Got no response code when fetching %s", $url); + return null; + } + + $body = $this->data; + + curl_close($c); + + $new_headers = $extra_headers; + + foreach ($this->headers as $header) { + if (strpos($header, ': ')) { + list($name, $value) = explode(': ', $header, 2); + $new_headers[$name] = $value; + } + + } + + Auth_OpenID::log("Successfully fetched '%s': POST response code %s", + $url, $code); + + return new Auth_Yadis_HTTPResponse($url, $code, + $new_headers, $body); + } +} + +?> \ No newline at end of file diff --git a/extlib/Auth/Yadis/ParseHTML.php b/extlib/Auth/Yadis/ParseHTML.php new file mode 100644 index 000000000..297ccbd2c --- /dev/null +++ b/extlib/Auth/Yadis/ParseHTML.php @@ -0,0 +1,259 @@ + + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * This class is responsible for scanning an HTML string to find META + * tags and their attributes. This is used by the Yadis discovery + * process. This class must be instantiated to be used. + * + * @package OpenID + */ +class Auth_Yadis_ParseHTML { + + /** + * @access private + */ + var $_re_flags = "si"; + + /** + * @access private + */ + var $_removed_re = + "||]*>.*?<\/script>"; + + /** + * @access private + */ + var $_tag_expr = "<%s%s(?:\s.*?)?%s>"; + + /** + * @access private + */ + var $_attr_find = '\b([-\w]+)=(".*?"|\'.*?\'|.+?)[\/\s>]'; + + function Auth_Yadis_ParseHTML() + { + $this->_attr_find = sprintf("/%s/%s", + $this->_attr_find, + $this->_re_flags); + + $this->_removed_re = sprintf("/%s/%s", + $this->_removed_re, + $this->_re_flags); + + $this->_entity_replacements = array( + 'amp' => '&', + 'lt' => '<', + 'gt' => '>', + 'quot' => '"' + ); + + $this->_ent_replace = + sprintf("&(%s);", implode("|", + $this->_entity_replacements)); + } + + /** + * Replace HTML entities (amp, lt, gt, and quot) as well as + * numeric entities (e.g. #x9f;) with their actual values and + * return the new string. + * + * @access private + * @param string $str The string in which to look for entities + * @return string $new_str The new string entities decoded + */ + function replaceEntities($str) + { + foreach ($this->_entity_replacements as $old => $new) { + $str = preg_replace(sprintf("/&%s;/", $old), $new, $str); + } + + // Replace numeric entities because html_entity_decode doesn't + // do it for us. + $str = preg_replace('~&#x([0-9a-f]+);~ei', 'chr(hexdec("\\1"))', $str); + $str = preg_replace('~&#([0-9]+);~e', 'chr(\\1)', $str); + + return $str; + } + + /** + * Strip single and double quotes off of a string, if they are + * present. + * + * @access private + * @param string $str The original string + * @return string $new_str The new string with leading and + * trailing quotes removed + */ + function removeQuotes($str) + { + $matches = array(); + $double = '/^"(.*)"$/'; + $single = "/^\'(.*)\'$/"; + + if (preg_match($double, $str, $matches)) { + return $matches[1]; + } else if (preg_match($single, $str, $matches)) { + return $matches[1]; + } else { + return $str; + } + } + + /** + * Create a regular expression that will match an opening + * or closing tag from a set of names. + * + * @access private + * @param mixed $tag_names Tag names to match + * @param mixed $close false/0 = no, true/1 = yes, other = maybe + * @param mixed $self_close false/0 = no, true/1 = yes, other = maybe + * @return string $regex A regular expression string to be used + * in, say, preg_match. + */ + function tagPattern($tag_names, $close, $self_close) + { + if (is_array($tag_names)) { + $tag_names = '(?:'.implode('|',$tag_names).')'; + } + if ($close) { + $close = '\/' . (($close == 1)? '' : '?'); + } else { + $close = ''; + } + if ($self_close) { + $self_close = '(?:\/\s*)' . (($self_close == 1)? '' : '?'); + } else { + $self_close = ''; + } + $expr = sprintf($this->_tag_expr, $close, $tag_names, $self_close); + + return sprintf("/%s/%s", $expr, $this->_re_flags); + } + + /** + * Given an HTML document string, this finds all the META tags in + * the document, provided they are found in the + * ... section of the document. The tag + * may be missing. + * + * @access private + * @param string $html_string An HTMl document string + * @return array $tag_list Array of tags; each tag is an array of + * attribute -> value. + */ + function getMetaTags($html_string) + { + $html_string = preg_replace($this->_removed_re, + "", + $html_string); + + $key_tags = array($this->tagPattern('html', false, false), + $this->tagPattern('head', false, false), + $this->tagPattern('head', true, false), + $this->tagPattern('html', true, false), + $this->tagPattern(array( + 'body', 'frameset', 'frame', 'p', 'div', + 'table','span','a'), 'maybe', 'maybe')); + $key_tags_pos = array(); + foreach ($key_tags as $pat) { + $matches = array(); + preg_match($pat, $html_string, $matches, PREG_OFFSET_CAPTURE); + if($matches) { + $key_tags_pos[] = $matches[0][1]; + } else { + $key_tags_pos[] = null; + } + } + // no opening head tag + if (is_null($key_tags_pos[1])) { + return array(); + } + // the effective is the min of the following + if (is_null($key_tags_pos[2])) { + $key_tags_pos[2] = strlen($html_string); + } + foreach (array($key_tags_pos[3], $key_tags_pos[4]) as $pos) { + if (!is_null($pos) && $pos < $key_tags_pos[2]) { + $key_tags_pos[2] = $pos; + } + } + // closing head tag comes before opening head tag + if ($key_tags_pos[1] > $key_tags_pos[2]) { + return array(); + } + // if there is an opening html tag, make sure the opening head tag + // comes after it + if (!is_null($key_tags_pos[0]) && $key_tags_pos[1] < $key_tags_pos[0]) { + return array(); + } + $html_string = substr($html_string, $key_tags_pos[1], + ($key_tags_pos[2]-$key_tags_pos[1])); + + $link_data = array(); + $link_matches = array(); + + if (!preg_match_all($this->tagPattern('meta', false, 'maybe'), + $html_string, $link_matches)) { + return array(); + } + + foreach ($link_matches[0] as $link) { + $attr_matches = array(); + preg_match_all($this->_attr_find, $link, $attr_matches); + $link_attrs = array(); + foreach ($attr_matches[0] as $index => $full_match) { + $name = $attr_matches[1][$index]; + $value = $this->replaceEntities( + $this->removeQuotes($attr_matches[2][$index])); + + $link_attrs[strtolower($name)] = $value; + } + $link_data[] = $link_attrs; + } + + return $link_data; + } + + /** + * Looks for a META tag with an "http-equiv" attribute whose value + * is one of ("x-xrds-location", "x-yadis-location"), ignoring + * case. If such a META tag is found, its "content" attribute + * value is returned. + * + * @param string $html_string An HTML document in string format + * @return mixed $content The "content" attribute value of the + * META tag, if found, or null if no such tag was found. + */ + function getHTTPEquiv($html_string) + { + $meta_tags = $this->getMetaTags($html_string); + + if ($meta_tags) { + foreach ($meta_tags as $tag) { + if (array_key_exists('http-equiv', $tag) && + (in_array(strtolower($tag['http-equiv']), + array('x-xrds-location', 'x-yadis-location'))) && + array_key_exists('content', $tag)) { + return $tag['content']; + } + } + } + + return null; + } +} + +?> \ No newline at end of file diff --git a/extlib/Auth/Yadis/PlainHTTPFetcher.php b/extlib/Auth/Yadis/PlainHTTPFetcher.php new file mode 100644 index 000000000..8882e3cef --- /dev/null +++ b/extlib/Auth/Yadis/PlainHTTPFetcher.php @@ -0,0 +1,251 @@ + + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Interface import + */ +require_once "Auth/Yadis/HTTPFetcher.php"; + +/** + * This class implements a plain, hand-built socket-based fetcher + * which will be used in the event that CURL is unavailable. + * + * @package OpenID + */ +class Auth_Yadis_PlainHTTPFetcher extends Auth_Yadis_HTTPFetcher { + /** + * Does this fetcher support SSL URLs? + */ + function supportsSSL() + { + return function_exists('openssl_open'); + } + + function get($url, $extra_headers = null) + { + if (!$this->canFetchURL($url)) { + return null; + } + + $redir = true; + + $stop = time() + $this->timeout; + $off = $this->timeout; + + while ($redir && ($off > 0)) { + + $parts = parse_url($url); + + $specify_port = true; + + // Set a default port. + if (!array_key_exists('port', $parts)) { + $specify_port = false; + if ($parts['scheme'] == 'http') { + $parts['port'] = 80; + } elseif ($parts['scheme'] == 'https') { + $parts['port'] = 443; + } else { + return null; + } + } + + if (!array_key_exists('path', $parts)) { + $parts['path'] = '/'; + } + + $host = $parts['host']; + + if ($parts['scheme'] == 'https') { + $host = 'ssl://' . $host; + } + + $user_agent = Auth_OpenID_USER_AGENT; + + $headers = array( + "GET ".$parts['path']. + (array_key_exists('query', $parts) ? + "?".$parts['query'] : ""). + " HTTP/1.0", + "User-Agent: $user_agent", + "Host: ".$parts['host']. + ($specify_port ? ":".$parts['port'] : ""), + "Range: 0-". + (1024*Auth_OpenID_FETCHER_MAX_RESPONSE_KB), + "Port: ".$parts['port']); + + $errno = 0; + $errstr = ''; + + if ($extra_headers) { + foreach ($extra_headers as $h) { + $headers[] = $h; + } + } + + @$sock = fsockopen($host, $parts['port'], $errno, $errstr, + $this->timeout); + if ($sock === false) { + return false; + } + + stream_set_timeout($sock, $this->timeout); + + fputs($sock, implode("\r\n", $headers) . "\r\n\r\n"); + + $data = ""; + $kilobytes = 0; + while (!feof($sock) && + $kilobytes < Auth_OpenID_FETCHER_MAX_RESPONSE_KB ) { + $data .= fgets($sock, 1024); + $kilobytes += 1; + } + + fclose($sock); + + // Split response into header and body sections + list($headers, $body) = explode("\r\n\r\n", $data, 2); + $headers = explode("\r\n", $headers); + + $http_code = explode(" ", $headers[0]); + $code = $http_code[1]; + + if (in_array($code, array('301', '302'))) { + $url = $this->_findRedirect($headers); + $redir = true; + } else { + $redir = false; + } + + $off = $stop - time(); + } + + $new_headers = array(); + + foreach ($headers as $header) { + if (preg_match("/:/", $header)) { + $parts = explode(": ", $header, 2); + + if (count($parts) == 2) { + list($name, $value) = $parts; + $new_headers[$name] = $value; + } + } + + } + + return new Auth_Yadis_HTTPResponse($url, $code, $new_headers, $body); + } + + function post($url, $body, $extra_headers = null) + { + if (!$this->canFetchURL($url)) { + return null; + } + + $parts = parse_url($url); + + $headers = array(); + + $post_path = $parts['path']; + if (isset($parts['query'])) { + $post_path .= '?' . $parts['query']; + } + + $headers[] = "POST ".$post_path." HTTP/1.0"; + $headers[] = "Host: " . $parts['host']; + $headers[] = "Content-type: application/x-www-form-urlencoded"; + $headers[] = "Content-length: " . strval(strlen($body)); + + if ($extra_headers && + is_array($extra_headers)) { + $headers = array_merge($headers, $extra_headers); + } + + // Join all headers together. + $all_headers = implode("\r\n", $headers); + + // Add headers, two newlines, and request body. + $request = $all_headers . "\r\n\r\n" . $body; + + // Set a default port. + if (!array_key_exists('port', $parts)) { + if ($parts['scheme'] == 'http') { + $parts['port'] = 80; + } elseif ($parts['scheme'] == 'https') { + $parts['port'] = 443; + } else { + return null; + } + } + + if ($parts['scheme'] == 'https') { + $parts['host'] = sprintf("ssl://%s", $parts['host']); + } + + // Connect to the remote server. + $errno = 0; + $errstr = ''; + + $sock = fsockopen($parts['host'], $parts['port'], $errno, $errstr, + $this->timeout); + + if ($sock === false) { + return null; + } + + stream_set_timeout($sock, $this->timeout); + + // Write the POST request. + fputs($sock, $request); + + // Get the response from the server. + $response = ""; + while (!feof($sock)) { + if ($data = fgets($sock, 128)) { + $response .= $data; + } else { + break; + } + } + + // Split the request into headers and body. + list($headers, $response_body) = explode("\r\n\r\n", $response, 2); + + $headers = explode("\r\n", $headers); + + // Expect the first line of the headers data to be something + // like HTTP/1.1 200 OK. Split the line on spaces and take + // the second token, which should be the return code. + $http_code = explode(" ", $headers[0]); + $code = $http_code[1]; + + $new_headers = array(); + + foreach ($headers as $header) { + if (preg_match("/:/", $header)) { + list($name, $value) = explode(": ", $header, 2); + $new_headers[$name] = $value; + } + + } + + return new Auth_Yadis_HTTPResponse($url, $code, + $new_headers, $response_body); + } +} + +?> \ No newline at end of file diff --git a/extlib/Auth/Yadis/XML.php b/extlib/Auth/Yadis/XML.php new file mode 100644 index 000000000..4854f12bb --- /dev/null +++ b/extlib/Auth/Yadis/XML.php @@ -0,0 +1,374 @@ + $ns_uri) + * to be registered with the XML parser. May be empty. + * @return boolean $result True if the initialization and + * namespace registration(s) succeeded; false otherwise. + */ + function init($xml_string, $namespace_map) + { + if (!$this->setXML($xml_string)) { + return false; + } + + foreach ($namespace_map as $prefix => $uri) { + if (!$this->registerNamespace($prefix, $uri)) { + return false; + } + } + + return true; + } + + /** + * Register a namespace with the XML parser. This should be + * overridden by subclasses. + * + * @param string $prefix The namespace prefix to appear in XML tag + * names. + * + * @param string $uri The namespace URI to be used to identify the + * namespace in the XML. + * + * @return boolean $result True if the registration succeeded; + * false otherwise. + */ + function registerNamespace($prefix, $uri) + { + // Not implemented. + } + + /** + * Set this parser object's XML payload. This should be + * overridden by subclasses. + * + * @param string $xml_string The XML string to pass to this + * object's XML parser. + * + * @return boolean $result True if the initialization succeeded; + * false otherwise. + */ + function setXML($xml_string) + { + // Not implemented. + } + + /** + * Evaluate an XPath expression and return the resulting node + * list. This should be overridden by subclasses. + * + * @param string $xpath The XPath expression to be evaluated. + * + * @param mixed $node A node object resulting from a previous + * evalXPath call. This node, if specified, provides the context + * for the evaluation of this xpath expression. + * + * @return array $node_list An array of matching opaque node + * objects to be used with other methods of this parser class. + */ + function evalXPath($xpath, $node = null) + { + // Not implemented. + } + + /** + * Return the textual content of a specified node. + * + * @param mixed $node A node object from a previous call to + * $this->evalXPath(). + * + * @return string $content The content of this node. + */ + function content($node) + { + // Not implemented. + } + + /** + * Return the attributes of a specified node. + * + * @param mixed $node A node object from a previous call to + * $this->evalXPath(). + * + * @return array $attrs An array mapping attribute names to + * values. + */ + function attributes($node) + { + // Not implemented. + } +} + +/** + * This concrete implementation of Auth_Yadis_XMLParser implements + * the appropriate API for the 'domxml' extension which is typically + * packaged with PHP 4. This class will be used whenever the 'domxml' + * extension is detected. See the Auth_Yadis_XMLParser class for + * details on this class's methods. + * + * @package OpenID + */ +class Auth_Yadis_domxml extends Auth_Yadis_XMLParser { + function Auth_Yadis_domxml() + { + $this->xml = null; + $this->doc = null; + $this->xpath = null; + $this->errors = array(); + } + + function setXML($xml_string) + { + $this->xml = $xml_string; + $this->doc = @domxml_open_mem($xml_string, DOMXML_LOAD_PARSING, + $this->errors); + + if (!$this->doc) { + return false; + } + + $this->xpath = $this->doc->xpath_new_context(); + + return true; + } + + function registerNamespace($prefix, $uri) + { + return xpath_register_ns($this->xpath, $prefix, $uri); + } + + function &evalXPath($xpath, $node = null) + { + if ($node) { + $result = @$this->xpath->xpath_eval($xpath, $node); + } else { + $result = @$this->xpath->xpath_eval($xpath); + } + + if (!$result) { + $n = array(); + return $n; + } + + if (!$result->nodeset) { + $n = array(); + return $n; + } + + return $result->nodeset; + } + + function content($node) + { + if ($node) { + return $node->get_content(); + } + } + + function attributes($node) + { + if ($node) { + $arr = $node->attributes(); + $result = array(); + + if ($arr) { + foreach ($arr as $attrnode) { + $result[$attrnode->name] = $attrnode->value; + } + } + + return $result; + } + } +} + +/** + * This concrete implementation of Auth_Yadis_XMLParser implements + * the appropriate API for the 'dom' extension which is typically + * packaged with PHP 5. This class will be used whenever the 'dom' + * extension is detected. See the Auth_Yadis_XMLParser class for + * details on this class's methods. + * + * @package OpenID + */ +class Auth_Yadis_dom extends Auth_Yadis_XMLParser { + function Auth_Yadis_dom() + { + $this->xml = null; + $this->doc = null; + $this->xpath = null; + $this->errors = array(); + } + + function setXML($xml_string) + { + $this->xml = $xml_string; + $this->doc = new DOMDocument; + + if (!$this->doc) { + return false; + } + + if (!@$this->doc->loadXML($xml_string)) { + return false; + } + + $this->xpath = new DOMXPath($this->doc); + + if ($this->xpath) { + return true; + } else { + return false; + } + } + + function registerNamespace($prefix, $uri) + { + return $this->xpath->registerNamespace($prefix, $uri); + } + + function &evalXPath($xpath, $node = null) + { + if ($node) { + $result = @$this->xpath->query($xpath, $node); + } else { + $result = @$this->xpath->query($xpath); + } + + $n = array(); + + if (!$result) { + return $n; + } + + for ($i = 0; $i < $result->length; $i++) { + $n[] = $result->item($i); + } + + return $n; + } + + function content($node) + { + if ($node) { + return $node->textContent; + } + } + + function attributes($node) + { + if ($node) { + $arr = $node->attributes; + $result = array(); + + if ($arr) { + for ($i = 0; $i < $arr->length; $i++) { + $node = $arr->item($i); + $result[$node->nodeName] = $node->nodeValue; + } + } + + return $result; + } + } +} + +global $__Auth_Yadis_defaultParser; +$__Auth_Yadis_defaultParser = null; + +/** + * Set a default parser to override the extension-driven selection of + * available parser classes. This is helpful in a test environment or + * one in which multiple parsers can be used but one is more + * desirable. + * + * @param Auth_Yadis_XMLParser $parser An instance of a + * Auth_Yadis_XMLParser subclass. + */ +function Auth_Yadis_setDefaultParser(&$parser) +{ + global $__Auth_Yadis_defaultParser; + $__Auth_Yadis_defaultParser =& $parser; +} + +function Auth_Yadis_getSupportedExtensions() +{ + return array( + 'dom' => array('classname' => 'Auth_Yadis_dom', + 'libname' => array('dom.so', 'dom.dll')), + 'domxml' => array('classname' => 'Auth_Yadis_domxml', + 'libname' => array('domxml.so', 'php_domxml.dll')), + ); +} + +/** + * Returns an instance of a Auth_Yadis_XMLParser subclass based on + * the availability of PHP extensions for XML parsing. If + * Auth_Yadis_setDefaultParser has been called, the parser used in + * that call will be returned instead. + */ +function &Auth_Yadis_getXMLParser() +{ + global $__Auth_Yadis_defaultParser; + + if (isset($__Auth_Yadis_defaultParser)) { + return $__Auth_Yadis_defaultParser; + } + + $p = null; + $classname = null; + + $extensions = Auth_Yadis_getSupportedExtensions(); + + // Return a wrapper for the resident implementation, if any. + foreach ($extensions as $name => $params) { + if (!extension_loaded($name)) { + foreach ($params['libname'] as $libname) { + if (@dl($libname)) { + $classname = $params['classname']; + } + } + } else { + $classname = $params['classname']; + } + if (isset($classname)) { + $p = new $classname(); + return $p; + } + } + + if (!isset($p)) { + trigger_error('No XML parser was found', E_USER_ERROR); + } else { + Auth_Yadis_setDefaultParser($p); + } + + return $p; +} + +?> diff --git a/extlib/Auth/Yadis/XRDS.php b/extlib/Auth/Yadis/XRDS.php new file mode 100644 index 000000000..f14a7948e --- /dev/null +++ b/extlib/Auth/Yadis/XRDS.php @@ -0,0 +1,478 @@ + + * @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 + */ + 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; + } +} + +?> \ No newline at end of file diff --git a/extlib/Auth/Yadis/XRI.php b/extlib/Auth/Yadis/XRI.php new file mode 100644 index 000000000..4e3462317 --- /dev/null +++ b/extlib/Auth/Yadis/XRI.php @@ -0,0 +1,234 @@ + + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'Auth/Yadis/Misc.php'; +require_once 'Auth/Yadis/Yadis.php'; +require_once 'Auth/OpenID.php'; + +function Auth_Yadis_getDefaultProxy() +{ + return 'http://xri.net/'; +} + +function Auth_Yadis_getXRIAuthorities() +{ + return array('!', '=', '@', '+', '$', '('); +} + +function Auth_Yadis_getEscapeRE() +{ + $parts = array(); + foreach (array_merge(Auth_Yadis_getUCSChars(), + Auth_Yadis_getIPrivateChars()) as $pair) { + list($m, $n) = $pair; + $parts[] = sprintf("%s-%s", chr($m), chr($n)); + } + + return sprintf('/[%s]/', implode('', $parts)); +} + +function Auth_Yadis_getXrefRE() +{ + return '/\((.*?)\)/'; +} + +function Auth_Yadis_identifierScheme($identifier) +{ + if (Auth_Yadis_startswith($identifier, 'xri://') || + ($identifier && + in_array($identifier[0], Auth_Yadis_getXRIAuthorities()))) { + return "XRI"; + } else { + return "URI"; + } +} + +function Auth_Yadis_toIRINormal($xri) +{ + if (!Auth_Yadis_startswith($xri, 'xri://')) { + $xri = 'xri://' . $xri; + } + + return Auth_Yadis_escapeForIRI($xri); +} + +function _escape_xref($xref_match) +{ + $xref = $xref_match[0]; + $xref = str_replace('/', '%2F', $xref); + $xref = str_replace('?', '%3F', $xref); + $xref = str_replace('#', '%23', $xref); + return $xref; +} + +function Auth_Yadis_escapeForIRI($xri) +{ + $xri = str_replace('%', '%25', $xri); + $xri = preg_replace_callback(Auth_Yadis_getXrefRE(), + '_escape_xref', $xri); + return $xri; +} + +function Auth_Yadis_toURINormal($xri) +{ + return Auth_Yadis_iriToURI(Auth_Yadis_toIRINormal($xri)); +} + +function Auth_Yadis_iriToURI($iri) +{ + if (1) { + return $iri; + } else { + // According to RFC 3987, section 3.1, "Mapping of IRIs to URIs" + return preg_replace_callback(Auth_Yadis_getEscapeRE(), + 'Auth_Yadis_pct_escape_unicode', $iri); + } +} + + +function Auth_Yadis_XRIAppendArgs($url, $args) +{ + // Append some arguments to an HTTP query. Yes, this is just like + // OpenID's appendArgs, but with special seasoning for XRI + // queries. + + if (count($args) == 0) { + return $url; + } + + // Non-empty array; if it is an array of arrays, use multisort; + // otherwise use sort. + if (array_key_exists(0, $args) && + is_array($args[0])) { + // Do nothing here. + } else { + $keys = array_keys($args); + sort($keys); + $new_args = array(); + foreach ($keys as $key) { + $new_args[] = array($key, $args[$key]); + } + $args = $new_args; + } + + // According to XRI Resolution section "QXRI query parameters": + // + // "If the original QXRI had a null query component (only a + // leading question mark), or a query component consisting of + // only question marks, one additional leading question mark MUST + // be added when adding any XRI resolution parameters." + if (strpos(rtrim($url, '?'), '?') !== false) { + $sep = '&'; + } else { + $sep = '?'; + } + + return $url . $sep . Auth_OpenID::httpBuildQuery($args); +} + +function Auth_Yadis_providerIsAuthoritative($providerID, $canonicalID) +{ + $lastbang = strrpos($canonicalID, '!'); + $p = substr($canonicalID, 0, $lastbang); + return $p == $providerID; +} + +function Auth_Yadis_rootAuthority($xri) +{ + // Return the root authority for an XRI. + + $root = null; + + if (Auth_Yadis_startswith($xri, 'xri://')) { + $xri = substr($xri, 6); + } + + $authority = explode('/', $xri, 2); + $authority = $authority[0]; + if ($authority[0] == '(') { + // Cross-reference. + // XXX: This is incorrect if someone nests cross-references so + // there is another close-paren in there. Hopefully nobody + // does that before we have a real xriparse function. + // Hopefully nobody does that *ever*. + $root = substr($authority, 0, strpos($authority, ')') + 1); + } else if (in_array($authority[0], Auth_Yadis_getXRIAuthorities())) { + // Other XRI reference. + $root = $authority[0]; + } else { + // IRI reference. + $_segments = explode("!", $authority); + $segments = array(); + foreach ($_segments as $s) { + $segments = array_merge($segments, explode("*", $s)); + } + $root = $segments[0]; + } + + return Auth_Yadis_XRI($root); +} + +function Auth_Yadis_XRI($xri) +{ + if (!Auth_Yadis_startswith($xri, 'xri://')) { + $xri = 'xri://' . $xri; + } + return $xri; +} + +function Auth_Yadis_getCanonicalID($iname, $xrds) +{ + // Returns false or a canonical ID value. + + // Now nodes are in reverse order. + $xrd_list = array_reverse($xrds->allXrdNodes); + $parser =& $xrds->parser; + $node = $xrd_list[0]; + + $canonicalID_nodes = $parser->evalXPath('xrd:CanonicalID', $node); + + if (!$canonicalID_nodes) { + return false; + } + + $canonicalID = $canonicalID_nodes[0]; + $canonicalID = Auth_Yadis_XRI($parser->content($canonicalID)); + + $childID = $canonicalID; + + for ($i = 1; $i < count($xrd_list); $i++) { + $xrd = $xrd_list[$i]; + + $parent_sought = substr($childID, 0, strrpos($childID, '!')); + $parentCID = $parser->evalXPath('xrd:CanonicalID', $xrd); + if (!$parentCID) { + return false; + } + $parentCID = Auth_Yadis_XRI($parser->content($parentCID[0])); + + if (strcasecmp($parent_sought, $parentCID)) { + // raise XRDSFraud. + return false; + } + + $childID = $parent_sought; + } + + $root = Auth_Yadis_rootAuthority($iname); + if (!Auth_Yadis_providerIsAuthoritative($root, $childID)) { + // raise XRDSFraud. + return false; + } + + return $canonicalID; +} + +?> diff --git a/extlib/Auth/Yadis/XRIRes.php b/extlib/Auth/Yadis/XRIRes.php new file mode 100644 index 000000000..4e8e8d037 --- /dev/null +++ b/extlib/Auth/Yadis/XRIRes.php @@ -0,0 +1,72 @@ +fetcher =& $fetcher; + $this->proxy_url = $proxy_url; + if (!$this->proxy_url) { + $this->proxy_url = Auth_Yadis_getDefaultProxy(); + } + } + + function queryURL($xri, $service_type = null) + { + // trim off the xri:// prefix + $qxri = substr(Auth_Yadis_toURINormal($xri), 6); + $hxri = $this->proxy_url . $qxri; + $args = array( + '_xrd_r' => 'application/xrds+xml' + ); + + if ($service_type) { + $args['_xrd_t'] = $service_type; + } else { + // Don't perform service endpoint selection. + $args['_xrd_r'] .= ';sep=false'; + } + + $query = Auth_Yadis_XRIAppendArgs($hxri, $args); + return $query; + } + + function query($xri, $service_types, $filters = array()) + { + $services = array(); + $canonicalID = null; + foreach ($service_types as $service_type) { + $url = $this->queryURL($xri, $service_type); + $response = $this->fetcher->get($url); + if ($response->status != 200 and $response->status != 206) { + continue; + } + $xrds = Auth_Yadis_XRDS::parseXRDS($response->body); + if (!$xrds) { + continue; + } + $canonicalID = Auth_Yadis_getCanonicalID($xri, + $xrds); + + if ($canonicalID === false) { + return null; + } + + $some_services = $xrds->services($filters); + $services = array_merge($services, $some_services); + // TODO: + // * If we do get hits for multiple service_types, we're + // almost certainly going to have duplicated service + // entries and broken priority ordering. + } + return array($canonicalID, $services); + } +} + +?> diff --git a/extlib/Auth/Yadis/Yadis.php b/extlib/Auth/Yadis/Yadis.php new file mode 100644 index 000000000..d89f77c6d --- /dev/null +++ b/extlib/Auth/Yadis/Yadis.php @@ -0,0 +1,382 @@ + + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Need both fetcher types so we can use the right one based on the + * presence or absence of CURL. + */ +require_once "Auth/Yadis/PlainHTTPFetcher.php"; +require_once "Auth/Yadis/ParanoidHTTPFetcher.php"; + +/** + * Need this for parsing HTML (looking for META tags). + */ +require_once "Auth/Yadis/ParseHTML.php"; + +/** + * Need this to parse the XRDS document during Yadis discovery. + */ +require_once "Auth/Yadis/XRDS.php"; + +/** + * XRDS (yadis) content type + */ +define('Auth_Yadis_CONTENT_TYPE', 'application/xrds+xml'); + +/** + * Yadis header + */ +define('Auth_Yadis_HEADER_NAME', 'X-XRDS-Location'); + +/** + * Contains the result of performing Yadis discovery on a URI. + * + * @package OpenID + */ +class Auth_Yadis_DiscoveryResult { + + // The URI that was passed to the fetcher + var $request_uri = null; + + // The result of following redirects from the request_uri + var $normalized_uri = null; + + // The URI from which the response text was returned (set to + // None if there was no XRDS document found) + var $xrds_uri = null; + + var $xrds = null; + + // The content-type returned with the response_text + var $content_type = null; + + // The document returned from the xrds_uri + var $response_text = null; + + // Did the discovery fail miserably? + var $failed = false; + + function Auth_Yadis_DiscoveryResult($request_uri) + { + // Initialize the state of the object + // sets all attributes to None except the request_uri + $this->request_uri = $request_uri; + } + + function fail() + { + $this->failed = true; + } + + function isFailure() + { + return $this->failed; + } + + /** + * Returns the list of service objects as described by the XRDS + * document, if this yadis object represents a successful Yadis + * discovery. + * + * @return array $services An array of {@link Auth_Yadis_Service} + * objects + */ + function services() + { + if ($this->xrds) { + return $this->xrds->services(); + } + + return null; + } + + function usedYadisLocation() + { + // Was the Yadis protocol's indirection used? + return $this->normalized_uri != $this->xrds_uri; + } + + function isXRDS() + { + // Is the response text supposed to be an XRDS document? + return ($this->usedYadisLocation() || + $this->content_type == Auth_Yadis_CONTENT_TYPE); + } +} + +/** + * + * Perform the Yadis protocol on the input URL and return an iterable + * of resulting endpoint objects. + * + * input_url: The URL on which to perform the Yadis protocol + * + * @return: The normalized identity URL and an iterable of endpoint + * objects generated by the filter function. + * + * xrds_parse_func: a callback which will take (uri, xrds_text) and + * return an array of service endpoint objects or null. Usually + * array('Auth_OpenID_ServiceEndpoint', 'fromXRDS'). + * + * discover_func: if not null, a callback which should take (uri) and + * return an Auth_Yadis_Yadis object or null. + */ +function Auth_Yadis_getServiceEndpoints($input_url, $xrds_parse_func, + $discover_func=null, $fetcher=null) +{ + if ($discover_func === null) { + $discover_function = array('Auth_Yadis_Yadis', 'discover'); + } + + $yadis_result = call_user_func_array($discover_func, + array($input_url, $fetcher)); + + if ($yadis_result === null) { + return array($input_url, array()); + } + + $endpoints = call_user_func_array($xrds_parse_func, + array($yadis_result->normalized_uri, + $yadis_result->response_text)); + + if ($endpoints === null) { + $endpoints = array(); + } + + return array($yadis_result->normalized_uri, $endpoints); +} + +/** + * This is the core of the PHP Yadis library. This is the only class + * a user needs to use to perform Yadis discovery. This class + * performs the discovery AND stores the result of the discovery. + * + * First, require this library into your program source: + * + *
  require_once "Auth/Yadis/Yadis.php";
+ * + * To perform Yadis discovery, first call the "discover" method + * statically with a URI parameter: + * + *
  $http_response = array();
+ *  $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
+ *  $yadis_object = Auth_Yadis_Yadis::discover($uri,
+ *                                    $http_response, $fetcher);
+ * + * If the discovery succeeds, $yadis_object will be an instance of + * {@link Auth_Yadis_Yadis}. If not, it will be null. The XRDS + * document found during discovery should have service descriptions, + * which can be accessed by calling + * + *
  $service_list = $yadis_object->services();
+ * + * which returns an array of objects which describe each service. + * These objects are instances of Auth_Yadis_Service. Each object + * describes exactly one whole Service element, complete with all of + * its Types and URIs (no expansion is performed). The common use + * case for using the service objects returned by services() is to + * write one or more filter functions and pass those to services(): + * + *
  $service_list = $yadis_object->services(
+ *                               array("filterByURI",
+ *                                     "filterByExtension"));
+ * + * The filter functions (whose names appear in the array passed to + * services()) take the following form: + * + *
  function myFilter(&$service) {
+ *       // Query $service object here.  Return true if the service
+ *       // matches your query; false if not.
+ *  }
+ * + * This is an example of a filter which uses a regular expression to + * match the content of URI tags (note that the Auth_Yadis_Service + * class provides a getURIs() method which you should use instead of + * this contrived example): + * + *
+ *  function URIMatcher(&$service) {
+ *      foreach ($service->getElements('xrd:URI') as $uri) {
+ *          if (preg_match("/some_pattern/",
+ *                         $service->parser->content($uri))) {
+ *              return true;
+ *          }
+ *      }
+ *      return false;
+ *  }
+ * + * The filter functions you pass will be called for each service + * object to determine which ones match the criteria your filters + * specify. The default behavior is that if a given service object + * matches ANY of the filters specified in the services() call, it + * will be returned. You can specify that a given service object will + * be returned ONLY if it matches ALL specified filters by changing + * the match mode of services(): + * + *
  $yadis_object->services(array("filter1", "filter2"),
+ *                          SERVICES_YADIS_MATCH_ALL);
+ * + * See {@link SERVICES_YADIS_MATCH_ALL} and {@link + * SERVICES_YADIS_MATCH_ANY}. + * + * Services described in an XRDS should have a library which you'll + * probably be using. Those libraries are responsible for defining + * filters that can be used with the "services()" call. If you need + * to write your own filter, see the documentation for {@link + * Auth_Yadis_Service}. + * + * @package OpenID + */ +class Auth_Yadis_Yadis { + + /** + * Returns an HTTP fetcher object. If the CURL extension is + * present, an instance of {@link Auth_Yadis_ParanoidHTTPFetcher} + * is returned. If not, an instance of + * {@link Auth_Yadis_PlainHTTPFetcher} is returned. + * + * If Auth_Yadis_CURL_OVERRIDE is defined, this method will always + * return a {@link Auth_Yadis_PlainHTTPFetcher}. + */ + function getHTTPFetcher($timeout = 20) + { + if (Auth_Yadis_Yadis::curlPresent() && + (!defined('Auth_Yadis_CURL_OVERRIDE'))) { + $fetcher = new Auth_Yadis_ParanoidHTTPFetcher($timeout); + } else { + $fetcher = new Auth_Yadis_PlainHTTPFetcher($timeout); + } + return $fetcher; + } + + function curlPresent() + { + return function_exists('curl_init'); + } + + /** + * @access private + */ + function _getHeader($header_list, $names) + { + foreach ($header_list as $name => $value) { + foreach ($names as $n) { + if (strtolower($name) == strtolower($n)) { + return $value; + } + } + } + + return null; + } + + /** + * @access private + */ + function _getContentType($content_type_header) + { + if ($content_type_header) { + $parts = explode(";", $content_type_header); + return strtolower($parts[0]); + } + } + + /** + * This should be called statically and will build a Yadis + * instance if the discovery process succeeds. This implements + * Yadis discovery as specified in the Yadis specification. + * + * @param string $uri The URI on which to perform Yadis discovery. + * + * @param array $http_response An array reference where the HTTP + * response object will be stored (see {@link + * Auth_Yadis_HTTPResponse}. + * + * @param Auth_Yadis_HTTPFetcher $fetcher An instance of a + * Auth_Yadis_HTTPFetcher subclass. + * + * @param array $extra_ns_map An array which maps namespace names + * to namespace URIs to be used when parsing the Yadis XRDS + * document. + * + * @param integer $timeout An optional fetcher timeout, in seconds. + * + * @return mixed $obj Either null or an instance of + * Auth_Yadis_Yadis, depending on whether the discovery + * succeeded. + */ + function discover($uri, &$fetcher, + $extra_ns_map = null, $timeout = 20) + { + $result = new Auth_Yadis_DiscoveryResult($uri); + + $request_uri = $uri; + $headers = array("Accept: " . Auth_Yadis_CONTENT_TYPE . + ', text/html; q=0.3, application/xhtml+xml; q=0.5'); + + if ($fetcher === null) { + $fetcher = Auth_Yadis_Yadis::getHTTPFetcher($timeout); + } + + $response = $fetcher->get($uri, $headers); + + if (!$response || ($response->status != 200 and + $response->status != 206)) { + $result->fail(); + return $result; + } + + $result->normalized_uri = $response->final_url; + $result->content_type = Auth_Yadis_Yadis::_getHeader( + $response->headers, + array('content-type')); + + if ($result->content_type && + (Auth_Yadis_Yadis::_getContentType($result->content_type) == + Auth_Yadis_CONTENT_TYPE)) { + $result->xrds_uri = $result->normalized_uri; + } else { + $yadis_location = Auth_Yadis_Yadis::_getHeader( + $response->headers, + array(Auth_Yadis_HEADER_NAME)); + + if (!$yadis_location) { + $parser = new Auth_Yadis_ParseHTML(); + $yadis_location = $parser->getHTTPEquiv($response->body); + } + + if ($yadis_location) { + $result->xrds_uri = $yadis_location; + + $response = $fetcher->get($yadis_location); + + if ((!$response) || ($response->status != 200 and + $response->status != 206)) { + $result->fail(); + return $result; + } + + $result->content_type = Auth_Yadis_Yadis::_getHeader( + $response->headers, + array('content-type')); + } + } + + $result->response_text = $response->body; + return $result; + } +} + +?> -- cgit v1.2.3-54-g00ecf