diff options
Diffstat (limited to 'Services')
-rw-r--r-- | Services/Yadis/HTTPFetcher.php | 92 | ||||
-rw-r--r-- | Services/Yadis/Manager.php | 303 | ||||
-rw-r--r-- | Services/Yadis/ParanoidHTTPFetcher.php | 189 | ||||
-rw-r--r-- | Services/Yadis/ParseHTML.php | 270 | ||||
-rw-r--r-- | Services/Yadis/PlainHTTPFetcher.php | 237 | ||||
-rw-r--r-- | Services/Yadis/XML.php | 346 | ||||
-rw-r--r-- | Services/Yadis/XRDS.php | 418 | ||||
-rw-r--r-- | Services/Yadis/Yadis.php | 309 |
8 files changed, 2164 insertions, 0 deletions
diff --git a/Services/Yadis/HTTPFetcher.php b/Services/Yadis/HTTPFetcher.php new file mode 100644 index 0000000..97940a4 --- /dev/null +++ b/Services/Yadis/HTTPFetcher.php @@ -0,0 +1,92 @@ +<?php + +/** + * This module contains the HTTP fetcher interface + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +class Services_Yadis_HTTPResponse { + function Services_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 Yadis + */ +class Services_Yadis_HTTPFetcher { + + var $timeout = 20; // timeout in seconds. + + /** + * 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); + } + + /** + * 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($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/Services/Yadis/Manager.php b/Services/Yadis/Manager.php new file mode 100644 index 0000000..67a9ad1 --- /dev/null +++ b/Services/Yadis/Manager.php @@ -0,0 +1,303 @@ +<?php + +/** + * Yadis service manager to be used during yadis-driven authentication + * attempts. + * + * @package Yadis + */ + +/** + * The base session class used by the Services_Yadis_Manager. This + * class wraps the default PHP session machinery and should be + * subclassed if your application doesn't use PHP sessioning. + * + * @package Yadis + */ +class Services_Yadis_PHPSession { + /** + * Set a session key/value pair. + * + * @param string $name The name of the session key to add. + * @param string $value The value to add to the session. + */ + function set($name, $value) + { + $_SESSION[$name] = $value; + } + + /** + * Get a key's value from the session. + * + * @param string $name The name of the key to retrieve. + * @param string $default The optional value to return if the key + * is not found in the session. + * @return string $result The key's value in the session or + * $default if it isn't found. + */ + function get($name, $default=null) + { + if (array_key_exists($name, $_SESSION)) { + return $_SESSION[$name]; + } else { + return $default; + } + } + + /** + * Remove a key/value pair from the session. + * + * @param string $name The name of the key to remove. + */ + function del($name) + { + unset($_SESSION[$name]); + } +} + +/** + * The Yadis service manager which stores state in a session and + * iterates over <Service> elements in a Yadis XRDS document and lets + * a caller attempt to use each one. This is used by the Yadis + * library internally. + * + * @package Yadis + */ +class Services_Yadis_Manager { + + /** + * Intialize a new yadis service manager. + * + * @access private + */ + function Services_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 .finish() to clean up the + * session state. + * + * @package Yadis + */ +class Services_Yadis_Discovery { + + /** + * @access private + */ + var $DEFAULT_SUFFIX = 'auth'; + + /** + * @access private + */ + var $PREFIX = '_yadis_services_'; + + /** + * Initialize a discovery object. + * + * @param Services_Yadis_PHPSession $session An object which + * implements the Services_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 Services_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->stale) { + $this->destroyManager(); + $http_response = array(); + + $services = call_user_func($discover_cb, $this->url, + $fetcher); + + $manager = $this->createManager($services, $this->url); + } + + if ($manager) { + $service = $manager->nextService(); + $this->session->set($this->session_key, serialize($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. + */ + function cleanup() + { + $manager = $this->getManager(); + if ($manager) { + $service = $manager->current(); + $this->destroyManager(); + } 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 + */ + function getManager() + { + // 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) { + $manager = unserialize($manager_str); + } + + if ($manager && $manager->forURL($this->url)) { + return $manager; + } else { + return null; + } + } + + /** + * @access private + */ + function &createManager($services, $yadis_url = null) + { + $key = $this->getSessionKey(); + if ($this->getManager()) { + return $this->getManager(); + } + + if (!$services) { + return null; + } + + $manager = new Services_Yadis_Manager($this->url, $yadis_url, + $services, $key); + $this->session->set($this->session_key, serialize($manager)); + return $manager; + } + + /** + * @access private + */ + function destroyManager() + { + if ($this->getManager() !== null) { + $key = $this->getSessionKey(); + $this->session->del($key); + } + } +} + +?>
\ No newline at end of file diff --git a/Services/Yadis/ParanoidHTTPFetcher.php b/Services/Yadis/ParanoidHTTPFetcher.php new file mode 100644 index 0000000..5ddd129 --- /dev/null +++ b/Services/Yadis/ParanoidHTTPFetcher.php @@ -0,0 +1,189 @@ +<?php + +/** + * This module contains the CURL-based HTTP fetcher implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Interface import + */ +require_once "Services/Yadis/HTTPFetcher.php"; + +/** + * Define this based on whether the CURL extension is available. + */ +define('Services_Yadis_CURL_PRESENT', function_exists('curl_init')); + +/** + * A paranoid {@link Services_Yadis_HTTPFetcher} class which uses CURL + * for fetching. + * + * @package Yadis + */ +class Services_Yadis_ParanoidHTTPFetcher extends Services_Yadis_HTTPFetcher { + function Services_Yadis_ParanoidHTTPFetcher() + { + if (!Services_Yadis_CURL_PRESENT) { + trigger_error("Cannot use this class; CURL extension not found", + E_USER_ERROR); + } + + $this->headers = array(); + $this->data = ""; + + $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) + { + $this->data .= $data; + return strlen($data); + } + + function get($url, $extra_headers = null) + { + $stop = time() + $this->timeout; + $off = $this->timeout; + + $redir = true; + + while ($redir && ($off > 0)) { + $this->reset(); + + $c = curl_init(); + curl_setopt($c, CURLOPT_NOSIGNAL, true); + + if (!$this->allowedURL($url)) { + trigger_error(sprintf("Fetching URL not allowed: %s", $url), + E_USER_WARNING); + 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); + } + + curl_setopt($c, CURLOPT_TIMEOUT, $off); + curl_setopt($c, CURLOPT_URL, $url); + + curl_exec($c); + + $code = curl_getinfo($c, CURLINFO_HTTP_CODE); + $body = $this->data; + $headers = $this->headers; + + if (!$code) { + trigger_error("No HTTP code returned", E_USER_WARNING); + 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 (preg_match("/:/", $header)) { + list($name, $value) = explode(": ", $header, 2); + $new_headers[$name] = $value; + } + } + + return new Services_Yadis_HTTPResponse($url, $code, + $new_headers, $body); + } + + $off = $stop - time(); + } + + trigger_error(sprintf("Timed out fetching: %s", $url), + E_USER_WARNING); + + return null; + } + + function post($url, $body) + { + $this->reset(); + + if (!$this->allowedURL($url)) { + trigger_error(sprintf("Fetching URL not allowed: %s", $url), + E_USER_WARNING); + return null; + } + + $c = curl_init(); + + 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) { + trigger_error("No HTTP code returned", E_USER_WARNING); + return null; + } + + $body = $this->data; + + curl_close($c); + + $new_headers = array(); + + foreach ($this->headers as $header) { + if (preg_match("/:/", $header)) { + list($name, $value) = explode(": ", $header, 2); + $new_headers[$name] = $value; + } + + } + + return new Services_Yadis_HTTPResponse($url, $code, + $new_headers, $body); + } +} + +?>
\ No newline at end of file diff --git a/Services/Yadis/ParseHTML.php b/Services/Yadis/ParseHTML.php new file mode 100644 index 0000000..fc0df17 --- /dev/null +++ b/Services/Yadis/ParseHTML.php @@ -0,0 +1,270 @@ +<?php + +/** + * This is the HTML pseudo-parser for the Yadis library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * 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 Yadis + */ +class Services_Yadis_ParseHTML { + + /** + * @access private + */ + var $_re_flags = "si"; + + /** + * @access private + */ + var $_tag_expr = "<%s\b(?!:)([^>]*?)(?:\/>|>(.*?)(?:<\/?%s\s*>|\Z))"; + + /** + * @access private + */ + var $_close_tag_expr = "<\/?%s\s*>"; + + /** + * @access private + */ + var $_removed_re = + "<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b(?!:)[^>]*>.*?<\/script>"; + + /** + * @access private + */ + var $_attr_find = '\b([-\w]+)=("[^"]*"|\'[^\']*\'|[^\'"\s\/<>]+)'; + + function Services_Yadis_ParseHTML() + { + $this->_meta_find = sprintf("/<meta\b(?!:)([^>]*)(?!<)>/%s", + $this->_re_flags); + + $this->_removed_re = sprintf("/%s/%s", + $this->_removed_re, + $this->_re_flags); + + $this->_attr_find = sprintf("/%s/%s", + $this->_attr_find, + $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 (and + * optional) closing tag of a given name. + * + * @access private + * @param string $tag_name The tag name to match + * @param array $close_tags An array of tag names which also + * constitute closing of the original tag + * @return string $regex A regular expression string to be used + * in, say, preg_match. + */ + function tagMatcher($tag_name, $close_tags = null) + { + if ($close_tags) { + $options = implode("|", array_merge(array($tag_name), $close_tags)); + $closer = sprintf("(?:%s)", $options); + } else { + $closer = $tag_name; + } + + $expr = sprintf($this->_tag_expr, $tag_name, $closer); + return sprintf("/%s/%s", $expr, $this->_re_flags); + } + + /** + * @access private + */ + function htmlFind($str) + { + return $this->tagMatcher('html', array('body')); + } + + /** + * @access private + */ + function headFind() + { + return $this->tagMatcher('head', array('body')); + } + + /** + * Given an HTML document string, this finds all the META tags in + * the document, provided they are found in the + * <HTML><HEAD>...</HEAD> section of the document. The <HTML> 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) + { + $stripped = preg_replace($this->_removed_re, + "", + $html_string); + + // Look for the closing body tag. + $body_closer = sprintf($this->_close_tag_expr, 'body'); + $body_matches = array(); + preg_match($body_closer, $html_string, $body_matches, + PREG_OFFSET_CAPTURE); + if ($body_matches) { + $html_string = substr($html_string, 0, $body_matches[0][1]); + } + + // Look for the opening body tag, and discard everything after + // that tag. + $body_re = $this->tagMatcher('body'); + $body_matches = array(); + preg_match($body_re, $html_string, $body_matches, PREG_OFFSET_CAPTURE); + if ($body_matches) { + $html_string = substr($html_string, 0, $body_matches[0][1]); + } + + // If an HTML tag is found at all, it must be in the right + // order; else, it may be missing (which is a case we allow + // for). + $html_re = $this->tagMatcher('html', array('body')); + preg_match($html_re, $html_string, $html_matches); + if ($html_matches) { + $html = $html_matches[0]; + } else { + $html = $html_string; + } + + // Try to find the <HEAD> tag. + $head_re = $this->headFind(); + $head_matches = array(); + if (!preg_match($head_re, $html, $head_matches)) { + return array(); + } + + $link_data = array(); + $link_matches = array(); + + if (!preg_match_all($this->_meta_find, $head_matches[0], + $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/Services/Yadis/PlainHTTPFetcher.php b/Services/Yadis/PlainHTTPFetcher.php new file mode 100644 index 0000000..4d8d45c --- /dev/null +++ b/Services/Yadis/PlainHTTPFetcher.php @@ -0,0 +1,237 @@ +<?php + +/** + * This module contains the plain non-curl HTTP fetcher + * implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Interface import + */ +require_once "Services/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 Yadis + */ +class Services_Yadis_PlainHTTPFetcher extends Services_Yadis_HTTPFetcher { + function get($url, $extra_headers = null) + { + if (!$this->allowedURL($url)) { + trigger_error("Bad URL scheme in url: " . $url, + E_USER_WARNING); + 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 { + trigger_error("fetcher post method doesn't support " . + " scheme '" . $parts['scheme'] . + "', no default port available", + E_USER_WARNING); + return null; + } + } + + $host = $parts['host']; + + if ($parts['scheme'] == 'https') { + $host = 'ssl://' . $host; + } + + $user_agent = "PHP Yadis Library Fetcher"; + + $headers = array( + "GET ".$parts['path']. + (array_key_exists('query', $parts) ? + "?".$parts['query'] : ""). + " HTTP/1.1", + "User-Agent: $user_agent", + "Host: ".$parts['host']. + ($specify_port ? ":".$parts['port'] : ""), + "Port: ".$parts['port'], + "Connection: close", + "Cache-Control: no-cache"); + + $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 = ""; + while (!feof($sock)) { + $data .= fgets($sock, 1024); + } + + 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)) { + list($name, $value) = explode(": ", $header, 2); + $new_headers[$name] = $value; + } + + } + + return new Services_Yadis_HTTPResponse($url, $code, $new_headers, $body); + } + + function post($url, $body) + { + if (!$this->allowedURL($url)) { + trigger_error("Bad URL scheme in url: " . $url, + E_USER_WARNING); + return null; + } + + $parts = parse_url($url); + + $headers = array(); + + $headers[] = "POST ".$parts['path']." HTTP/1.1"; + $headers[] = "Host: " . $parts['host']; + $headers[] = "Content-type: application/x-www-form-urlencoded"; + $headers[] = "Content-length: " . strval(strlen($body)); + + // 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 { + trigger_error("fetcher post method doesn't support scheme '" . + $parts['scheme'] . + "', no default port available", + E_USER_WARNING); + 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) { + trigger_error("Could not connect to " . $parts['host'] . + " port " . $parts['port'], + E_USER_WARNING); + 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 Services_Yadis_HTTPResponse($url, $code, + $headers, $response_body); + } +} + +?>
\ No newline at end of file diff --git a/Services/Yadis/XML.php b/Services/Yadis/XML.php new file mode 100644 index 0000000..1f1b18b --- /dev/null +++ b/Services/Yadis/XML.php @@ -0,0 +1,346 @@ +<?php + +/** + * XML-parsing classes to wrap the domxml and DOM extensions for PHP 4 + * and 5, respectively. + * + * @package Yadis + */ + +/** + * The base class for wrappers for available PHP XML-parsing + * extensions. To work with this Yadis library, subclasses of this + * class MUST implement the API as defined in the remarks for this + * class. Subclasses of Services_Yadis_XMLParser are used to wrap + * particular PHP XML extensions such as 'domxml'. These are used + * internally by the library depending on the availability of + * supported PHP XML extensions. + * + * @package Yadis + */ +class Services_Yadis_XMLParser { + /** + * Initialize an instance of Services_Yadis_XMLParser with some + * XML and namespaces. This SHOULD NOT be overridden by + * subclasses. + * + * @param string $xml_string A string of XML to be parsed. + * @param array $namespace_map An array of ($ns_name => $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 Services_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 Services_Yadis_XMLParser class for + * details on this class's methods. + * + * @package Yadis + */ +class Services_Yadis_domxml extends Services_Yadis_XMLParser { + function Services_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->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 Services_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 Services_Yadis_XMLParser class for + * details on this class's methods. + * + * @package Yadis + */ +class Services_Yadis_dom extends Services_Yadis_XMLParser { + function Services_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->evaluate($xpath, $node); + } else { + $result = @$this->xpath->evaluate($xpath); + } + + $n = array(); + + 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; + } + } +} + +$__Services_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 Services_Yadis_XMLParser $parser An instance of a + * Services_Yadis_XMLParser subclass. + */ +function Services_Yadis_setDefaultParser(&$parser) +{ + global $__Services_Yadis_defaultParser; + $__Services_Yadis_defaultParser =& $parser; +} + +$__Services_Yadis_xml_extensions = array( + 'dom' => 'Services_Yadis_dom', + 'domxml' => 'Services_Yadis_domxml' + ); + +/** + * Returns an instance of a Services_Yadis_XMLParser subclass based on + * the availability of PHP extensions for XML parsing. If + * Services_Yadis_setDefaultParser has been called, the parser used in + * that call will be returned instead. + */ +function &Services_Yadis_getXMLParser() +{ + global $__Services_Yadis_defaultParser, + $__Services_Yadis_xml_extensions; + + if ($__Services_Yadis_defaultParser) { + return $__Services_Yadis_defaultParser; + } + + $p = null; + + // Return a wrapper for the resident implementation, if any. + foreach ($__Services_Yadis_xml_extensions as $name => $cls) { + if (extension_loaded($name) || + @dl($name . '.so')) { + // First create a dummy variable because PHP doesn't let + // you return things by reference unless they're + // variables. Feh. + $p = new $cls(); + return $p; + } + } + + return null; +} + +?>
\ No newline at end of file diff --git a/Services/Yadis/XRDS.php b/Services/Yadis/XRDS.php new file mode 100644 index 0000000..55674c4 --- /dev/null +++ b/Services/Yadis/XRDS.php @@ -0,0 +1,418 @@ +<?php + +/** + * This module contains the XRDS parsing code. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Require the XPath implementation. + */ +require_once 'Services/Yadis/XML.php'; + +/** + * This match mode means a given service must match ALL filters passed + * to the Services_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 Services_Yadis_XRDS::services() call. + */ +define('SERVICES_YADIS_MATCH_ANY', 102); + +global $_Services_Yadis_ns_map; +$_Services_Yadis_ns_map = array('xrds' => 'xri://$xrds', + 'xrd' => 'xri://$xrd*($v*2.0)'); + +define('SERVICES_YADIS_MAX_PRIORITY', pow(2, 30)); + +/** + * @access private + */ +function Services_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 <Service> element in an XRDS document. + * Objects of this type are returned by + * Services_Yadis_XRDS::services() and + * Services_Yadis_Yadis::services(). Each object corresponds directly + * to a <Service> element in the XRDS and supplies a + * getElements($name) method which you should use to inspect the + * element's contents. See {@link Services_Yadis_Yadis} for more + * information on the role this class plays in Yadis discovery. + * + * @package Yadis + */ +class Services_Yadis_Service { + + /** + * Creates an empty service object. + */ + function Services_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; + } + + /** + * 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 = Services_Yadis_array_scramble($uris[$k]); + $result = array_merge($result, $new_uris); + } + + $result = array_merge($result, + Services_Yadis_array_scramble($last)); + + return $result; + } + + /** + * Returns the "priority" attribute value of this <Service> + * 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 <Service> 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 <Service> 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 <Service> element. The + * nodes returned by this function can be passed to $this->parser + * methods (see {@link Services_Yadis_XMLParser}). + */ + function getElements($name) + { + return $this->parser->evalXPath($name, $this->element); + } +} + +/** + * This class performs parsing of XRDS documents. + * + * You should not instantiate this class directly; rather, call + * parseXRDS statically: + * + * <pre> $xrds = Services_Yadis_XRDS::parseXRDS($xml_string);</pre> + * + * If the XRDS can be parsed and is valid, an instance of + * Services_Yadis_XRDS will be returned. Otherwise, null will be + * returned. This class is used by the Services_Yadis_Yadis::discover + * method. + * + * @package Yadis + */ +class Services_Yadis_XRDS { + + /** + * Instantiate a Services_Yadis_XRDS object. Requires an XPath + * instance which has been used to parse a valid XRDS document. + */ + function Services_Yadis_XRDS(&$xmlParser, &$xrdNode) + { + $this->parser =& $xmlParser; + $this->xrdNode = $xrdNode; + $this->serviceList = array(); + $this->_parse(); + } + + /** + * Parse an XML string (XRDS document) and return either a + * Services_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 Services_Yadis_XRDS or null, + * depending on the validity of $xml_string + */ + function parseXRDS($xml_string, $extra_ns_map = null) + { + global $_Services_Yadis_ns_map; + + if (!$xml_string) { + return null; + } + + $parser = Services_Yadis_getXMLParser(); + + $ns_map = $_Services_Yadis_ns_map; + + 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'] != 'xri://$xrd*($v*2.0)') { + return null; + } else if (array_key_exists('xmlns', $attrs) && + preg_match('/xri/', $attrs['xmlns']) && + $attrs['xmlns'] != 'xri://$xrd*($v*2.0)') { + return null; + } + + // Get the last XRD node. + $xrd_nodes = $parser->evalXPath('/xrds:XRDS[1]/xrd:XRD[last()]'); + + if (!$xrd_nodes) { + return null; + } + + $xrds = new Services_Yadis_XRDS($parser, $xrd_nodes[0]); + 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 Services_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 <Service> + * 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 + * Services_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 + * Services_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/Services/Yadis/Yadis.php b/Services/Yadis/Yadis.php new file mode 100644 index 0000000..78fb11c --- /dev/null +++ b/Services/Yadis/Yadis.php @@ -0,0 +1,309 @@ +<?php + +/** + * The core PHP Yadis implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Need both fetcher types so we can use the right one based on the + * presence or absence of CURL. + */ +require_once "Services/Yadis/PlainHTTPFetcher.php"; +require_once "Services/Yadis/ParanoidHTTPFetcher.php"; + +/** + * Need this for parsing HTML (looking for META tags). + */ +require_once "Services/Yadis/ParseHTML.php"; + +/** + * Need this to parse the XRDS document during Yadis discovery. + */ +require_once "Services/Yadis/XRDS.php"; + +/** + * 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: + * + * <pre> require_once "Services/Yadis/Yadis.php";</pre> + * + * To perform Yadis discovery, first call the "discover" method + * statically with a URI parameter: + * + * <pre> $http_response = array(); + * $fetcher = Services_Yadis_Yadis::getHTTPFetcher(); + * $yadis_object = Services_Yadis_Yadis::discover($uri, + * $http_response, $fetcher);</pre> + * + * If the discovery succeeds, $yadis_object will be an instance of + * {@link Services_Yadis_Yadis}. If not, it will be null. The XRDS + * document found during discovery should have service descriptions, + * which can be accessed by calling + * + * <pre> $service_list = $yadis_object->services();</pre> + * + * which returns an array of objects which describe each service. + * These objects are instances of Services_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(): + * + * <pre> $service_list = $yadis_object->services( + * array("filterByURI", + * "filterByExtension"));</pre> + * + * The filter functions (whose names appear in the array passed to + * services()) take the following form: + * + * <pre> function myFilter(&$service) { + * // Query $service object here. Return true if the service + * // matches your query; false if not. + * }</pre> + * + * This is an example of a filter which uses a regular expression to + * match the content of URI tags (note that the Services_Yadis_Service + * class provides a getURIs() method which you should use instead of + * this contrived example): + * + * <pre> + * function URIMatcher(&$service) { + * foreach ($service->getElements('xrd:URI') as $uri) { + * if (preg_match("/some_pattern/", + * $service->parser->content($uri))) { + * return true; + * } + * } + * return false; + * }</pre> + * + * 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(): + * + * <pre> $yadis_object->services(array("filter1", "filter2"), + * SERVICES_YADIS_MATCH_ALL);</pre> + * + * 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 + * Services_Yadis_Service}. + * + * @package Yadis + */ +class Services_Yadis_Yadis { + + /** + * Returns an HTTP fetcher object. If the CURL extension is + * present, an instance of {@link Services_Yadis_ParanoidHTTPFetcher} + * is returned. If not, an instance of + * {@link Services_Yadis_PlainHTTPFetcher} is returned. + */ + function getHTTPFetcher($timeout = 20) + { + if (defined('Services_Yadis_CURL_PRESENT') && + Services_Yadis_CURL_PRESENT) { + $fetcher = new Services_Yadis_ParanoidHTTPFetcher($timeout); + } else { + $fetcher = new Services_Yadis_PlainHTTPFetcher($timeout); + } + return $fetcher; + } + + /** + * @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 + * Services_Yadis_HTTPResponse}. + * + * @param Services_Yadis_HTTPFetcher $fetcher An instance of a + * Services_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 + * Services_Yadis_Yadis, depending on whether the discovery + * succeeded. + */ + function discover($uri, &$http_response, &$fetcher, + $extra_ns_map = null, $timeout = 20) + { + if (!$uri) { + return null; + } + + $request_uri = $uri; + $headers = array("Accept: application/xrds+xml"); + + if (!$fetcher) { + $fetcher = Services_Yadis_Yadis::getHTTPFetcher($timeout); + } + + $response = $fetcher->get($uri, $headers); + $http_response = $response; + + if (!$response) { + return null; + } + + if ($response->status != 200) { + return null; + } + + $xrds_uri = $response->final_url; + $uri = $response->final_url; + $body = $response->body; + + $xrds_header_uri = Services_Yadis_Yadis::_getHeader( + $response->headers, + array('x-xrds-location', + 'x-yadis-location')); + + $content_type = Services_Yadis_Yadis::_getHeader($response->headers, + array('content-type')); + + if ($xrds_header_uri) { + $xrds_uri = $xrds_header_uri; + $response = $fetcher->get($xrds_uri); + $http_response = $response; + if (!$response) { + return null; + } else { + $body = $response->body; + $headers = $response->headers; + $content_type = Services_Yadis_Yadis::_getHeader($headers, + array('content-type')); + } + } + + if (Services_Yadis_Yadis::_getContentType($content_type) != + 'application/xrds+xml') { + // Treat the body as HTML and look for a META tag. + $parser = new Services_Yadis_ParseHTML(); + $new_uri = $parser->getHTTPEquiv($body); + $xrds_uri = null; + if ($new_uri) { + $response = $fetcher->get($new_uri); + if ($response->status != 200) { + return null; + } + $http_response = $response; + $body = $response->body; + $xrds_uri = $new_uri; + $content_type = Services_Yadis_Yadis::_getHeader( + $response->headers, + array('content-type')); + } + } + + $xrds = Services_Yadis_XRDS::parseXRDS($body, $extra_ns_map); + + if ($xrds !== null) { + $y = new Services_Yadis_Yadis(); + + $y->request_uri = $request_uri; + $y->xrds = $xrds; + $y->uri = $uri; + $y->xrds_uri = $xrds_uri; + $y->body = $body; + $y->content_type = $content_type; + + return $y; + } else { + return null; + } + } + + /** + * Instantiates an empty Services_Yadis_Yadis object. This + * constructor should not be used by any user of the library. + * This constructor results in a completely useless object which + * must be populated with valid discovery information. Instead of + * using this constructor, call + * Services_Yadis_Yadis::discover($uri). + */ + function Services_Yadis_Yadis() + { + $this->request_uri = null; + $this->uri = null; + $this->xrds = null; + $this->xrds_uri = null; + $this->body = null; + $this->content_type = null; + } + + /** + * 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 Services_Yadis_Service} + * objects + */ + function services() + { + if ($this->xrds) { + return $this->xrds->services(); + } + + return null; + } +} + +?>
\ No newline at end of file |