diff options
Diffstat (limited to 'extlib')
76 files changed, 41102 insertions, 0 deletions
diff --git a/extlib/Auth/OpenID.php b/extlib/Auth/OpenID.php new file mode 100644 index 000000000..6a6e54f8b --- /dev/null +++ b/extlib/Auth/OpenID.php @@ -0,0 +1,552 @@ +<?php + +/** + * This is the PHP OpenID library by JanRain, Inc. + * + * This module contains core utility functionality used by the + * library. See Consumer.php and Server.php for the consumer and + * server implementations. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * The library version string + */ +define('Auth_OpenID_VERSION', '2.1.1'); + +/** + * Require the fetcher code. + */ +require_once "Auth/Yadis/PlainHTTPFetcher.php"; +require_once "Auth/Yadis/ParanoidHTTPFetcher.php"; +require_once "Auth/OpenID/BigMath.php"; +require_once "Auth/OpenID/URINorm.php"; + +/** + * Status code returned by the server when the only option is to show + * an error page, since we do not have enough information to redirect + * back to the consumer. The associated value is an error message that + * should be displayed on an HTML error page. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_LOCAL_ERROR', 'local_error'); + +/** + * Status code returned when there is an error to return in key-value + * form to the consumer. The caller should return a 400 Bad Request + * response with content-type text/plain and the value as the body. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_REMOTE_ERROR', 'remote_error'); + +/** + * Status code returned when there is a key-value form OK response to + * the consumer. The value associated with this code is the + * response. The caller should return a 200 OK response with + * content-type text/plain and the value as the body. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_REMOTE_OK', 'remote_ok'); + +/** + * Status code returned when there is a redirect back to the + * consumer. The value is the URL to redirect back to. The caller + * should return a 302 Found redirect with a Location: header + * containing the URL. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_REDIRECT', 'redirect'); + +/** + * Status code returned when the caller needs to authenticate the + * user. The associated value is a {@link Auth_OpenID_ServerRequest} + * object that can be used to complete the authentication. If the user + * has taken some authentication action, use the retry() method of the + * {@link Auth_OpenID_ServerRequest} object to complete the request. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_DO_AUTH', 'do_auth'); + +/** + * Status code returned when there were no OpenID arguments + * passed. This code indicates that the caller should return a 200 OK + * response and display an HTML page that says that this is an OpenID + * server endpoint. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_DO_ABOUT', 'do_about'); + +/** + * Defines for regexes and format checking. + */ +define('Auth_OpenID_letters', + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); + +define('Auth_OpenID_digits', + "0123456789"); + +define('Auth_OpenID_punct', + "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"); + +if (Auth_OpenID_getMathLib() === null) { + Auth_OpenID_setNoMathSupport(); +} + +/** + * The OpenID utility function class. + * + * @package OpenID + * @access private + */ +class Auth_OpenID { + + /** + * Return true if $thing is an Auth_OpenID_FailureResponse object; + * false if not. + * + * @access private + */ + function isFailure($thing) + { + return is_a($thing, 'Auth_OpenID_FailureResponse'); + } + + /** + * Gets the query data from the server environment based on the + * request method used. If GET was used, this looks at + * $_SERVER['QUERY_STRING'] directly. If POST was used, this + * fetches data from the special php://input file stream. + * + * Returns an associative array of the query arguments. + * + * Skips invalid key/value pairs (i.e. keys with no '=value' + * portion). + * + * Returns an empty array if neither GET nor POST was used, or if + * POST was used but php://input cannot be opened. + * + * @access private + */ + function getQuery($query_str=null) + { + $data = array(); + + if ($query_str !== null) { + $data = Auth_OpenID::params_from_string($query_str); + } else if (!array_key_exists('REQUEST_METHOD', $_SERVER)) { + // Do nothing. + } else { + // XXX HACK FIXME HORRIBLE. + // + // POSTing to a URL with query parameters is acceptable, but + // we don't have a clean way to distinguish those parameters + // when we need to do things like return_to verification + // which only want to look at one kind of parameter. We're + // going to emulate the behavior of some other environments + // by defaulting to GET and overwriting with POST if POST + // data is available. + $data = Auth_OpenID::params_from_string($_SERVER['QUERY_STRING']); + + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $str = file_get_contents('php://input'); + + if ($str === false) { + $post = array(); + } else { + $post = Auth_OpenID::params_from_string($str); + } + + $data = array_merge($data, $post); + } + } + + return $data; + } + + function params_from_string($str) + { + $chunks = explode("&", $str); + + $data = array(); + foreach ($chunks as $chunk) { + $parts = explode("=", $chunk, 2); + + if (count($parts) != 2) { + continue; + } + + list($k, $v) = $parts; + $data[$k] = urldecode($v); + } + + return $data; + } + + /** + * Create dir_name as a directory if it does not exist. If it + * exists, make sure that it is, in fact, a directory. Returns + * true if the operation succeeded; false if not. + * + * @access private + */ + function ensureDir($dir_name) + { + if (is_dir($dir_name) || @mkdir($dir_name)) { + return true; + } else { + $parent_dir = dirname($dir_name); + + // Terminal case; there is no parent directory to create. + if ($parent_dir == $dir_name) { + return true; + } + + return (Auth_OpenID::ensureDir($parent_dir) && @mkdir($dir_name)); + } + } + + /** + * Adds a string prefix to all values of an array. Returns a new + * array containing the prefixed values. + * + * @access private + */ + function addPrefix($values, $prefix) + { + $new_values = array(); + foreach ($values as $s) { + $new_values[] = $prefix . $s; + } + return $new_values; + } + + /** + * Convenience function for getting array values. Given an array + * $arr and a key $key, get the corresponding value from the array + * or return $default if the key is absent. + * + * @access private + */ + function arrayGet($arr, $key, $fallback = null) + { + if (is_array($arr)) { + if (array_key_exists($key, $arr)) { + return $arr[$key]; + } else { + return $fallback; + } + } else { + trigger_error("Auth_OpenID::arrayGet (key = ".$key.") expected " . + "array as first parameter, got " . + gettype($arr), E_USER_WARNING); + + return false; + } + } + + /** + * Replacement for PHP's broken parse_str. + */ + function parse_str($query) + { + if ($query === null) { + return null; + } + + $parts = explode('&', $query); + + $new_parts = array(); + for ($i = 0; $i < count($parts); $i++) { + $pair = explode('=', $parts[$i]); + + if (count($pair) != 2) { + continue; + } + + list($key, $value) = $pair; + $new_parts[$key] = urldecode($value); + } + + return $new_parts; + } + + /** + * Implements the PHP 5 'http_build_query' functionality. + * + * @access private + * @param array $data Either an array key/value pairs or an array + * of arrays, each of which holding two values: a key and a value, + * sequentially. + * @return string $result The result of url-encoding the key/value + * pairs from $data into a URL query string + * (e.g. "username=bob&id=56"). + */ + function httpBuildQuery($data) + { + $pairs = array(); + foreach ($data as $key => $value) { + if (is_array($value)) { + $pairs[] = urlencode($value[0])."=".urlencode($value[1]); + } else { + $pairs[] = urlencode($key)."=".urlencode($value); + } + } + return implode("&", $pairs); + } + + /** + * "Appends" query arguments onto a URL. The URL may or may not + * already have arguments (following a question mark). + * + * @access private + * @param string $url A URL, which may or may not already have + * arguments. + * @param array $args Either an array key/value pairs or an array of + * arrays, each of which holding two values: a key and a value, + * sequentially. If $args is an ordinary key/value array, the + * parameters will be added to the URL in sorted alphabetical order; + * if $args is an array of arrays, their order will be preserved. + * @return string $url The original URL with the new parameters added. + * + */ + function appendArgs($url, $args) + { + 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; + } + + $sep = '?'; + if (strpos($url, '?') !== false) { + $sep = '&'; + } + + return $url . $sep . Auth_OpenID::httpBuildQuery($args); + } + + /** + * Implements python's urlunparse, which is not available in PHP. + * Given the specified components of a URL, this function rebuilds + * and returns the URL. + * + * @access private + * @param string $scheme The scheme (e.g. 'http'). Defaults to 'http'. + * @param string $host The host. Required. + * @param string $port The port. + * @param string $path The path. + * @param string $query The query. + * @param string $fragment The fragment. + * @return string $url The URL resulting from assembling the + * specified components. + */ + function urlunparse($scheme, $host, $port = null, $path = '/', + $query = '', $fragment = '') + { + + if (!$scheme) { + $scheme = 'http'; + } + + if (!$host) { + return false; + } + + if (!$path) { + $path = ''; + } + + $result = $scheme . "://" . $host; + + if ($port) { + $result .= ":" . $port; + } + + $result .= $path; + + if ($query) { + $result .= "?" . $query; + } + + if ($fragment) { + $result .= "#" . $fragment; + } + + return $result; + } + + /** + * Given a URL, this "normalizes" it by adding a trailing slash + * and / or a leading http:// scheme where necessary. Returns + * null if the original URL is malformed and cannot be normalized. + * + * @access private + * @param string $url The URL to be normalized. + * @return mixed $new_url The URL after normalization, or null if + * $url was malformed. + */ + function normalizeUrl($url) + { + @$parsed = parse_url($url); + + if (!$parsed) { + return null; + } + + if (isset($parsed['scheme']) && + isset($parsed['host'])) { + $scheme = strtolower($parsed['scheme']); + if (!in_array($scheme, array('http', 'https'))) { + return null; + } + } else { + $url = 'http://' . $url; + } + + $normalized = Auth_OpenID_urinorm($url); + if ($normalized === null) { + return null; + } + list($defragged, $frag) = Auth_OpenID::urldefrag($normalized); + return $defragged; + } + + /** + * Replacement (wrapper) for PHP's intval() because it's broken. + * + * @access private + */ + function intval($value) + { + $re = "/^\\d+$/"; + + if (!preg_match($re, $value)) { + return false; + } + + return intval($value); + } + + /** + * Count the number of bytes in a string independently of + * multibyte support conditions. + * + * @param string $str The string of bytes to count. + * @return int The number of bytes in $str. + */ + function bytes($str) + { + return strlen(bin2hex($str)) / 2; + } + + /** + * Get the bytes in a string independently of multibyte support + * conditions. + */ + function toBytes($str) + { + $hex = bin2hex($str); + + if (!$hex) { + return array(); + } + + $b = array(); + for ($i = 0; $i < strlen($hex); $i += 2) { + $b[] = chr(base_convert(substr($hex, $i, 2), 16, 10)); + } + + return $b; + } + + function urldefrag($url) + { + $parts = explode("#", $url, 2); + + if (count($parts) == 1) { + return array($parts[0], ""); + } else { + return $parts; + } + } + + function filter($callback, &$sequence) + { + $result = array(); + + foreach ($sequence as $item) { + if (call_user_func_array($callback, array($item))) { + $result[] = $item; + } + } + + return $result; + } + + function update(&$dest, &$src) + { + foreach ($src as $k => $v) { + $dest[$k] = $v; + } + } + + /** + * Wrap PHP's standard error_log functionality. Use this to + * perform all logging. It will interpolate any additional + * arguments into the format string before logging. + * + * @param string $format_string The sprintf format for the message + */ + function log($format_string) + { + $args = func_get_args(); + $message = call_user_func_array('sprintf', $args); + error_log($message); + } + + function autoSubmitHTML($form, $title="OpenId transaction in progress") + { + return("<html>". + "<head><title>". + $title . + "</title></head>". + "<body onload='document.forms[0].submit();'>". + $form . + "<script>". + "var elements = document.forms[0].elements;". + "for (var i = 0; i < elements.length; i++) {". + " elements[i].style.display = \"none\";". + "}". + "</script>". + "</body>". + "</html>"); + } +} +?> diff --git a/extlib/Auth/OpenID/AX.php b/extlib/Auth/OpenID/AX.php new file mode 100644 index 000000000..4a617ae30 --- /dev/null +++ b/extlib/Auth/OpenID/AX.php @@ -0,0 +1,1023 @@ +<?php + +/** + * Implements the OpenID attribute exchange specification, version 1.0 + * as of svn revision 370 from openid.net svn. + * + * @package OpenID + */ + +/** + * Require utility classes and functions for the consumer. + */ +require_once "Auth/OpenID/Extension.php"; +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/TrustRoot.php"; + +define('Auth_OpenID_AX_NS_URI', + 'http://openid.net/srv/ax/1.0'); + +// Use this as the 'count' value for an attribute in a FetchRequest to +// ask for as many values as the OP can provide. +define('Auth_OpenID_AX_UNLIMITED_VALUES', 'unlimited'); + +// Minimum supported alias length in characters. Here for +// completeness. +define('Auth_OpenID_AX_MINIMUM_SUPPORTED_ALIAS_LENGTH', 32); + +/** + * AX utility class. + * + * @package OpenID + */ +class Auth_OpenID_AX { + /** + * @param mixed $thing Any object which may be an + * Auth_OpenID_AX_Error object. + * + * @return bool true if $thing is an Auth_OpenID_AX_Error; false + * if not. + */ + function isError($thing) + { + return is_a($thing, 'Auth_OpenID_AX_Error'); + } +} + +/** + * Check an alias for invalid characters; raise AXError if any are + * found. Return None if the alias is valid. + */ +function Auth_OpenID_AX_checkAlias($alias) +{ + if (strpos($alias, ',') !== false) { + return new Auth_OpenID_AX_Error(sprintf( + "Alias %s must not contain comma", $alias)); + } + if (strpos($alias, '.') !== false) { + return new Auth_OpenID_AX_Error(sprintf( + "Alias %s must not contain period", $alias)); + } + + return true; +} + +/** + * Results from data that does not meet the attribute exchange 1.0 + * specification + * + * @package OpenID + */ +class Auth_OpenID_AX_Error { + function Auth_OpenID_AX_Error($message=null) + { + $this->message = $message; + } +} + +/** + * Abstract class containing common code for attribute exchange + * messages. + * + * @package OpenID + */ +class Auth_OpenID_AX_Message extends Auth_OpenID_Extension { + /** + * ns_alias: The preferred namespace alias for attribute exchange + * messages + */ + var $ns_alias = 'ax'; + + /** + * mode: The type of this attribute exchange message. This must be + * overridden in subclasses. + */ + var $mode = null; + + var $ns_uri = Auth_OpenID_AX_NS_URI; + + /** + * Return Auth_OpenID_AX_Error if the mode in the attribute + * exchange arguments does not match what is expected for this + * class; true otherwise. + * + * @access private + */ + function _checkMode($ax_args) + { + $mode = Auth_OpenID::arrayGet($ax_args, 'mode'); + if ($mode != $this->mode) { + return new Auth_OpenID_AX_Error( + sprintf( + "Expected mode '%s'; got '%s'", + $this->mode, $mode)); + } + + return true; + } + + /** + * Return a set of attribute exchange arguments containing the + * basic information that must be in every attribute exchange + * message. + * + * @access private + */ + function _newArgs() + { + return array('mode' => $this->mode); + } +} + +/** + * Represents a single attribute in an attribute exchange + * request. This should be added to an AXRequest object in order to + * request the attribute. + * + * @package OpenID + */ +class Auth_OpenID_AX_AttrInfo { + /** + * Construct an attribute information object. Do not call this + * directly; call make(...) instead. + * + * @param string $type_uri The type URI for this attribute. + * + * @param int $count The number of values of this type to request. + * + * @param bool $required Whether the attribute will be marked as + * required in the request. + * + * @param string $alias The name that should be given to this + * attribute in the request. + */ + function Auth_OpenID_AX_AttrInfo($type_uri, $count, $required, + $alias) + { + /** + * required: Whether the attribute will be marked as required + * when presented to the subject of the attribute exchange + * request. + */ + $this->required = $required; + + /** + * count: How many values of this type to request from the + * subject. Defaults to one. + */ + $this->count = $count; + + /** + * type_uri: The identifier that determines what the attribute + * represents and how it is serialized. For example, one type + * URI representing dates could represent a Unix timestamp in + * base 10 and another could represent a human-readable + * string. + */ + $this->type_uri = $type_uri; + + /** + * alias: The name that should be given to this attribute in + * the request. If it is not supplied, a generic name will be + * assigned. For example, if you want to call a Unix timestamp + * value 'tstamp', set its alias to that value. If two + * attributes in the same message request to use the same + * alias, the request will fail to be generated. + */ + $this->alias = $alias; + } + + /** + * Construct an attribute information object. For parameter + * details, see the constructor. + */ + function make($type_uri, $count=1, $required=false, + $alias=null) + { + if ($alias !== null) { + $result = Auth_OpenID_AX_checkAlias($alias); + + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + } + + return new Auth_OpenID_AX_AttrInfo($type_uri, $count, $required, + $alias); + } + + /** + * When processing a request for this attribute, the OP should + * call this method to determine whether all available attribute + * values were requested. If self.count == UNLIMITED_VALUES, this + * returns True. Otherwise this returns False, in which case + * self.count is an integer. + */ + function wantsUnlimitedValues() + { + return $this->count === Auth_OpenID_AX_UNLIMITED_VALUES; + } +} + +/** + * Given a namespace mapping and a string containing a comma-separated + * list of namespace aliases, return a list of type URIs that + * correspond to those aliases. + * + * @param $namespace_map The mapping from namespace URI to alias + * @param $alias_list_s The string containing the comma-separated + * list of aliases. May also be None for convenience. + * + * @return $seq The list of namespace URIs that corresponds to the + * supplied list of aliases. If the string was zero-length or None, an + * empty list will be returned. + * + * return null If an alias is present in the list of aliases but + * is not present in the namespace map. + */ +function Auth_OpenID_AX_toTypeURIs(&$namespace_map, $alias_list_s) +{ + $uris = array(); + + if ($alias_list_s) { + foreach (explode(',', $alias_list_s) as $alias) { + $type_uri = $namespace_map->getNamespaceURI($alias); + if ($type_uri === null) { + // raise KeyError( + // 'No type is defined for attribute name %r' % (alias,)) + return new Auth_OpenID_AX_Error( + sprintf('No type is defined for attribute name %s', + $alias) + ); + } else { + $uris[] = $type_uri; + } + } + } + + return $uris; +} + +/** + * An attribute exchange 'fetch_request' message. This message is sent + * by a relying party when it wishes to obtain attributes about the + * subject of an OpenID authentication request. + * + * @package OpenID + */ +class Auth_OpenID_AX_FetchRequest extends Auth_OpenID_AX_Message { + + var $mode = 'fetch_request'; + + function Auth_OpenID_AX_FetchRequest($update_url=null) + { + /** + * requested_attributes: The attributes that have been + * requested thus far, indexed by the type URI. + */ + $this->requested_attributes = array(); + + /** + * update_url: A URL that will accept responses for this + * attribute exchange request, even in the absence of the user + * who made this request. + */ + $this->update_url = $update_url; + } + + /** + * Add an attribute to this attribute exchange request. + * + * @param attribute: The attribute that is being requested + * @return true on success, false when the requested attribute is + * already present in this fetch request. + */ + function add($attribute) + { + if ($this->contains($attribute->type_uri)) { + return new Auth_OpenID_AX_Error( + sprintf("The attribute %s has already been requested", + $attribute->type_uri)); + } + + $this->requested_attributes[$attribute->type_uri] = $attribute; + + return true; + } + + /** + * Get the serialized form of this attribute fetch request. + * + * @returns Auth_OpenID_AX_FetchRequest The fetch request message parameters + */ + function getExtensionArgs() + { + $aliases = new Auth_OpenID_NamespaceMap(); + + $required = array(); + $if_available = array(); + + $ax_args = $this->_newArgs(); + + foreach ($this->requested_attributes as $type_uri => $attribute) { + if ($attribute->alias === null) { + $alias = $aliases->add($type_uri); + } else { + $alias = $aliases->addAlias($type_uri, $attribute->alias); + + if ($alias === null) { + return new Auth_OpenID_AX_Error( + sprintf("Could not add alias %s for URI %s", + $attribute->alias, $type_uri + )); + } + } + + if ($attribute->required) { + $required[] = $alias; + } else { + $if_available[] = $alias; + } + + if ($attribute->count != 1) { + $ax_args['count.' . $alias] = strval($attribute->count); + } + + $ax_args['type.' . $alias] = $type_uri; + } + + if ($required) { + $ax_args['required'] = implode(',', $required); + } + + if ($if_available) { + $ax_args['if_available'] = implode(',', $if_available); + } + + return $ax_args; + } + + /** + * Get the type URIs for all attributes that have been marked as + * required. + * + * @return A list of the type URIs for attributes that have been + * marked as required. + */ + function getRequiredAttrs() + { + $required = array(); + foreach ($this->requested_attributes as $type_uri => $attribute) { + if ($attribute->required) { + $required[] = $type_uri; + } + } + + return $required; + } + + /** + * Extract a FetchRequest from an OpenID message + * + * @param request: The OpenID request containing the attribute + * fetch request + * + * @returns mixed An Auth_OpenID_AX_Error or the + * Auth_OpenID_AX_FetchRequest extracted from the request message if + * successful + */ + function &fromOpenIDRequest($request) + { + $m = $request->message; + $obj = new Auth_OpenID_AX_FetchRequest(); + $ax_args = $m->getArgs($obj->ns_uri); + + $result = $obj->parseExtensionArgs($ax_args); + + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + + if ($obj->update_url) { + // Update URL must match the openid.realm of the + // underlying OpenID 2 message. + $realm = $m->getArg(Auth_OpenID_OPENID_NS, 'realm', + $m->getArg( + Auth_OpenID_OPENID_NS, + 'return_to')); + + if (!$realm) { + $obj = new Auth_OpenID_AX_Error( + sprintf("Cannot validate update_url %s " . + "against absent realm", $obj->update_url)); + } else if (!Auth_OpenID_TrustRoot::match($realm, + $obj->update_url)) { + $obj = new Auth_OpenID_AX_Error( + sprintf("Update URL %s failed validation against realm %s", + $obj->update_url, $realm)); + } + } + + return $obj; + } + + /** + * Given attribute exchange arguments, populate this FetchRequest. + * + * @return $result Auth_OpenID_AX_Error if the data to be parsed + * does not follow the attribute exchange specification. At least + * when 'if_available' or 'required' is not specified for a + * particular attribute type. Returns true otherwise. + */ + function parseExtensionArgs($ax_args) + { + $result = $this->_checkMode($ax_args); + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + + $aliases = new Auth_OpenID_NamespaceMap(); + + foreach ($ax_args as $key => $value) { + if (strpos($key, 'type.') === 0) { + $alias = substr($key, 5); + $type_uri = $value; + + $alias = $aliases->addAlias($type_uri, $alias); + + if ($alias === null) { + return new Auth_OpenID_AX_Error( + sprintf("Could not add alias %s for URI %s", + $alias, $type_uri) + ); + } + + $count_s = Auth_OpenID::arrayGet($ax_args, 'count.' . $alias); + if ($count_s) { + $count = Auth_OpenID::intval($count_s); + if (($count === false) && + ($count_s === Auth_OpenID_AX_UNLIMITED_VALUES)) { + $count = $count_s; + } + } else { + $count = 1; + } + + if ($count === false) { + return new Auth_OpenID_AX_Error( + sprintf("Integer value expected for %s, got %s", + 'count.' . $alias, $count_s)); + } + + $attrinfo = Auth_OpenID_AX_AttrInfo::make($type_uri, $count, + false, $alias); + + if (Auth_OpenID_AX::isError($attrinfo)) { + return $attrinfo; + } + + $this->add($attrinfo); + } + } + + $required = Auth_OpenID_AX_toTypeURIs($aliases, + Auth_OpenID::arrayGet($ax_args, 'required')); + + foreach ($required as $type_uri) { + $attrib =& $this->requested_attributes[$type_uri]; + $attrib->required = true; + } + + $if_available = Auth_OpenID_AX_toTypeURIs($aliases, + Auth_OpenID::arrayGet($ax_args, 'if_available')); + + $all_type_uris = array_merge($required, $if_available); + + foreach ($aliases->iterNamespaceURIs() as $type_uri) { + if (!in_array($type_uri, $all_type_uris)) { + return new Auth_OpenID_AX_Error( + sprintf('Type URI %s was in the request but not ' . + 'present in "required" or "if_available"', + $type_uri)); + + } + } + + $this->update_url = Auth_OpenID::arrayGet($ax_args, 'update_url'); + + return true; + } + + /** + * Iterate over the AttrInfo objects that are contained in this + * fetch_request. + */ + function iterAttrs() + { + return array_values($this->requested_attributes); + } + + function iterTypes() + { + return array_keys($this->requested_attributes); + } + + /** + * Is the given type URI present in this fetch_request? + */ + function contains($type_uri) + { + return in_array($type_uri, $this->iterTypes()); + } +} + +/** + * An abstract class that implements a message that has attribute keys + * and values. It contains the common code between fetch_response and + * store_request. + * + * @package OpenID + */ +class Auth_OpenID_AX_KeyValueMessage extends Auth_OpenID_AX_Message { + + function Auth_OpenID_AX_KeyValueMessage() + { + $this->data = array(); + } + + /** + * Add a single value for the given attribute type to the + * message. If there are already values specified for this type, + * this value will be sent in addition to the values already + * specified. + * + * @param type_uri: The URI for the attribute + * @param value: The value to add to the response to the relying + * party for this attribute + * @return null + */ + function addValue($type_uri, $value) + { + if (!array_key_exists($type_uri, $this->data)) { + $this->data[$type_uri] = array(); + } + + $values =& $this->data[$type_uri]; + $values[] = $value; + } + + /** + * Set the values for the given attribute type. This replaces any + * values that have already been set for this attribute. + * + * @param type_uri: The URI for the attribute + * @param values: A list of values to send for this attribute. + */ + function setValues($type_uri, &$values) + { + $this->data[$type_uri] =& $values; + } + + /** + * Get the extension arguments for the key/value pairs contained + * in this message. + * + * @param aliases: An alias mapping. Set to None if you don't care + * about the aliases for this request. + * + * @access private + */ + function _getExtensionKVArgs(&$aliases) + { + if ($aliases === null) { + $aliases = new Auth_OpenID_NamespaceMap(); + } + + $ax_args = array(); + + foreach ($this->data as $type_uri => $values) { + $alias = $aliases->add($type_uri); + + $ax_args['type.' . $alias] = $type_uri; + $ax_args['count.' . $alias] = strval(count($values)); + + foreach ($values as $i => $value) { + $key = sprintf('value.%s.%d', $alias, $i + 1); + $ax_args[$key] = $value; + } + } + + return $ax_args; + } + + /** + * Parse attribute exchange key/value arguments into this object. + * + * @param ax_args: The attribute exchange fetch_response + * arguments, with namespacing removed. + * + * @return Auth_OpenID_AX_Error or true + */ + function parseExtensionArgs($ax_args) + { + $result = $this->_checkMode($ax_args); + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + + $aliases = new Auth_OpenID_NamespaceMap(); + + foreach ($ax_args as $key => $value) { + if (strpos($key, 'type.') === 0) { + $type_uri = $value; + $alias = substr($key, 5); + + $result = Auth_OpenID_AX_checkAlias($alias); + + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + + $alias = $aliases->addAlias($type_uri, $alias); + + if ($alias === null) { + return new Auth_OpenID_AX_Error( + sprintf("Could not add alias %s for URI %s", + $alias, $type_uri) + ); + } + } + } + + foreach ($aliases->iteritems() as $pair) { + list($type_uri, $alias) = $pair; + + if (array_key_exists('count.' . $alias, $ax_args)) { + + $count_key = 'count.' . $alias; + $count_s = $ax_args[$count_key]; + + $count = Auth_OpenID::intval($count_s); + + if ($count === false) { + return new Auth_OpenID_AX_Error( + sprintf("Integer value expected for %s, got %s", + 'count. %s' . $alias, $count_s, + Auth_OpenID_AX_UNLIMITED_VALUES) + ); + } + + $values = array(); + for ($i = 1; $i < $count + 1; $i++) { + $value_key = sprintf('value.%s.%d', $alias, $i); + + if (!array_key_exists($value_key, $ax_args)) { + return new Auth_OpenID_AX_Error( + sprintf( + "No value found for key %s", + $value_key)); + } + + $value = $ax_args[$value_key]; + $values[] = $value; + } + } else { + $key = 'value.' . $alias; + + if (!array_key_exists($key, $ax_args)) { + return new Auth_OpenID_AX_Error( + sprintf( + "No value found for key %s", + $key)); + } + + $value = $ax_args['value.' . $alias]; + + if ($value == '') { + $values = array(); + } else { + $values = array($value); + } + } + + $this->data[$type_uri] = $values; + } + + return true; + } + + /** + * Get a single value for an attribute. If no value was sent for + * this attribute, use the supplied default. If there is more than + * one value for this attribute, this method will fail. + * + * @param type_uri: The URI for the attribute + * @param default: The value to return if the attribute was not + * sent in the fetch_response. + * + * @return $value Auth_OpenID_AX_Error on failure or the value of + * the attribute in the fetch_response message, or the default + * supplied + */ + function getSingle($type_uri, $default=null) + { + $values = Auth_OpenID::arrayGet($this->data, $type_uri); + if (!$values) { + return $default; + } else if (count($values) == 1) { + return $values[0]; + } else { + return new Auth_OpenID_AX_Error( + sprintf('More than one value present for %s', + $type_uri) + ); + } + } + + /** + * Get the list of values for this attribute in the + * fetch_response. + * + * XXX: what to do if the values are not present? default + * parameter? this is funny because it's always supposed to return + * a list, so the default may break that, though it's provided by + * the user's code, so it might be okay. If no default is + * supplied, should the return be None or []? + * + * @param type_uri: The URI of the attribute + * + * @return $values The list of values for this attribute in the + * response. May be an empty list. If the attribute was not sent + * in the response, returns Auth_OpenID_AX_Error. + */ + function get($type_uri) + { + if (array_key_exists($type_uri, $this->data)) { + return $this->data[$type_uri]; + } else { + return new Auth_OpenID_AX_Error( + sprintf("Type URI %s not found in response", + $type_uri) + ); + } + } + + /** + * Get the number of responses for a particular attribute in this + * fetch_response message. + * + * @param type_uri: The URI of the attribute + * + * @returns int The number of values sent for this attribute. If + * the attribute was not sent in the response, returns + * Auth_OpenID_AX_Error. + */ + function count($type_uri) + { + if (array_key_exists($type_uri, $this->data)) { + return count($this->get($type_uri)); + } else { + return new Auth_OpenID_AX_Error( + sprintf("Type URI %s not found in response", + $type_uri) + ); + } + } +} + +/** + * A fetch_response attribute exchange message. + * + * @package OpenID + */ +class Auth_OpenID_AX_FetchResponse extends Auth_OpenID_AX_KeyValueMessage { + var $mode = 'fetch_response'; + + function Auth_OpenID_AX_FetchResponse($update_url=null) + { + $this->Auth_OpenID_AX_KeyValueMessage(); + $this->update_url = $update_url; + } + + /** + * Serialize this object into arguments in the attribute exchange + * namespace + * + * @return $args The dictionary of unqualified attribute exchange + * arguments that represent this fetch_response, or + * Auth_OpenID_AX_Error on error. + */ + function getExtensionArgs($request=null) + { + $aliases = new Auth_OpenID_NamespaceMap(); + + $zero_value_types = array(); + + if ($request !== null) { + // Validate the data in the context of the request (the + // same attributes should be present in each, and the + // counts in the response must be no more than the counts + // in the request) + + foreach ($this->data as $type_uri => $unused) { + if (!$request->contains($type_uri)) { + return new Auth_OpenID_AX_Error( + sprintf("Response attribute not present in request: %s", + $type_uri) + ); + } + } + + foreach ($request->iterAttrs() as $attr_info) { + // Copy the aliases from the request so that reading + // the response in light of the request is easier + if ($attr_info->alias === null) { + $aliases->add($attr_info->type_uri); + } else { + $alias = $aliases->addAlias($attr_info->type_uri, + $attr_info->alias); + + if ($alias === null) { + return new Auth_OpenID_AX_Error( + sprintf("Could not add alias %s for URI %s", + $attr_info->alias, $attr_info->type_uri) + ); + } + } + + if (array_key_exists($attr_info->type_uri, $this->data)) { + $values = $this->data[$attr_info->type_uri]; + } else { + $values = array(); + $zero_value_types[] = $attr_info; + } + + if (($attr_info->count != Auth_OpenID_AX_UNLIMITED_VALUES) && + ($attr_info->count < count($values))) { + return new Auth_OpenID_AX_Error( + sprintf("More than the number of requested values " . + "were specified for %s", + $attr_info->type_uri) + ); + } + } + } + + $kv_args = $this->_getExtensionKVArgs($aliases); + + // Add the KV args into the response with the args that are + // unique to the fetch_response + $ax_args = $this->_newArgs(); + + // For each requested attribute, put its type/alias and count + // into the response even if no data were returned. + foreach ($zero_value_types as $attr_info) { + $alias = $aliases->getAlias($attr_info->type_uri); + $kv_args['type.' . $alias] = $attr_info->type_uri; + $kv_args['count.' . $alias] = '0'; + } + + $update_url = null; + if ($request) { + $update_url = $request->update_url; + } else { + $update_url = $this->update_url; + } + + if ($update_url) { + $ax_args['update_url'] = $update_url; + } + + Auth_OpenID::update(&$ax_args, $kv_args); + + return $ax_args; + } + + /** + * @return $result Auth_OpenID_AX_Error on failure or true on + * success. + */ + function parseExtensionArgs($ax_args) + { + $result = parent::parseExtensionArgs($ax_args); + + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + + $this->update_url = Auth_OpenID::arrayGet($ax_args, 'update_url'); + + return true; + } + + /** + * Construct a FetchResponse object from an OpenID library + * SuccessResponse object. + * + * @param success_response: A successful id_res response object + * + * @param signed: Whether non-signed args should be processsed. If + * True (the default), only signed arguments will be processsed. + * + * @return $response A FetchResponse containing the data from the + * OpenID message + */ + function fromSuccessResponse($success_response, $signed=true) + { + $obj = new Auth_OpenID_AX_FetchResponse(); + if ($signed) { + $ax_args = $success_response->getSignedNS($obj->ns_uri); + } else { + $ax_args = $success_response->message->getArgs($obj->ns_uri); + } + if ($ax_args === null || Auth_OpenID::isFailure($ax_args) || + sizeof($ax_args) == 0) { + return null; + } + + $result = $obj->parseExtensionArgs($ax_args); + if (Auth_OpenID_AX::isError($result)) { + #XXX log me + return null; + } + return $obj; + } +} + +/** + * A store request attribute exchange message representation. + * + * @package OpenID + */ +class Auth_OpenID_AX_StoreRequest extends Auth_OpenID_AX_KeyValueMessage { + var $mode = 'store_request'; + + /** + * @param array $aliases The namespace aliases to use when making + * this store response. Leave as None to use defaults. + */ + function getExtensionArgs($aliases=null) + { + $ax_args = $this->_newArgs(); + $kv_args = $this->_getExtensionKVArgs($aliases); + Auth_OpenID::update(&$ax_args, $kv_args); + return $ax_args; + } +} + +/** + * An indication that the store request was processed along with this + * OpenID transaction. Use make(), NOT the constructor, to create + * response objects. + * + * @package OpenID + */ +class Auth_OpenID_AX_StoreResponse extends Auth_OpenID_AX_Message { + var $SUCCESS_MODE = 'store_response_success'; + var $FAILURE_MODE = 'store_response_failure'; + + /** + * Returns Auth_OpenID_AX_Error on error or an + * Auth_OpenID_AX_StoreResponse object on success. + */ + function &make($succeeded=true, $error_message=null) + { + if (($succeeded) && ($error_message !== null)) { + return new Auth_OpenID_AX_Error('An error message may only be '. + 'included in a failing fetch response'); + } + + return new Auth_OpenID_AX_StoreResponse($succeeded, $error_message); + } + + function Auth_OpenID_AX_StoreResponse($succeeded=true, $error_message=null) + { + if ($succeeded) { + $this->mode = $this->SUCCESS_MODE; + } else { + $this->mode = $this->FAILURE_MODE; + } + + $this->error_message = $error_message; + } + + /** + * Was this response a success response? + */ + function succeeded() + { + return $this->mode == $this->SUCCESS_MODE; + } + + function getExtensionArgs() + { + $ax_args = $this->_newArgs(); + if ((!$this->succeeded()) && $this->error_message) { + $ax_args['error'] = $this->error_message; + } + + return $ax_args; + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/Association.php b/extlib/Auth/OpenID/Association.php new file mode 100644 index 000000000..37ce0cbf4 --- /dev/null +++ b/extlib/Auth/OpenID/Association.php @@ -0,0 +1,613 @@ +<?php + +/** + * This module contains code for dealing with associations between + * consumers and servers. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * @access private + */ +require_once 'Auth/OpenID/CryptUtil.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID/KVForm.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID/HMAC.php'; + +/** + * This class represents an association between a server and a + * consumer. In general, users of this library will never see + * instances of this object. The only exception is if you implement a + * custom {@link Auth_OpenID_OpenIDStore}. + * + * If you do implement such a store, it will need to store the values + * of the handle, secret, issued, lifetime, and assoc_type instance + * variables. + * + * @package OpenID + */ +class Auth_OpenID_Association { + + /** + * This is a HMAC-SHA1 specific value. + * + * @access private + */ + var $SIG_LENGTH = 20; + + /** + * The ordering and name of keys as stored by serialize. + * + * @access private + */ + var $assoc_keys = array( + 'version', + 'handle', + 'secret', + 'issued', + 'lifetime', + 'assoc_type' + ); + + var $_macs = array( + 'HMAC-SHA1' => 'Auth_OpenID_HMACSHA1', + 'HMAC-SHA256' => 'Auth_OpenID_HMACSHA256' + ); + + /** + * This is an alternate constructor (factory method) used by the + * OpenID consumer library to create associations. OpenID store + * implementations shouldn't use this constructor. + * + * @access private + * + * @param integer $expires_in This is the amount of time this + * association is good for, measured in seconds since the + * association was issued. + * + * @param string $handle This is the handle the server gave this + * association. + * + * @param string secret This is the shared secret the server + * generated for this association. + * + * @param assoc_type This is the type of association this + * instance represents. The only valid values of this field at + * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may + * be defined in the future. + * + * @return association An {@link Auth_OpenID_Association} + * instance. + */ + function fromExpiresIn($expires_in, $handle, $secret, $assoc_type) + { + $issued = time(); + $lifetime = $expires_in; + return new Auth_OpenID_Association($handle, $secret, + $issued, $lifetime, $assoc_type); + } + + /** + * This is the standard constructor for creating an association. + * The library should create all of the necessary associations, so + * this constructor is not part of the external API. + * + * @access private + * + * @param string $handle This is the handle the server gave this + * association. + * + * @param string $secret This is the shared secret the server + * generated for this association. + * + * @param integer $issued This is the time this association was + * issued, in seconds since 00:00 GMT, January 1, 1970. (ie, a + * unix timestamp) + * + * @param integer $lifetime This is the amount of time this + * association is good for, measured in seconds since the + * association was issued. + * + * @param string $assoc_type This is the type of association this + * instance represents. The only valid values of this field at + * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may + * be defined in the future. + */ + function Auth_OpenID_Association( + $handle, $secret, $issued, $lifetime, $assoc_type) + { + if (!in_array($assoc_type, + Auth_OpenID_getSupportedAssociationTypes())) { + $fmt = 'Unsupported association type (%s)'; + trigger_error(sprintf($fmt, $assoc_type), E_USER_ERROR); + } + + $this->handle = $handle; + $this->secret = $secret; + $this->issued = $issued; + $this->lifetime = $lifetime; + $this->assoc_type = $assoc_type; + } + + /** + * This returns the number of seconds this association is still + * valid for, or 0 if the association is no longer valid. + * + * @return integer $seconds The number of seconds this association + * is still valid for, or 0 if the association is no longer valid. + */ + function getExpiresIn($now = null) + { + if ($now == null) { + $now = time(); + } + + return max(0, $this->issued + $this->lifetime - $now); + } + + /** + * This checks to see if two {@link Auth_OpenID_Association} + * instances represent the same association. + * + * @return bool $result true if the two instances represent the + * same association, false otherwise. + */ + function equal($other) + { + return ((gettype($this) == gettype($other)) + && ($this->handle == $other->handle) + && ($this->secret == $other->secret) + && ($this->issued == $other->issued) + && ($this->lifetime == $other->lifetime) + && ($this->assoc_type == $other->assoc_type)); + } + + /** + * Convert an association to KV form. + * + * @return string $result String in KV form suitable for + * deserialization by deserialize. + */ + function serialize() + { + $data = array( + 'version' => '2', + 'handle' => $this->handle, + 'secret' => base64_encode($this->secret), + 'issued' => strval(intval($this->issued)), + 'lifetime' => strval(intval($this->lifetime)), + 'assoc_type' => $this->assoc_type + ); + + assert(array_keys($data) == $this->assoc_keys); + + return Auth_OpenID_KVForm::fromArray($data, $strict = true); + } + + /** + * Parse an association as stored by serialize(). This is the + * inverse of serialize. + * + * @param string $assoc_s Association as serialized by serialize() + * @return Auth_OpenID_Association $result instance of this class + */ + function deserialize($class_name, $assoc_s) + { + $pairs = Auth_OpenID_KVForm::toArray($assoc_s, $strict = true); + $keys = array(); + $values = array(); + foreach ($pairs as $key => $value) { + if (is_array($value)) { + list($key, $value) = $value; + } + $keys[] = $key; + $values[] = $value; + } + + $class_vars = get_class_vars($class_name); + $class_assoc_keys = $class_vars['assoc_keys']; + + sort($keys); + sort($class_assoc_keys); + + if ($keys != $class_assoc_keys) { + trigger_error('Unexpected key values: ' . var_export($keys, true), + E_USER_WARNING); + return null; + } + + $version = $pairs['version']; + $handle = $pairs['handle']; + $secret = $pairs['secret']; + $issued = $pairs['issued']; + $lifetime = $pairs['lifetime']; + $assoc_type = $pairs['assoc_type']; + + if ($version != '2') { + trigger_error('Unknown version: ' . $version, E_USER_WARNING); + return null; + } + + $issued = intval($issued); + $lifetime = intval($lifetime); + $secret = base64_decode($secret); + + return new $class_name( + $handle, $secret, $issued, $lifetime, $assoc_type); + } + + /** + * Generate a signature for a sequence of (key, value) pairs + * + * @access private + * @param array $pairs The pairs to sign, in order. This is an + * array of two-tuples. + * @return string $signature The binary signature of this sequence + * of pairs + */ + function sign($pairs) + { + $kv = Auth_OpenID_KVForm::fromArray($pairs); + + /* Invalid association types should be caught at constructor */ + $callback = $this->_macs[$this->assoc_type]; + + return call_user_func_array($callback, array($this->secret, $kv)); + } + + /** + * Generate a signature for some fields in a dictionary + * + * @access private + * @param array $fields The fields to sign, in order; this is an + * array of strings. + * @param array $data Dictionary of values to sign (an array of + * string => string pairs). + * @return string $signature The signature, base64 encoded + */ + function signMessage($message) + { + if ($message->hasKey(Auth_OpenID_OPENID_NS, 'sig') || + $message->hasKey(Auth_OpenID_OPENID_NS, 'signed')) { + // Already has a sig + return null; + } + + $extant_handle = $message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_handle'); + + if ($extant_handle && ($extant_handle != $this->handle)) { + // raise ValueError("Message has a different association handle") + return null; + } + + $signed_message = $message; + $signed_message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle', + $this->handle); + + $message_keys = array_keys($signed_message->toPostArgs()); + $signed_list = array(); + $signed_prefix = 'openid.'; + + foreach ($message_keys as $k) { + if (strpos($k, $signed_prefix) === 0) { + $signed_list[] = substr($k, strlen($signed_prefix)); + } + } + + $signed_list[] = 'signed'; + sort($signed_list); + + $signed_message->setArg(Auth_OpenID_OPENID_NS, 'signed', + implode(',', $signed_list)); + $sig = $this->getMessageSignature($signed_message); + $signed_message->setArg(Auth_OpenID_OPENID_NS, 'sig', $sig); + return $signed_message; + } + + /** + * Given a {@link Auth_OpenID_Message}, return the key/value pairs + * to be signed according to the signed list in the message. If + * the message lacks a signed list, return null. + * + * @access private + */ + function _makePairs(&$message) + { + $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); + if (!$signed || Auth_OpenID::isFailure($signed)) { + // raise ValueError('Message has no signed list: %s' % (message,)) + return null; + } + + $signed_list = explode(',', $signed); + $pairs = array(); + $data = $message->toPostArgs(); + foreach ($signed_list as $field) { + $pairs[] = array($field, Auth_OpenID::arrayGet($data, + 'openid.' . + $field, '')); + } + return $pairs; + } + + /** + * Given an {@link Auth_OpenID_Message}, return the signature for + * the signed list in the message. + * + * @access private + */ + function getMessageSignature(&$message) + { + $pairs = $this->_makePairs($message); + return base64_encode($this->sign($pairs)); + } + + /** + * Confirm that the signature of these fields matches the + * signature contained in the data. + * + * @access private + */ + function checkMessageSignature(&$message) + { + $sig = $message->getArg(Auth_OpenID_OPENID_NS, + 'sig'); + + if (!$sig || Auth_OpenID::isFailure($sig)) { + return false; + } + + $calculated_sig = $this->getMessageSignature($message); + return $calculated_sig == $sig; + } +} + +function Auth_OpenID_getSecretSize($assoc_type) +{ + if ($assoc_type == 'HMAC-SHA1') { + return 20; + } else if ($assoc_type == 'HMAC-SHA256') { + return 32; + } else { + return null; + } +} + +function Auth_OpenID_getAllAssociationTypes() +{ + return array('HMAC-SHA1', 'HMAC-SHA256'); +} + +function Auth_OpenID_getSupportedAssociationTypes() +{ + $a = array('HMAC-SHA1'); + + if (Auth_OpenID_HMACSHA256_SUPPORTED) { + $a[] = 'HMAC-SHA256'; + } + + return $a; +} + +function Auth_OpenID_getSessionTypes($assoc_type) +{ + $assoc_to_session = array( + 'HMAC-SHA1' => array('DH-SHA1', 'no-encryption')); + + if (Auth_OpenID_HMACSHA256_SUPPORTED) { + $assoc_to_session['HMAC-SHA256'] = + array('DH-SHA256', 'no-encryption'); + } + + return Auth_OpenID::arrayGet($assoc_to_session, $assoc_type, array()); +} + +function Auth_OpenID_checkSessionType($assoc_type, $session_type) +{ + if (!in_array($session_type, + Auth_OpenID_getSessionTypes($assoc_type))) { + return false; + } + + return true; +} + +function Auth_OpenID_getDefaultAssociationOrder() +{ + $order = array(); + + if (!Auth_OpenID_noMathSupport()) { + $order[] = array('HMAC-SHA1', 'DH-SHA1'); + + if (Auth_OpenID_HMACSHA256_SUPPORTED) { + $order[] = array('HMAC-SHA256', 'DH-SHA256'); + } + } + + $order[] = array('HMAC-SHA1', 'no-encryption'); + + if (Auth_OpenID_HMACSHA256_SUPPORTED) { + $order[] = array('HMAC-SHA256', 'no-encryption'); + } + + return $order; +} + +function Auth_OpenID_getOnlyEncryptedOrder() +{ + $result = array(); + + foreach (Auth_OpenID_getDefaultAssociationOrder() as $pair) { + list($assoc, $session) = $pair; + + if ($session != 'no-encryption') { + if (Auth_OpenID_HMACSHA256_SUPPORTED && + ($assoc == 'HMAC-SHA256')) { + $result[] = $pair; + } else if ($assoc != 'HMAC-SHA256') { + $result[] = $pair; + } + } + } + + return $result; +} + +function &Auth_OpenID_getDefaultNegotiator() +{ + $x = new Auth_OpenID_SessionNegotiator( + Auth_OpenID_getDefaultAssociationOrder()); + return $x; +} + +function &Auth_OpenID_getEncryptedNegotiator() +{ + $x = new Auth_OpenID_SessionNegotiator( + Auth_OpenID_getOnlyEncryptedOrder()); + return $x; +} + +/** + * A session negotiator controls the allowed and preferred association + * types and association session types. Both the {@link + * Auth_OpenID_Consumer} and {@link Auth_OpenID_Server} use + * negotiators when creating associations. + * + * You can create and use negotiators if you: + + * - Do not want to do Diffie-Hellman key exchange because you use + * transport-layer encryption (e.g. SSL) + * + * - Want to use only SHA-256 associations + * + * - Do not want to support plain-text associations over a non-secure + * channel + * + * It is up to you to set a policy for what kinds of associations to + * accept. By default, the library will make any kind of association + * that is allowed in the OpenID 2.0 specification. + * + * Use of negotiators in the library + * ================================= + * + * When a consumer makes an association request, it calls {@link + * getAllowedType} to get the preferred association type and + * association session type. + * + * The server gets a request for a particular association/session type + * and calls {@link isAllowed} to determine if it should create an + * association. If it is supported, negotiation is complete. If it is + * not, the server calls {@link getAllowedType} to get an allowed + * association type to return to the consumer. + * + * If the consumer gets an error response indicating that the + * requested association/session type is not supported by the server + * that contains an assocation/session type to try, it calls {@link + * isAllowed} to determine if it should try again with the given + * combination of association/session type. + * + * @package OpenID + */ +class Auth_OpenID_SessionNegotiator { + function Auth_OpenID_SessionNegotiator($allowed_types) + { + $this->allowed_types = array(); + $this->setAllowedTypes($allowed_types); + } + + /** + * Set the allowed association types, checking to make sure each + * combination is valid. + * + * @access private + */ + function setAllowedTypes($allowed_types) + { + foreach ($allowed_types as $pair) { + list($assoc_type, $session_type) = $pair; + if (!Auth_OpenID_checkSessionType($assoc_type, $session_type)) { + return false; + } + } + + $this->allowed_types = $allowed_types; + return true; + } + + /** + * Add an association type and session type to the allowed types + * list. The assocation/session pairs are tried in the order that + * they are added. + * + * @access private + */ + function addAllowedType($assoc_type, $session_type = null) + { + if ($this->allowed_types === null) { + $this->allowed_types = array(); + } + + if ($session_type === null) { + $available = Auth_OpenID_getSessionTypes($assoc_type); + + if (!$available) { + return false; + } + + foreach ($available as $session_type) { + $this->addAllowedType($assoc_type, $session_type); + } + } else { + if (Auth_OpenID_checkSessionType($assoc_type, $session_type)) { + $this->allowed_types[] = array($assoc_type, $session_type); + } else { + return false; + } + } + + return true; + } + + // Is this combination of association type and session type allowed? + function isAllowed($assoc_type, $session_type) + { + $assoc_good = in_array(array($assoc_type, $session_type), + $this->allowed_types); + + $matches = in_array($session_type, + Auth_OpenID_getSessionTypes($assoc_type)); + + return ($assoc_good && $matches); + } + + /** + * Get a pair of assocation type and session type that are + * supported. + */ + function getAllowedType() + { + if (!$this->allowed_types) { + return array(null, null); + } + + return $this->allowed_types[0]; + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/BigMath.php b/extlib/Auth/OpenID/BigMath.php new file mode 100644 index 000000000..45104947d --- /dev/null +++ b/extlib/Auth/OpenID/BigMath.php @@ -0,0 +1,471 @@ +<?php + +/** + * BigMath: A math library wrapper that abstracts out the underlying + * long integer library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Needed for random number generation + */ +require_once 'Auth/OpenID/CryptUtil.php'; + +/** + * Need Auth_OpenID::bytes(). + */ +require_once 'Auth/OpenID.php'; + +/** + * The superclass of all big-integer math implementations + * @access private + * @package OpenID + */ +class Auth_OpenID_MathLibrary { + /** + * Given a long integer, returns the number converted to a binary + * string. This function accepts long integer values of arbitrary + * magnitude and uses the local large-number math library when + * available. + * + * @param integer $long The long number (can be a normal PHP + * integer or a number created by one of the available long number + * libraries) + * @return string $binary The binary version of $long + */ + function longToBinary($long) + { + $cmp = $this->cmp($long, 0); + if ($cmp < 0) { + $msg = __FUNCTION__ . " takes only positive integers."; + trigger_error($msg, E_USER_ERROR); + return null; + } + + if ($cmp == 0) { + return "\x00"; + } + + $bytes = array(); + + while ($this->cmp($long, 0) > 0) { + array_unshift($bytes, $this->mod($long, 256)); + $long = $this->div($long, pow(2, 8)); + } + + if ($bytes && ($bytes[0] > 127)) { + array_unshift($bytes, 0); + } + + $string = ''; + foreach ($bytes as $byte) { + $string .= pack('C', $byte); + } + + return $string; + } + + /** + * Given a binary string, returns the binary string converted to a + * long number. + * + * @param string $binary The binary version of a long number, + * probably as a result of calling longToBinary + * @return integer $long The long number equivalent of the binary + * string $str + */ + function binaryToLong($str) + { + if ($str === null) { + return null; + } + + // Use array_merge to return a zero-indexed array instead of a + // one-indexed array. + $bytes = array_merge(unpack('C*', $str)); + + $n = $this->init(0); + + if ($bytes && ($bytes[0] > 127)) { + trigger_error("bytesToNum works only for positive integers.", + E_USER_WARNING); + return null; + } + + foreach ($bytes as $byte) { + $n = $this->mul($n, pow(2, 8)); + $n = $this->add($n, $byte); + } + + return $n; + } + + function base64ToLong($str) + { + $b64 = base64_decode($str); + + if ($b64 === false) { + return false; + } + + return $this->binaryToLong($b64); + } + + function longToBase64($str) + { + return base64_encode($this->longToBinary($str)); + } + + /** + * Returns a random number in the specified range. This function + * accepts $start, $stop, and $step values of arbitrary magnitude + * and will utilize the local large-number math library when + * available. + * + * @param integer $start The start of the range, or the minimum + * random number to return + * @param integer $stop The end of the range, or the maximum + * random number to return + * @param integer $step The step size, such that $result - ($step + * * N) = $start for some N + * @return integer $result The resulting randomly-generated number + */ + function rand($stop) + { + static $duplicate_cache = array(); + + // Used as the key for the duplicate cache + $rbytes = $this->longToBinary($stop); + + if (array_key_exists($rbytes, $duplicate_cache)) { + list($duplicate, $nbytes) = $duplicate_cache[$rbytes]; + } else { + if ($rbytes[0] == "\x00") { + $nbytes = Auth_OpenID::bytes($rbytes) - 1; + } else { + $nbytes = Auth_OpenID::bytes($rbytes); + } + + $mxrand = $this->pow(256, $nbytes); + + // If we get a number less than this, then it is in the + // duplicated range. + $duplicate = $this->mod($mxrand, $stop); + + if (count($duplicate_cache) > 10) { + $duplicate_cache = array(); + } + + $duplicate_cache[$rbytes] = array($duplicate, $nbytes); + } + + do { + $bytes = "\x00" . Auth_OpenID_CryptUtil::getBytes($nbytes); + $n = $this->binaryToLong($bytes); + // Keep looping if this value is in the low duplicated range + } while ($this->cmp($n, $duplicate) < 0); + + return $this->mod($n, $stop); + } +} + +/** + * Exposes BCmath math library functionality. + * + * {@link Auth_OpenID_BcMathWrapper} wraps the functionality provided + * by the BCMath extension. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_BcMathWrapper extends Auth_OpenID_MathLibrary{ + var $type = 'bcmath'; + + function add($x, $y) + { + return bcadd($x, $y); + } + + function sub($x, $y) + { + return bcsub($x, $y); + } + + function pow($base, $exponent) + { + return bcpow($base, $exponent); + } + + function cmp($x, $y) + { + return bccomp($x, $y); + } + + function init($number, $base = 10) + { + return $number; + } + + function mod($base, $modulus) + { + return bcmod($base, $modulus); + } + + function mul($x, $y) + { + return bcmul($x, $y); + } + + function div($x, $y) + { + return bcdiv($x, $y); + } + + /** + * Same as bcpowmod when bcpowmod is missing + * + * @access private + */ + function _powmod($base, $exponent, $modulus) + { + $square = $this->mod($base, $modulus); + $result = 1; + while($this->cmp($exponent, 0) > 0) { + if ($this->mod($exponent, 2)) { + $result = $this->mod($this->mul($result, $square), $modulus); + } + $square = $this->mod($this->mul($square, $square), $modulus); + $exponent = $this->div($exponent, 2); + } + return $result; + } + + function powmod($base, $exponent, $modulus) + { + if (function_exists('bcpowmod')) { + return bcpowmod($base, $exponent, $modulus); + } else { + return $this->_powmod($base, $exponent, $modulus); + } + } + + function toString($num) + { + return $num; + } +} + +/** + * Exposes GMP math library functionality. + * + * {@link Auth_OpenID_GmpMathWrapper} wraps the functionality provided + * by the GMP extension. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_GmpMathWrapper extends Auth_OpenID_MathLibrary{ + var $type = 'gmp'; + + function add($x, $y) + { + return gmp_add($x, $y); + } + + function sub($x, $y) + { + return gmp_sub($x, $y); + } + + function pow($base, $exponent) + { + return gmp_pow($base, $exponent); + } + + function cmp($x, $y) + { + return gmp_cmp($x, $y); + } + + function init($number, $base = 10) + { + return gmp_init($number, $base); + } + + function mod($base, $modulus) + { + return gmp_mod($base, $modulus); + } + + function mul($x, $y) + { + return gmp_mul($x, $y); + } + + function div($x, $y) + { + return gmp_div_q($x, $y); + } + + function powmod($base, $exponent, $modulus) + { + return gmp_powm($base, $exponent, $modulus); + } + + function toString($num) + { + return gmp_strval($num); + } +} + +/** + * Define the supported extensions. An extension array has keys + * 'modules', 'extension', and 'class'. 'modules' is an array of PHP + * module names which the loading code will attempt to load. These + * values will be suffixed with a library file extension (e.g. ".so"). + * 'extension' is the name of a PHP extension which will be tested + * before 'modules' are loaded. 'class' is the string name of a + * {@link Auth_OpenID_MathWrapper} subclass which should be + * instantiated if a given extension is present. + * + * You can define new math library implementations and add them to + * this array. + */ +function Auth_OpenID_math_extensions() +{ + $result = array(); + + if (!defined('Auth_OpenID_BUGGY_GMP')) { + $result[] = + array('modules' => array('gmp', 'php_gmp'), + 'extension' => 'gmp', + 'class' => 'Auth_OpenID_GmpMathWrapper'); + } + + $result[] = array( + 'modules' => array('bcmath', 'php_bcmath'), + 'extension' => 'bcmath', + 'class' => 'Auth_OpenID_BcMathWrapper'); + + return $result; +} + +/** + * Detect which (if any) math library is available + */ +function Auth_OpenID_detectMathLibrary($exts) +{ + $loaded = false; + + foreach ($exts as $extension) { + // See if the extension specified is already loaded. + if ($extension['extension'] && + extension_loaded($extension['extension'])) { + $loaded = true; + } + + // Try to load dynamic modules. + if (!$loaded) { + foreach ($extension['modules'] as $module) { + if (@dl($module . "." . PHP_SHLIB_SUFFIX)) { + $loaded = true; + break; + } + } + } + + // If the load succeeded, supply an instance of + // Auth_OpenID_MathWrapper which wraps the specified + // module's functionality. + if ($loaded) { + return $extension; + } + } + + return false; +} + +/** + * {@link Auth_OpenID_getMathLib} checks for the presence of long + * number extension modules and returns an instance of + * {@link Auth_OpenID_MathWrapper} which exposes the module's + * functionality. + * + * Checks for the existence of an extension module described by the + * result of {@link Auth_OpenID_math_extensions()} and returns an + * instance of a wrapper for that extension module. If no extension + * module is found, an instance of {@link Auth_OpenID_MathWrapper} is + * returned, which wraps the native PHP integer implementation. The + * proper calling convention for this method is $lib =& + * Auth_OpenID_getMathLib(). + * + * This function checks for the existence of specific long number + * implementations in the following order: GMP followed by BCmath. + * + * @return Auth_OpenID_MathWrapper $instance An instance of + * {@link Auth_OpenID_MathWrapper} or one of its subclasses + * + * @package OpenID + */ +function &Auth_OpenID_getMathLib() +{ + // The instance of Auth_OpenID_MathWrapper that we choose to + // supply will be stored here, so that subseqent calls to this + // method will return a reference to the same object. + static $lib = null; + + if (isset($lib)) { + return $lib; + } + + if (Auth_OpenID_noMathSupport()) { + $null = null; + return $null; + } + + // If this method has not been called before, look at + // Auth_OpenID_math_extensions and try to find an extension that + // works. + $ext = Auth_OpenID_detectMathLibrary(Auth_OpenID_math_extensions()); + if ($ext === false) { + $tried = array(); + foreach (Auth_OpenID_math_extensions() as $extinfo) { + $tried[] = $extinfo['extension']; + } + $triedstr = implode(", ", $tried); + + Auth_OpenID_setNoMathSupport(); + + $result = null; + return $result; + } + + // Instantiate a new wrapper + $class = $ext['class']; + $lib = new $class(); + + return $lib; +} + +function Auth_OpenID_setNoMathSupport() +{ + if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + define('Auth_OpenID_NO_MATH_SUPPORT', true); + } +} + +function Auth_OpenID_noMathSupport() +{ + return defined('Auth_OpenID_NO_MATH_SUPPORT'); +} + +?> diff --git a/extlib/Auth/OpenID/Consumer.php b/extlib/Auth/OpenID/Consumer.php new file mode 100644 index 000000000..6631cbaa9 --- /dev/null +++ b/extlib/Auth/OpenID/Consumer.php @@ -0,0 +1,2227 @@ +<?php + +/** + * This module documents the main interface with the OpenID consumer + * library. The only part of the library which has to be used and + * isn't documented in full here is the store required to create an + * Auth_OpenID_Consumer instance. More on the abstract store type and + * concrete implementations of it that are provided in the + * documentation for the Auth_OpenID_Consumer constructor. + * + * OVERVIEW + * + * The OpenID identity verification process most commonly uses the + * following steps, as visible to the user of this library: + * + * 1. The user enters their OpenID into a field on the consumer's + * site, and hits a login button. + * 2. The consumer site discovers the user's OpenID server using the + * YADIS protocol. + * 3. The consumer site sends the browser a redirect to the identity + * server. This is the authentication request as described in + * the OpenID specification. + * 4. The identity server's site sends the browser a redirect back + * to the consumer site. This redirect contains the server's + * response to the authentication request. + * + * The most important part of the flow to note is the consumer's site + * must handle two separate HTTP requests in order to perform the full + * identity check. + * + * LIBRARY DESIGN + * + * This consumer library is designed with that flow in mind. The goal + * is to make it as easy as possible to perform the above steps + * securely. + * + * At a high level, there are two important parts in the consumer + * library. The first important part is this module, which contains + * the interface to actually use this library. The second is the + * Auth_OpenID_Interface class, which describes the interface to use + * if you need to create a custom method for storing the state this + * library needs to maintain between requests. + * + * In general, the second part is less important for users of the + * library to know about, as several implementations are provided + * which cover a wide variety of situations in which consumers may use + * the library. + * + * This module contains a class, Auth_OpenID_Consumer, with methods + * corresponding to the actions necessary in each of steps 2, 3, and 4 + * described in the overview. Use of this library should be as easy + * as creating an Auth_OpenID_Consumer instance and calling the + * methods appropriate for the action the site wants to take. + * + * STORES AND DUMB MODE + * + * OpenID is a protocol that works best when the consumer site is able + * to store some state. This is the normal mode of operation for the + * protocol, and is sometimes referred to as smart mode. There is + * also a fallback mode, known as dumb mode, which is available when + * the consumer site is not able to store state. This mode should be + * avoided when possible, as it leaves the implementation more + * vulnerable to replay attacks. + * + * The mode the library works in for normal operation is determined by + * the store that it is given. The store is an abstraction that + * handles the data that the consumer needs to manage between http + * requests in order to operate efficiently and securely. + * + * Several store implementation are provided, and the interface is + * fully documented so that custom stores can be used as well. See + * the documentation for the Auth_OpenID_Consumer class for more + * information on the interface for stores. The implementations that + * are provided allow the consumer site to store the necessary data in + * several different ways, including several SQL databases and normal + * files on disk. + * + * There is an additional concrete store provided that puts the system + * in dumb mode. This is not recommended, as it removes the library's + * ability to stop replay attacks reliably. It still uses time-based + * checking to make replay attacks only possible within a small + * window, but they remain possible within that window. This store + * should only be used if the consumer site has no way to retain data + * between requests at all. + * + * IMMEDIATE MODE + * + * In the flow described above, the user may need to confirm to the + * lidentity server that it's ok to authorize his or her identity. + * The server may draw pages asking for information from the user + * before it redirects the browser back to the consumer's site. This + * is generally transparent to the consumer site, so it is typically + * ignored as an implementation detail. + * + * There can be times, however, where the consumer site wants to get a + * response immediately. When this is the case, the consumer can put + * the library in immediate mode. In immediate mode, there is an + * extra response possible from the server, which is essentially the + * server reporting that it doesn't have enough information to answer + * the question yet. + * + * USING THIS LIBRARY + * + * Integrating this library into an application is usually a + * relatively straightforward process. The process should basically + * follow this plan: + * + * Add an OpenID login field somewhere on your site. When an OpenID + * is entered in that field and the form is submitted, it should make + * a request to the your site which includes that OpenID URL. + * + * First, the application should instantiate the Auth_OpenID_Consumer + * class using the store of choice (Auth_OpenID_FileStore or one of + * the SQL-based stores). If the application has a custom + * session-management implementation, an object implementing the + * {@link Auth_Yadis_PHPSession} interface should be passed as the + * second parameter. Otherwise, the default uses $_SESSION. + * + * Next, the application should call the Auth_OpenID_Consumer object's + * 'begin' method. This method takes the OpenID URL. The 'begin' + * method returns an Auth_OpenID_AuthRequest object. + * + * Next, the application should call the 'redirectURL' method of the + * Auth_OpenID_AuthRequest object. The 'return_to' URL parameter is + * the URL that the OpenID server will send the user back to after + * attempting to verify his or her identity. The 'trust_root' is the + * URL (or URL pattern) that identifies your web site to the user when + * he or she is authorizing it. Send a redirect to the resulting URL + * to the user's browser. + * + * That's the first half of the authentication process. The second + * half of the process is done after the user's ID server sends the + * user's browser a redirect back to your site to complete their + * login. + * + * When that happens, the user will contact your site at the URL given + * as the 'return_to' URL to the Auth_OpenID_AuthRequest::redirectURL + * call made above. The request will have several query parameters + * added to the URL by the identity server as the information + * necessary to finish the request. + * + * Lastly, instantiate an Auth_OpenID_Consumer instance as above and + * call its 'complete' method, passing in all the received query + * arguments. + * + * There are multiple possible return types possible from that + * method. These indicate the whether or not the login was successful, + * and include any additional information appropriate for their type. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require utility classes and functions for the consumer. + */ +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/HMAC.php"; +require_once "Auth/OpenID/Association.php"; +require_once "Auth/OpenID/CryptUtil.php"; +require_once "Auth/OpenID/DiffieHellman.php"; +require_once "Auth/OpenID/KVForm.php"; +require_once "Auth/OpenID/Nonce.php"; +require_once "Auth/OpenID/Discover.php"; +require_once "Auth/OpenID/URINorm.php"; +require_once "Auth/Yadis/Manager.php"; +require_once "Auth/Yadis/XRI.php"; + +/** + * This is the status code returned when the complete method returns + * successfully. + */ +define('Auth_OpenID_SUCCESS', 'success'); + +/** + * Status to indicate cancellation of OpenID authentication. + */ +define('Auth_OpenID_CANCEL', 'cancel'); + +/** + * This is the status code completeAuth returns when the value it + * received indicated an invalid login. + */ +define('Auth_OpenID_FAILURE', 'failure'); + +/** + * This is the status code completeAuth returns when the + * {@link Auth_OpenID_Consumer} instance is in immediate mode, and the + * identity server sends back a URL to send the user to to complete his + * or her login. + */ +define('Auth_OpenID_SETUP_NEEDED', 'setup needed'); + +/** + * This is the status code beginAuth returns when the page fetched + * from the entered OpenID URL doesn't contain the necessary link tags + * to function as an identity page. + */ +define('Auth_OpenID_PARSE_ERROR', 'parse error'); + +/** + * An OpenID consumer implementation that performs discovery and does + * session management. See the Consumer.php file documentation for + * more information. + * + * @package OpenID + */ +class Auth_OpenID_Consumer { + + /** + * @access private + */ + var $discoverMethod = 'Auth_OpenID_discover'; + + /** + * @access private + */ + var $session_key_prefix = "_openid_consumer_"; + + /** + * @access private + */ + var $_token_suffix = "last_token"; + + /** + * Initialize a Consumer instance. + * + * You should create a new instance of the Consumer object with + * every HTTP request that handles OpenID transactions. + * + * @param Auth_OpenID_OpenIDStore $store This must be an object + * that implements the interface in {@link + * Auth_OpenID_OpenIDStore}. Several concrete implementations are + * provided, to cover most common use cases. For stores backed by + * MySQL, PostgreSQL, or SQLite, see the {@link + * Auth_OpenID_SQLStore} class and its sublcasses. For a + * filesystem-backed store, see the {@link Auth_OpenID_FileStore} + * module. As a last resort, if it isn't possible for the server + * to store state at all, an instance of {@link + * Auth_OpenID_DumbStore} can be used. + * + * @param mixed $session An object which implements the interface + * of the {@link Auth_Yadis_PHPSession} class. Particularly, this + * object is expected to have these methods: get($key), set($key), + * $value), and del($key). This defaults to a session object + * which wraps PHP's native session machinery. You should only + * need to pass something here if you have your own sessioning + * implementation. + * + * @param str $consumer_cls The name of the class to instantiate + * when creating the internal consumer object. This is used for + * testing. + */ + function Auth_OpenID_Consumer(&$store, $session = null, + $consumer_cls = null) + { + if ($session === null) { + $session = new Auth_Yadis_PHPSession(); + } + + $this->session =& $session; + + if ($consumer_cls !== null) { + $this->consumer =& new $consumer_cls($store); + } else { + $this->consumer =& new Auth_OpenID_GenericConsumer($store); + } + + $this->_token_key = $this->session_key_prefix . $this->_token_suffix; + } + + /** + * Used in testing to define the discovery mechanism. + * + * @access private + */ + function getDiscoveryObject(&$session, $openid_url, + $session_key_prefix) + { + return new Auth_Yadis_Discovery($session, $openid_url, + $session_key_prefix); + } + + /** + * Start the OpenID authentication process. See steps 1-2 in the + * overview at the top of this file. + * + * @param string $user_url Identity URL given by the user. This + * method performs a textual transformation of the URL to try and + * make sure it is normalized. For example, a user_url of + * example.com will be normalized to http://example.com/ + * normalizing and resolving any redirects the server might issue. + * + * @param bool $anonymous True if the OpenID request is to be sent + * to the server without any identifier information. Use this + * when you want to transport data but don't want to do OpenID + * authentication with identifiers. + * + * @return Auth_OpenID_AuthRequest $auth_request An object + * containing the discovered information will be returned, with a + * method for building a redirect URL to the server, as described + * in step 3 of the overview. This object may also be used to add + * extension arguments to the request, using its 'addExtensionArg' + * method. + */ + function begin($user_url, $anonymous=false) + { + $openid_url = $user_url; + + $disco = $this->getDiscoveryObject($this->session, + $openid_url, + $this->session_key_prefix); + + // Set the 'stale' attribute of the manager. If discovery + // fails in a fatal way, the stale flag will cause the manager + // to be cleaned up next time discovery is attempted. + + $m = $disco->getManager(); + $loader = new Auth_Yadis_ManagerLoader(); + + if ($m) { + if ($m->stale) { + $disco->destroyManager(); + } else { + $m->stale = true; + $disco->session->set($disco->session_key, + serialize($loader->toSession($m))); + } + } + + $endpoint = $disco->getNextService($this->discoverMethod, + $this->consumer->fetcher); + + // Reset the 'stale' attribute of the manager. + $m =& $disco->getManager(); + if ($m) { + $m->stale = false; + $disco->session->set($disco->session_key, + serialize($loader->toSession($m))); + } + + if ($endpoint === null) { + return null; + } else { + return $this->beginWithoutDiscovery($endpoint, + $anonymous); + } + } + + /** + * Start OpenID verification without doing OpenID server + * discovery. This method is used internally by Consumer.begin + * after discovery is performed, and exists to provide an + * interface for library users needing to perform their own + * discovery. + * + * @param Auth_OpenID_ServiceEndpoint $endpoint an OpenID service + * endpoint descriptor. + * + * @param bool anonymous Set to true if you want to perform OpenID + * without identifiers. + * + * @return Auth_OpenID_AuthRequest $auth_request An OpenID + * authentication request object. + */ + function &beginWithoutDiscovery($endpoint, $anonymous=false) + { + $loader = new Auth_OpenID_ServiceEndpointLoader(); + $auth_req = $this->consumer->begin($endpoint); + $this->session->set($this->_token_key, + $loader->toSession($auth_req->endpoint)); + if (!$auth_req->setAnonymous($anonymous)) { + return new Auth_OpenID_FailureResponse(null, + "OpenID 1 requests MUST include the identifier " . + "in the request."); + } + return $auth_req; + } + + /** + * Called to interpret the server's response to an OpenID + * request. It is called in step 4 of the flow described in the + * consumer overview. + * + * @param string $current_url The URL used to invoke the application. + * Extract the URL from your application's web + * request framework and specify it here to have it checked + * against the openid.current_url value in the response. If + * the current_url URL check fails, the status of the + * completion will be FAILURE. + * + * @param array $query An array of the query parameters (key => + * value pairs) for this HTTP request. Defaults to null. If + * null, the GET or POST data are automatically gotten from the + * PHP environment. It is only useful to override $query for + * testing. + * + * @return Auth_OpenID_ConsumerResponse $response A instance of an + * Auth_OpenID_ConsumerResponse subclass. The type of response is + * indicated by the status attribute, which will be one of + * SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED. + */ + function complete($current_url, $query=null) + { + if ($current_url && !is_string($current_url)) { + // This is ugly, but we need to complain loudly when + // someone uses the API incorrectly. + trigger_error("current_url must be a string; see NEWS file " . + "for upgrading notes.", + E_USER_ERROR); + } + + if ($query === null) { + $query = Auth_OpenID::getQuery(); + } + + $loader = new Auth_OpenID_ServiceEndpointLoader(); + $endpoint_data = $this->session->get($this->_token_key); + $endpoint = + $loader->fromSession($endpoint_data); + + $message = Auth_OpenID_Message::fromPostArgs($query); + $response = $this->consumer->complete($message, $endpoint, + $current_url); + $this->session->del($this->_token_key); + + if (in_array($response->status, array(Auth_OpenID_SUCCESS, + Auth_OpenID_CANCEL))) { + if ($response->identity_url !== null) { + $disco = $this->getDiscoveryObject($this->session, + $response->identity_url, + $this->session_key_prefix); + $disco->cleanup(true); + } + } + + return $response; + } +} + +/** + * A class implementing HMAC/DH-SHA1 consumer sessions. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellmanSHA1ConsumerSession { + var $session_type = 'DH-SHA1'; + var $hash_func = 'Auth_OpenID_SHA1'; + var $secret_size = 20; + var $allowed_assoc_types = array('HMAC-SHA1'); + + function Auth_OpenID_DiffieHellmanSHA1ConsumerSession($dh = null) + { + if ($dh === null) { + $dh = new Auth_OpenID_DiffieHellman(); + } + + $this->dh = $dh; + } + + function getRequest() + { + $math =& Auth_OpenID_getMathLib(); + + $cpub = $math->longToBase64($this->dh->public); + + $args = array('dh_consumer_public' => $cpub); + + if (!$this->dh->usingDefaultValues()) { + $args = array_merge($args, array( + 'dh_modulus' => + $math->longToBase64($this->dh->mod), + 'dh_gen' => + $math->longToBase64($this->dh->gen))); + } + + return $args; + } + + function extractSecret($response) + { + if (!$response->hasKey(Auth_OpenID_OPENID_NS, + 'dh_server_public')) { + return null; + } + + if (!$response->hasKey(Auth_OpenID_OPENID_NS, + 'enc_mac_key')) { + return null; + } + + $math =& Auth_OpenID_getMathLib(); + + $spub = $math->base64ToLong($response->getArg(Auth_OpenID_OPENID_NS, + 'dh_server_public')); + $enc_mac_key = base64_decode($response->getArg(Auth_OpenID_OPENID_NS, + 'enc_mac_key')); + + return $this->dh->xorSecret($spub, $enc_mac_key, $this->hash_func); + } +} + +/** + * A class implementing HMAC/DH-SHA256 consumer sessions. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellmanSHA256ConsumerSession extends + Auth_OpenID_DiffieHellmanSHA1ConsumerSession { + var $session_type = 'DH-SHA256'; + var $hash_func = 'Auth_OpenID_SHA256'; + var $secret_size = 32; + var $allowed_assoc_types = array('HMAC-SHA256'); +} + +/** + * A class implementing plaintext consumer sessions. + * + * @package OpenID + */ +class Auth_OpenID_PlainTextConsumerSession { + var $session_type = 'no-encryption'; + var $allowed_assoc_types = array('HMAC-SHA1', 'HMAC-SHA256'); + + function getRequest() + { + return array(); + } + + function extractSecret($response) + { + if (!$response->hasKey(Auth_OpenID_OPENID_NS, 'mac_key')) { + return null; + } + + return base64_decode($response->getArg(Auth_OpenID_OPENID_NS, + 'mac_key')); + } +} + +/** + * Returns available session types. + */ +function Auth_OpenID_getAvailableSessionTypes() +{ + $types = array( + 'no-encryption' => 'Auth_OpenID_PlainTextConsumerSession', + 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ConsumerSession', + 'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ConsumerSession'); + + return $types; +} + +/** + * This class is the interface to the OpenID consumer logic. + * Instances of it maintain no per-request state, so they can be + * reused (or even used by multiple threads concurrently) as needed. + * + * @package OpenID + */ +class Auth_OpenID_GenericConsumer { + /** + * @access private + */ + var $discoverMethod = 'Auth_OpenID_discover'; + + /** + * This consumer's store object. + */ + var $store; + + /** + * @access private + */ + var $_use_assocs; + + /** + * @access private + */ + var $openid1_nonce_query_arg_name = 'janrain_nonce'; + + /** + * Another query parameter that gets added to the return_to for + * OpenID 1; if the user's session state is lost, use this claimed + * identifier to do discovery when verifying the response. + */ + var $openid1_return_to_identifier_name = 'openid1_claimed_id'; + + /** + * This method initializes a new {@link Auth_OpenID_Consumer} + * instance to access the library. + * + * @param Auth_OpenID_OpenIDStore $store This must be an object + * that implements the interface in {@link Auth_OpenID_OpenIDStore}. + * Several concrete implementations are provided, to cover most common use + * cases. For stores backed by MySQL, PostgreSQL, or SQLite, see + * the {@link Auth_OpenID_SQLStore} class and its sublcasses. For a + * filesystem-backed store, see the {@link Auth_OpenID_FileStore} module. + * As a last resort, if it isn't possible for the server to store + * state at all, an instance of {@link Auth_OpenID_DumbStore} can be used. + * + * @param bool $immediate This is an optional boolean value. It + * controls whether the library uses immediate mode, as explained + * in the module description. The default value is False, which + * disables immediate mode. + */ + function Auth_OpenID_GenericConsumer(&$store) + { + $this->store =& $store; + $this->negotiator =& Auth_OpenID_getDefaultNegotiator(); + $this->_use_assocs = ($this->store ? true : false); + + $this->fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + + $this->session_types = Auth_OpenID_getAvailableSessionTypes(); + } + + /** + * Called to begin OpenID authentication using the specified + * {@link Auth_OpenID_ServiceEndpoint}. + * + * @access private + */ + function begin($service_endpoint) + { + $assoc = $this->_getAssociation($service_endpoint); + $r = new Auth_OpenID_AuthRequest($service_endpoint, $assoc); + $r->return_to_args[$this->openid1_nonce_query_arg_name] = + Auth_OpenID_mkNonce(); + + if ($r->message->isOpenID1()) { + $r->return_to_args[$this->openid1_return_to_identifier_name] = + $r->endpoint->claimed_id; + } + + return $r; + } + + /** + * Given an {@link Auth_OpenID_Message}, {@link + * Auth_OpenID_ServiceEndpoint} and optional return_to URL, + * complete OpenID authentication. + * + * @access private + */ + function complete($message, $endpoint, $return_to) + { + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode', + '<no mode set>'); + + $mode_methods = array( + 'cancel' => '_complete_cancel', + 'error' => '_complete_error', + 'setup_needed' => '_complete_setup_needed', + 'id_res' => '_complete_id_res', + ); + + $method = Auth_OpenID::arrayGet($mode_methods, $mode, + '_completeInvalid'); + + return call_user_func_array(array(&$this, $method), + array($message, $endpoint, $return_to)); + } + + /** + * @access private + */ + function _completeInvalid($message, &$endpoint, $unused) + { + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode', + '<No mode set>'); + + return new Auth_OpenID_FailureResponse($endpoint, + sprintf("Invalid openid.mode '%s'", $mode)); + } + + /** + * @access private + */ + function _complete_cancel($message, &$endpoint, $unused) + { + return new Auth_OpenID_CancelResponse($endpoint); + } + + /** + * @access private + */ + function _complete_error($message, &$endpoint, $unused) + { + $error = $message->getArg(Auth_OpenID_OPENID_NS, 'error'); + $contact = $message->getArg(Auth_OpenID_OPENID_NS, 'contact'); + $reference = $message->getArg(Auth_OpenID_OPENID_NS, 'reference'); + + return new Auth_OpenID_FailureResponse($endpoint, $error, + $contact, $reference); + } + + /** + * @access private + */ + function _complete_setup_needed($message, &$endpoint, $unused) + { + if (!$message->isOpenID2()) { + return $this->_completeInvalid($message, $endpoint); + } + + return new Auth_OpenID_SetupNeededResponse($endpoint); + } + + /** + * @access private + */ + function _complete_id_res($message, &$endpoint, $return_to) + { + $user_setup_url = $message->getArg(Auth_OpenID_OPENID1_NS, + 'user_setup_url'); + + if ($this->_checkSetupNeeded($message)) { + return new Auth_OpenID_SetupNeededResponse( + $endpoint, $user_setup_url); + } else { + return $this->_doIdRes($message, $endpoint, $return_to); + } + } + + /** + * @access private + */ + function _checkSetupNeeded($message) + { + // In OpenID 1, we check to see if this is a cancel from + // immediate mode by the presence of the user_setup_url + // parameter. + if ($message->isOpenID1()) { + $user_setup_url = $message->getArg(Auth_OpenID_OPENID1_NS, + 'user_setup_url'); + if ($user_setup_url !== null) { + return true; + } + } + + return false; + } + + /** + * @access private + */ + function _doIdRes($message, $endpoint, $return_to) + { + // Checks for presence of appropriate fields (and checks + // signed list fields) + $result = $this->_idResCheckForFields($message); + + if (Auth_OpenID::isFailure($result)) { + return $result; + } + + if (!$this->_checkReturnTo($message, $return_to)) { + return new Auth_OpenID_FailureResponse(null, + sprintf("return_to does not match return URL. Expected %s, got %s", + $return_to, + $message->getArg(Auth_OpenID_OPENID_NS, 'return_to'))); + } + + // Verify discovery information: + $result = $this->_verifyDiscoveryResults($message, $endpoint); + + if (Auth_OpenID::isFailure($result)) { + return $result; + } + + $endpoint = $result; + + $result = $this->_idResCheckSignature($message, + $endpoint->server_url); + + if (Auth_OpenID::isFailure($result)) { + return $result; + } + + $result = $this->_idResCheckNonce($message, $endpoint); + + if (Auth_OpenID::isFailure($result)) { + return $result; + } + + $signed_list_str = $message->getArg(Auth_OpenID_OPENID_NS, 'signed', + Auth_OpenID_NO_DEFAULT); + if (Auth_OpenID::isFailure($signed_list_str)) { + return $signed_list_str; + } + $signed_list = explode(',', $signed_list_str); + + $signed_fields = Auth_OpenID::addPrefix($signed_list, "openid."); + + return new Auth_OpenID_SuccessResponse($endpoint, $message, + $signed_fields); + + } + + /** + * @access private + */ + function _checkReturnTo($message, $return_to) + { + // Check an OpenID message and its openid.return_to value + // against a return_to URL from an application. Return True + // on success, False on failure. + + // Check the openid.return_to args against args in the + // original message. + $result = Auth_OpenID_GenericConsumer::_verifyReturnToArgs( + $message->toPostArgs()); + if (Auth_OpenID::isFailure($result)) { + return false; + } + + // Check the return_to base URL against the one in the + // message. + $msg_return_to = $message->getArg(Auth_OpenID_OPENID_NS, + 'return_to'); + if (Auth_OpenID::isFailure($return_to)) { + // XXX log me + return false; + } + + $return_to_parts = parse_url(Auth_OpenID_urinorm($return_to)); + $msg_return_to_parts = parse_url(Auth_OpenID_urinorm($msg_return_to)); + + // If port is absent from both, add it so it's equal in the + // check below. + if ((!array_key_exists('port', $return_to_parts)) && + (!array_key_exists('port', $msg_return_to_parts))) { + $return_to_parts['port'] = null; + $msg_return_to_parts['port'] = null; + } + + // If path is absent from both, add it so it's equal in the + // check below. + if ((!array_key_exists('path', $return_to_parts)) && + (!array_key_exists('path', $msg_return_to_parts))) { + $return_to_parts['path'] = null; + $msg_return_to_parts['path'] = null; + } + + // The URL scheme, authority, and path MUST be the same + // between the two URLs. + foreach (array('scheme', 'host', 'port', 'path') as $component) { + // If the url component is absent in either URL, fail. + // There should always be a scheme, host, port, and path. + if (!array_key_exists($component, $return_to_parts)) { + return false; + } + + if (!array_key_exists($component, $msg_return_to_parts)) { + return false; + } + + if (Auth_OpenID::arrayGet($return_to_parts, $component) !== + Auth_OpenID::arrayGet($msg_return_to_parts, $component)) { + return false; + } + } + + return true; + } + + /** + * @access private + */ + function _verifyReturnToArgs($query) + { + // Verify that the arguments in the return_to URL are present in this + // response. + + $message = Auth_OpenID_Message::fromPostArgs($query); + $return_to = $message->getArg(Auth_OpenID_OPENID_NS, 'return_to'); + + if (Auth_OpenID::isFailure($return_to)) { + return $return_to; + } + // XXX: this should be checked by _idResCheckForFields + if (!$return_to) { + return new Auth_OpenID_FailureResponse(null, + "Response has no return_to"); + } + + $parsed_url = parse_url($return_to); + + $q = array(); + if (array_key_exists('query', $parsed_url)) { + $rt_query = $parsed_url['query']; + $q = Auth_OpenID::parse_str($rt_query); + } + + foreach ($q as $rt_key => $rt_value) { + if (!array_key_exists($rt_key, $query)) { + return new Auth_OpenID_FailureResponse(null, + sprintf("return_to parameter %s absent from query", $rt_key)); + } else { + $value = $query[$rt_key]; + if ($rt_value != $value) { + return new Auth_OpenID_FailureResponse(null, + sprintf("parameter %s value %s does not match " . + "return_to value %s", $rt_key, + $value, $rt_value)); + } + } + } + + // Make sure all non-OpenID arguments in the response are also + // in the signed return_to. + $bare_args = $message->getArgs(Auth_OpenID_BARE_NS); + foreach ($bare_args as $key => $value) { + if (Auth_OpenID::arrayGet($q, $key) != $value) { + return new Auth_OpenID_FailureResponse(null, + sprintf("Parameter %s = %s not in return_to URL", + $key, $value)); + } + } + + return true; + } + + /** + * @access private + */ + function _idResCheckSignature($message, $server_url) + { + $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_handle'); + if (Auth_OpenID::isFailure($assoc_handle)) { + return $assoc_handle; + } + + $assoc = $this->store->getAssociation($server_url, $assoc_handle); + + if ($assoc) { + if ($assoc->getExpiresIn() <= 0) { + // XXX: It might be a good idea sometimes to re-start + // the authentication with a new association. Doing it + // automatically opens the possibility for + // denial-of-service by a server that just returns + // expired associations (or really short-lived + // associations) + return new Auth_OpenID_FailureResponse(null, + 'Association with ' . $server_url . ' expired'); + } + + if (!$assoc->checkMessageSignature($message)) { + return new Auth_OpenID_FailureResponse(null, + "Bad signature"); + } + } else { + // It's not an association we know about. Stateless mode + // is our only possible path for recovery. XXX - async + // framework will not want to block on this call to + // _checkAuth. + if (!$this->_checkAuth($message, $server_url)) { + return new Auth_OpenID_FailureResponse(null, + "Server denied check_authentication"); + } + } + + return null; + } + + /** + * @access private + */ + function _verifyDiscoveryResults($message, $endpoint=null) + { + if ($message->getOpenIDNamespace() == Auth_OpenID_OPENID2_NS) { + return $this->_verifyDiscoveryResultsOpenID2($message, + $endpoint); + } else { + return $this->_verifyDiscoveryResultsOpenID1($message, + $endpoint); + } + } + + /** + * @access private + */ + function _verifyDiscoveryResultsOpenID1($message, $endpoint) + { + $claimed_id = $message->getArg(Auth_OpenID_BARE_NS, + $this->openid1_return_to_identifier_name); + + if (($endpoint === null) && ($claimed_id === null)) { + return new Auth_OpenID_FailureResponse($endpoint, + 'When using OpenID 1, the claimed ID must be supplied, ' . + 'either by passing it through as a return_to parameter ' . + 'or by using a session, and supplied to the GenericConsumer ' . + 'as the argument to complete()'); + } else if (($endpoint !== null) && ($claimed_id === null)) { + $claimed_id = $endpoint->claimed_id; + } + + $to_match = new Auth_OpenID_ServiceEndpoint(); + $to_match->type_uris = array(Auth_OpenID_TYPE_1_1); + $to_match->local_id = $message->getArg(Auth_OpenID_OPENID1_NS, + 'identity'); + + // Restore delegate information from the initiation phase + $to_match->claimed_id = $claimed_id; + + if ($to_match->local_id === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "Missing required field openid.identity"); + } + + $to_match_1_0 = $to_match->copy(); + $to_match_1_0->type_uris = array(Auth_OpenID_TYPE_1_0); + + if ($endpoint !== null) { + $result = $this->_verifyDiscoverySingle($endpoint, $to_match); + + if (is_a($result, 'Auth_OpenID_TypeURIMismatch')) { + $result = $this->_verifyDiscoverySingle($endpoint, + $to_match_1_0); + } + + if (Auth_OpenID::isFailure($result)) { + // oidutil.log("Error attempting to use stored + // discovery information: " + str(e)) + // oidutil.log("Attempting discovery to + // verify endpoint") + } else { + return $endpoint; + } + } + + // Endpoint is either bad (failed verification) or None + return $this->_discoverAndVerify($to_match->claimed_id, + array($to_match, $to_match_1_0)); + } + + /** + * @access private + */ + function _verifyDiscoverySingle($endpoint, $to_match) + { + // Every type URI that's in the to_match endpoint has to be + // present in the discovered endpoint. + foreach ($to_match->type_uris as $type_uri) { + if (!$endpoint->usesExtension($type_uri)) { + return new Auth_OpenID_TypeURIMismatch($endpoint, + "Required type ".$type_uri." not present"); + } + } + + // Fragments do not influence discovery, so we can't compare a + // claimed identifier with a fragment to discovered + // information. + list($defragged_claimed_id, $_) = + Auth_OpenID::urldefrag($to_match->claimed_id); + + if ($defragged_claimed_id != $endpoint->claimed_id) { + return new Auth_OpenID_FailureResponse($endpoint, + sprintf('Claimed ID does not match (different subjects!), ' . + 'Expected %s, got %s', $defragged_claimed_id, + $endpoint->claimed_id)); + } + + if ($to_match->getLocalID() != $endpoint->getLocalID()) { + return new Auth_OpenID_FailureResponse($endpoint, + sprintf('local_id mismatch. Expected %s, got %s', + $to_match->getLocalID(), $endpoint->getLocalID())); + } + + // If the server URL is None, this must be an OpenID 1 + // response, because op_endpoint is a required parameter in + // OpenID 2. In that case, we don't actually care what the + // discovered server_url is, because signature checking or + // check_auth should take care of that check for us. + if ($to_match->server_url === null) { + if ($to_match->preferredNamespace() != Auth_OpenID_OPENID1_NS) { + return new Auth_OpenID_FailureResponse($endpoint, + "Preferred namespace mismatch (bug)"); + } + } else if ($to_match->server_url != $endpoint->server_url) { + return new Auth_OpenID_FailureResponse($endpoint, + sprintf('OP Endpoint mismatch. Expected %s, got %s', + $to_match->server_url, $endpoint->server_url)); + } + + return null; + } + + /** + * @access private + */ + function _verifyDiscoveryResultsOpenID2($message, $endpoint) + { + $to_match = new Auth_OpenID_ServiceEndpoint(); + $to_match->type_uris = array(Auth_OpenID_TYPE_2_0); + $to_match->claimed_id = $message->getArg(Auth_OpenID_OPENID2_NS, + 'claimed_id'); + + $to_match->local_id = $message->getArg(Auth_OpenID_OPENID2_NS, + 'identity'); + + $to_match->server_url = $message->getArg(Auth_OpenID_OPENID2_NS, + 'op_endpoint'); + + if ($to_match->server_url === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "OP Endpoint URL missing"); + } + + // claimed_id and identifier must both be present or both be + // absent + if (($to_match->claimed_id === null) && + ($to_match->local_id !== null)) { + return new Auth_OpenID_FailureResponse($endpoint, + 'openid.identity is present without openid.claimed_id'); + } + + if (($to_match->claimed_id !== null) && + ($to_match->local_id === null)) { + return new Auth_OpenID_FailureResponse($endpoint, + 'openid.claimed_id is present without openid.identity'); + } + + if ($to_match->claimed_id === null) { + // This is a response without identifiers, so there's + // really no checking that we can do, so return an + // endpoint that's for the specified `openid.op_endpoint' + return Auth_OpenID_ServiceEndpoint::fromOPEndpointURL( + $to_match->server_url); + } + + if (!$endpoint) { + // The claimed ID doesn't match, so we have to do + // discovery again. This covers not using sessions, OP + // identifier endpoints and responses that didn't match + // the original request. + // oidutil.log('No pre-discovered information supplied.') + return $this->_discoverAndVerify($to_match->claimed_id, + array($to_match)); + } else { + + // The claimed ID matches, so we use the endpoint that we + // discovered in initiation. This should be the most + // common case. + $result = $this->_verifyDiscoverySingle($endpoint, $to_match); + + if (Auth_OpenID::isFailure($result)) { + $endpoint = $this->_discoverAndVerify($to_match->claimed_id, + array($to_match)); + if (Auth_OpenID::isFailure($endpoint)) { + return $endpoint; + } + } + } + + // The endpoint we return should have the claimed ID from the + // message we just verified, fragment and all. + if ($endpoint->claimed_id != $to_match->claimed_id) { + $endpoint->claimed_id = $to_match->claimed_id; + } + + return $endpoint; + } + + /** + * @access private + */ + function _discoverAndVerify($claimed_id, $to_match_endpoints) + { + // oidutil.log('Performing discovery on %s' % (claimed_id,)) + list($unused, $services) = call_user_func($this->discoverMethod, + $claimed_id, + $this->fetcher); + + if (!$services) { + return new Auth_OpenID_FailureResponse(null, + sprintf("No OpenID information found at %s", + $claimed_id)); + } + + return $this->_verifyDiscoveryServices($claimed_id, $services, + $to_match_endpoints); + } + + /** + * @access private + */ + function _verifyDiscoveryServices($claimed_id, + &$services, &$to_match_endpoints) + { + // Search the services resulting from discovery to find one + // that matches the information from the assertion + + foreach ($services as $endpoint) { + foreach ($to_match_endpoints as $to_match_endpoint) { + $result = $this->_verifyDiscoverySingle($endpoint, + $to_match_endpoint); + + if (!Auth_OpenID::isFailure($result)) { + // It matches, so discover verification has + // succeeded. Return this endpoint. + return $endpoint; + } + } + } + + return new Auth_OpenID_FailureResponse(null, + sprintf('No matching endpoint found after discovering %s', + $claimed_id)); + } + + /** + * Extract the nonce from an OpenID 1 response. Return the nonce + * from the BARE_NS since we independently check the return_to + * arguments are the same as those in the response message. + * + * See the openid1_nonce_query_arg_name class variable + * + * @returns $nonce The nonce as a string or null + * + * @access private + */ + function _idResGetNonceOpenID1($message, $endpoint) + { + return $message->getArg(Auth_OpenID_BARE_NS, + $this->openid1_nonce_query_arg_name); + } + + /** + * @access private + */ + function _idResCheckNonce($message, $endpoint) + { + if ($message->isOpenID1()) { + // This indicates that the nonce was generated by the consumer + $nonce = $this->_idResGetNonceOpenID1($message, $endpoint); + $server_url = ''; + } else { + $nonce = $message->getArg(Auth_OpenID_OPENID2_NS, + 'response_nonce'); + + $server_url = $endpoint->server_url; + } + + if ($nonce === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "Nonce missing from response"); + } + + $parts = Auth_OpenID_splitNonce($nonce); + + if ($parts === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "Malformed nonce in response"); + } + + list($timestamp, $salt) = $parts; + + if (!$this->store->useNonce($server_url, $timestamp, $salt)) { + return new Auth_OpenID_FailureResponse($endpoint, + "Nonce already used or out of range"); + } + + return null; + } + + /** + * @access private + */ + function _idResCheckForFields($message) + { + $basic_fields = array('return_to', 'assoc_handle', 'sig', 'signed'); + $basic_sig_fields = array('return_to', 'identity'); + + $require_fields = array( + Auth_OpenID_OPENID2_NS => array_merge($basic_fields, + array('op_endpoint')), + + Auth_OpenID_OPENID1_NS => array_merge($basic_fields, + array('identity')) + ); + + $require_sigs = array( + Auth_OpenID_OPENID2_NS => array_merge($basic_sig_fields, + array('response_nonce', + 'claimed_id', + 'assoc_handle')), + Auth_OpenID_OPENID1_NS => array_merge($basic_sig_fields, + array('nonce')) + ); + + foreach ($require_fields[$message->getOpenIDNamespace()] as $field) { + if (!$message->hasKey(Auth_OpenID_OPENID_NS, $field)) { + return new Auth_OpenID_FailureResponse(null, + "Missing required field '".$field."'"); + } + } + + $signed_list_str = $message->getArg(Auth_OpenID_OPENID_NS, + 'signed', + Auth_OpenID_NO_DEFAULT); + if (Auth_OpenID::isFailure($signed_list_str)) { + return $signed_list_str; + } + $signed_list = explode(',', $signed_list_str); + + foreach ($require_sigs[$message->getOpenIDNamespace()] as $field) { + // Field is present and not in signed list + if ($message->hasKey(Auth_OpenID_OPENID_NS, $field) && + (!in_array($field, $signed_list))) { + return new Auth_OpenID_FailureResponse(null, + "'".$field."' not signed"); + } + } + + return null; + } + + /** + * @access private + */ + function _checkAuth($message, $server_url) + { + $request = $this->_createCheckAuthRequest($message); + if ($request === null) { + return false; + } + + $resp_message = $this->_makeKVPost($request, $server_url); + if (($resp_message === null) || + (is_a($resp_message, 'Auth_OpenID_ServerErrorContainer'))) { + return false; + } + + return $this->_processCheckAuthResponse($resp_message, $server_url); + } + + /** + * @access private + */ + function _createCheckAuthRequest($message) + { + $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); + if ($signed) { + foreach (explode(',', $signed) as $k) { + $value = $message->getAliasedArg($k); + if ($value === null) { + return null; + } + } + } + $ca_message = $message->copy(); + $ca_message->setArg(Auth_OpenID_OPENID_NS, 'mode', + 'check_authentication'); + return $ca_message; + } + + /** + * @access private + */ + function _processCheckAuthResponse($response, $server_url) + { + $is_valid = $response->getArg(Auth_OpenID_OPENID_NS, 'is_valid', + 'false'); + + $invalidate_handle = $response->getArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle'); + + if ($invalidate_handle !== null) { + $this->store->removeAssociation($server_url, + $invalidate_handle); + } + + if ($is_valid == 'true') { + return true; + } + + return false; + } + + /** + * Adapt a POST response to a Message. + * + * @param $response Result of a POST to an OpenID endpoint. + * + * @access private + */ + function _httpResponseToMessage($response, $server_url) + { + // Should this function be named Message.fromHTTPResponse instead? + $response_message = Auth_OpenID_Message::fromKVForm($response->body); + + if ($response->status == 400) { + return Auth_OpenID_ServerErrorContainer::fromMessage( + $response_message); + } else if ($response->status != 200 and $response->status != 206) { + return null; + } + + return $response_message; + } + + /** + * @access private + */ + function _makeKVPost($message, $server_url) + { + $body = $message->toURLEncoded(); + $resp = $this->fetcher->post($server_url, $body); + + if ($resp === null) { + return null; + } + + return $this->_httpResponseToMessage($resp, $server_url); + } + + /** + * @access private + */ + function _getAssociation($endpoint) + { + if (!$this->_use_assocs) { + return null; + } + + $assoc = $this->store->getAssociation($endpoint->server_url); + + if (($assoc === null) || + ($assoc->getExpiresIn() <= 0)) { + + $assoc = $this->_negotiateAssociation($endpoint); + + if ($assoc !== null) { + $this->store->storeAssociation($endpoint->server_url, + $assoc); + } + } + + return $assoc; + } + + /** + * Handle ServerErrors resulting from association requests. + * + * @return $result If server replied with an C{unsupported-type} + * error, return a tuple of supported C{association_type}, + * C{session_type}. Otherwise logs the error and returns null. + * + * @access private + */ + function _extractSupportedAssociationType(&$server_error, &$endpoint, + $assoc_type) + { + // Any error message whose code is not 'unsupported-type' + // should be considered a total failure. + if (($server_error->error_code != 'unsupported-type') || + ($server_error->message->isOpenID1())) { + return null; + } + + // The server didn't like the association/session type that we + // sent, and it sent us back a message that might tell us how + // to handle it. + + // Extract the session_type and assoc_type from the error + // message + $assoc_type = $server_error->message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_type'); + + $session_type = $server_error->message->getArg(Auth_OpenID_OPENID_NS, + 'session_type'); + + if (($assoc_type === null) || ($session_type === null)) { + return null; + } else if (!$this->negotiator->isAllowed($assoc_type, + $session_type)) { + return null; + } else { + return array($assoc_type, $session_type); + } + } + + /** + * @access private + */ + function _negotiateAssociation($endpoint) + { + // Get our preferred session/association type from the negotiatior. + list($assoc_type, $session_type) = $this->negotiator->getAllowedType(); + + $assoc = $this->_requestAssociation( + $endpoint, $assoc_type, $session_type); + + if (Auth_OpenID::isFailure($assoc)) { + return null; + } + + if (is_a($assoc, 'Auth_OpenID_ServerErrorContainer')) { + $why = $assoc; + + $supportedTypes = $this->_extractSupportedAssociationType( + $why, $endpoint, $assoc_type); + + if ($supportedTypes !== null) { + list($assoc_type, $session_type) = $supportedTypes; + + // Attempt to create an association from the assoc_type + // and session_type that the server told us it + // supported. + $assoc = $this->_requestAssociation( + $endpoint, $assoc_type, $session_type); + + if (is_a($assoc, 'Auth_OpenID_ServerErrorContainer')) { + // Do not keep trying, since it rejected the + // association type that it told us to use. + // oidutil.log('Server %s refused its suggested association + // 'type: session_type=%s, assoc_type=%s' + // % (endpoint.server_url, session_type, + // assoc_type)) + return null; + } else { + return $assoc; + } + } else { + return null; + } + } else { + return $assoc; + } + } + + /** + * @access private + */ + function _requestAssociation($endpoint, $assoc_type, $session_type) + { + list($assoc_session, $args) = $this->_createAssociateRequest( + $endpoint, $assoc_type, $session_type); + + $response_message = $this->_makeKVPost($args, $endpoint->server_url); + + if ($response_message === null) { + // oidutil.log('openid.associate request failed: %s' % (why[0],)) + return null; + } else if (is_a($response_message, + 'Auth_OpenID_ServerErrorContainer')) { + return $response_message; + } + + return $this->_extractAssociation($response_message, $assoc_session); + } + + /** + * @access private + */ + function _extractAssociation(&$assoc_response, &$assoc_session) + { + // Extract the common fields from the response, raising an + // exception if they are not found + $assoc_type = $assoc_response->getArg( + Auth_OpenID_OPENID_NS, 'assoc_type', + Auth_OpenID_NO_DEFAULT); + + if (Auth_OpenID::isFailure($assoc_type)) { + return $assoc_type; + } + + $assoc_handle = $assoc_response->getArg( + Auth_OpenID_OPENID_NS, 'assoc_handle', + Auth_OpenID_NO_DEFAULT); + + if (Auth_OpenID::isFailure($assoc_handle)) { + return $assoc_handle; + } + + // expires_in is a base-10 string. The Python parsing will + // accept literals that have whitespace around them and will + // accept negative values. Neither of these are really in-spec, + // but we think it's OK to accept them. + $expires_in_str = $assoc_response->getArg( + Auth_OpenID_OPENID_NS, 'expires_in', + Auth_OpenID_NO_DEFAULT); + + if (Auth_OpenID::isFailure($expires_in_str)) { + return $expires_in_str; + } + + $expires_in = Auth_OpenID::intval($expires_in_str); + if ($expires_in === false) { + + $err = sprintf("Could not parse expires_in from association ". + "response %s", print_r($assoc_response, true)); + return new Auth_OpenID_FailureResponse(null, $err); + } + + // OpenID 1 has funny association session behaviour. + if ($assoc_response->isOpenID1()) { + $session_type = $this->_getOpenID1SessionType($assoc_response); + } else { + $session_type = $assoc_response->getArg( + Auth_OpenID_OPENID2_NS, 'session_type', + Auth_OpenID_NO_DEFAULT); + + if (Auth_OpenID::isFailure($session_type)) { + return $session_type; + } + } + + // Session type mismatch + if ($assoc_session->session_type != $session_type) { + if ($assoc_response->isOpenID1() && + ($session_type == 'no-encryption')) { + // In OpenID 1, any association request can result in + // a 'no-encryption' association response. Setting + // assoc_session to a new no-encryption session should + // make the rest of this function work properly for + // that case. + $assoc_session = new Auth_OpenID_PlainTextConsumerSession(); + } else { + // Any other mismatch, regardless of protocol version + // results in the failure of the association session + // altogether. + return null; + } + } + + // Make sure assoc_type is valid for session_type + if (!in_array($assoc_type, $assoc_session->allowed_assoc_types)) { + return null; + } + + // Delegate to the association session to extract the secret + // from the response, however is appropriate for that session + // type. + $secret = $assoc_session->extractSecret($assoc_response); + + if ($secret === null) { + return null; + } + + return Auth_OpenID_Association::fromExpiresIn( + $expires_in, $assoc_handle, $secret, $assoc_type); + } + + /** + * @access private + */ + function _createAssociateRequest($endpoint, $assoc_type, $session_type) + { + if (array_key_exists($session_type, $this->session_types)) { + $session_type_class = $this->session_types[$session_type]; + + if (is_callable($session_type_class)) { + $assoc_session = $session_type_class(); + } else { + $assoc_session = new $session_type_class(); + } + } else { + return null; + } + + $args = array( + 'mode' => 'associate', + 'assoc_type' => $assoc_type); + + if (!$endpoint->compatibilityMode()) { + $args['ns'] = Auth_OpenID_OPENID2_NS; + } + + // Leave out the session type if we're in compatibility mode + // *and* it's no-encryption. + if ((!$endpoint->compatibilityMode()) || + ($assoc_session->session_type != 'no-encryption')) { + $args['session_type'] = $assoc_session->session_type; + } + + $args = array_merge($args, $assoc_session->getRequest()); + $message = Auth_OpenID_Message::fromOpenIDArgs($args); + return array($assoc_session, $message); + } + + /** + * Given an association response message, extract the OpenID 1.X + * session type. + * + * This function mostly takes care of the 'no-encryption' default + * behavior in OpenID 1. + * + * If the association type is plain-text, this function will + * return 'no-encryption' + * + * @access private + * @return $typ The association type for this message + */ + function _getOpenID1SessionType($assoc_response) + { + // If it's an OpenID 1 message, allow session_type to default + // to None (which signifies "no-encryption") + $session_type = $assoc_response->getArg(Auth_OpenID_OPENID1_NS, + 'session_type'); + + // Handle the differences between no-encryption association + // respones in OpenID 1 and 2: + + // no-encryption is not really a valid session type for OpenID + // 1, but we'll accept it anyway, while issuing a warning. + if ($session_type == 'no-encryption') { + // oidutil.log('WARNING: OpenID server sent "no-encryption"' + // 'for OpenID 1.X') + } else if (($session_type == '') || ($session_type === null)) { + // Missing or empty session type is the way to flag a + // 'no-encryption' response. Change the session type to + // 'no-encryption' so that it can be handled in the same + // way as OpenID 2 'no-encryption' respones. + $session_type = 'no-encryption'; + } + + return $session_type; + } +} + +/** + * This class represents an authentication request from a consumer to + * an OpenID server. + * + * @package OpenID + */ +class Auth_OpenID_AuthRequest { + + /** + * Initialize an authentication request with the specified token, + * association, and endpoint. + * + * Users of this library should not create instances of this + * class. Instances of this class are created by the library when + * needed. + */ + function Auth_OpenID_AuthRequest(&$endpoint, $assoc) + { + $this->assoc = $assoc; + $this->endpoint =& $endpoint; + $this->return_to_args = array(); + $this->message = new Auth_OpenID_Message( + $endpoint->preferredNamespace()); + $this->_anonymous = false; + } + + /** + * Add an extension to this checkid request. + * + * $extension_request: An object that implements the extension + * request interface for adding arguments to an OpenID message. + */ + function addExtension(&$extension_request) + { + $extension_request->toMessage($this->message); + } + + /** + * Add an extension argument to this OpenID authentication + * request. + * + * Use caution when adding arguments, because they will be + * URL-escaped and appended to the redirect URL, which can easily + * get quite long. + * + * @param string $namespace The namespace for the extension. For + * example, the simple registration extension uses the namespace + * 'sreg'. + * + * @param string $key The key within the extension namespace. For + * example, the nickname field in the simple registration + * extension's key is 'nickname'. + * + * @param string $value The value to provide to the server for + * this argument. + */ + function addExtensionArg($namespace, $key, $value) + { + return $this->message->setArg($namespace, $key, $value); + } + + /** + * Set whether this request should be made anonymously. If a + * request is anonymous, the identifier will not be sent in the + * request. This is only useful if you are making another kind of + * request with an extension in this request. + * + * Anonymous requests are not allowed when the request is made + * with OpenID 1. + */ + function setAnonymous($is_anonymous) + { + if ($is_anonymous && $this->message->isOpenID1()) { + return false; + } else { + $this->_anonymous = $is_anonymous; + return true; + } + } + + /** + * Produce a {@link Auth_OpenID_Message} representing this + * request. + * + * @param string $realm The URL (or URL pattern) that identifies + * your web site to the user when she is authorizing it. + * + * @param string $return_to The URL that the OpenID provider will + * send the user back to after attempting to verify her identity. + * + * Not specifying a return_to URL means that the user will not be + * returned to the site issuing the request upon its completion. + * + * @param bool $immediate If true, the OpenID provider is to send + * back a response immediately, useful for behind-the-scenes + * authentication attempts. Otherwise the OpenID provider may + * engage the user before providing a response. This is the + * default case, as the user may need to provide credentials or + * approve the request before a positive response can be sent. + */ + function getMessage($realm, $return_to=null, $immediate=false) + { + if ($return_to) { + $return_to = Auth_OpenID::appendArgs($return_to, + $this->return_to_args); + } else if ($immediate) { + // raise ValueError( + // '"return_to" is mandatory when + //using "checkid_immediate"') + return new Auth_OpenID_FailureResponse(null, + "'return_to' is mandatory when using checkid_immediate"); + } else if ($this->message->isOpenID1()) { + // raise ValueError('"return_to" is + // mandatory for OpenID 1 requests') + return new Auth_OpenID_FailureResponse(null, + "'return_to' is mandatory for OpenID 1 requests"); + } else if ($this->return_to_args) { + // raise ValueError('extra "return_to" arguments + // were specified, but no return_to was specified') + return new Auth_OpenID_FailureResponse(null, + "extra 'return_to' arguments where specified, " . + "but no return_to was specified"); + } + + if ($immediate) { + $mode = 'checkid_immediate'; + } else { + $mode = 'checkid_setup'; + } + + $message = $this->message->copy(); + if ($message->isOpenID1()) { + $realm_key = 'trust_root'; + } else { + $realm_key = 'realm'; + } + + $message->updateArgs(Auth_OpenID_OPENID_NS, + array( + $realm_key => $realm, + 'mode' => $mode, + 'return_to' => $return_to)); + + if (!$this->_anonymous) { + if ($this->endpoint->isOPIdentifier()) { + // This will never happen when we're in compatibility + // mode, as long as isOPIdentifier() returns False + // whenever preferredNamespace() returns OPENID1_NS. + $claimed_id = $request_identity = + Auth_OpenID_IDENTIFIER_SELECT; + } else { + $request_identity = $this->endpoint->getLocalID(); + $claimed_id = $this->endpoint->claimed_id; + } + + // This is true for both OpenID 1 and 2 + $message->setArg(Auth_OpenID_OPENID_NS, 'identity', + $request_identity); + + if ($message->isOpenID2()) { + $message->setArg(Auth_OpenID_OPENID2_NS, 'claimed_id', + $claimed_id); + } + } + + if ($this->assoc) { + $message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle', + $this->assoc->handle); + } + + return $message; + } + + function redirectURL($realm, $return_to = null, + $immediate = false) + { + $message = $this->getMessage($realm, $return_to, $immediate); + + if (Auth_OpenID::isFailure($message)) { + return $message; + } + + return $message->toURL($this->endpoint->server_url); + } + + /** + * Get html for a form to submit this request to the IDP. + * + * form_tag_attrs: An array of attributes to be added to the form + * tag. 'accept-charset' and 'enctype' have defaults that can be + * overridden. If a value is supplied for 'action' or 'method', it + * will be replaced. + */ + function formMarkup($realm, $return_to=null, $immediate=false, + $form_tag_attrs=null) + { + $message = $this->getMessage($realm, $return_to, $immediate); + + if (Auth_OpenID::isFailure($message)) { + return $message; + } + + return $message->toFormMarkup($this->endpoint->server_url, + $form_tag_attrs); + } + + /** + * Get a complete html document that will autosubmit the request + * to the IDP. + * + * Wraps formMarkup. See the documentation for that function. + */ + function htmlMarkup($realm, $return_to=null, $immediate=false, + $form_tag_attrs=null) + { + $form = $this->formMarkup($realm, $return_to, $immediate, + $form_tag_attrs); + + if (Auth_OpenID::isFailure($form)) { + return $form; + } + return Auth_OpenID::autoSubmitHTML($form); + } + + function shouldSendRedirect() + { + return $this->endpoint->compatibilityMode(); + } +} + +/** + * The base class for responses from the Auth_OpenID_Consumer. + * + * @package OpenID + */ +class Auth_OpenID_ConsumerResponse { + var $status = null; + + function setEndpoint($endpoint) + { + $this->endpoint = $endpoint; + if ($endpoint === null) { + $this->identity_url = null; + } else { + $this->identity_url = $endpoint->claimed_id; + } + } + + /** + * Return the display identifier for this response. + * + * The display identifier is related to the Claimed Identifier, but the + * two are not always identical. The display identifier is something the + * user should recognize as what they entered, whereas the response's + * claimed identifier (in the identity_url attribute) may have extra + * information for better persistence. + * + * URLs will be stripped of their fragments for display. XRIs will + * display the human-readable identifier (i-name) instead of the + * persistent identifier (i-number). + * + * Use the display identifier in your user interface. Use + * identity_url for querying your database or authorization server. + * + */ + function getDisplayIdentifier() + { + if ($this->endpoint !== null) { + return $this->endpoint->getDisplayIdentifier(); + } + return null; + } +} + +/** + * A response with a status of Auth_OpenID_SUCCESS. Indicates that + * this request is a successful acknowledgement from the OpenID server + * that the supplied URL is, indeed controlled by the requesting + * agent. This has three relevant attributes: + * + * claimed_id - The identity URL that has been authenticated + * + * signed_args - The arguments in the server's response that were + * signed and verified. + * + * status - Auth_OpenID_SUCCESS. + * + * @package OpenID + */ +class Auth_OpenID_SuccessResponse extends Auth_OpenID_ConsumerResponse { + var $status = Auth_OpenID_SUCCESS; + + /** + * @access private + */ + function Auth_OpenID_SuccessResponse($endpoint, $message, $signed_args=null) + { + $this->endpoint = $endpoint; + $this->identity_url = $endpoint->claimed_id; + $this->signed_args = $signed_args; + $this->message = $message; + + if ($this->signed_args === null) { + $this->signed_args = array(); + } + } + + /** + * Extract signed extension data from the server's response. + * + * @param string $prefix The extension namespace from which to + * extract the extension data. + */ + function extensionResponse($namespace_uri, $require_signed) + { + if ($require_signed) { + return $this->getSignedNS($namespace_uri); + } else { + return $this->message->getArgs($namespace_uri); + } + } + + function isOpenID1() + { + return $this->message->isOpenID1(); + } + + function isSigned($ns_uri, $ns_key) + { + // Return whether a particular key is signed, regardless of + // its namespace alias + return in_array($this->message->getKey($ns_uri, $ns_key), + $this->signed_args); + } + + function getSigned($ns_uri, $ns_key, $default = null) + { + // Return the specified signed field if available, otherwise + // return default + if ($this->isSigned($ns_uri, $ns_key)) { + return $this->message->getArg($ns_uri, $ns_key, $default); + } else { + return $default; + } + } + + function getSignedNS($ns_uri) + { + $args = array(); + + $msg_args = $this->message->getArgs($ns_uri); + if (Auth_OpenID::isFailure($msg_args)) { + return null; + } + + foreach ($msg_args as $key => $value) { + if (!$this->isSigned($ns_uri, $key)) { + return null; + } + } + + return $msg_args; + } + + /** + * Get the openid.return_to argument from this response. + * + * This is useful for verifying that this request was initiated by + * this consumer. + * + * @return string $return_to The return_to URL supplied to the + * server on the initial request, or null if the response did not + * contain an 'openid.return_to' argument. + */ + function getReturnTo() + { + return $this->getSigned(Auth_OpenID_OPENID_NS, 'return_to'); + } +} + +/** + * A response with a status of Auth_OpenID_FAILURE. Indicates that the + * OpenID protocol has failed. This could be locally or remotely + * triggered. This has three relevant attributes: + * + * claimed_id - The identity URL for which authentication was + * attempted, if it can be determined. Otherwise, null. + * + * message - A message indicating why the request failed, if one is + * supplied. Otherwise, null. + * + * status - Auth_OpenID_FAILURE. + * + * @package OpenID + */ +class Auth_OpenID_FailureResponse extends Auth_OpenID_ConsumerResponse { + var $status = Auth_OpenID_FAILURE; + + function Auth_OpenID_FailureResponse($endpoint, $message = null, + $contact = null, $reference = null) + { + $this->setEndpoint($endpoint); + $this->message = $message; + $this->contact = $contact; + $this->reference = $reference; + } +} + +/** + * A specific, internal failure used to detect type URI mismatch. + * + * @package OpenID + */ +class Auth_OpenID_TypeURIMismatch extends Auth_OpenID_FailureResponse { +} + +/** + * Exception that is raised when the server returns a 400 response + * code to a direct request. + * + * @package OpenID + */ +class Auth_OpenID_ServerErrorContainer { + function Auth_OpenID_ServerErrorContainer($error_text, + $error_code, + $message) + { + $this->error_text = $error_text; + $this->error_code = $error_code; + $this->message = $message; + } + + /** + * @access private + */ + function fromMessage($message) + { + $error_text = $message->getArg( + Auth_OpenID_OPENID_NS, 'error', '<no error message supplied>'); + $error_code = $message->getArg(Auth_OpenID_OPENID_NS, 'error_code'); + return new Auth_OpenID_ServerErrorContainer($error_text, + $error_code, + $message); + } +} + +/** + * A response with a status of Auth_OpenID_CANCEL. Indicates that the + * user cancelled the OpenID authentication request. This has two + * relevant attributes: + * + * claimed_id - The identity URL for which authentication was + * attempted, if it can be determined. Otherwise, null. + * + * status - Auth_OpenID_SUCCESS. + * + * @package OpenID + */ +class Auth_OpenID_CancelResponse extends Auth_OpenID_ConsumerResponse { + var $status = Auth_OpenID_CANCEL; + + function Auth_OpenID_CancelResponse($endpoint) + { + $this->setEndpoint($endpoint); + } +} + +/** + * A response with a status of Auth_OpenID_SETUP_NEEDED. Indicates + * that the request was in immediate mode, and the server is unable to + * authenticate the user without further interaction. + * + * claimed_id - The identity URL for which authentication was + * attempted. + * + * setup_url - A URL that can be used to send the user to the server + * to set up for authentication. The user should be redirected in to + * the setup_url, either in the current window or in a new browser + * window. Null in OpenID 2. + * + * status - Auth_OpenID_SETUP_NEEDED. + * + * @package OpenID + */ +class Auth_OpenID_SetupNeededResponse extends Auth_OpenID_ConsumerResponse { + var $status = Auth_OpenID_SETUP_NEEDED; + + function Auth_OpenID_SetupNeededResponse($endpoint, + $setup_url = null) + { + $this->setEndpoint($endpoint); + $this->setup_url = $setup_url; + } +} + +?> diff --git a/extlib/Auth/OpenID/CryptUtil.php b/extlib/Auth/OpenID/CryptUtil.php new file mode 100644 index 000000000..aacc3cd39 --- /dev/null +++ b/extlib/Auth/OpenID/CryptUtil.php @@ -0,0 +1,109 @@ +<?php + +/** + * CryptUtil: A suite of wrapper utility functions for the OpenID + * library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +if (!defined('Auth_OpenID_RAND_SOURCE')) { + /** + * The filename for a source of random bytes. Define this yourself + * if you have a different source of randomness. + */ + define('Auth_OpenID_RAND_SOURCE', '/dev/urandom'); +} + +class Auth_OpenID_CryptUtil { + /** + * Get the specified number of random bytes. + * + * Attempts to use a cryptographically secure (not predictable) + * source of randomness if available. If there is no high-entropy + * randomness source available, it will fail. As a last resort, + * for non-critical systems, define + * <code>Auth_OpenID_RAND_SOURCE</code> as <code>null</code>, and + * the code will fall back on a pseudo-random number generator. + * + * @param int $num_bytes The length of the return value + * @return string $bytes random bytes + */ + function getBytes($num_bytes) + { + static $f = null; + $bytes = ''; + if ($f === null) { + if (Auth_OpenID_RAND_SOURCE === null) { + $f = false; + } else { + $f = @fopen(Auth_OpenID_RAND_SOURCE, "r"); + if ($f === false) { + $msg = 'Define Auth_OpenID_RAND_SOURCE as null to ' . + ' continue with an insecure random number generator.'; + trigger_error($msg, E_USER_ERROR); + } + } + } + if ($f === false) { + // pseudorandom used + $bytes = ''; + for ($i = 0; $i < $num_bytes; $i += 4) { + $bytes .= pack('L', mt_rand()); + } + $bytes = substr($bytes, 0, $num_bytes); + } else { + $bytes = fread($f, $num_bytes); + } + return $bytes; + } + + /** + * Produce a string of length random bytes, chosen from chrs. If + * $chrs is null, the resulting string may contain any characters. + * + * @param integer $length The length of the resulting + * randomly-generated string + * @param string $chrs A string of characters from which to choose + * to build the new string + * @return string $result A string of randomly-chosen characters + * from $chrs + */ + function randomString($length, $population = null) + { + if ($population === null) { + return Auth_OpenID_CryptUtil::getBytes($length); + } + + $popsize = strlen($population); + + if ($popsize > 256) { + $msg = 'More than 256 characters supplied to ' . __FUNCTION__; + trigger_error($msg, E_USER_ERROR); + } + + $duplicate = 256 % $popsize; + + $str = ""; + for ($i = 0; $i < $length; $i++) { + do { + $n = ord(Auth_OpenID_CryptUtil::getBytes(1)); + } while ($n < $duplicate); + + $n %= $popsize; + $str .= $population[$n]; + } + + return $str; + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/DatabaseConnection.php b/extlib/Auth/OpenID/DatabaseConnection.php new file mode 100644 index 000000000..9db6e0eb3 --- /dev/null +++ b/extlib/Auth/OpenID/DatabaseConnection.php @@ -0,0 +1,131 @@ +<?php + +/** + * The Auth_OpenID_DatabaseConnection class, which is used to emulate + * a PEAR database connection. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * An empty base class intended to emulate PEAR connection + * functionality in applications that supply their own database + * abstraction mechanisms. See {@link Auth_OpenID_SQLStore} for more + * information. You should subclass this class if you need to create + * an SQL store that needs to access its database using an + * application's database abstraction layer instead of a PEAR database + * connection. Any subclass of Auth_OpenID_DatabaseConnection MUST + * adhere to the interface specified here. + * + * @package OpenID + */ +class Auth_OpenID_DatabaseConnection { + /** + * Sets auto-commit mode on this database connection. + * + * @param bool $mode True if auto-commit is to be used; false if + * not. + */ + function autoCommit($mode) + { + } + + /** + * Run an SQL query with the specified parameters, if any. + * + * @param string $sql An SQL string with placeholders. The + * placeholders are assumed to be specific to the database engine + * for this connection. + * + * @param array $params An array of parameters to insert into the + * SQL string using this connection's escaping mechanism. + * + * @return mixed $result The result of calling this connection's + * internal query function. The type of result depends on the + * underlying database engine. This method is usually used when + * the result of a query is not important, like a DDL query. + */ + function query($sql, $params = array()) + { + } + + /** + * Starts a transaction on this connection, if supported. + */ + function begin() + { + } + + /** + * Commits a transaction on this connection, if supported. + */ + function commit() + { + } + + /** + * Performs a rollback on this connection, if supported. + */ + function rollback() + { + } + + /** + * Run an SQL query and return the first column of the first row + * of the result set, if any. + * + * @param string $sql An SQL string with placeholders. The + * placeholders are assumed to be specific to the database engine + * for this connection. + * + * @param array $params An array of parameters to insert into the + * SQL string using this connection's escaping mechanism. + * + * @return mixed $result The value of the first column of the + * first row of the result set. False if no such result was + * found. + */ + function getOne($sql, $params = array()) + { + } + + /** + * Run an SQL query and return the first row of the result set, if + * any. + * + * @param string $sql An SQL string with placeholders. The + * placeholders are assumed to be specific to the database engine + * for this connection. + * + * @param array $params An array of parameters to insert into the + * SQL string using this connection's escaping mechanism. + * + * @return array $result The first row of the result set, if any, + * keyed on column name. False if no such result was found. + */ + function getRow($sql, $params = array()) + { + } + + /** + * Run an SQL query with the specified parameters, if any. + * + * @param string $sql An SQL string with placeholders. The + * placeholders are assumed to be specific to the database engine + * for this connection. + * + * @param array $params An array of parameters to insert into the + * SQL string using this connection's escaping mechanism. + * + * @return array $result An array of arrays representing the + * result of the query; each array is keyed on column name. + */ + function getAll($sql, $params = array()) + { + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/DiffieHellman.php b/extlib/Auth/OpenID/DiffieHellman.php new file mode 100644 index 000000000..f4ded7eba --- /dev/null +++ b/extlib/Auth/OpenID/DiffieHellman.php @@ -0,0 +1,113 @@ +<?php + +/** + * The OpenID library's Diffie-Hellman implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'Auth/OpenID.php'; +require_once 'Auth/OpenID/BigMath.php'; + +function Auth_OpenID_getDefaultMod() +{ + return '155172898181473697471232257763715539915724801'. + '966915404479707795314057629378541917580651227423'. + '698188993727816152646631438561595825688188889951'. + '272158842675419950341258706556549803580104870537'. + '681476726513255747040765857479291291572334510643'. + '245094715007229621094194349783925984760375594985'. + '848253359305585439638443'; +} + +function Auth_OpenID_getDefaultGen() +{ + return '2'; +} + +/** + * The Diffie-Hellman key exchange class. This class relies on + * {@link Auth_OpenID_MathLibrary} to perform large number operations. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_DiffieHellman { + + var $mod; + var $gen; + var $private; + var $lib = null; + + function Auth_OpenID_DiffieHellman($mod = null, $gen = null, + $private = null, $lib = null) + { + if ($lib === null) { + $this->lib =& Auth_OpenID_getMathLib(); + } else { + $this->lib =& $lib; + } + + if ($mod === null) { + $this->mod = $this->lib->init(Auth_OpenID_getDefaultMod()); + } else { + $this->mod = $mod; + } + + if ($gen === null) { + $this->gen = $this->lib->init(Auth_OpenID_getDefaultGen()); + } else { + $this->gen = $gen; + } + + if ($private === null) { + $r = $this->lib->rand($this->mod); + $this->private = $this->lib->add($r, 1); + } else { + $this->private = $private; + } + + $this->public = $this->lib->powmod($this->gen, $this->private, + $this->mod); + } + + function getSharedSecret($composite) + { + return $this->lib->powmod($composite, $this->private, $this->mod); + } + + function getPublicKey() + { + return $this->public; + } + + function usingDefaultValues() + { + return ($this->mod == Auth_OpenID_getDefaultMod() && + $this->gen == Auth_OpenID_getDefaultGen()); + } + + function xorSecret($composite, $secret, $hash_func) + { + $dh_shared = $this->getSharedSecret($composite); + $dh_shared_str = $this->lib->longToBinary($dh_shared); + $hash_dh_shared = $hash_func($dh_shared_str); + + $xsecret = ""; + for ($i = 0; $i < Auth_OpenID::bytes($secret); $i++) { + $xsecret .= chr(ord($secret[$i]) ^ ord($hash_dh_shared[$i])); + } + + return $xsecret; + } +} + +?> diff --git a/extlib/Auth/OpenID/Discover.php b/extlib/Auth/OpenID/Discover.php new file mode 100644 index 000000000..62aeb1d2b --- /dev/null +++ b/extlib/Auth/OpenID/Discover.php @@ -0,0 +1,548 @@ +<?php + +/** + * The OpenID and Yadis discovery implementation for OpenID 1.2. + */ + +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/Parse.php"; +require_once "Auth/OpenID/Message.php"; +require_once "Auth/Yadis/XRIRes.php"; +require_once "Auth/Yadis/Yadis.php"; + +// XML namespace value +define('Auth_OpenID_XMLNS_1_0', 'http://openid.net/xmlns/1.0'); + +// Yadis service types +define('Auth_OpenID_TYPE_1_2', 'http://openid.net/signon/1.2'); +define('Auth_OpenID_TYPE_1_1', 'http://openid.net/signon/1.1'); +define('Auth_OpenID_TYPE_1_0', 'http://openid.net/signon/1.0'); +define('Auth_OpenID_TYPE_2_0_IDP', 'http://specs.openid.net/auth/2.0/server'); +define('Auth_OpenID_TYPE_2_0', 'http://specs.openid.net/auth/2.0/signon'); +define('Auth_OpenID_RP_RETURN_TO_URL_TYPE', + 'http://specs.openid.net/auth/2.0/return_to'); + +function Auth_OpenID_getOpenIDTypeURIs() +{ + return array(Auth_OpenID_TYPE_2_0_IDP, + Auth_OpenID_TYPE_2_0, + Auth_OpenID_TYPE_1_2, + Auth_OpenID_TYPE_1_1, + Auth_OpenID_TYPE_1_0, + Auth_OpenID_RP_RETURN_TO_URL_TYPE); +} + +/** + * Object representing an OpenID service endpoint. + */ +class Auth_OpenID_ServiceEndpoint { + function Auth_OpenID_ServiceEndpoint() + { + $this->claimed_id = null; + $this->server_url = null; + $this->type_uris = array(); + $this->local_id = null; + $this->canonicalID = null; + $this->used_yadis = false; // whether this came from an XRDS + $this->display_identifier = null; + } + + function getDisplayIdentifier() + { + if ($this->display_identifier) { + return $this->display_identifier; + } + if (! $this->claimed_id) { + return $this->claimed_id; + } + $parsed = parse_url($this->claimed_id); + $scheme = $parsed['scheme']; + $host = $parsed['host']; + $path = $parsed['path']; + if (array_key_exists('query', $parsed)) { + $query = $parsed['query']; + $no_frag = "$scheme://$host$path?$query"; + } else { + $no_frag = "$scheme://$host$path"; + } + return $no_frag; + } + + function usesExtension($extension_uri) + { + return in_array($extension_uri, $this->type_uris); + } + + function preferredNamespace() + { + if (in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris) || + in_array(Auth_OpenID_TYPE_2_0, $this->type_uris)) { + return Auth_OpenID_OPENID2_NS; + } else { + return Auth_OpenID_OPENID1_NS; + } + } + + /* + * Query this endpoint to see if it has any of the given type + * URIs. This is useful for implementing other endpoint classes + * that e.g. need to check for the presence of multiple versions + * of a single protocol. + * + * @param $type_uris The URIs that you wish to check + * + * @return all types that are in both in type_uris and + * $this->type_uris + */ + function matchTypes($type_uris) + { + $result = array(); + foreach ($type_uris as $test_uri) { + if ($this->supportsType($test_uri)) { + $result[] = $test_uri; + } + } + + return $result; + } + + function supportsType($type_uri) + { + // Does this endpoint support this type? + return ((in_array($type_uri, $this->type_uris)) || + (($type_uri == Auth_OpenID_TYPE_2_0) && + $this->isOPIdentifier())); + } + + function compatibilityMode() + { + return $this->preferredNamespace() != Auth_OpenID_OPENID2_NS; + } + + function isOPIdentifier() + { + return in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris); + } + + function fromOPEndpointURL($op_endpoint_url) + { + // Construct an OP-Identifier OpenIDServiceEndpoint object for + // a given OP Endpoint URL + $obj = new Auth_OpenID_ServiceEndpoint(); + $obj->server_url = $op_endpoint_url; + $obj->type_uris = array(Auth_OpenID_TYPE_2_0_IDP); + return $obj; + } + + function parseService($yadis_url, $uri, $type_uris, $service_element) + { + // Set the state of this object based on the contents of the + // service element. Return true if successful, false if not + // (if findOPLocalIdentifier returns false). + $this->type_uris = $type_uris; + $this->server_url = $uri; + $this->used_yadis = true; + + if (!$this->isOPIdentifier()) { + $this->claimed_id = $yadis_url; + $this->local_id = Auth_OpenID_findOPLocalIdentifier( + $service_element, + $this->type_uris); + if ($this->local_id === false) { + return false; + } + } + + return true; + } + + function getLocalID() + { + // Return the identifier that should be sent as the + // openid.identity_url parameter to the server. + if ($this->local_id === null && $this->canonicalID === null) { + return $this->claimed_id; + } else { + if ($this->local_id) { + return $this->local_id; + } else { + return $this->canonicalID; + } + } + } + + /* + * Parse the given document as XRDS looking for OpenID services. + * + * @return array of Auth_OpenID_ServiceEndpoint or null if the + * document cannot be parsed. + */ + function fromXRDS($uri, $xrds_text) + { + $xrds =& Auth_Yadis_XRDS::parseXRDS($xrds_text); + + if ($xrds) { + $yadis_services = + $xrds->services(array('filter_MatchesAnyOpenIDType')); + return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services); + } + + return null; + } + + /* + * Create endpoints from a DiscoveryResult. + * + * @param discoveryResult Auth_Yadis_DiscoveryResult + * @return array of Auth_OpenID_ServiceEndpoint or null if + * endpoints cannot be created. + */ + function fromDiscoveryResult($discoveryResult) + { + if ($discoveryResult->isXRDS()) { + return Auth_OpenID_ServiceEndpoint::fromXRDS( + $discoveryResult->normalized_uri, + $discoveryResult->response_text); + } else { + return Auth_OpenID_ServiceEndpoint::fromHTML( + $discoveryResult->normalized_uri, + $discoveryResult->response_text); + } + } + + function fromHTML($uri, $html) + { + $discovery_types = array( + array(Auth_OpenID_TYPE_2_0, + 'openid2.provider', 'openid2.local_id'), + array(Auth_OpenID_TYPE_1_1, + 'openid.server', 'openid.delegate') + ); + + $services = array(); + + foreach ($discovery_types as $triple) { + list($type_uri, $server_rel, $delegate_rel) = $triple; + + $urls = Auth_OpenID_legacy_discover($html, $server_rel, + $delegate_rel); + + if ($urls === false) { + continue; + } + + list($delegate_url, $server_url) = $urls; + + $service = new Auth_OpenID_ServiceEndpoint(); + $service->claimed_id = $uri; + $service->local_id = $delegate_url; + $service->server_url = $server_url; + $service->type_uris = array($type_uri); + + $services[] = $service; + } + + return $services; + } + + function copy() + { + $x = new Auth_OpenID_ServiceEndpoint(); + + $x->claimed_id = $this->claimed_id; + $x->server_url = $this->server_url; + $x->type_uris = $this->type_uris; + $x->local_id = $this->local_id; + $x->canonicalID = $this->canonicalID; + $x->used_yadis = $this->used_yadis; + + return $x; + } +} + +function Auth_OpenID_findOPLocalIdentifier($service, $type_uris) +{ + // Extract a openid:Delegate value from a Yadis Service element. + // If no delegate is found, returns null. Returns false on + // discovery failure (when multiple delegate/localID tags have + // different values). + + $service->parser->registerNamespace('openid', + Auth_OpenID_XMLNS_1_0); + + $service->parser->registerNamespace('xrd', + Auth_Yadis_XMLNS_XRD_2_0); + + $parser =& $service->parser; + + $permitted_tags = array(); + + if (in_array(Auth_OpenID_TYPE_1_1, $type_uris) || + in_array(Auth_OpenID_TYPE_1_0, $type_uris)) { + $permitted_tags[] = 'openid:Delegate'; + } + + if (in_array(Auth_OpenID_TYPE_2_0, $type_uris)) { + $permitted_tags[] = 'xrd:LocalID'; + } + + $local_id = null; + + foreach ($permitted_tags as $tag_name) { + $tags = $service->getElements($tag_name); + + foreach ($tags as $tag) { + $content = $parser->content($tag); + + if ($local_id === null) { + $local_id = $content; + } else if ($local_id != $content) { + return false; + } + } + } + + return $local_id; +} + +function filter_MatchesAnyOpenIDType(&$service) +{ + $uris = $service->getTypes(); + + foreach ($uris as $uri) { + if (in_array($uri, Auth_OpenID_getOpenIDTypeURIs())) { + return true; + } + } + + return false; +} + +function Auth_OpenID_bestMatchingService($service, $preferred_types) +{ + // Return the index of the first matching type, or something + // higher if no type matches. + // + // This provides an ordering in which service elements that + // contain a type that comes earlier in the preferred types list + // come before service elements that come later. If a service + // element has more than one type, the most preferred one wins. + + foreach ($preferred_types as $index => $typ) { + if (in_array($typ, $service->type_uris)) { + return $index; + } + } + + return count($preferred_types); +} + +function Auth_OpenID_arrangeByType($service_list, $preferred_types) +{ + // Rearrange service_list in a new list so services are ordered by + // types listed in preferred_types. Return the new list. + + // Build a list with the service elements in tuples whose + // comparison will prefer the one with the best matching service + $prio_services = array(); + foreach ($service_list as $index => $service) { + $prio_services[] = array(Auth_OpenID_bestMatchingService($service, + $preferred_types), + $index, $service); + } + + sort($prio_services); + + // Now that the services are sorted by priority, remove the sort + // keys from the list. + foreach ($prio_services as $index => $s) { + $prio_services[$index] = $prio_services[$index][2]; + } + + return $prio_services; +} + +// Extract OP Identifier services. If none found, return the rest, +// sorted with most preferred first according to +// OpenIDServiceEndpoint.openid_type_uris. +// +// openid_services is a list of OpenIDServiceEndpoint objects. +// +// Returns a list of OpenIDServiceEndpoint objects.""" +function Auth_OpenID_getOPOrUserServices($openid_services) +{ + $op_services = Auth_OpenID_arrangeByType($openid_services, + array(Auth_OpenID_TYPE_2_0_IDP)); + + $openid_services = Auth_OpenID_arrangeByType($openid_services, + Auth_OpenID_getOpenIDTypeURIs()); + + if ($op_services) { + return $op_services; + } else { + return $openid_services; + } +} + +function Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services) +{ + $s = array(); + + if (!$yadis_services) { + return $s; + } + + foreach ($yadis_services as $service) { + $type_uris = $service->getTypes(); + $uris = $service->getURIs(); + + // If any Type URIs match and there is an endpoint URI + // specified, then this is an OpenID endpoint + if ($type_uris && + $uris) { + foreach ($uris as $service_uri) { + $openid_endpoint = new Auth_OpenID_ServiceEndpoint(); + if ($openid_endpoint->parseService($uri, + $service_uri, + $type_uris, + $service)) { + $s[] = $openid_endpoint; + } + } + } + } + + return $s; +} + +function Auth_OpenID_discoverWithYadis($uri, &$fetcher, + $endpoint_filter='Auth_OpenID_getOPOrUserServices', + $discover_function=null) +{ + // Discover OpenID services for a URI. Tries Yadis and falls back + // on old-style <link rel='...'> discovery if Yadis fails. + + // Might raise a yadis.discover.DiscoveryFailure if no document + // came back for that URI at all. I don't think falling back to + // OpenID 1.0 discovery on the same URL will help, so don't bother + // to catch it. + if ($discover_function === null) { + $discover_function = array('Auth_Yadis_Yadis', 'discover'); + } + + $openid_services = array(); + + $response = call_user_func_array($discover_function, + array($uri, &$fetcher)); + + $yadis_url = $response->normalized_uri; + $yadis_services = array(); + + if ($response->isFailure()) { + return array($uri, array()); + } + + $openid_services = Auth_OpenID_ServiceEndpoint::fromXRDS( + $yadis_url, + $response->response_text); + + if (!$openid_services) { + if ($response->isXRDS()) { + return Auth_OpenID_discoverWithoutYadis($uri, + $fetcher); + } + + // Try to parse the response as HTML to get OpenID 1.0/1.1 + // <link rel="..."> + $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML( + $yadis_url, + $response->response_text); + } + + $openid_services = call_user_func_array($endpoint_filter, + array(&$openid_services)); + + return array($yadis_url, $openid_services); +} + +function Auth_OpenID_discoverURI($uri, &$fetcher) +{ + $uri = Auth_OpenID::normalizeUrl($uri); + return Auth_OpenID_discoverWithYadis($uri, $fetcher); +} + +function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher) +{ + $http_resp = @$fetcher->get($uri); + + if ($http_resp->status != 200 and $http_resp->status != 206) { + return array($uri, array()); + } + + $identity_url = $http_resp->final_url; + + // Try to parse the response as HTML to get OpenID 1.0/1.1 <link + // rel="..."> + $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML( + $identity_url, + $http_resp->body); + + return array($identity_url, $openid_services); +} + +function Auth_OpenID_discoverXRI($iname, &$fetcher) +{ + $resolver = new Auth_Yadis_ProxyResolver($fetcher); + list($canonicalID, $yadis_services) = + $resolver->query($iname, + Auth_OpenID_getOpenIDTypeURIs(), + array('filter_MatchesAnyOpenIDType')); + + $openid_services = Auth_OpenID_makeOpenIDEndpoints($iname, + $yadis_services); + + $openid_services = Auth_OpenID_getOPOrUserServices($openid_services); + + for ($i = 0; $i < count($openid_services); $i++) { + $openid_services[$i]->canonicalID = $canonicalID; + $openid_services[$i]->claimed_id = $canonicalID; + $openid_services[$i]->display_identifier = $iname; + } + + // FIXME: returned xri should probably be in some normal form + return array($iname, $openid_services); +} + +function Auth_OpenID_discover($uri, &$fetcher) +{ + // If the fetcher (i.e., PHP) doesn't support SSL, we can't do + // discovery on an HTTPS URL. + if ($fetcher->isHTTPS($uri) && !$fetcher->supportsSSL()) { + return array($uri, array()); + } + + if (Auth_Yadis_identifierScheme($uri) == 'XRI') { + $result = Auth_OpenID_discoverXRI($uri, $fetcher); + } else { + $result = Auth_OpenID_discoverURI($uri, $fetcher); + } + + // If the fetcher doesn't support SSL, we can't interact with + // HTTPS server URLs; remove those endpoints from the list. + if (!$fetcher->supportsSSL()) { + $http_endpoints = array(); + list($new_uri, $endpoints) = $result; + + foreach ($endpoints as $e) { + if (!$fetcher->isHTTPS($e->server_url)) { + $http_endpoints[] = $e; + } + } + + $result = array($new_uri, $http_endpoints); + } + + return $result; +} + +?> diff --git a/extlib/Auth/OpenID/DumbStore.php b/extlib/Auth/OpenID/DumbStore.php new file mode 100644 index 000000000..22fd2d366 --- /dev/null +++ b/extlib/Auth/OpenID/DumbStore.php @@ -0,0 +1,100 @@ +<?php + +/** + * This file supplies a dumb store backend for OpenID servers and + * consumers. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Import the interface for creating a new store class. + */ +require_once 'Auth/OpenID/Interface.php'; +require_once 'Auth/OpenID/HMAC.php'; + +/** + * This is a store for use in the worst case, when you have no way of + * saving state on the consumer site. Using this store makes the + * consumer vulnerable to replay attacks, as it's unable to use + * nonces. Avoid using this store if it is at all possible. + * + * Most of the methods of this class are implementation details. + * Users of this class need to worry only about the constructor. + * + * @package OpenID + */ +class Auth_OpenID_DumbStore extends Auth_OpenID_OpenIDStore { + + /** + * Creates a new {@link Auth_OpenID_DumbStore} instance. For the security + * of the tokens generated by the library, this class attempts to + * at least have a secure implementation of getAuthKey. + * + * When you create an instance of this class, pass in a secret + * phrase. The phrase is hashed with sha1 to make it the correct + * length and form for an auth key. That allows you to use a long + * string as the secret phrase, which means you can make it very + * difficult to guess. + * + * Each {@link Auth_OpenID_DumbStore} instance that is created for use by + * your consumer site needs to use the same $secret_phrase. + * + * @param string secret_phrase The phrase used to create the auth + * key returned by getAuthKey + */ + function Auth_OpenID_DumbStore($secret_phrase) + { + $this->auth_key = Auth_OpenID_SHA1($secret_phrase); + } + + /** + * This implementation does nothing. + */ + function storeAssociation($server_url, $association) + { + } + + /** + * This implementation always returns null. + */ + function getAssociation($server_url, $handle = null) + { + return null; + } + + /** + * This implementation always returns false. + */ + function removeAssociation($server_url, $handle) + { + return false; + } + + /** + * In a system truly limited to dumb mode, nonces must all be + * accepted. This therefore always returns true, which makes + * replay attacks feasible. + */ + function useNonce($server_url, $timestamp, $salt) + { + return true; + } + + /** + * This method returns the auth key generated by the constructor. + */ + function getAuthKey() + { + return $this->auth_key; + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/Extension.php b/extlib/Auth/OpenID/Extension.php new file mode 100644 index 000000000..f362a4b38 --- /dev/null +++ b/extlib/Auth/OpenID/Extension.php @@ -0,0 +1,62 @@ +<?php + +/** + * An interface for OpenID extensions. + * + * @package OpenID + */ + +/** + * Require the Message implementation. + */ +require_once 'Auth/OpenID/Message.php'; + +/** + * A base class for accessing extension request and response data for + * the OpenID 2 protocol. + * + * @package OpenID + */ +class Auth_OpenID_Extension { + /** + * ns_uri: The namespace to which to add the arguments for this + * extension + */ + var $ns_uri = null; + var $ns_alias = null; + + /** + * Get the string arguments that should be added to an OpenID + * message for this extension. + */ + function getExtensionArgs() + { + return null; + } + + /** + * Add the arguments from this extension to the provided message. + * + * Returns the message with the extension arguments added. + */ + function toMessage(&$message) + { + $implicit = $message->isOpenID1(); + $added = $message->namespaces->addAlias($this->ns_uri, + $this->ns_alias, + $implicit); + + if ($added === null) { + if ($message->namespaces->getAlias($this->ns_uri) != + $this->ns_alias) { + return null; + } + } + + $message->updateArgs($this->ns_uri, + $this->getExtensionArgs()); + return $message; + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/FileStore.php b/extlib/Auth/OpenID/FileStore.php new file mode 100644 index 000000000..29d8d20e7 --- /dev/null +++ b/extlib/Auth/OpenID/FileStore.php @@ -0,0 +1,618 @@ +<?php + +/** + * This file supplies a Memcached store backend for OpenID servers and + * consumers. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require base class for creating a new interface. + */ +require_once 'Auth/OpenID.php'; +require_once 'Auth/OpenID/Interface.php'; +require_once 'Auth/OpenID/HMAC.php'; +require_once 'Auth/OpenID/Nonce.php'; + +/** + * This is a filesystem-based store for OpenID associations and + * nonces. This store should be safe for use in concurrent systems on + * both windows and unix (excluding NFS filesystems). There are a + * couple race conditions in the system, but those failure cases have + * been set up in such a way that the worst-case behavior is someone + * having to try to log in a second time. + * + * Most of the methods of this class are implementation details. + * People wishing to just use this store need only pay attention to + * the constructor. + * + * @package OpenID + */ +class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore { + + /** + * Initializes a new {@link Auth_OpenID_FileStore}. This + * initializes the nonce and association directories, which are + * subdirectories of the directory passed in. + * + * @param string $directory This is the directory to put the store + * directories in. + */ + function Auth_OpenID_FileStore($directory) + { + if (!Auth_OpenID::ensureDir($directory)) { + trigger_error('Not a directory and failed to create: ' + . $directory, E_USER_ERROR); + } + $directory = realpath($directory); + + $this->directory = $directory; + $this->active = true; + + $this->nonce_dir = $directory . DIRECTORY_SEPARATOR . 'nonces'; + + $this->association_dir = $directory . DIRECTORY_SEPARATOR . + 'associations'; + + // Temp dir must be on the same filesystem as the assciations + // $directory. + $this->temp_dir = $directory . DIRECTORY_SEPARATOR . 'temp'; + + $this->max_nonce_age = 6 * 60 * 60; // Six hours, in seconds + + if (!$this->_setup()) { + trigger_error('Failed to initialize OpenID file store in ' . + $directory, E_USER_ERROR); + } + } + + function destroy() + { + Auth_OpenID_FileStore::_rmtree($this->directory); + $this->active = false; + } + + /** + * Make sure that the directories in which we store our data + * exist. + * + * @access private + */ + function _setup() + { + return (Auth_OpenID::ensureDir($this->nonce_dir) && + Auth_OpenID::ensureDir($this->association_dir) && + Auth_OpenID::ensureDir($this->temp_dir)); + } + + /** + * Create a temporary file on the same filesystem as + * $this->association_dir. + * + * The temporary directory should not be cleaned if there are any + * processes using the store. If there is no active process using + * the store, it is safe to remove all of the files in the + * temporary directory. + * + * @return array ($fd, $filename) + * @access private + */ + function _mktemp() + { + $name = Auth_OpenID_FileStore::_mkstemp($dir = $this->temp_dir); + $file_obj = @fopen($name, 'wb'); + if ($file_obj !== false) { + return array($file_obj, $name); + } else { + Auth_OpenID_FileStore::_removeIfPresent($name); + } + } + + function cleanupNonces() + { + global $Auth_OpenID_SKEW; + + $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir); + $now = time(); + + $removed = 0; + // Check all nonces for expiry + foreach ($nonces as $nonce_fname) { + $base = basename($nonce_fname); + $parts = explode('-', $base, 2); + $timestamp = $parts[0]; + $timestamp = intval($timestamp, 16); + if (abs($timestamp - $now) > $Auth_OpenID_SKEW) { + Auth_OpenID_FileStore::_removeIfPresent($nonce_fname); + $removed += 1; + } + } + return $removed; + } + + /** + * Create a unique filename for a given server url and + * handle. This implementation does not assume anything about the + * format of the handle. The filename that is returned will + * contain the domain name from the server URL for ease of human + * inspection of the data directory. + * + * @return string $filename + */ + function getAssociationFilename($server_url, $handle) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + if (strpos($server_url, '://') === false) { + trigger_error(sprintf("Bad server URL: %s", $server_url), + E_USER_WARNING); + return null; + } + + list($proto, $rest) = explode('://', $server_url, 2); + $parts = explode('/', $rest); + $domain = Auth_OpenID_FileStore::_filenameEscape($parts[0]); + $url_hash = Auth_OpenID_FileStore::_safe64($server_url); + if ($handle) { + $handle_hash = Auth_OpenID_FileStore::_safe64($handle); + } else { + $handle_hash = ''; + } + + $filename = sprintf('%s-%s-%s-%s', $proto, $domain, $url_hash, + $handle_hash); + + return $this->association_dir. DIRECTORY_SEPARATOR . $filename; + } + + /** + * Store an association in the association directory. + */ + function storeAssociation($server_url, $association) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return false; + } + + $association_s = $association->serialize(); + $filename = $this->getAssociationFilename($server_url, + $association->handle); + list($tmp_file, $tmp) = $this->_mktemp(); + + if (!$tmp_file) { + trigger_error("_mktemp didn't return a valid file descriptor", + E_USER_WARNING); + return false; + } + + fwrite($tmp_file, $association_s); + + fflush($tmp_file); + + fclose($tmp_file); + + if (@rename($tmp, $filename)) { + return true; + } else { + // In case we are running on Windows, try unlinking the + // file in case it exists. + @unlink($filename); + + // Now the target should not exist. Try renaming again, + // giving up if it fails. + if (@rename($tmp, $filename)) { + return true; + } + } + + // If there was an error, don't leave the temporary file + // around. + Auth_OpenID_FileStore::_removeIfPresent($tmp); + return false; + } + + /** + * Retrieve an association. If no handle is specified, return the + * association with the most recent issue time. + * + * @return mixed $association + */ + function getAssociation($server_url, $handle = null) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + if ($handle === null) { + $handle = ''; + } + + // The filename with the empty handle is a prefix of all other + // associations for the given server URL. + $filename = $this->getAssociationFilename($server_url, $handle); + + if ($handle) { + return $this->_getAssociation($filename); + } else { + $association_files = + Auth_OpenID_FileStore::_listdir($this->association_dir); + $matching_files = array(); + + // strip off the path to do the comparison + $name = basename($filename); + foreach ($association_files as $association_file) { + $base = basename($association_file); + if (strpos($base, $name) === 0) { + $matching_files[] = $association_file; + } + } + + $matching_associations = array(); + // read the matching files and sort by time issued + foreach ($matching_files as $full_name) { + $association = $this->_getAssociation($full_name); + if ($association !== null) { + $matching_associations[] = array($association->issued, + $association); + } + } + + $issued = array(); + $assocs = array(); + foreach ($matching_associations as $key => $assoc) { + $issued[$key] = $assoc[0]; + $assocs[$key] = $assoc[1]; + } + + array_multisort($issued, SORT_DESC, $assocs, SORT_DESC, + $matching_associations); + + // return the most recently issued one. + if ($matching_associations) { + list($issued, $assoc) = $matching_associations[0]; + return $assoc; + } else { + return null; + } + } + } + + /** + * @access private + */ + function _getAssociation($filename) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $assoc_file = @fopen($filename, 'rb'); + + if ($assoc_file === false) { + return null; + } + + $assoc_s = fread($assoc_file, filesize($filename)); + fclose($assoc_file); + + if (!$assoc_s) { + return null; + } + + $association = + Auth_OpenID_Association::deserialize('Auth_OpenID_Association', + $assoc_s); + + if (!$association) { + Auth_OpenID_FileStore::_removeIfPresent($filename); + return null; + } + + if ($association->getExpiresIn() == 0) { + Auth_OpenID_FileStore::_removeIfPresent($filename); + return null; + } else { + return $association; + } + } + + /** + * Remove an association if it exists. Do nothing if it does not. + * + * @return bool $success + */ + function removeAssociation($server_url, $handle) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $assoc = $this->getAssociation($server_url, $handle); + if ($assoc === null) { + return false; + } else { + $filename = $this->getAssociationFilename($server_url, $handle); + return Auth_OpenID_FileStore::_removeIfPresent($filename); + } + } + + /** + * Return whether this nonce is present. As a side effect, mark it + * as no longer present. + * + * @return bool $present + */ + function useNonce($server_url, $timestamp, $salt) + { + global $Auth_OpenID_SKEW; + + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + if ( abs($timestamp - time()) > $Auth_OpenID_SKEW ) { + return False; + } + + if ($server_url) { + list($proto, $rest) = explode('://', $server_url, 2); + } else { + $proto = ''; + $rest = ''; + } + + $parts = explode('/', $rest, 2); + $domain = $this->_filenameEscape($parts[0]); + $url_hash = $this->_safe64($server_url); + $salt_hash = $this->_safe64($salt); + + $filename = sprintf('%08x-%s-%s-%s-%s', $timestamp, $proto, + $domain, $url_hash, $salt_hash); + $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $filename; + + $result = @fopen($filename, 'x'); + + if ($result === false) { + return false; + } else { + fclose($result); + return true; + } + } + + /** + * Remove expired entries from the database. This is potentially + * expensive, so only run when it is acceptable to take time. + * + * @access private + */ + function _allAssocs() + { + $all_associations = array(); + + $association_filenames = + Auth_OpenID_FileStore::_listdir($this->association_dir); + + foreach ($association_filenames as $association_filename) { + $association_file = fopen($association_filename, 'rb'); + + if ($association_file !== false) { + $assoc_s = fread($association_file, + filesize($association_filename)); + fclose($association_file); + + // Remove expired or corrupted associations + $association = + Auth_OpenID_Association::deserialize( + 'Auth_OpenID_Association', $assoc_s); + + if ($association === null) { + Auth_OpenID_FileStore::_removeIfPresent( + $association_filename); + } else { + if ($association->getExpiresIn() == 0) { + $all_associations[] = array($association_filename, + $association); + } + } + } + } + + return $all_associations; + } + + function clean() + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir); + $now = time(); + + // Check all nonces for expiry + foreach ($nonces as $nonce) { + if (!Auth_OpenID_checkTimestamp($nonce, $now)) { + $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce; + Auth_OpenID_FileStore::_removeIfPresent($filename); + } + } + + foreach ($this->_allAssocs() as $pair) { + list($assoc_filename, $assoc) = $pair; + if ($assoc->getExpiresIn() == 0) { + Auth_OpenID_FileStore::_removeIfPresent($assoc_filename); + } + } + } + + /** + * @access private + */ + function _rmtree($dir) + { + if ($dir[strlen($dir) - 1] != DIRECTORY_SEPARATOR) { + $dir .= DIRECTORY_SEPARATOR; + } + + if ($handle = opendir($dir)) { + while ($item = readdir($handle)) { + if (!in_array($item, array('.', '..'))) { + if (is_dir($dir . $item)) { + + if (!Auth_OpenID_FileStore::_rmtree($dir . $item)) { + return false; + } + } else if (is_file($dir . $item)) { + if (!unlink($dir . $item)) { + return false; + } + } + } + } + + closedir($handle); + + if (!@rmdir($dir)) { + return false; + } + + return true; + } else { + // Couldn't open directory. + return false; + } + } + + /** + * @access private + */ + function _mkstemp($dir) + { + foreach (range(0, 4) as $i) { + $name = tempnam($dir, "php_openid_filestore_"); + + if ($name !== false) { + return $name; + } + } + return false; + } + + /** + * @access private + */ + function _mkdtemp($dir) + { + foreach (range(0, 4) as $i) { + $name = $dir . strval(DIRECTORY_SEPARATOR) . strval(getmypid()) . + "-" . strval(rand(1, time())); + if (!mkdir($name, 0700)) { + return false; + } else { + return $name; + } + } + return false; + } + + /** + * @access private + */ + function _listdir($dir) + { + $handle = opendir($dir); + $files = array(); + while (false !== ($filename = readdir($handle))) { + if (!in_array($filename, array('.', '..'))) { + $files[] = $dir . DIRECTORY_SEPARATOR . $filename; + } + } + return $files; + } + + /** + * @access private + */ + function _isFilenameSafe($char) + { + $_Auth_OpenID_filename_allowed = Auth_OpenID_letters . + Auth_OpenID_digits . "."; + return (strpos($_Auth_OpenID_filename_allowed, $char) !== false); + } + + /** + * @access private + */ + function _safe64($str) + { + $h64 = base64_encode(Auth_OpenID_SHA1($str)); + $h64 = str_replace('+', '_', $h64); + $h64 = str_replace('/', '.', $h64); + $h64 = str_replace('=', '', $h64); + return $h64; + } + + /** + * @access private + */ + function _filenameEscape($str) + { + $filename = ""; + $b = Auth_OpenID::toBytes($str); + + for ($i = 0; $i < count($b); $i++) { + $c = $b[$i]; + if (Auth_OpenID_FileStore::_isFilenameSafe($c)) { + $filename .= $c; + } else { + $filename .= sprintf("_%02X", ord($c)); + } + } + return $filename; + } + + /** + * Attempt to remove a file, returning whether the file existed at + * the time of the call. + * + * @access private + * @return bool $result True if the file was present, false if not. + */ + function _removeIfPresent($filename) + { + return @unlink($filename); + } + + function cleanupAssociations() + { + $removed = 0; + foreach ($this->_allAssocs() as $pair) { + list($assoc_filename, $assoc) = $pair; + if ($assoc->getExpiresIn() == 0) { + $this->_removeIfPresent($assoc_filename); + $removed += 1; + } + } + return $removed; + } +} + +?> diff --git a/extlib/Auth/OpenID/HMAC.php b/extlib/Auth/OpenID/HMAC.php new file mode 100644 index 000000000..ec42db8df --- /dev/null +++ b/extlib/Auth/OpenID/HMAC.php @@ -0,0 +1,99 @@ +<?php + +/** + * This is the HMACSHA1 implementation for the OpenID library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'Auth/OpenID.php'; + +/** + * SHA1_BLOCKSIZE is this module's SHA1 blocksize used by the fallback + * implementation. + */ +define('Auth_OpenID_SHA1_BLOCKSIZE', 64); + +function Auth_OpenID_SHA1($text) +{ + if (function_exists('hash') && + function_exists('hash_algos') && + (in_array('sha1', hash_algos()))) { + // PHP 5 case (sometimes): 'hash' available and 'sha1' algo + // supported. + return hash('sha1', $text, true); + } else if (function_exists('sha1')) { + // PHP 4 case: 'sha1' available. + $hex = sha1($text); + $raw = ''; + for ($i = 0; $i < 40; $i += 2) { + $hexcode = substr($hex, $i, 2); + $charcode = (int)base_convert($hexcode, 16, 10); + $raw .= chr($charcode); + } + return $raw; + } else { + // Explode. + trigger_error('No SHA1 function found', E_USER_ERROR); + } +} + +/** + * Compute an HMAC/SHA1 hash. + * + * @access private + * @param string $key The HMAC key + * @param string $text The message text to hash + * @return string $mac The MAC + */ +function Auth_OpenID_HMACSHA1($key, $text) +{ + if (Auth_OpenID::bytes($key) > Auth_OpenID_SHA1_BLOCKSIZE) { + $key = Auth_OpenID_SHA1($key, true); + } + + $key = str_pad($key, Auth_OpenID_SHA1_BLOCKSIZE, chr(0x00)); + $ipad = str_repeat(chr(0x36), Auth_OpenID_SHA1_BLOCKSIZE); + $opad = str_repeat(chr(0x5c), Auth_OpenID_SHA1_BLOCKSIZE); + $hash1 = Auth_OpenID_SHA1(($key ^ $ipad) . $text, true); + $hmac = Auth_OpenID_SHA1(($key ^ $opad) . $hash1, true); + return $hmac; +} + +if (function_exists('hash') && + function_exists('hash_algos') && + (in_array('sha256', hash_algos()))) { + function Auth_OpenID_SHA256($text) + { + // PHP 5 case: 'hash' available and 'sha256' algo supported. + return hash('sha256', $text, true); + } + define('Auth_OpenID_SHA256_SUPPORTED', true); +} else { + define('Auth_OpenID_SHA256_SUPPORTED', false); +} + +if (function_exists('hash_hmac') && + function_exists('hash_algos') && + (in_array('sha256', hash_algos()))) { + + function Auth_OpenID_HMACSHA256($key, $text) + { + // Return raw MAC (not hex string). + return hash_hmac('sha256', $text, $key, true); + } + + define('Auth_OpenID_HMACSHA256_SUPPORTED', true); +} else { + define('Auth_OpenID_HMACSHA256_SUPPORTED', false); +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/Interface.php b/extlib/Auth/OpenID/Interface.php new file mode 100644 index 000000000..f4c6062f8 --- /dev/null +++ b/extlib/Auth/OpenID/Interface.php @@ -0,0 +1,197 @@ +<?php + +/** + * This file specifies the interface for PHP OpenID store implementations. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * This is the interface for the store objects the OpenID library + * uses. It is a single class that provides all of the persistence + * mechanisms that the OpenID library needs, for both servers and + * consumers. If you want to create an SQL-driven store, please see + * then {@link Auth_OpenID_SQLStore} class. + * + * Change: Version 2.0 removed the storeNonce, getAuthKey, and isDumb + * methods, and changed the behavior of the useNonce method to support + * one-way nonces. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + */ +class Auth_OpenID_OpenIDStore { + /** + * This method puts an Association object into storage, + * retrievable by server URL and handle. + * + * @param string $server_url The URL of the identity server that + * this association is with. Because of the way the server portion + * of the library uses this interface, don't assume there are any + * limitations on the character set of the input string. In + * particular, expect to see unescaped non-url-safe characters in + * the server_url field. + * + * @param Association $association The Association to store. + */ + function storeAssociation($server_url, $association) + { + trigger_error("Auth_OpenID_OpenIDStore::storeAssociation ". + "not implemented", E_USER_ERROR); + } + + /* + * Remove expired nonces from the store. + * + * Discards any nonce from storage that is old enough that its + * timestamp would not pass useNonce(). + * + * This method is not called in the normal operation of the + * library. It provides a way for store admins to keep their + * storage from filling up with expired data. + * + * @return the number of nonces expired + */ + function cleanupNonces() + { + trigger_error("Auth_OpenID_OpenIDStore::cleanupNonces ". + "not implemented", E_USER_ERROR); + } + + /* + * Remove expired associations from the store. + * + * This method is not called in the normal operation of the + * library. It provides a way for store admins to keep their + * storage from filling up with expired data. + * + * @return the number of associations expired. + */ + function cleanupAssociations() + { + trigger_error("Auth_OpenID_OpenIDStore::cleanupAssociations ". + "not implemented", E_USER_ERROR); + } + + /* + * Shortcut for cleanupNonces(), cleanupAssociations(). + * + * This method is not called in the normal operation of the + * library. It provides a way for store admins to keep their + * storage from filling up with expired data. + */ + function cleanup() + { + return array($this->cleanupNonces(), + $this->cleanupAssociations()); + } + + /** + * Report whether this storage supports cleanup + */ + function supportsCleanup() + { + return true; + } + + /** + * This method returns an Association object from storage that + * matches the server URL and, if specified, handle. It returns + * null if no such association is found or if the matching + * association is expired. + * + * If no handle is specified, the store may return any association + * which matches the server URL. If multiple associations are + * valid, the recommended return value for this method is the one + * most recently issued. + * + * This method is allowed (and encouraged) to garbage collect + * expired associations when found. This method must not return + * expired associations. + * + * @param string $server_url The URL of the identity server to get + * the association for. Because of the way the server portion of + * the library uses this interface, don't assume there are any + * limitations on the character set of the input string. In + * particular, expect to see unescaped non-url-safe characters in + * the server_url field. + * + * @param mixed $handle This optional parameter is the handle of + * the specific association to get. If no specific handle is + * provided, any valid association matching the server URL is + * returned. + * + * @return Association The Association for the given identity + * server. + */ + function getAssociation($server_url, $handle = null) + { + trigger_error("Auth_OpenID_OpenIDStore::getAssociation ". + "not implemented", E_USER_ERROR); + } + + /** + * This method removes the matching association if it's found, and + * returns whether the association was removed or not. + * + * @param string $server_url The URL of the identity server the + * association to remove belongs to. Because of the way the server + * portion of the library uses this interface, don't assume there + * are any limitations on the character set of the input + * string. In particular, expect to see unescaped non-url-safe + * characters in the server_url field. + * + * @param string $handle This is the handle of the association to + * remove. If there isn't an association found that matches both + * the given URL and handle, then there was no matching handle + * found. + * + * @return mixed Returns whether or not the given association existed. + */ + function removeAssociation($server_url, $handle) + { + trigger_error("Auth_OpenID_OpenIDStore::removeAssociation ". + "not implemented", E_USER_ERROR); + } + + /** + * Called when using a nonce. + * + * This method should return C{True} if the nonce has not been + * used before, and store it for a while to make sure nobody + * tries to use the same value again. If the nonce has already + * been used, return C{False}. + * + * Change: In earlier versions, round-trip nonces were used and a + * nonce was only valid if it had been previously stored with + * storeNonce. Version 2.0 uses one-way nonces, requiring a + * different implementation here that does not depend on a + * storeNonce call. (storeNonce is no longer part of the + * interface. + * + * @param string $nonce The nonce to use. + * + * @return bool Whether or not the nonce was valid. + */ + function useNonce($server_url, $timestamp, $salt) + { + trigger_error("Auth_OpenID_OpenIDStore::useNonce ". + "not implemented", E_USER_ERROR); + } + + /** + * Removes all entries from the store; implementation is optional. + */ + function reset() + { + } + +} +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/KVForm.php b/extlib/Auth/OpenID/KVForm.php new file mode 100644 index 000000000..fb342a001 --- /dev/null +++ b/extlib/Auth/OpenID/KVForm.php @@ -0,0 +1,112 @@ +<?php + +/** + * OpenID protocol key-value/comma-newline format parsing and + * serialization + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Container for key-value/comma-newline OpenID format and parsing + */ +class Auth_OpenID_KVForm { + /** + * Convert an OpenID colon/newline separated string into an + * associative array + * + * @static + * @access private + */ + function toArray($kvs, $strict=false) + { + $lines = explode("\n", $kvs); + + $last = array_pop($lines); + if ($last !== '') { + array_push($lines, $last); + if ($strict) { + return false; + } + } + + $values = array(); + + for ($lineno = 0; $lineno < count($lines); $lineno++) { + $line = $lines[$lineno]; + $kv = explode(':', $line, 2); + if (count($kv) != 2) { + if ($strict) { + return false; + } + continue; + } + + $key = $kv[0]; + $tkey = trim($key); + if ($tkey != $key) { + if ($strict) { + return false; + } + } + + $value = $kv[1]; + $tval = trim($value); + if ($tval != $value) { + if ($strict) { + return false; + } + } + + $values[$tkey] = $tval; + } + + return $values; + } + + /** + * Convert an array into an OpenID colon/newline separated string + * + * @static + * @access private + */ + function fromArray($values) + { + if ($values === null) { + return null; + } + + ksort($values); + + $serialized = ''; + foreach ($values as $key => $value) { + if (is_array($value)) { + list($key, $value) = array($value[0], $value[1]); + } + + if (strpos($key, ':') !== false) { + return null; + } + + if (strpos($key, "\n") !== false) { + return null; + } + + if (strpos($value, "\n") !== false) { + return null; + } + $serialized .= "$key:$value\n"; + } + return $serialized; + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/MemcachedStore.php b/extlib/Auth/OpenID/MemcachedStore.php new file mode 100644 index 000000000..d357c6b11 --- /dev/null +++ b/extlib/Auth/OpenID/MemcachedStore.php @@ -0,0 +1,208 @@ +<?php + +/** + * This file supplies a memcached store backend for OpenID servers and + * consumers. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author Artemy Tregubenko <me@arty.name> + * @copyright 2008 JanRain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + * Contributed by Open Web Technologies <http://openwebtech.ru/> + */ + +/** + * Import the interface for creating a new store class. + */ +require_once 'Auth/OpenID/Interface.php'; + +/** + * This is a memcached-based store for OpenID associations and + * nonces. + * + * As memcache has limit of 250 chars for key length, + * server_url, handle and salt are hashed with sha1(). + * + * Most of the methods of this class are implementation details. + * People wishing to just use this store need only pay attention to + * the constructor. + * + * @package OpenID + */ +class Auth_OpenID_MemcachedStore extends Auth_OpenID_OpenIDStore { + + /** + * Initializes a new {@link Auth_OpenID_MemcachedStore} instance. + * Just saves memcached object as property. + * + * @param resource connection Memcache connection resourse + */ + function Auth_OpenID_MemcachedStore($connection, $compress = false) + { + $this->connection = $connection; + $this->compress = $compress ? MEMCACHE_COMPRESSED : 0; + } + + /** + * Store association until its expiration time in memcached. + * Overwrites any existing association with same server_url and + * handle. Handles list of associations for every server. + */ + function storeAssociation($server_url, $association) + { + // create memcached keys for association itself + // and list of associations for this server + $associationKey = $this->associationKey($server_url, + $association->handle); + $serverKey = $this->associationServerKey($server_url); + + // get list of associations + $serverAssociations = $this->connection->get($serverKey); + + // if no such list, initialize it with empty array + if (!$serverAssociations) { + $serverAssociations = array(); + } + // and store given association key in it + $serverAssociations[$association->issued] = $associationKey; + + // save associations' keys list + $this->connection->set( + $serverKey, + $serverAssociations, + $this->compress + ); + // save association itself + $this->connection->set( + $associationKey, + $association, + $this->compress, + $association->issued + $association->lifetime); + } + + /** + * Read association from memcached. If no handle given + * and multiple associations found, returns latest issued + */ + function getAssociation($server_url, $handle = null) + { + // simple case: handle given + if ($handle !== null) { + // get association, return null if failed + $association = $this->connection->get( + $this->associationKey($server_url, $handle)); + return $association ? $association : null; + } + + // no handle given, working with list + // create key for list of associations + $serverKey = $this->associationServerKey($server_url); + + // get list of associations + $serverAssociations = $this->connection->get($serverKey); + // return null if failed or got empty list + if (!$serverAssociations) { + return null; + } + + // get key of most recently issued association + $keys = array_keys($serverAssociations); + sort($keys); + $lastKey = $serverAssociations[array_pop($keys)]; + + // get association, return null if failed + $association = $this->connection->get($lastKey); + return $association ? $association : null; + } + + /** + * Immediately delete association from memcache. + */ + function removeAssociation($server_url, $handle) + { + // create memcached keys for association itself + // and list of associations for this server + $serverKey = $this->associationServerKey($server_url); + $associationKey = $this->associationKey($server_url, + $handle); + + // get list of associations + $serverAssociations = $this->connection->get($serverKey); + // return null if failed or got empty list + if (!$serverAssociations) { + return false; + } + + // ensure that given association key exists in list + $serverAssociations = array_flip($serverAssociations); + if (!array_key_exists($associationKey, $serverAssociations)) { + return false; + } + + // remove given association key from list + unset($serverAssociations[$associationKey]); + $serverAssociations = array_flip($serverAssociations); + + // save updated list + $this->connection->set( + $serverKey, + $serverAssociations, + $this->compress + ); + + // delete association + return $this->connection->delete($associationKey); + } + + /** + * Create nonce for server and salt, expiring after + * $Auth_OpenID_SKEW seconds. + */ + function useNonce($server_url, $timestamp, $salt) + { + global $Auth_OpenID_SKEW; + + // save one request to memcache when nonce obviously expired + if (abs($timestamp - time()) > $Auth_OpenID_SKEW) { + return false; + } + + // returns false when nonce already exists + // otherwise adds nonce + return $this->connection->add( + 'openid_nonce_' . sha1($server_url) . '_' . sha1($salt), + 1, // any value here + $this->compress, + $Auth_OpenID_SKEW); + } + + /** + * Memcache key is prefixed with 'openid_association_' string. + */ + function associationKey($server_url, $handle = null) + { + return 'openid_association_' . sha1($server_url) . '_' . sha1($handle); + } + + /** + * Memcache key is prefixed with 'openid_association_' string. + */ + function associationServerKey($server_url) + { + return 'openid_association_server_' . sha1($server_url); + } + + /** + * Report that this storage doesn't support cleanup + */ + function supportsCleanup() + { + return false; + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/Message.php b/extlib/Auth/OpenID/Message.php new file mode 100644 index 000000000..fd23e67a3 --- /dev/null +++ b/extlib/Auth/OpenID/Message.php @@ -0,0 +1,915 @@ +<?php + +/** + * Extension argument processing code + * + * @package OpenID + */ + +/** + * Import tools needed to deal with messages. + */ +require_once 'Auth/OpenID.php'; +require_once 'Auth/OpenID/KVForm.php'; +require_once 'Auth/Yadis/XML.php'; +require_once 'Auth/OpenID/Consumer.php'; // For Auth_OpenID_FailureResponse + +// This doesn't REALLY belong here, but where is better? +define('Auth_OpenID_IDENTIFIER_SELECT', + "http://specs.openid.net/auth/2.0/identifier_select"); + +// URI for Simple Registration extension, the only commonly deployed +// OpenID 1.x extension, and so a special case +define('Auth_OpenID_SREG_URI', 'http://openid.net/sreg/1.0'); + +// The OpenID 1.X namespace URI +define('Auth_OpenID_OPENID1_NS', 'http://openid.net/signon/1.0'); +define('Auth_OpenID_THE_OTHER_OPENID1_NS', 'http://openid.net/signon/1.1'); + +function Auth_OpenID_isOpenID1($ns) +{ + return ($ns == Auth_OpenID_THE_OTHER_OPENID1_NS) || + ($ns == Auth_OpenID_OPENID1_NS); +} + +// The OpenID 2.0 namespace URI +define('Auth_OpenID_OPENID2_NS', 'http://specs.openid.net/auth/2.0'); + +// The namespace consisting of pairs with keys that are prefixed with +// "openid." but not in another namespace. +define('Auth_OpenID_NULL_NAMESPACE', 'Null namespace'); + +// The null namespace, when it is an allowed OpenID namespace +define('Auth_OpenID_OPENID_NS', 'OpenID namespace'); + +// The top-level namespace, excluding all pairs with keys that start +// with "openid." +define('Auth_OpenID_BARE_NS', 'Bare namespace'); + +// Sentinel for Message implementation to indicate that getArg should +// return null instead of returning a default. +define('Auth_OpenID_NO_DEFAULT', 'NO DEFAULT ALLOWED'); + +// Limit, in bytes, of identity provider and return_to URLs, including +// response payload. See OpenID 1.1 specification, Appendix D. +define('Auth_OpenID_OPENID1_URL_LIMIT', 2047); + +// All OpenID protocol fields. Used to check namespace aliases. +global $Auth_OpenID_OPENID_PROTOCOL_FIELDS; +$Auth_OpenID_OPENID_PROTOCOL_FIELDS = array( + 'ns', 'mode', 'error', 'return_to', 'contact', 'reference', + 'signed', 'assoc_type', 'session_type', 'dh_modulus', 'dh_gen', + 'dh_consumer_public', 'claimed_id', 'identity', 'realm', + 'invalidate_handle', 'op_endpoint', 'response_nonce', 'sig', + 'assoc_handle', 'trust_root', 'openid'); + +// Global namespace / alias registration map. See +// Auth_OpenID_registerNamespaceAlias. +global $Auth_OpenID_registered_aliases; +$Auth_OpenID_registered_aliases = array(); + +/** + * Registers a (namespace URI, alias) mapping in a global namespace + * alias map. Raises NamespaceAliasRegistrationError if either the + * namespace URI or alias has already been registered with a different + * value. This function is required if you want to use a namespace + * with an OpenID 1 message. + */ +function Auth_OpenID_registerNamespaceAlias($namespace_uri, $alias) +{ + global $Auth_OpenID_registered_aliases; + + if (Auth_OpenID::arrayGet($Auth_OpenID_registered_aliases, + $alias) == $namespace_uri) { + return true; + } + + if (in_array($namespace_uri, + array_values($Auth_OpenID_registered_aliases))) { + return false; + } + + if (in_array($alias, array_keys($Auth_OpenID_registered_aliases))) { + return false; + } + + $Auth_OpenID_registered_aliases[$alias] = $namespace_uri; + return true; +} + +/** + * Removes a (namespace_uri, alias) registration from the global + * namespace alias map. Returns true if the removal succeeded; false + * if not (if the mapping did not exist). + */ +function Auth_OpenID_removeNamespaceAlias($namespace_uri, $alias) +{ + global $Auth_OpenID_registered_aliases; + + if (Auth_OpenID::arrayGet($Auth_OpenID_registered_aliases, + $alias) === $namespace_uri) { + unset($Auth_OpenID_registered_aliases[$alias]); + return true; + } + + return false; +} + +/** + * An Auth_OpenID_Mapping maintains a mapping from arbitrary keys to + * arbitrary values. (This is unlike an ordinary PHP array, whose + * keys may be only simple scalars.) + * + * @package OpenID + */ +class Auth_OpenID_Mapping { + /** + * Initialize a mapping. If $classic_array is specified, its keys + * and values are used to populate the mapping. + */ + function Auth_OpenID_Mapping($classic_array = null) + { + $this->keys = array(); + $this->values = array(); + + if (is_array($classic_array)) { + foreach ($classic_array as $key => $value) { + $this->set($key, $value); + } + } + } + + /** + * Returns true if $thing is an Auth_OpenID_Mapping object; false + * if not. + */ + function isA($thing) + { + return (is_object($thing) && + strtolower(get_class($thing)) == 'auth_openid_mapping'); + } + + /** + * Returns an array of the keys in the mapping. + */ + function keys() + { + return $this->keys; + } + + /** + * Returns an array of values in the mapping. + */ + function values() + { + return $this->values; + } + + /** + * Returns an array of (key, value) pairs in the mapping. + */ + function items() + { + $temp = array(); + + for ($i = 0; $i < count($this->keys); $i++) { + $temp[] = array($this->keys[$i], + $this->values[$i]); + } + return $temp; + } + + /** + * Returns the "length" of the mapping, or the number of keys. + */ + function len() + { + return count($this->keys); + } + + /** + * Sets a key-value pair in the mapping. If the key already + * exists, its value is replaced with the new value. + */ + function set($key, $value) + { + $index = array_search($key, $this->keys); + + if ($index !== false) { + $this->values[$index] = $value; + } else { + $this->keys[] = $key; + $this->values[] = $value; + } + } + + /** + * Gets a specified value from the mapping, associated with the + * specified key. If the key does not exist in the mapping, + * $default is returned instead. + */ + function get($key, $default = null) + { + $index = array_search($key, $this->keys); + + if ($index !== false) { + return $this->values[$index]; + } else { + return $default; + } + } + + /** + * @access private + */ + function _reflow() + { + // PHP is broken yet again. Sort the arrays to remove the + // hole in the numeric indexes that make up the array. + $old_keys = $this->keys; + $old_values = $this->values; + + $this->keys = array(); + $this->values = array(); + + foreach ($old_keys as $k) { + $this->keys[] = $k; + } + + foreach ($old_values as $v) { + $this->values[] = $v; + } + } + + /** + * Deletes a key-value pair from the mapping with the specified + * key. + */ + function del($key) + { + $index = array_search($key, $this->keys); + + if ($index !== false) { + unset($this->keys[$index]); + unset($this->values[$index]); + $this->_reflow(); + return true; + } + return false; + } + + /** + * Returns true if the specified value has a key in the mapping; + * false if not. + */ + function contains($value) + { + return (array_search($value, $this->keys) !== false); + } +} + +/** + * Maintains a bijective map between namespace uris and aliases. + * + * @package OpenID + */ +class Auth_OpenID_NamespaceMap { + function Auth_OpenID_NamespaceMap() + { + $this->alias_to_namespace = new Auth_OpenID_Mapping(); + $this->namespace_to_alias = new Auth_OpenID_Mapping(); + $this->implicit_namespaces = array(); + } + + function getAlias($namespace_uri) + { + return $this->namespace_to_alias->get($namespace_uri); + } + + function getNamespaceURI($alias) + { + return $this->alias_to_namespace->get($alias); + } + + function iterNamespaceURIs() + { + // Return an iterator over the namespace URIs + return $this->namespace_to_alias->keys(); + } + + function iterAliases() + { + // Return an iterator over the aliases""" + return $this->alias_to_namespace->keys(); + } + + function iteritems() + { + return $this->namespace_to_alias->items(); + } + + function isImplicit($namespace_uri) + { + return in_array($namespace_uri, $this->implicit_namespaces); + } + + function addAlias($namespace_uri, $desired_alias, $implicit=false) + { + // Add an alias from this namespace URI to the desired alias + global $Auth_OpenID_OPENID_PROTOCOL_FIELDS; + + // Check that desired_alias is not an openid protocol field as + // per the spec. + if (in_array($desired_alias, $Auth_OpenID_OPENID_PROTOCOL_FIELDS)) { + Auth_OpenID::log("\"%s\" is not an allowed namespace alias", + $desired_alias); + return null; + } + + // Check that desired_alias does not contain a period as per + // the spec. + if (strpos($desired_alias, '.') !== false) { + Auth_OpenID::log('"%s" must not contain a dot', $desired_alias); + return null; + } + + // Check that there is not a namespace already defined for the + // desired alias + $current_namespace_uri = + $this->alias_to_namespace->get($desired_alias); + + if (($current_namespace_uri !== null) && + ($current_namespace_uri != $namespace_uri)) { + Auth_OpenID::log('Cannot map "%s" because previous mapping exists', + $namespace_uri); + return null; + } + + // Check that there is not already a (different) alias for + // this namespace URI + $alias = $this->namespace_to_alias->get($namespace_uri); + + if (($alias !== null) && ($alias != $desired_alias)) { + Auth_OpenID::log('Cannot map %s to alias %s. ' . + 'It is already mapped to alias %s', + $namespace_uri, $desired_alias, $alias); + return null; + } + + assert((Auth_OpenID_NULL_NAMESPACE === $desired_alias) || + is_string($desired_alias)); + + $this->alias_to_namespace->set($desired_alias, $namespace_uri); + $this->namespace_to_alias->set($namespace_uri, $desired_alias); + if ($implicit) { + array_push($this->implicit_namespaces, $namespace_uri); + } + + return $desired_alias; + } + + function add($namespace_uri) + { + // Add this namespace URI to the mapping, without caring what + // alias it ends up with + + // See if this namespace is already mapped to an alias + $alias = $this->namespace_to_alias->get($namespace_uri); + + if ($alias !== null) { + return $alias; + } + + // Fall back to generating a numerical alias + $i = 0; + while (1) { + $alias = 'ext' . strval($i); + if ($this->addAlias($namespace_uri, $alias) === null) { + $i += 1; + } else { + return $alias; + } + } + + // Should NEVER be reached! + return null; + } + + function contains($namespace_uri) + { + return $this->isDefined($namespace_uri); + } + + function isDefined($namespace_uri) + { + return $this->namespace_to_alias->contains($namespace_uri); + } +} + +/** + * In the implementation of this object, null represents the global + * namespace as well as a namespace with no key. + * + * @package OpenID + */ +class Auth_OpenID_Message { + + function Auth_OpenID_Message($openid_namespace = null) + { + // Create an empty Message + $this->allowed_openid_namespaces = array( + Auth_OpenID_OPENID1_NS, + Auth_OpenID_THE_OTHER_OPENID1_NS, + Auth_OpenID_OPENID2_NS); + + $this->args = new Auth_OpenID_Mapping(); + $this->namespaces = new Auth_OpenID_NamespaceMap(); + if ($openid_namespace === null) { + $this->_openid_ns_uri = null; + } else { + $implicit = Auth_OpenID_isOpenID1($openid_namespace); + $this->setOpenIDNamespace($openid_namespace, $implicit); + } + } + + function isOpenID1() + { + return Auth_OpenID_isOpenID1($this->getOpenIDNamespace()); + } + + function isOpenID2() + { + return $this->getOpenIDNamespace() == Auth_OpenID_OPENID2_NS; + } + + function fromPostArgs($args) + { + // Construct a Message containing a set of POST arguments + $obj = new Auth_OpenID_Message(); + + // Partition into "openid." args and bare args + $openid_args = array(); + foreach ($args as $key => $value) { + + if (is_array($value)) { + return null; + } + + $parts = explode('.', $key, 2); + + if (count($parts) == 2) { + list($prefix, $rest) = $parts; + } else { + $prefix = null; + } + + if ($prefix != 'openid') { + $obj->args->set(array(Auth_OpenID_BARE_NS, $key), $value); + } else { + $openid_args[$rest] = $value; + } + } + + if ($obj->_fromOpenIDArgs($openid_args)) { + return $obj; + } else { + return null; + } + } + + function fromOpenIDArgs($openid_args) + { + // Takes an array. + + // Construct a Message from a parsed KVForm message + $obj = new Auth_OpenID_Message(); + if ($obj->_fromOpenIDArgs($openid_args)) { + return $obj; + } else { + return null; + } + } + + /** + * @access private + */ + function _fromOpenIDArgs($openid_args) + { + global $Auth_OpenID_registered_aliases; + + // Takes an Auth_OpenID_Mapping instance OR an array. + + if (!Auth_OpenID_Mapping::isA($openid_args)) { + $openid_args = new Auth_OpenID_Mapping($openid_args); + } + + $ns_args = array(); + + // Resolve namespaces + foreach ($openid_args->items() as $pair) { + list($rest, $value) = $pair; + + $parts = explode('.', $rest, 2); + + if (count($parts) == 2) { + list($ns_alias, $ns_key) = $parts; + } else { + $ns_alias = Auth_OpenID_NULL_NAMESPACE; + $ns_key = $rest; + } + + if ($ns_alias == 'ns') { + if ($this->namespaces->addAlias($value, $ns_key) === null) { + return false; + } + } else if (($ns_alias == Auth_OpenID_NULL_NAMESPACE) && + ($ns_key == 'ns')) { + // null namespace + if ($this->setOpenIDNamespace($value, false) === false) { + return false; + } + } else { + $ns_args[] = array($ns_alias, $ns_key, $value); + } + } + + if (!$this->getOpenIDNamespace()) { + if ($this->setOpenIDNamespace(Auth_OpenID_OPENID1_NS, true) === + false) { + return false; + } + } + + // Actually put the pairs into the appropriate namespaces + foreach ($ns_args as $triple) { + list($ns_alias, $ns_key, $value) = $triple; + $ns_uri = $this->namespaces->getNamespaceURI($ns_alias); + if ($ns_uri === null) { + $ns_uri = $this->_getDefaultNamespace($ns_alias); + if ($ns_uri === null) { + + $ns_uri = Auth_OpenID_OPENID_NS; + $ns_key = sprintf('%s.%s', $ns_alias, $ns_key); + } else { + $this->namespaces->addAlias($ns_uri, $ns_alias, true); + } + } + + $this->setArg($ns_uri, $ns_key, $value); + } + + return true; + } + + function _getDefaultNamespace($mystery_alias) + { + global $Auth_OpenID_registered_aliases; + if ($this->isOpenID1()) { + return @$Auth_OpenID_registered_aliases[$mystery_alias]; + } + return null; + } + + function setOpenIDNamespace($openid_ns_uri, $implicit) + { + if (!in_array($openid_ns_uri, $this->allowed_openid_namespaces)) { + Auth_OpenID::log('Invalid null namespace: "%s"', $openid_ns_uri); + return false; + } + + $succeeded = $this->namespaces->addAlias($openid_ns_uri, + Auth_OpenID_NULL_NAMESPACE, + $implicit); + if ($succeeded === false) { + return false; + } + + $this->_openid_ns_uri = $openid_ns_uri; + + return true; + } + + function getOpenIDNamespace() + { + return $this->_openid_ns_uri; + } + + function fromKVForm($kvform_string) + { + // Create a Message from a KVForm string + return Auth_OpenID_Message::fromOpenIDArgs( + Auth_OpenID_KVForm::toArray($kvform_string)); + } + + function copy() + { + return $this; + } + + function toPostArgs() + { + // Return all arguments with openid. in front of namespaced + // arguments. + + $args = array(); + + // Add namespace definitions to the output + foreach ($this->namespaces->iteritems() as $pair) { + list($ns_uri, $alias) = $pair; + if ($this->namespaces->isImplicit($ns_uri)) { + continue; + } + if ($alias == Auth_OpenID_NULL_NAMESPACE) { + $ns_key = 'openid.ns'; + } else { + $ns_key = 'openid.ns.' . $alias; + } + $args[$ns_key] = $ns_uri; + } + + foreach ($this->args->items() as $pair) { + list($ns_parts, $value) = $pair; + list($ns_uri, $ns_key) = $ns_parts; + $key = $this->getKey($ns_uri, $ns_key); + $args[$key] = $value; + } + + return $args; + } + + function toArgs() + { + // Return all namespaced arguments, failing if any + // non-namespaced arguments exist. + $post_args = $this->toPostArgs(); + $kvargs = array(); + foreach ($post_args as $k => $v) { + if (strpos($k, 'openid.') !== 0) { + // raise ValueError( + // 'This message can only be encoded as a POST, because it ' + // 'contains arguments that are not prefixed with "openid."') + return null; + } else { + $kvargs[substr($k, 7)] = $v; + } + } + + return $kvargs; + } + + function toFormMarkup($action_url, $form_tag_attrs = null, + $submit_text = "Continue") + { + $form = "<form accept-charset=\"UTF-8\" ". + "enctype=\"application/x-www-form-urlencoded\""; + + if (!$form_tag_attrs) { + $form_tag_attrs = array(); + } + + $form_tag_attrs['action'] = $action_url; + $form_tag_attrs['method'] = 'post'; + + unset($form_tag_attrs['enctype']); + unset($form_tag_attrs['accept-charset']); + + if ($form_tag_attrs) { + foreach ($form_tag_attrs as $name => $attr) { + $form .= sprintf(" %s=\"%s\"", $name, $attr); + } + } + + $form .= ">\n"; + + foreach ($this->toPostArgs() as $name => $value) { + $form .= sprintf( + "<input type=\"hidden\" name=\"%s\" value=\"%s\" />\n", + $name, $value); + } + + $form .= sprintf("<input type=\"submit\" value=\"%s\" />\n", + $submit_text); + + $form .= "</form>\n"; + + return $form; + } + + function toURL($base_url) + { + // Generate a GET URL with the parameters in this message + // attached as query parameters. + return Auth_OpenID::appendArgs($base_url, $this->toPostArgs()); + } + + function toKVForm() + { + // Generate a KVForm string that contains the parameters in + // this message. This will fail if the message contains + // arguments outside of the 'openid.' prefix. + return Auth_OpenID_KVForm::fromArray($this->toArgs()); + } + + function toURLEncoded() + { + // Generate an x-www-urlencoded string + $args = array(); + + foreach ($this->toPostArgs() as $k => $v) { + $args[] = array($k, $v); + } + + sort($args); + return Auth_OpenID::httpBuildQuery($args); + } + + /** + * @access private + */ + function _fixNS($namespace) + { + // Convert an input value into the internally used values of + // this object + + if ($namespace == Auth_OpenID_OPENID_NS) { + if ($this->_openid_ns_uri === null) { + return new Auth_OpenID_FailureResponse(null, + 'OpenID namespace not set'); + } else { + $namespace = $this->_openid_ns_uri; + } + } + + if (($namespace != Auth_OpenID_BARE_NS) && + (!is_string($namespace))) { + //TypeError + $err_msg = sprintf("Namespace must be Auth_OpenID_BARE_NS, ". + "Auth_OpenID_OPENID_NS or a string. got %s", + print_r($namespace, true)); + return new Auth_OpenID_FailureResponse(null, $err_msg); + } + + if (($namespace != Auth_OpenID_BARE_NS) && + (strpos($namespace, ':') === false)) { + // fmt = 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r' + // warnings.warn(fmt % (namespace,), DeprecationWarning) + + if ($namespace == 'sreg') { + // fmt = 'Using %r instead of "sreg" as namespace' + // warnings.warn(fmt % (SREG_URI,), DeprecationWarning,) + return Auth_OpenID_SREG_URI; + } + } + + return $namespace; + } + + function hasKey($namespace, $ns_key) + { + $namespace = $this->_fixNS($namespace); + if (Auth_OpenID::isFailure($namespace)) { + // XXX log me + return false; + } else { + return $this->args->contains(array($namespace, $ns_key)); + } + } + + function getKey($namespace, $ns_key) + { + // Get the key for a particular namespaced argument + $namespace = $this->_fixNS($namespace); + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } + if ($namespace == Auth_OpenID_BARE_NS) { + return $ns_key; + } + + $ns_alias = $this->namespaces->getAlias($namespace); + + // No alias is defined, so no key can exist + if ($ns_alias === null) { + return null; + } + + if ($ns_alias == Auth_OpenID_NULL_NAMESPACE) { + $tail = $ns_key; + } else { + $tail = sprintf('%s.%s', $ns_alias, $ns_key); + } + + return 'openid.' . $tail; + } + + function getArg($namespace, $key, $default = null) + { + // Get a value for a namespaced key. + $namespace = $this->_fixNS($namespace); + + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { + if ((!$this->args->contains(array($namespace, $key))) && + ($default == Auth_OpenID_NO_DEFAULT)) { + $err_msg = sprintf("Namespace %s missing required field %s", + $namespace, $key); + return new Auth_OpenID_FailureResponse(null, $err_msg); + } else { + return $this->args->get(array($namespace, $key), $default); + } + } + } + + function getArgs($namespace) + { + // Get the arguments that are defined for this namespace URI + + $namespace = $this->_fixNS($namespace); + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { + $stuff = array(); + foreach ($this->args->items() as $pair) { + list($key, $value) = $pair; + list($pair_ns, $ns_key) = $key; + if ($pair_ns == $namespace) { + $stuff[$ns_key] = $value; + } + } + + return $stuff; + } + } + + function updateArgs($namespace, $updates) + { + // Set multiple key/value pairs in one call + + $namespace = $this->_fixNS($namespace); + + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { + foreach ($updates as $k => $v) { + $this->setArg($namespace, $k, $v); + } + return true; + } + } + + function setArg($namespace, $key, $value) + { + // Set a single argument in this namespace + $namespace = $this->_fixNS($namespace); + + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { + $this->args->set(array($namespace, $key), $value); + if ($namespace !== Auth_OpenID_BARE_NS) { + $this->namespaces->add($namespace); + } + return true; + } + } + + function delArg($namespace, $key) + { + $namespace = $this->_fixNS($namespace); + + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { + return $this->args->del(array($namespace, $key)); + } + } + + function getAliasedArg($aliased_key, $default = null) + { + $parts = explode('.', $aliased_key, 2); + + if (count($parts) != 2) { + $ns = null; + } else { + list($alias, $key) = $parts; + + if ($alias == 'ns') { + // Return the namespace URI for a namespace alias + // parameter. + return $this->namespaces->getNamespaceURI($key); + } else { + $ns = $this->namespaces->getNamespaceURI($alias); + } + } + + if ($ns === null) { + $key = $aliased_key; + $ns = $this->getOpenIDNamespace(); + } + + return $this->getArg($ns, $key, $default); + } +} + +?> diff --git a/extlib/Auth/OpenID/MySQLStore.php b/extlib/Auth/OpenID/MySQLStore.php new file mode 100644 index 000000000..eb08af016 --- /dev/null +++ b/extlib/Auth/OpenID/MySQLStore.php @@ -0,0 +1,78 @@ +<?php + +/** + * A MySQL store. + * + * @package OpenID + */ + +/** + * Require the base class file. + */ +require_once "Auth/OpenID/SQLStore.php"; + +/** + * An SQL store that uses MySQL as its backend. + * + * @package OpenID + */ +class Auth_OpenID_MySQLStore extends Auth_OpenID_SQLStore { + /** + * @access private + */ + function setSQL() + { + $this->sql['nonce_table'] = + "CREATE TABLE %s (\n". + " server_url VARCHAR(2047) NOT NULL,\n". + " timestamp INTEGER NOT NULL,\n". + " salt CHAR(40) NOT NULL,\n". + " UNIQUE (server_url(255), timestamp, salt)\n". + ") ENGINE=InnoDB"; + + $this->sql['assoc_table'] = + "CREATE TABLE %s (\n". + " server_url BLOB NOT NULL,\n". + " handle VARCHAR(255) NOT NULL,\n". + " secret BLOB NOT NULL,\n". + " issued INTEGER NOT NULL,\n". + " lifetime INTEGER NOT NULL,\n". + " assoc_type VARCHAR(64) NOT NULL,\n". + " PRIMARY KEY (server_url(255), handle)\n". + ") ENGINE=InnoDB"; + + $this->sql['set_assoc'] = + "REPLACE INTO %s (server_url, handle, secret, issued,\n". + " lifetime, assoc_type) VALUES (?, ?, !, ?, ?, ?)"; + + $this->sql['get_assocs'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ?"; + + $this->sql['get_assoc'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ? AND handle = ?"; + + $this->sql['remove_assoc'] = + "DELETE FROM %s WHERE server_url = ? AND handle = ?"; + + $this->sql['add_nonce'] = + "INSERT INTO %s (server_url, timestamp, salt) VALUES (?, ?, ?)"; + + $this->sql['clean_nonce'] = + "DELETE FROM %s WHERE timestamp < ?"; + + $this->sql['clean_assoc'] = + "DELETE FROM %s WHERE issued + lifetime < ?"; + } + + /** + * @access private + */ + function blobEncode($blob) + { + return "0x" . bin2hex($blob); + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/Nonce.php b/extlib/Auth/OpenID/Nonce.php new file mode 100644 index 000000000..effecac38 --- /dev/null +++ b/extlib/Auth/OpenID/Nonce.php @@ -0,0 +1,109 @@ +<?php + +/** + * Nonce-related functionality. + * + * @package OpenID + */ + +/** + * Need CryptUtil to generate random strings. + */ +require_once 'Auth/OpenID/CryptUtil.php'; + +/** + * This is the characters that the nonces are made from. + */ +define('Auth_OpenID_Nonce_CHRS',"abcdefghijklmnopqrstuvwxyz" . + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); + +// Keep nonces for five hours (allow five hours for the combination of +// request time and clock skew). This is probably way more than is +// necessary, but there is not much overhead in storing nonces. +global $Auth_OpenID_SKEW; +$Auth_OpenID_SKEW = 60 * 60 * 5; + +define('Auth_OpenID_Nonce_REGEX', + '/(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z(.*)/'); + +define('Auth_OpenID_Nonce_TIME_FMT', + '%Y-%m-%dT%H:%M:%SZ'); + +function Auth_OpenID_splitNonce($nonce_string) +{ + // Extract a timestamp from the given nonce string + $result = preg_match(Auth_OpenID_Nonce_REGEX, $nonce_string, $matches); + if ($result != 1 || count($matches) != 8) { + return null; + } + + list($unused, + $tm_year, + $tm_mon, + $tm_mday, + $tm_hour, + $tm_min, + $tm_sec, + $uniquifier) = $matches; + + $timestamp = + @gmmktime($tm_hour, $tm_min, $tm_sec, $tm_mon, $tm_mday, $tm_year); + + if ($timestamp === false || $timestamp < 0) { + return null; + } + + return array($timestamp, $uniquifier); +} + +function Auth_OpenID_checkTimestamp($nonce_string, + $allowed_skew = null, + $now = null) +{ + // Is the timestamp that is part of the specified nonce string + // within the allowed clock-skew of the current time? + global $Auth_OpenID_SKEW; + + if ($allowed_skew === null) { + $allowed_skew = $Auth_OpenID_SKEW; + } + + $parts = Auth_OpenID_splitNonce($nonce_string); + if ($parts == null) { + return false; + } + + if ($now === null) { + $now = time(); + } + + $stamp = $parts[0]; + + // Time after which we should not use the nonce + $past = $now - $allowed_skew; + + // Time that is too far in the future for us to allow + $future = $now + $allowed_skew; + + // the stamp is not too far in the future and is not too far + // in the past + return (($past <= $stamp) && ($stamp <= $future)); +} + +function Auth_OpenID_mkNonce($when = null) +{ + // Generate a nonce with the current timestamp + $salt = Auth_OpenID_CryptUtil::randomString( + 6, Auth_OpenID_Nonce_CHRS); + if ($when === null) { + // It's safe to call time() with no arguments; it returns a + // GMT unix timestamp on PHP 4 and PHP 5. gmmktime() with no + // args returns a local unix timestamp on PHP 4, so don't use + // that. + $when = time(); + } + $time_str = gmstrftime(Auth_OpenID_Nonce_TIME_FMT, $when); + return $time_str . $salt; +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/PAPE.php b/extlib/Auth/OpenID/PAPE.php new file mode 100644 index 000000000..62cba8a91 --- /dev/null +++ b/extlib/Auth/OpenID/PAPE.php @@ -0,0 +1,301 @@ +<?php + +/** + * An implementation of the OpenID Provider Authentication Policy + * Extension 1.0 + * + * See: + * http://openid.net/developers/specs/ + */ + +require_once "Auth/OpenID/Extension.php"; + +define('Auth_OpenID_PAPE_NS_URI', + "http://specs.openid.net/extensions/pape/1.0"); + +define('PAPE_AUTH_MULTI_FACTOR_PHYSICAL', + 'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical'); +define('PAPE_AUTH_MULTI_FACTOR', + 'http://schemas.openid.net/pape/policies/2007/06/multi-factor'); +define('PAPE_AUTH_PHISHING_RESISTANT', + 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'); + +define('PAPE_TIME_VALIDATOR', + '^[0-9]{4,4}-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z$'); +/** + * A Provider Authentication Policy request, sent from a relying party + * to a provider + * + * preferred_auth_policies: The authentication policies that + * the relying party prefers + * + * max_auth_age: The maximum time, in seconds, that the relying party + * wants to allow to have elapsed before the user must re-authenticate + */ +class Auth_OpenID_PAPE_Request extends Auth_OpenID_Extension { + + var $ns_alias = 'pape'; + var $ns_uri = Auth_OpenID_PAPE_NS_URI; + + function Auth_OpenID_PAPE_Request($preferred_auth_policies=null, + $max_auth_age=null) + { + if ($preferred_auth_policies === null) { + $preferred_auth_policies = array(); + } + + $this->preferred_auth_policies = $preferred_auth_policies; + $this->max_auth_age = $max_auth_age; + } + + /** + * Add an acceptable authentication policy URI to this request + * + * This method is intended to be used by the relying party to add + * acceptable authentication types to the request. + * + * policy_uri: The identifier for the preferred type of + * authentication. + */ + function addPolicyURI($policy_uri) + { + if (!in_array($policy_uri, $this->preferred_auth_policies)) { + $this->preferred_auth_policies[] = $policy_uri; + } + } + + function getExtensionArgs() + { + $ns_args = array( + 'preferred_auth_policies' => + implode(' ', $this->preferred_auth_policies) + ); + + if ($this->max_auth_age !== null) { + $ns_args['max_auth_age'] = strval($this->max_auth_age); + } + + return $ns_args; + } + + /** + * Instantiate a Request object from the arguments in a checkid_* + * OpenID message + */ + function fromOpenIDRequest($request) + { + $obj = new Auth_OpenID_PAPE_Request(); + $args = $request->message->getArgs(Auth_OpenID_PAPE_NS_URI); + + if ($args === null || $args === array()) { + return null; + } + + $obj->parseExtensionArgs($args); + return $obj; + } + + /** + * Set the state of this request to be that expressed in these + * PAPE arguments + * + * @param args: The PAPE arguments without a namespace + */ + function parseExtensionArgs($args) + { + // preferred_auth_policies is a space-separated list of policy + // URIs + $this->preferred_auth_policies = array(); + + $policies_str = Auth_OpenID::arrayGet($args, 'preferred_auth_policies'); + if ($policies_str) { + foreach (explode(' ', $policies_str) as $uri) { + if (!in_array($uri, $this->preferred_auth_policies)) { + $this->preferred_auth_policies[] = $uri; + } + } + } + + // max_auth_age is base-10 integer number of seconds + $max_auth_age_str = Auth_OpenID::arrayGet($args, 'max_auth_age'); + if ($max_auth_age_str) { + $this->max_auth_age = Auth_OpenID::intval($max_auth_age_str); + } else { + $this->max_auth_age = null; + } + } + + /** + * Given a list of authentication policy URIs that a provider + * supports, this method returns the subsequence of those types + * that are preferred by the relying party. + * + * @param supported_types: A sequence of authentication policy + * type URIs that are supported by a provider + * + * @return array The sub-sequence of the supported types that are + * preferred by the relying party. This list will be ordered in + * the order that the types appear in the supported_types + * sequence, and may be empty if the provider does not prefer any + * of the supported authentication types. + */ + function preferredTypes($supported_types) + { + $result = array(); + + foreach ($supported_types as $st) { + if (in_array($st, $this->preferred_auth_policies)) { + $result[] = $st; + } + } + return $result; + } +} + +/** + * A Provider Authentication Policy response, sent from a provider to + * a relying party + */ +class Auth_OpenID_PAPE_Response extends Auth_OpenID_Extension { + + var $ns_alias = 'pape'; + var $ns_uri = Auth_OpenID_PAPE_NS_URI; + + function Auth_OpenID_PAPE_Response($auth_policies=null, $auth_time=null, + $nist_auth_level=null) + { + if ($auth_policies) { + $this->auth_policies = $auth_policies; + } else { + $this->auth_policies = array(); + } + + $this->auth_time = $auth_time; + $this->nist_auth_level = $nist_auth_level; + } + + /** + * Add a authentication policy to this response + * + * This method is intended to be used by the provider to add a + * policy that the provider conformed to when authenticating the + * user. + * + * @param policy_uri: The identifier for the preferred type of + * authentication. + */ + function addPolicyURI($policy_uri) + { + if (!in_array($policy_uri, $this->auth_policies)) { + $this->auth_policies[] = $policy_uri; + } + } + + /** + * Create an Auth_OpenID_PAPE_Response object from a successful + * OpenID library response. + * + * @param success_response $success_response A SuccessResponse + * from Auth_OpenID_Consumer::complete() + * + * @returns: A provider authentication policy response from the + * data that was supplied with the id_res response. + */ + function fromSuccessResponse($success_response) + { + $obj = new Auth_OpenID_PAPE_Response(); + + // PAPE requires that the args be signed. + $args = $success_response->getSignedNS(Auth_OpenID_PAPE_NS_URI); + + if ($args === null || $args === array()) { + return null; + } + + $result = $obj->parseExtensionArgs($args); + + if ($result === false) { + return null; + } else { + return $obj; + } + } + + /** + * Parse the provider authentication policy arguments into the + * internal state of this object + * + * @param args: unqualified provider authentication policy + * arguments + * + * @param strict: Whether to return false when bad data is + * encountered + * + * @return null The data is parsed into the internal fields of + * this object. + */ + function parseExtensionArgs($args, $strict=false) + { + $policies_str = Auth_OpenID::arrayGet($args, 'auth_policies'); + if ($policies_str && $policies_str != "none") { + $this->auth_policies = explode(" ", $policies_str); + } + + $nist_level_str = Auth_OpenID::arrayGet($args, 'nist_auth_level'); + if ($nist_level_str !== null) { + $nist_level = Auth_OpenID::intval($nist_level_str); + + if ($nist_level === false) { + if ($strict) { + return false; + } else { + $nist_level = null; + } + } + + if (0 <= $nist_level && $nist_level < 5) { + $this->nist_auth_level = $nist_level; + } else if ($strict) { + return false; + } + } + + $auth_time = Auth_OpenID::arrayGet($args, 'auth_time'); + if ($auth_time !== null) { + if (ereg(PAPE_TIME_VALIDATOR, $auth_time)) { + $this->auth_time = $auth_time; + } else if ($strict) { + return false; + } + } + } + + function getExtensionArgs() + { + $ns_args = array(); + if (count($this->auth_policies) > 0) { + $ns_args['auth_policies'] = implode(' ', $this->auth_policies); + } else { + $ns_args['auth_policies'] = 'none'; + } + + if ($this->nist_auth_level !== null) { + if (!in_array($this->nist_auth_level, range(0, 4), true)) { + return false; + } + $ns_args['nist_auth_level'] = strval($this->nist_auth_level); + } + + if ($this->auth_time !== null) { + if (!ereg(PAPE_TIME_VALIDATOR, $this->auth_time)) { + return false; + } + + $ns_args['auth_time'] = $this->auth_time; + } + + return $ns_args; + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/Parse.php b/extlib/Auth/OpenID/Parse.php new file mode 100644 index 000000000..546f34f6b --- /dev/null +++ b/extlib/Auth/OpenID/Parse.php @@ -0,0 +1,352 @@ +<?php + +/** + * This module implements a VERY limited parser that finds <link> tags + * in the head of HTML or XHTML documents and parses out their + * attributes according to the OpenID spec. It is a liberal parser, + * but it requires these things from the data in order to work: + * + * - There must be an open <html> tag + * + * - There must be an open <head> tag inside of the <html> tag + * + * - Only <link>s that are found inside of the <head> tag are parsed + * (this is by design) + * + * - The parser follows the OpenID specification in resolving the + * attributes of the link tags. This means that the attributes DO + * NOT get resolved as they would by an XML or HTML parser. In + * particular, only certain entities get replaced, and href + * attributes do not get resolved relative to a base URL. + * + * From http://openid.net/specs.bml: + * + * - The openid.server URL MUST be an absolute URL. OpenID consumers + * MUST NOT attempt to resolve relative URLs. + * + * - The openid.server URL MUST NOT include entities other than &, + * <, >, and ". + * + * The parser ignores SGML comments and <![CDATA[blocks]]>. Both kinds + * of quoting are allowed for attributes. + * + * The parser deals with invalid markup in these ways: + * + * - Tag names are not case-sensitive + * + * - The <html> tag is accepted even when it is not at the top level + * + * - The <head> tag is accepted even when it is not a direct child of + * the <html> tag, but a <html> tag must be an ancestor of the + * <head> tag + * + * - <link> tags are accepted even when they are not direct children + * of the <head> tag, but a <head> tag must be an ancestor of the + * <link> tag + * + * - If there is no closing tag for an open <html> or <head> tag, the + * remainder of the document is viewed as being inside of the + * tag. If there is no closing tag for a <link> tag, the link tag is + * treated as a short tag. Exceptions to this rule are that <html> + * closes <html> and <body> or <head> closes <head> + * + * - Attributes of the <link> tag are not required to be quoted. + * + * - In the case of duplicated attribute names, the attribute coming + * last in the tag will be the value returned. + * + * - Any text that does not parse as an attribute within a link tag + * will be ignored. (e.g. <link pumpkin rel='openid.server' /> will + * ignore pumpkin) + * + * - If there are more than one <html> or <head> tag, the parser only + * looks inside of the first one. + * + * - The contents of <script> tags are ignored entirely, except + * unclosed <script> tags. Unclosed <script> tags are ignored. + * + * - Any other invalid markup is ignored, including unclosed SGML + * comments and unclosed <![CDATA[blocks. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require Auth_OpenID::arrayGet(). + */ +require_once "Auth/OpenID.php"; + +class Auth_OpenID_Parse { + + /** + * Specify some flags for use with regex matching. + */ + var $_re_flags = "si"; + + /** + * Stuff to remove before we start looking for tags + */ + var $_removed_re = + "<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b(?!:)[^>]*>.*?<\/script>"; + + /** + * Starts with the tag name at a word boundary, where the tag name + * is not a namespace + */ + var $_tag_expr = "<%s\b(?!:)([^>]*?)(?:\/>|>(.*?)(?:<\/?%s\s*>|\Z))"; + + var $_attr_find = '\b(\w+)=("[^"]*"|\'[^\']*\'|[^\'"\s\/<>]+)'; + + var $_open_tag_expr = "<%s\b"; + var $_close_tag_expr = "<((\/%s\b)|(%s[^>\/]*\/))>"; + + function Auth_OpenID_Parse() + { + $this->_link_find = sprintf("/<link\b(?!:)([^>]*)(?!<)>/%s", + $this->_re_flags); + + $this->_entity_replacements = array( + 'amp' => '&', + 'lt' => '<', + 'gt' => '>', + 'quot' => '"' + ); + + $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->_ent_replace = + sprintf("&(%s);", implode("|", + $this->_entity_replacements)); + } + + /** + * Returns a regular expression that will match a given tag in an + * SGML string. + */ + function tagMatcher($tag_name, $close_tags = null) + { + $expr = $this->_tag_expr; + + if ($close_tags) { + $options = implode("|", array_merge(array($tag_name), $close_tags)); + $closer = sprintf("(?:%s)", $options); + } else { + $closer = $tag_name; + } + + $expr = sprintf($expr, $tag_name, $closer); + return sprintf("/%s/%s", $expr, $this->_re_flags); + } + + function openTag($tag_name) + { + $expr = sprintf($this->_open_tag_expr, $tag_name); + return sprintf("/%s/%s", $expr, $this->_re_flags); + } + + function closeTag($tag_name) + { + $expr = sprintf($this->_close_tag_expr, $tag_name, $tag_name); + return sprintf("/%s/%s", $expr, $this->_re_flags); + } + + function htmlBegin($s) + { + $matches = array(); + $result = preg_match($this->openTag('html'), $s, + $matches, PREG_OFFSET_CAPTURE); + if ($result === false || !$matches) { + return false; + } + // Return the offset of the first match. + return $matches[0][1]; + } + + function htmlEnd($s) + { + $matches = array(); + $result = preg_match($this->closeTag('html'), $s, + $matches, PREG_OFFSET_CAPTURE); + if ($result === false || !$matches) { + return false; + } + // Return the offset of the first match. + return $matches[count($matches) - 1][1]; + } + + function headFind() + { + return $this->tagMatcher('head', array('body', 'html')); + } + + function replaceEntities($str) + { + foreach ($this->_entity_replacements as $old => $new) { + $str = preg_replace(sprintf("/&%s;/", $old), $new, $str); + } + return $str; + } + + 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; + } + } + + /** + * Find all link tags in a string representing a HTML document and + * return a list of their attributes. + * + * @param string $html The text to parse + * @return array $list An array of arrays of attributes, one for each + * link tag + */ + function parseLinkAttrs($html) + { + $stripped = preg_replace($this->_removed_re, + "", + $html); + + $html_begin = $this->htmlBegin($stripped); + $html_end = $this->htmlEnd($stripped); + + if ($html_begin === false) { + return array(); + } + + if ($html_end === false) { + $html_end = strlen($stripped); + } + + $stripped = substr($stripped, $html_begin, + $html_end - $html_begin); + + // Try to find the <HEAD> tag. + $head_re = $this->headFind(); + $head_matches = array(); + if (!preg_match($head_re, $stripped, $head_matches)) { + return array(); + } + + $link_data = array(); + $link_matches = array(); + + if (!preg_match_all($this->_link_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; + } + + function relMatches($rel_attr, $target_rel) + { + // Does this target_rel appear in the rel_str? + // XXX: TESTME + $rels = preg_split("/\s+/", trim($rel_attr)); + foreach ($rels as $rel) { + $rel = strtolower($rel); + if ($rel == $target_rel) { + return 1; + } + } + + return 0; + } + + function linkHasRel($link_attrs, $target_rel) + { + // Does this link have target_rel as a relationship? + // XXX: TESTME + $rel_attr = Auth_OpeniD::arrayGet($link_attrs, 'rel', null); + return ($rel_attr && $this->relMatches($rel_attr, + $target_rel)); + } + + function findLinksRel($link_attrs_list, $target_rel) + { + // Filter the list of link attributes on whether it has + // target_rel as a relationship. + // XXX: TESTME + $result = array(); + foreach ($link_attrs_list as $attr) { + if ($this->linkHasRel($attr, $target_rel)) { + $result[] = $attr; + } + } + + return $result; + } + + function findFirstHref($link_attrs_list, $target_rel) + { + // Return the value of the href attribute for the first link + // tag in the list that has target_rel as a relationship. + // XXX: TESTME + $matches = $this->findLinksRel($link_attrs_list, + $target_rel); + if (!$matches) { + return null; + } + $first = $matches[0]; + return Auth_OpenID::arrayGet($first, 'href', null); + } +} + +function Auth_OpenID_legacy_discover($html_text, $server_rel, + $delegate_rel) +{ + $p = new Auth_OpenID_Parse(); + + $link_attrs = $p->parseLinkAttrs($html_text); + + $server_url = $p->findFirstHref($link_attrs, + $server_rel); + + if ($server_url === null) { + return false; + } else { + $delegate_url = $p->findFirstHref($link_attrs, + $delegate_rel); + return array($delegate_url, $server_url); + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/PostgreSQLStore.php b/extlib/Auth/OpenID/PostgreSQLStore.php new file mode 100644 index 000000000..69d95e7b8 --- /dev/null +++ b/extlib/Auth/OpenID/PostgreSQLStore.php @@ -0,0 +1,113 @@ +<?php + +/** + * A PostgreSQL store. + * + * @package OpenID + */ + +/** + * Require the base class file. + */ +require_once "Auth/OpenID/SQLStore.php"; + +/** + * An SQL store that uses PostgreSQL as its backend. + * + * @package OpenID + */ +class Auth_OpenID_PostgreSQLStore extends Auth_OpenID_SQLStore { + /** + * @access private + */ + function setSQL() + { + $this->sql['nonce_table'] = + "CREATE TABLE %s (server_url VARCHAR(2047) NOT NULL, ". + "timestamp INTEGER NOT NULL, ". + "salt CHAR(40) NOT NULL, ". + "UNIQUE (server_url, timestamp, salt))"; + + $this->sql['assoc_table'] = + "CREATE TABLE %s (server_url VARCHAR(2047) NOT NULL, ". + "handle VARCHAR(255) NOT NULL, ". + "secret BYTEA NOT NULL, ". + "issued INTEGER NOT NULL, ". + "lifetime INTEGER NOT NULL, ". + "assoc_type VARCHAR(64) NOT NULL, ". + "PRIMARY KEY (server_url, handle), ". + "CONSTRAINT secret_length_constraint CHECK ". + "(LENGTH(secret) <= 128))"; + + $this->sql['set_assoc'] = + array( + 'insert_assoc' => "INSERT INTO %s (server_url, handle, ". + "secret, issued, lifetime, assoc_type) VALUES ". + "(?, ?, '!', ?, ?, ?)", + 'update_assoc' => "UPDATE %s SET secret = '!', issued = ?, ". + "lifetime = ?, assoc_type = ? WHERE server_url = ? AND ". + "handle = ?" + ); + + $this->sql['get_assocs'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ?"; + + $this->sql['get_assoc'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ? AND handle = ?"; + + $this->sql['remove_assoc'] = + "DELETE FROM %s WHERE server_url = ? AND handle = ?"; + + $this->sql['add_nonce'] = + "INSERT INTO %s (server_url, timestamp, salt) VALUES ". + "(?, ?, ?)" + ; + + $this->sql['clean_nonce'] = + "DELETE FROM %s WHERE timestamp < ?"; + + $this->sql['clean_assoc'] = + "DELETE FROM %s WHERE issued + lifetime < ?"; + } + + /** + * @access private + */ + function _set_assoc($server_url, $handle, $secret, $issued, $lifetime, + $assoc_type) + { + $result = $this->_get_assoc($server_url, $handle); + if ($result) { + // Update the table since this associations already exists. + $this->connection->query($this->sql['set_assoc']['update_assoc'], + array($secret, $issued, $lifetime, + $assoc_type, $server_url, $handle)); + } else { + // Insert a new record because this association wasn't + // found. + $this->connection->query($this->sql['set_assoc']['insert_assoc'], + array($server_url, $handle, $secret, + $issued, $lifetime, $assoc_type)); + } + } + + /** + * @access private + */ + function blobEncode($blob) + { + return $this->_octify($blob); + } + + /** + * @access private + */ + function blobDecode($blob) + { + return $this->_unoctify($blob); + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/SQLStore.php b/extlib/Auth/OpenID/SQLStore.php new file mode 100644 index 000000000..da93c6aa2 --- /dev/null +++ b/extlib/Auth/OpenID/SQLStore.php @@ -0,0 +1,569 @@ +<?php + +/** + * SQL-backed OpenID stores. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require the PEAR DB module because we'll need it for the SQL-based + * stores implemented here. We silence any errors from the inclusion + * because it might not be present, and a user of the SQL stores may + * supply an Auth_OpenID_DatabaseConnection instance that implements + * its own storage. + */ +global $__Auth_OpenID_PEAR_AVAILABLE; +$__Auth_OpenID_PEAR_AVAILABLE = @include_once 'DB.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID/Interface.php'; +require_once 'Auth/OpenID/Nonce.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID/Nonce.php'; + +/** + * This is the parent class for the SQL stores, which contains the + * logic common to all of the SQL stores. + * + * The table names used are determined by the class variables + * associations_table_name and nonces_table_name. To change the name + * of the tables used, pass new table names into the constructor. + * + * To create the tables with the proper schema, see the createTables + * method. + * + * This class shouldn't be used directly. Use one of its subclasses + * instead, as those contain the code necessary to use a specific + * database. If you're an OpenID integrator and you'd like to create + * an SQL-driven store that wraps an application's database + * abstraction, be sure to create a subclass of + * {@link Auth_OpenID_DatabaseConnection} that calls the application's + * database abstraction calls. Then, pass an instance of your new + * database connection class to your SQLStore subclass constructor. + * + * All methods other than the constructor and createTables should be + * considered implementation details. + * + * @package OpenID + */ +class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore { + + /** + * This creates a new SQLStore instance. It requires an + * established database connection be given to it, and it allows + * overriding the default table names. + * + * @param connection $connection This must be an established + * connection to a database of the correct type for the SQLStore + * subclass you're using. This must either be an PEAR DB + * connection handle or an instance of a subclass of + * Auth_OpenID_DatabaseConnection. + * + * @param associations_table: This is an optional parameter to + * specify the name of the table used for storing associations. + * The default value is 'oid_associations'. + * + * @param nonces_table: This is an optional parameter to specify + * the name of the table used for storing nonces. The default + * value is 'oid_nonces'. + */ + function Auth_OpenID_SQLStore($connection, + $associations_table = null, + $nonces_table = null) + { + global $__Auth_OpenID_PEAR_AVAILABLE; + + $this->associations_table_name = "oid_associations"; + $this->nonces_table_name = "oid_nonces"; + + // Check the connection object type to be sure it's a PEAR + // database connection. + if (!(is_object($connection) && + (is_subclass_of($connection, 'db_common') || + is_subclass_of($connection, + 'auth_openid_databaseconnection')))) { + trigger_error("Auth_OpenID_SQLStore expected PEAR connection " . + "object (got ".get_class($connection).")", + E_USER_ERROR); + return; + } + + $this->connection = $connection; + + // Be sure to set the fetch mode so the results are keyed on + // column name instead of column index. This is a PEAR + // constant, so only try to use it if PEAR is present. Note + // that Auth_Openid_Databaseconnection instances need not + // implement ::setFetchMode for this reason. + if ($__Auth_OpenID_PEAR_AVAILABLE) { + $this->connection->setFetchMode(DB_FETCHMODE_ASSOC); + } + + if ($associations_table) { + $this->associations_table_name = $associations_table; + } + + if ($nonces_table) { + $this->nonces_table_name = $nonces_table; + } + + $this->max_nonce_age = 6 * 60 * 60; + + // Be sure to run the database queries with auto-commit mode + // turned OFF, because we want every function to run in a + // transaction, implicitly. As a rule, methods named with a + // leading underscore will NOT control transaction behavior. + // Callers of these methods will worry about transactions. + $this->connection->autoCommit(false); + + // Create an empty SQL strings array. + $this->sql = array(); + + // Call this method (which should be overridden by subclasses) + // to populate the $this->sql array with SQL strings. + $this->setSQL(); + + // Verify that all required SQL statements have been set, and + // raise an error if any expected SQL strings were either + // absent or empty. + list($missing, $empty) = $this->_verifySQL(); + + if ($missing) { + trigger_error("Expected keys in SQL query list: " . + implode(", ", $missing), + E_USER_ERROR); + return; + } + + if ($empty) { + trigger_error("SQL list keys have no SQL strings: " . + implode(", ", $empty), + E_USER_ERROR); + return; + } + + // Add table names to queries. + $this->_fixSQL(); + } + + function tableExists($table_name) + { + return !$this->isError( + $this->connection->query( + sprintf("SELECT * FROM %s LIMIT 0", + $table_name))); + } + + /** + * Returns true if $value constitutes a database error; returns + * false otherwise. + */ + function isError($value) + { + return PEAR::isError($value); + } + + /** + * Converts a query result to a boolean. If the result is a + * database error according to $this->isError(), this returns + * false; otherwise, this returns true. + */ + function resultToBool($obj) + { + if ($this->isError($obj)) { + return false; + } else { + return true; + } + } + + /** + * This method should be overridden by subclasses. This method is + * called by the constructor to set values in $this->sql, which is + * an array keyed on sql name. + */ + function setSQL() + { + } + + /** + * Resets the store by removing all records from the store's + * tables. + */ + function reset() + { + $this->connection->query(sprintf("DELETE FROM %s", + $this->associations_table_name)); + + $this->connection->query(sprintf("DELETE FROM %s", + $this->nonces_table_name)); + } + + /** + * @access private + */ + function _verifySQL() + { + $missing = array(); + $empty = array(); + + $required_sql_keys = array( + 'nonce_table', + 'assoc_table', + 'set_assoc', + 'get_assoc', + 'get_assocs', + 'remove_assoc' + ); + + foreach ($required_sql_keys as $key) { + if (!array_key_exists($key, $this->sql)) { + $missing[] = $key; + } else if (!$this->sql[$key]) { + $empty[] = $key; + } + } + + return array($missing, $empty); + } + + /** + * @access private + */ + function _fixSQL() + { + $replacements = array( + array( + 'value' => $this->nonces_table_name, + 'keys' => array('nonce_table', + 'add_nonce', + 'clean_nonce') + ), + array( + 'value' => $this->associations_table_name, + 'keys' => array('assoc_table', + 'set_assoc', + 'get_assoc', + 'get_assocs', + 'remove_assoc', + 'clean_assoc') + ) + ); + + foreach ($replacements as $item) { + $value = $item['value']; + $keys = $item['keys']; + + foreach ($keys as $k) { + if (is_array($this->sql[$k])) { + foreach ($this->sql[$k] as $part_key => $part_value) { + $this->sql[$k][$part_key] = sprintf($part_value, + $value); + } + } else { + $this->sql[$k] = sprintf($this->sql[$k], $value); + } + } + } + } + + function blobDecode($blob) + { + return $blob; + } + + function blobEncode($str) + { + return $str; + } + + function createTables() + { + $this->connection->autoCommit(true); + $n = $this->create_nonce_table(); + $a = $this->create_assoc_table(); + $this->connection->autoCommit(false); + + if ($n && $a) { + return true; + } else { + return false; + } + } + + function create_nonce_table() + { + if (!$this->tableExists($this->nonces_table_name)) { + $r = $this->connection->query($this->sql['nonce_table']); + return $this->resultToBool($r); + } + return true; + } + + function create_assoc_table() + { + if (!$this->tableExists($this->associations_table_name)) { + $r = $this->connection->query($this->sql['assoc_table']); + return $this->resultToBool($r); + } + return true; + } + + /** + * @access private + */ + function _set_assoc($server_url, $handle, $secret, $issued, + $lifetime, $assoc_type) + { + return $this->connection->query($this->sql['set_assoc'], + array( + $server_url, + $handle, + $secret, + $issued, + $lifetime, + $assoc_type)); + } + + function storeAssociation($server_url, $association) + { + if ($this->resultToBool($this->_set_assoc( + $server_url, + $association->handle, + $this->blobEncode( + $association->secret), + $association->issued, + $association->lifetime, + $association->assoc_type + ))) { + $this->connection->commit(); + } else { + $this->connection->rollback(); + } + } + + /** + * @access private + */ + function _get_assoc($server_url, $handle) + { + $result = $this->connection->getRow($this->sql['get_assoc'], + array($server_url, $handle)); + if ($this->isError($result)) { + return null; + } else { + return $result; + } + } + + /** + * @access private + */ + function _get_assocs($server_url) + { + $result = $this->connection->getAll($this->sql['get_assocs'], + array($server_url)); + + if ($this->isError($result)) { + return array(); + } else { + return $result; + } + } + + function removeAssociation($server_url, $handle) + { + if ($this->_get_assoc($server_url, $handle) == null) { + return false; + } + + if ($this->resultToBool($this->connection->query( + $this->sql['remove_assoc'], + array($server_url, $handle)))) { + $this->connection->commit(); + } else { + $this->connection->rollback(); + } + + return true; + } + + function getAssociation($server_url, $handle = null) + { + if ($handle !== null) { + $assoc = $this->_get_assoc($server_url, $handle); + + $assocs = array(); + if ($assoc) { + $assocs[] = $assoc; + } + } else { + $assocs = $this->_get_assocs($server_url); + } + + if (!$assocs || (count($assocs) == 0)) { + return null; + } else { + $associations = array(); + + foreach ($assocs as $assoc_row) { + $assoc = new Auth_OpenID_Association($assoc_row['handle'], + $assoc_row['secret'], + $assoc_row['issued'], + $assoc_row['lifetime'], + $assoc_row['assoc_type']); + + $assoc->secret = $this->blobDecode($assoc->secret); + + if ($assoc->getExpiresIn() == 0) { + $this->removeAssociation($server_url, $assoc->handle); + } else { + $associations[] = array($assoc->issued, $assoc); + } + } + + if ($associations) { + $issued = array(); + $assocs = array(); + foreach ($associations as $key => $assoc) { + $issued[$key] = $assoc[0]; + $assocs[$key] = $assoc[1]; + } + + array_multisort($issued, SORT_DESC, $assocs, SORT_DESC, + $associations); + + // return the most recently issued one. + list($issued, $assoc) = $associations[0]; + return $assoc; + } else { + return null; + } + } + } + + /** + * @access private + */ + function _add_nonce($server_url, $timestamp, $salt) + { + $sql = $this->sql['add_nonce']; + $result = $this->connection->query($sql, array($server_url, + $timestamp, + $salt)); + if ($this->isError($result)) { + $this->connection->rollback(); + } else { + $this->connection->commit(); + } + return $this->resultToBool($result); + } + + function useNonce($server_url, $timestamp, $salt) + { + global $Auth_OpenID_SKEW; + + if ( abs($timestamp - time()) > $Auth_OpenID_SKEW ) { + return False; + } + + return $this->_add_nonce($server_url, $timestamp, $salt); + } + + /** + * "Octifies" a binary string by returning a string with escaped + * octal bytes. This is used for preparing binary data for + * PostgreSQL BYTEA fields. + * + * @access private + */ + function _octify($str) + { + $result = ""; + for ($i = 0; $i < Auth_OpenID::bytes($str); $i++) { + $ch = substr($str, $i, 1); + if ($ch == "\\") { + $result .= "\\\\\\\\"; + } else if (ord($ch) == 0) { + $result .= "\\\\000"; + } else { + $result .= "\\" . strval(decoct(ord($ch))); + } + } + return $result; + } + + /** + * "Unoctifies" octal-escaped data from PostgreSQL and returns the + * resulting ASCII (possibly binary) string. + * + * @access private + */ + function _unoctify($str) + { + $result = ""; + $i = 0; + while ($i < strlen($str)) { + $char = $str[$i]; + if ($char == "\\") { + // Look to see if the next char is a backslash and + // append it. + if ($str[$i + 1] != "\\") { + $octal_digits = substr($str, $i + 1, 3); + $dec = octdec($octal_digits); + $char = chr($dec); + $i += 4; + } else { + $char = "\\"; + $i += 2; + } + } else { + $i += 1; + } + + $result .= $char; + } + + return $result; + } + + function cleanupNonces() + { + global $Auth_OpenID_SKEW; + $v = time() - $Auth_OpenID_SKEW; + + $this->connection->query($this->sql['clean_nonce'], array($v)); + $num = $this->connection->affectedRows(); + $this->connection->commit(); + return $num; + } + + function cleanupAssociations() + { + $this->connection->query($this->sql['clean_assoc'], + array(time())); + $num = $this->connection->affectedRows(); + $this->connection->commit(); + return $num; + } +} + +?> diff --git a/extlib/Auth/OpenID/SQLiteStore.php b/extlib/Auth/OpenID/SQLiteStore.php new file mode 100644 index 000000000..ec2bf58e4 --- /dev/null +++ b/extlib/Auth/OpenID/SQLiteStore.php @@ -0,0 +1,71 @@ +<?php + +/** + * An SQLite store. + * + * @package OpenID + */ + +/** + * Require the base class file. + */ +require_once "Auth/OpenID/SQLStore.php"; + +/** + * An SQL store that uses SQLite as its backend. + * + * @package OpenID + */ +class Auth_OpenID_SQLiteStore extends Auth_OpenID_SQLStore { + function setSQL() + { + $this->sql['nonce_table'] = + "CREATE TABLE %s (server_url VARCHAR(2047), timestamp INTEGER, ". + "salt CHAR(40), UNIQUE (server_url, timestamp, salt))"; + + $this->sql['assoc_table'] = + "CREATE TABLE %s (server_url VARCHAR(2047), handle VARCHAR(255), ". + "secret BLOB(128), issued INTEGER, lifetime INTEGER, ". + "assoc_type VARCHAR(64), PRIMARY KEY (server_url, handle))"; + + $this->sql['set_assoc'] = + "INSERT OR REPLACE INTO %s VALUES (?, ?, ?, ?, ?, ?)"; + + $this->sql['get_assocs'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ?"; + + $this->sql['get_assoc'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ? AND handle = ?"; + + $this->sql['remove_assoc'] = + "DELETE FROM %s WHERE server_url = ? AND handle = ?"; + + $this->sql['add_nonce'] = + "INSERT INTO %s (server_url, timestamp, salt) VALUES (?, ?, ?)"; + + $this->sql['clean_nonce'] = + "DELETE FROM %s WHERE timestamp < ?"; + + $this->sql['clean_assoc'] = + "DELETE FROM %s WHERE issued + lifetime < ?"; + } + + /** + * @access private + */ + function _add_nonce($server_url, $timestamp, $salt) + { + // PECL SQLite extensions 1.0.3 and older (1.0.3 is the + // current release at the time of this writing) have a broken + // sqlite_escape_string function that breaks when passed the + // empty string. Prefixing all strings with one character + // keeps them unique and avoids this bug. The nonce table is + // write-only, so we don't have to worry about updating other + // functions with this same bad hack. + return parent::_add_nonce('x' . $server_url, $timestamp, $salt); + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/SReg.php b/extlib/Auth/OpenID/SReg.php new file mode 100644 index 000000000..63280769f --- /dev/null +++ b/extlib/Auth/OpenID/SReg.php @@ -0,0 +1,521 @@ +<?php + +/** + * Simple registration request and response parsing and object + * representation. + * + * This module contains objects representing simple registration + * requests and responses that can be used with both OpenID relying + * parties and OpenID providers. + * + * 1. The relying party creates a request object and adds it to the + * {@link Auth_OpenID_AuthRequest} object before making the + * checkid request to the OpenID provider: + * + * $sreg_req = Auth_OpenID_SRegRequest::build(array('email')); + * $auth_request->addExtension($sreg_req); + * + * 2. The OpenID provider extracts the simple registration request + * from the OpenID request using {@link + * Auth_OpenID_SRegRequest::fromOpenIDRequest}, gets the user's + * approval and data, creates an {@link Auth_OpenID_SRegResponse} + * object and adds it to the id_res response: + * + * $sreg_req = Auth_OpenID_SRegRequest::fromOpenIDRequest( + * $checkid_request); + * // [ get the user's approval and data, informing the user that + * // the fields in sreg_response were requested ] + * $sreg_resp = Auth_OpenID_SRegResponse::extractResponse( + * $sreg_req, $user_data); + * $sreg_resp->toMessage($openid_response->fields); + * + * 3. The relying party uses {@link + * Auth_OpenID_SRegResponse::fromSuccessResponse} to extract the data + * from the OpenID response: + * + * $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse( + * $success_response); + * + * @package OpenID + */ + +/** + * Import message and extension internals. + */ +require_once 'Auth/OpenID/Message.php'; +require_once 'Auth/OpenID/Extension.php'; + +// The data fields that are listed in the sreg spec +global $Auth_OpenID_sreg_data_fields; +$Auth_OpenID_sreg_data_fields = array( + 'fullname' => 'Full Name', + 'nickname' => 'Nickname', + 'dob' => 'Date of Birth', + 'email' => 'E-mail Address', + 'gender' => 'Gender', + 'postcode' => 'Postal Code', + 'country' => 'Country', + 'language' => 'Language', + 'timezone' => 'Time Zone'); + +/** + * Check to see that the given value is a valid simple registration + * data field name. Return true if so, false if not. + */ +function Auth_OpenID_checkFieldName($field_name) +{ + global $Auth_OpenID_sreg_data_fields; + + if (!in_array($field_name, array_keys($Auth_OpenID_sreg_data_fields))) { + return false; + } + return true; +} + +// URI used in the wild for Yadis documents advertising simple +// registration support +define('Auth_OpenID_SREG_NS_URI_1_0', 'http://openid.net/sreg/1.0'); + +// URI in the draft specification for simple registration 1.1 +// <http://openid.net/specs/openid-simple-registration-extension-1_1-01.html> +define('Auth_OpenID_SREG_NS_URI_1_1', 'http://openid.net/extensions/sreg/1.1'); + +// This attribute will always hold the preferred URI to use when +// adding sreg support to an XRDS file or in an OpenID namespace +// declaration. +define('Auth_OpenID_SREG_NS_URI', Auth_OpenID_SREG_NS_URI_1_1); + +Auth_OpenID_registerNamespaceAlias(Auth_OpenID_SREG_NS_URI_1_1, 'sreg'); + +/** + * Does the given endpoint advertise support for simple + * registration? + * + * $endpoint: The endpoint object as returned by OpenID discovery. + * returns whether an sreg type was advertised by the endpoint + */ +function Auth_OpenID_supportsSReg(&$endpoint) +{ + return ($endpoint->usesExtension(Auth_OpenID_SREG_NS_URI_1_1) || + $endpoint->usesExtension(Auth_OpenID_SREG_NS_URI_1_0)); +} + +/** + * A base class for classes dealing with Simple Registration protocol + * messages. + * + * @package OpenID + */ +class Auth_OpenID_SRegBase extends Auth_OpenID_Extension { + /** + * Extract the simple registration namespace URI from the given + * OpenID message. Handles OpenID 1 and 2, as well as both sreg + * namespace URIs found in the wild, as well as missing namespace + * definitions (for OpenID 1) + * + * $message: The OpenID message from which to parse simple + * registration fields. This may be a request or response message. + * + * Returns the sreg namespace URI for the supplied message. The + * message may be modified to define a simple registration + * namespace. + * + * @access private + */ + function _getSRegNS(&$message) + { + $alias = null; + $found_ns_uri = null; + + // See if there exists an alias for one of the two defined + // simple registration types. + foreach (array(Auth_OpenID_SREG_NS_URI_1_1, + Auth_OpenID_SREG_NS_URI_1_0) as $sreg_ns_uri) { + $alias = $message->namespaces->getAlias($sreg_ns_uri); + if ($alias !== null) { + $found_ns_uri = $sreg_ns_uri; + break; + } + } + + if ($alias === null) { + // There is no alias for either of the types, so try to + // add one. We default to using the modern value (1.1) + $found_ns_uri = Auth_OpenID_SREG_NS_URI_1_1; + if ($message->namespaces->addAlias(Auth_OpenID_SREG_NS_URI_1_1, + 'sreg') === null) { + // An alias for the string 'sreg' already exists, but + // it's defined for something other than simple + // registration + return null; + } + } + + return $found_ns_uri; + } +} + +/** + * An object to hold the state of a simple registration request. + * + * required: A list of the required fields in this simple registration + * request + * + * optional: A list of the optional fields in this simple registration + * request + * + * @package OpenID + */ +class Auth_OpenID_SRegRequest extends Auth_OpenID_SRegBase { + + var $ns_alias = 'sreg'; + + /** + * Initialize an empty simple registration request. + */ + function build($required=null, $optional=null, + $policy_url=null, + $sreg_ns_uri=Auth_OpenID_SREG_NS_URI, + $cls='Auth_OpenID_SRegRequest') + { + $obj = new $cls(); + + $obj->required = array(); + $obj->optional = array(); + $obj->policy_url = $policy_url; + $obj->ns_uri = $sreg_ns_uri; + + if ($required) { + if (!$obj->requestFields($required, true, true)) { + return null; + } + } + + if ($optional) { + if (!$obj->requestFields($optional, false, true)) { + return null; + } + } + + return $obj; + } + + /** + * Create a simple registration request that contains the fields + * that were requested in the OpenID request with the given + * arguments + * + * $request: The OpenID authentication request from which to + * extract an sreg request. + * + * $cls: name of class to use when creating sreg request object. + * Used for testing. + * + * Returns the newly created simple registration request + */ + function fromOpenIDRequest($request, $cls='Auth_OpenID_SRegRequest') + { + + $obj = call_user_func_array(array($cls, 'build'), + array(null, null, null, Auth_OpenID_SREG_NS_URI, $cls)); + + // Since we're going to mess with namespace URI mapping, don't + // mutate the object that was passed in. + $m = $request->message; + + $obj->ns_uri = $obj->_getSRegNS($m); + $args = $m->getArgs($obj->ns_uri); + + if ($args === null || Auth_OpenID::isFailure($args)) { + return null; + } + + $obj->parseExtensionArgs($args); + + return $obj; + } + + /** + * Parse the unqualified simple registration request parameters + * and add them to this object. + * + * This method is essentially the inverse of + * getExtensionArgs. This method restores the serialized simple + * registration request fields. + * + * If you are extracting arguments from a standard OpenID + * checkid_* request, you probably want to use fromOpenIDRequest, + * which will extract the sreg namespace and arguments from the + * OpenID request. This method is intended for cases where the + * OpenID server needs more control over how the arguments are + * parsed than that method provides. + * + * $args == $message->getArgs($ns_uri); + * $request->parseExtensionArgs($args); + * + * $args: The unqualified simple registration arguments + * + * strict: Whether requests with fields that are not defined in + * the simple registration specification should be tolerated (and + * ignored) + */ + function parseExtensionArgs($args, $strict=false) + { + foreach (array('required', 'optional') as $list_name) { + $required = ($list_name == 'required'); + $items = Auth_OpenID::arrayGet($args, $list_name); + if ($items) { + foreach (explode(',', $items) as $field_name) { + if (!$this->requestField($field_name, $required, $strict)) { + if ($strict) { + return false; + } + } + } + } + } + + $this->policy_url = Auth_OpenID::arrayGet($args, 'policy_url'); + + return true; + } + + /** + * A list of all of the simple registration fields that were + * requested, whether they were required or optional. + */ + function allRequestedFields() + { + return array_merge($this->required, $this->optional); + } + + /** + * Have any simple registration fields been requested? + */ + function wereFieldsRequested() + { + return count($this->allRequestedFields()); + } + + /** + * Was this field in the request? + */ + function contains($field_name) + { + return (in_array($field_name, $this->required) || + in_array($field_name, $this->optional)); + } + + /** + * Request the specified field from the OpenID user + * + * $field_name: the unqualified simple registration field name + * + * required: whether the given field should be presented to the + * user as being a required to successfully complete the request + * + * strict: whether to raise an exception when a field is added to + * a request more than once + */ + function requestField($field_name, + $required=false, $strict=false) + { + if (!Auth_OpenID_checkFieldName($field_name)) { + return false; + } + + if ($strict) { + if ($this->contains($field_name)) { + return false; + } + } else { + if (in_array($field_name, $this->required)) { + return true; + } + + if (in_array($field_name, $this->optional)) { + if ($required) { + unset($this->optional[array_search($field_name, + $this->optional)]); + } else { + return true; + } + } + } + + if ($required) { + $this->required[] = $field_name; + } else { + $this->optional[] = $field_name; + } + + return true; + } + + /** + * Add the given list of fields to the request + * + * field_names: The simple registration data fields to request + * + * required: Whether these values should be presented to the user + * as required + * + * strict: whether to raise an exception when a field is added to + * a request more than once + */ + function requestFields($field_names, $required=false, $strict=false) + { + if (!is_array($field_names)) { + return false; + } + + foreach ($field_names as $field_name) { + if (!$this->requestField($field_name, $required, $strict=$strict)) { + return false; + } + } + + return true; + } + + /** + * Get a dictionary of unqualified simple registration arguments + * representing this request. + * + * This method is essentially the inverse of + * C{L{parseExtensionArgs}}. This method serializes the simple + * registration request fields. + */ + function getExtensionArgs() + { + $args = array(); + + if ($this->required) { + $args['required'] = implode(',', $this->required); + } + + if ($this->optional) { + $args['optional'] = implode(',', $this->optional); + } + + if ($this->policy_url) { + $args['policy_url'] = $this->policy_url; + } + + return $args; + } +} + +/** + * Represents the data returned in a simple registration response + * inside of an OpenID C{id_res} response. This object will be created + * by the OpenID server, added to the C{id_res} response object, and + * then extracted from the C{id_res} message by the Consumer. + * + * @package OpenID + */ +class Auth_OpenID_SRegResponse extends Auth_OpenID_SRegBase { + + var $ns_alias = 'sreg'; + + function Auth_OpenID_SRegResponse($data=null, + $sreg_ns_uri=Auth_OpenID_SREG_NS_URI) + { + if ($data === null) { + $this->data = array(); + } else { + $this->data = $data; + } + + $this->ns_uri = $sreg_ns_uri; + } + + /** + * Take a C{L{SRegRequest}} and a dictionary of simple + * registration values and create a C{L{SRegResponse}} object + * containing that data. + * + * request: The simple registration request object + * + * data: The simple registration data for this response, as a + * dictionary from unqualified simple registration field name to + * string (unicode) value. For instance, the nickname should be + * stored under the key 'nickname'. + */ + function extractResponse($request, $data) + { + $obj = new Auth_OpenID_SRegResponse(); + $obj->ns_uri = $request->ns_uri; + + foreach ($request->allRequestedFields() as $field) { + $value = Auth_OpenID::arrayGet($data, $field); + if ($value !== null) { + $obj->data[$field] = $value; + } + } + + return $obj; + } + + /** + * Create a C{L{SRegResponse}} object from a successful OpenID + * library response + * (C{L{openid.consumer.consumer.SuccessResponse}}) response + * message + * + * success_response: A SuccessResponse from consumer.complete() + * + * signed_only: Whether to process only data that was + * signed in the id_res message from the server. + * + * Returns a simple registration response containing the data that + * was supplied with the C{id_res} response. + */ + function fromSuccessResponse(&$success_response, $signed_only=true) + { + global $Auth_OpenID_sreg_data_fields; + + $obj = new Auth_OpenID_SRegResponse(); + $obj->ns_uri = $obj->_getSRegNS($success_response->message); + + if ($signed_only) { + $args = $success_response->getSignedNS($obj->ns_uri); + } else { + $args = $success_response->message->getArgs($obj->ns_uri); + } + + if ($args === null || Auth_OpenID::isFailure($args)) { + return null; + } + + foreach ($Auth_OpenID_sreg_data_fields as $field_name => $desc) { + if (in_array($field_name, array_keys($args))) { + $obj->data[$field_name] = $args[$field_name]; + } + } + + return $obj; + } + + function getExtensionArgs() + { + return $this->data; + } + + // Read-only dictionary interface + function get($field_name, $default=null) + { + if (!Auth_OpenID_checkFieldName($field_name)) { + return null; + } + + return Auth_OpenID::arrayGet($this->data, $field_name, $default); + } + + function contents() + { + return $this->data; + } +} + +?> diff --git a/extlib/Auth/OpenID/Server.php b/extlib/Auth/OpenID/Server.php new file mode 100644 index 000000000..e746bcc57 --- /dev/null +++ b/extlib/Auth/OpenID/Server.php @@ -0,0 +1,1754 @@ +<?php + +/** + * OpenID server protocol and logic. + * + * Overview + * + * An OpenID server must perform three tasks: + * + * 1. Examine the incoming request to determine its nature and validity. + * 2. Make a decision about how to respond to this request. + * 3. Format the response according to the protocol. + * + * The first and last of these tasks may performed by the {@link + * Auth_OpenID_Server::decodeRequest()} and {@link + * Auth_OpenID_Server::encodeResponse} methods. Who gets to do the + * intermediate task -- deciding how to respond to the request -- will + * depend on what type of request it is. + * + * If it's a request to authenticate a user (a 'checkid_setup' or + * 'checkid_immediate' request), you need to decide if you will assert + * that this user may claim the identity in question. Exactly how you + * do that is a matter of application policy, but it generally + * involves making sure the user has an account with your system and + * is logged in, checking to see if that identity is hers to claim, + * and verifying with the user that she does consent to releasing that + * information to the party making the request. + * + * Examine the properties of the {@link Auth_OpenID_CheckIDRequest} + * object, and if and when you've come to a decision, form a response + * by calling {@link Auth_OpenID_CheckIDRequest::answer()}. + * + * Other types of requests relate to establishing associations between + * client and server and verifing the authenticity of previous + * communications. {@link Auth_OpenID_Server} contains all the logic + * and data necessary to respond to such requests; just pass it to + * {@link Auth_OpenID_Server::handleRequest()}. + * + * OpenID Extensions + * + * Do you want to provide other information for your users in addition + * to authentication? Version 1.2 of the OpenID protocol allows + * consumers to add extensions to their requests. For example, with + * sites using the Simple Registration + * Extension + * (http://www.openidenabled.com/openid/simple-registration-extension/), + * a user can agree to have their nickname and e-mail address sent to + * a site when they sign up. + * + * Since extensions do not change the way OpenID authentication works, + * code to handle extension requests may be completely separate from + * the {@link Auth_OpenID_Request} class here. But you'll likely want + * data sent back by your extension to be signed. {@link + * Auth_OpenID_ServerResponse} provides methods with which you can add + * data to it which can be signed with the other data in the OpenID + * signature. + * + * For example: + * + * <pre> // when request is a checkid_* request + * $response = $request->answer(true); + * // this will a signed 'openid.sreg.timezone' parameter to the response + * response.addField('sreg', 'timezone', 'America/Los_Angeles')</pre> + * + * Stores + * + * The OpenID server needs to maintain state between requests in order + * to function. Its mechanism for doing this is called a store. The + * store interface is defined in Interface.php. Additionally, several + * concrete store implementations are provided, so that most sites + * won't need to implement a custom store. For a store backed by flat + * files on disk, see {@link Auth_OpenID_FileStore}. For stores based + * on MySQL, SQLite, or PostgreSQL, see the {@link + * Auth_OpenID_SQLStore} subclasses. + * + * Upgrading + * + * The keys by which a server looks up associations in its store have + * changed in version 1.2 of this library. If your store has entries + * created from version 1.0 code, you should empty it. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Required imports + */ +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/Association.php"; +require_once "Auth/OpenID/CryptUtil.php"; +require_once "Auth/OpenID/BigMath.php"; +require_once "Auth/OpenID/DiffieHellman.php"; +require_once "Auth/OpenID/KVForm.php"; +require_once "Auth/OpenID/TrustRoot.php"; +require_once "Auth/OpenID/ServerRequest.php"; +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/Nonce.php"; + +define('AUTH_OPENID_HTTP_OK', 200); +define('AUTH_OPENID_HTTP_REDIRECT', 302); +define('AUTH_OPENID_HTTP_ERROR', 400); + +/** + * @access private + */ +global $_Auth_OpenID_Request_Modes; +$_Auth_OpenID_Request_Modes = array('checkid_setup', + 'checkid_immediate'); + +/** + * @access private + */ +define('Auth_OpenID_ENCODE_KVFORM', 'kfvorm'); + +/** + * @access private + */ +define('Auth_OpenID_ENCODE_URL', 'URL/redirect'); + +/** + * @access private + */ +define('Auth_OpenID_ENCODE_HTML_FORM', 'HTML form'); + +/** + * @access private + */ +function Auth_OpenID_isError($obj, $cls = 'Auth_OpenID_ServerError') +{ + return is_a($obj, $cls); +} + +/** + * An error class which gets instantiated and returned whenever an + * OpenID protocol error occurs. Be prepared to use this in place of + * an ordinary server response. + * + * @package OpenID + */ +class Auth_OpenID_ServerError { + /** + * @access private + */ + function Auth_OpenID_ServerError($message = null, $text = null, + $reference = null, $contact = null) + { + $this->message = $message; + $this->text = $text; + $this->contact = $contact; + $this->reference = $reference; + } + + function getReturnTo() + { + if ($this->message && + $this->message->hasKey(Auth_OpenID_OPENID_NS, 'return_to')) { + return $this->message->getArg(Auth_OpenID_OPENID_NS, + 'return_to'); + } else { + return null; + } + } + + /** + * Returns the return_to URL for the request which caused this + * error. + */ + function hasReturnTo() + { + return $this->getReturnTo() !== null; + } + + /** + * Encodes this error's response as a URL suitable for + * redirection. If the response has no return_to, another + * Auth_OpenID_ServerError is returned. + */ + function encodeToURL() + { + if (!$this->message) { + return null; + } + + $msg = $this->toMessage(); + return $msg->toURL($this->getReturnTo()); + } + + /** + * Encodes the response to key-value form. This is a + * machine-readable format used to respond to messages which came + * directly from the consumer and not through the user-agent. See + * the OpenID specification. + */ + function encodeToKVForm() + { + return Auth_OpenID_KVForm::fromArray( + array('mode' => 'error', + 'error' => $this->toString())); + } + + function toFormMarkup($form_tag_attrs=null) + { + $msg = $this->toMessage(); + return $msg->toFormMarkup($this->getReturnTo(), $form_tag_attrs); + } + + function toHTML($form_tag_attrs=null) + { + return Auth_OpenID::autoSubmitHTML( + $this->toFormMarkup($form_tag_attrs)); + } + + function toMessage() + { + // Generate a Message object for sending to the relying party, + // after encoding. + $namespace = $this->message->getOpenIDNamespace(); + $reply = new Auth_OpenID_Message($namespace); + $reply->setArg(Auth_OpenID_OPENID_NS, 'mode', 'error'); + $reply->setArg(Auth_OpenID_OPENID_NS, 'error', $this->toString()); + + if ($this->contact !== null) { + $reply->setArg(Auth_OpenID_OPENID_NS, 'contact', $this->contact); + } + + if ($this->reference !== null) { + $reply->setArg(Auth_OpenID_OPENID_NS, 'reference', + $this->reference); + } + + return $reply; + } + + /** + * Returns one of Auth_OpenID_ENCODE_URL, + * Auth_OpenID_ENCODE_KVFORM, or null, depending on the type of + * encoding expected for this error's payload. + */ + function whichEncoding() + { + global $_Auth_OpenID_Request_Modes; + + if ($this->hasReturnTo()) { + if ($this->message->isOpenID2() && + (strlen($this->encodeToURL()) > + Auth_OpenID_OPENID1_URL_LIMIT)) { + return Auth_OpenID_ENCODE_HTML_FORM; + } else { + return Auth_OpenID_ENCODE_URL; + } + } + + if (!$this->message) { + return null; + } + + $mode = $this->message->getArg(Auth_OpenID_OPENID_NS, + 'mode'); + + if ($mode) { + if (!in_array($mode, $_Auth_OpenID_Request_Modes)) { + return Auth_OpenID_ENCODE_KVFORM; + } + } + return null; + } + + /** + * Returns this error message. + */ + function toString() + { + if ($this->text) { + return $this->text; + } else { + return get_class($this) . " error"; + } + } +} + +/** + * Error returned by the server code when a return_to is absent from a + * request. + * + * @package OpenID + */ +class Auth_OpenID_NoReturnToError extends Auth_OpenID_ServerError { + function Auth_OpenID_NoReturnToError($message = null, + $text = "No return_to URL available") + { + parent::Auth_OpenID_ServerError($message, $text); + } + + function toString() + { + return "No return_to available"; + } +} + +/** + * An error indicating that the return_to URL is malformed. + * + * @package OpenID + */ +class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError { + function Auth_OpenID_MalformedReturnURL($message, $return_to) + { + $this->return_to = $return_to; + parent::Auth_OpenID_ServerError($message, "malformed return_to URL"); + } +} + +/** + * This error is returned when the trust_root value is malformed. + * + * @package OpenID + */ +class Auth_OpenID_MalformedTrustRoot extends Auth_OpenID_ServerError { + function Auth_OpenID_MalformedTrustRoot($message = null, + $text = "Malformed trust root") + { + parent::Auth_OpenID_ServerError($message, $text); + } + + function toString() + { + return "Malformed trust root"; + } +} + +/** + * The base class for all server request classes. + * + * @package OpenID + */ +class Auth_OpenID_Request { + var $mode = null; +} + +/** + * A request to verify the validity of a previous response. + * + * @package OpenID + */ +class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request { + var $mode = "check_authentication"; + var $invalidate_handle = null; + + function Auth_OpenID_CheckAuthRequest($assoc_handle, $signed, + $invalidate_handle = null) + { + $this->assoc_handle = $assoc_handle; + $this->signed = $signed; + if ($invalidate_handle !== null) { + $this->invalidate_handle = $invalidate_handle; + } + $this->namespace = Auth_OpenID_OPENID2_NS; + $this->message = null; + } + + function fromMessage($message, $server=null) + { + $required_keys = array('assoc_handle', 'sig', 'signed'); + + foreach ($required_keys as $k) { + if (!$message->getArg(Auth_OpenID_OPENID_NS, $k)) { + return new Auth_OpenID_ServerError($message, + sprintf("%s request missing required parameter %s from \ + query", "check_authentication", $k)); + } + } + + $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, 'assoc_handle'); + $sig = $message->getArg(Auth_OpenID_OPENID_NS, 'sig'); + + $signed_list = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); + $signed_list = explode(",", $signed_list); + + $signed = $message; + if ($signed->hasKey(Auth_OpenID_OPENID_NS, 'mode')) { + $signed->setArg(Auth_OpenID_OPENID_NS, 'mode', 'id_res'); + } + + $result = new Auth_OpenID_CheckAuthRequest($assoc_handle, $signed); + $result->message = $message; + $result->sig = $sig; + $result->invalidate_handle = $message->getArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle'); + return $result; + } + + function answer(&$signatory) + { + $is_valid = $signatory->verify($this->assoc_handle, $this->signed); + + // Now invalidate that assoc_handle so it this checkAuth + // message cannot be replayed. + $signatory->invalidate($this->assoc_handle, true); + $response = new Auth_OpenID_ServerResponse($this); + + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'is_valid', + ($is_valid ? "true" : "false")); + + if ($this->invalidate_handle) { + $assoc = $signatory->getAssociation($this->invalidate_handle, + false); + if (!$assoc) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle', + $this->invalidate_handle); + } + } + return $response; + } +} + +/** + * A class implementing plaintext server sessions. + * + * @package OpenID + */ +class Auth_OpenID_PlainTextServerSession { + /** + * An object that knows how to handle association requests with no + * session type. + */ + var $session_type = 'no-encryption'; + var $needs_math = false; + var $allowed_assoc_types = array('HMAC-SHA1', 'HMAC-SHA256'); + + function fromMessage($unused_request) + { + return new Auth_OpenID_PlainTextServerSession(); + } + + function answer($secret) + { + return array('mac_key' => base64_encode($secret)); + } +} + +/** + * A class implementing DH-SHA1 server sessions. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellmanSHA1ServerSession { + /** + * An object that knows how to handle association requests with + * the Diffie-Hellman session type. + */ + + var $session_type = 'DH-SHA1'; + var $needs_math = true; + var $allowed_assoc_types = array('HMAC-SHA1'); + var $hash_func = 'Auth_OpenID_SHA1'; + + function Auth_OpenID_DiffieHellmanSHA1ServerSession($dh, $consumer_pubkey) + { + $this->dh = $dh; + $this->consumer_pubkey = $consumer_pubkey; + } + + function getDH($message) + { + $dh_modulus = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_modulus'); + $dh_gen = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_gen'); + + if ((($dh_modulus === null) && ($dh_gen !== null)) || + (($dh_gen === null) && ($dh_modulus !== null))) { + + if ($dh_modulus === null) { + $missing = 'modulus'; + } else { + $missing = 'generator'; + } + + return new Auth_OpenID_ServerError($message, + 'If non-default modulus or generator is '. + 'supplied, both must be supplied. Missing '. + $missing); + } + + $lib =& Auth_OpenID_getMathLib(); + + if ($dh_modulus || $dh_gen) { + $dh_modulus = $lib->base64ToLong($dh_modulus); + $dh_gen = $lib->base64ToLong($dh_gen); + if ($lib->cmp($dh_modulus, 0) == 0 || + $lib->cmp($dh_gen, 0) == 0) { + return new Auth_OpenID_ServerError( + $message, "Failed to parse dh_mod or dh_gen"); + } + $dh = new Auth_OpenID_DiffieHellman($dh_modulus, $dh_gen); + } else { + $dh = new Auth_OpenID_DiffieHellman(); + } + + $consumer_pubkey = $message->getArg(Auth_OpenID_OPENID_NS, + 'dh_consumer_public'); + if ($consumer_pubkey === null) { + return new Auth_OpenID_ServerError($message, + 'Public key for DH-SHA1 session '. + 'not found in query'); + } + + $consumer_pubkey = + $lib->base64ToLong($consumer_pubkey); + + if ($consumer_pubkey === false) { + return new Auth_OpenID_ServerError($message, + "dh_consumer_public is not base64"); + } + + return array($dh, $consumer_pubkey); + } + + function fromMessage($message) + { + $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message); + + if (is_a($result, 'Auth_OpenID_ServerError')) { + return $result; + } else { + list($dh, $consumer_pubkey) = $result; + return new Auth_OpenID_DiffieHellmanSHA1ServerSession($dh, + $consumer_pubkey); + } + } + + function answer($secret) + { + $lib =& Auth_OpenID_getMathLib(); + $mac_key = $this->dh->xorSecret($this->consumer_pubkey, $secret, + $this->hash_func); + return array( + 'dh_server_public' => + $lib->longToBase64($this->dh->public), + 'enc_mac_key' => base64_encode($mac_key)); + } +} + +/** + * A class implementing DH-SHA256 server sessions. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellmanSHA256ServerSession + extends Auth_OpenID_DiffieHellmanSHA1ServerSession { + + var $session_type = 'DH-SHA256'; + var $hash_func = 'Auth_OpenID_SHA256'; + var $allowed_assoc_types = array('HMAC-SHA256'); + + function fromMessage($message) + { + $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message); + + if (is_a($result, 'Auth_OpenID_ServerError')) { + return $result; + } else { + list($dh, $consumer_pubkey) = $result; + return new Auth_OpenID_DiffieHellmanSHA256ServerSession($dh, + $consumer_pubkey); + } + } +} + +/** + * A request to associate with the server. + * + * @package OpenID + */ +class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request { + var $mode = "associate"; + + function getSessionClasses() + { + return array( + 'no-encryption' => 'Auth_OpenID_PlainTextServerSession', + 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ServerSession', + 'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ServerSession'); + } + + function Auth_OpenID_AssociateRequest(&$session, $assoc_type) + { + $this->session =& $session; + $this->namespace = Auth_OpenID_OPENID2_NS; + $this->assoc_type = $assoc_type; + } + + function fromMessage($message, $server=null) + { + if ($message->isOpenID1()) { + $session_type = $message->getArg(Auth_OpenID_OPENID_NS, + 'session_type'); + + if ($session_type == 'no-encryption') { + // oidutil.log('Received OpenID 1 request with a no-encryption ' + // 'assocaition session type. Continuing anyway.') + } else if (!$session_type) { + $session_type = 'no-encryption'; + } + } else { + $session_type = $message->getArg(Auth_OpenID_OPENID_NS, + 'session_type'); + if ($session_type === null) { + return new Auth_OpenID_ServerError($message, + "session_type missing from request"); + } + } + + $session_class = Auth_OpenID::arrayGet( + Auth_OpenID_AssociateRequest::getSessionClasses(), + $session_type); + + if ($session_class === null) { + return new Auth_OpenID_ServerError($message, + "Unknown session type " . + $session_type); + } + + $session = call_user_func(array($session_class, 'fromMessage'), + $message); + if (is_a($session, 'Auth_OpenID_ServerError')) { + return $session; + } + + $assoc_type = $message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_type', 'HMAC-SHA1'); + + if (!in_array($assoc_type, $session->allowed_assoc_types)) { + $fmt = "Session type %s does not support association type %s"; + return new Auth_OpenID_ServerError($message, + sprintf($fmt, $session_type, $assoc_type)); + } + + $obj = new Auth_OpenID_AssociateRequest($session, $assoc_type); + $obj->message = $message; + $obj->namespace = $message->getOpenIDNamespace(); + return $obj; + } + + function answer($assoc) + { + $response = new Auth_OpenID_ServerResponse($this); + $response->fields->updateArgs(Auth_OpenID_OPENID_NS, + array( + 'expires_in' => sprintf('%d', $assoc->getExpiresIn()), + 'assoc_type' => $this->assoc_type, + 'assoc_handle' => $assoc->handle)); + + $response->fields->updateArgs(Auth_OpenID_OPENID_NS, + $this->session->answer($assoc->secret)); + + if (! ($this->session->session_type == 'no-encryption' + && $this->message->isOpenID1())) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'session_type', + $this->session->session_type); + } + + return $response; + } + + function answerUnsupported($text_message, + $preferred_association_type=null, + $preferred_session_type=null) + { + if ($this->message->isOpenID1()) { + return new Auth_OpenID_ServerError($this->message); + } + + $response = new Auth_OpenID_ServerResponse($this); + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'error_code', 'unsupported-type'); + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'error', $text_message); + + if ($preferred_association_type) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'assoc_type', + $preferred_association_type); + } + + if ($preferred_session_type) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'session_type', + $preferred_session_type); + } + + return $response; + } +} + +/** + * A request to confirm the identity of a user. + * + * @package OpenID + */ +class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { + /** + * Return-to verification callback. Default is + * Auth_OpenID_verifyReturnTo from TrustRoot.php. + */ + var $verifyReturnTo = 'Auth_OpenID_verifyReturnTo'; + + /** + * The mode of this request. + */ + var $mode = "checkid_setup"; // or "checkid_immediate" + + /** + * Whether this request is for immediate mode. + */ + var $immediate = false; + + /** + * The trust_root value for this request. + */ + var $trust_root = null; + + /** + * The OpenID namespace for this request. + * deprecated since version 2.0.2 + */ + var $namespace; + + function make(&$message, $identity, $return_to, $trust_root = null, + $immediate = false, $assoc_handle = null, $server = null) + { + if ($server === null) { + return new Auth_OpenID_ServerError($message, + "server must not be null"); + } + + if ($return_to && + !Auth_OpenID_TrustRoot::_parse($return_to)) { + return new Auth_OpenID_MalformedReturnURL($message, $return_to); + } + + $r = new Auth_OpenID_CheckIDRequest($identity, $return_to, + $trust_root, $immediate, + $assoc_handle, $server); + + $r->namespace = $message->getOpenIDNamespace(); + $r->message =& $message; + + if (!$r->trustRootValid()) { + return new Auth_OpenID_UntrustedReturnURL($message, + $return_to, + $trust_root); + } else { + return $r; + } + } + + function Auth_OpenID_CheckIDRequest($identity, $return_to, + $trust_root = null, $immediate = false, + $assoc_handle = null, $server = null) + { + $this->namespace = Auth_OpenID_OPENID2_NS; + $this->assoc_handle = $assoc_handle; + $this->identity = $identity; + $this->claimed_id = $identity; + $this->return_to = $return_to; + $this->trust_root = $trust_root; + $this->server =& $server; + + if ($immediate) { + $this->immediate = true; + $this->mode = "checkid_immediate"; + } else { + $this->immediate = false; + $this->mode = "checkid_setup"; + } + } + + function equals($other) + { + return ( + (is_a($other, 'Auth_OpenID_CheckIDRequest')) && + ($this->namespace == $other->namespace) && + ($this->assoc_handle == $other->assoc_handle) && + ($this->identity == $other->identity) && + ($this->claimed_id == $other->claimed_id) && + ($this->return_to == $other->return_to) && + ($this->trust_root == $other->trust_root)); + } + + /* + * Does the relying party publish the return_to URL for this + * response under the realm? It is up to the provider to set a + * policy for what kinds of realms should be allowed. This + * return_to URL verification reduces vulnerability to data-theft + * attacks based on open proxies, corss-site-scripting, or open + * redirectors. + * + * This check should only be performed after making sure that the + * return_to URL matches the realm. + * + * @return true if the realm publishes a document with the + * return_to URL listed, false if not or if discovery fails + */ + function returnToVerified() + { + return call_user_func_array($this->verifyReturnTo, + array($this->trust_root, $this->return_to)); + } + + function fromMessage(&$message, $server) + { + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); + $immediate = null; + + if ($mode == "checkid_immediate") { + $immediate = true; + $mode = "checkid_immediate"; + } else { + $immediate = false; + $mode = "checkid_setup"; + } + + $return_to = $message->getArg(Auth_OpenID_OPENID_NS, + 'return_to'); + + if (($message->isOpenID1()) && + (!$return_to)) { + $fmt = "Missing required field 'return_to' from checkid request"; + return new Auth_OpenID_ServerError($message, $fmt); + } + + $identity = $message->getArg(Auth_OpenID_OPENID_NS, + 'identity'); + $claimed_id = $message->getArg(Auth_OpenID_OPENID_NS, 'claimed_id'); + if ($message->isOpenID1()) { + if ($identity === null) { + $s = "OpenID 1 message did not contain openid.identity"; + return new Auth_OpenID_ServerError($message, $s); + } + } else { + if ($identity && !$claimed_id) { + $s = "OpenID 2.0 message contained openid.identity but not " . + "claimed_id"; + return new Auth_OpenID_ServerError($message, $s); + } else if ($claimed_id && !$identity) { + $s = "OpenID 2.0 message contained openid.claimed_id " . + "but not identity"; + return new Auth_OpenID_ServerError($message, $s); + } + } + + // There's a case for making self.trust_root be a TrustRoot + // here. But if TrustRoot isn't currently part of the + // "public" API, I'm not sure it's worth doing. + if ($message->isOpenID1()) { + $trust_root_param = 'trust_root'; + } else { + $trust_root_param = 'realm'; + } + $trust_root = $message->getArg(Auth_OpenID_OPENID_NS, + $trust_root_param); + if (! $trust_root) { + $trust_root = $return_to; + } + + if (! $message->isOpenID1() && + ($return_to === null) && + ($trust_root === null)) { + return new Auth_OpenID_ServerError($message, + "openid.realm required when openid.return_to absent"); + } + + $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_handle'); + + $obj = Auth_OpenID_CheckIDRequest::make($message, + $identity, + $return_to, + $trust_root, + $immediate, + $assoc_handle, + $server); + + if (is_a($obj, 'Auth_OpenID_ServerError')) { + return $obj; + } + + $obj->claimed_id = $claimed_id; + + return $obj; + } + + function idSelect() + { + // Is the identifier to be selected by the IDP? + // So IDPs don't have to import the constant + return $this->identity == Auth_OpenID_IDENTIFIER_SELECT; + } + + function trustRootValid() + { + if (!$this->trust_root) { + return true; + } + + $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root); + if ($tr === false) { + return new Auth_OpenID_MalformedTrustRoot($this->message, + $this->trust_root); + } + + if ($this->return_to !== null) { + return Auth_OpenID_TrustRoot::match($this->trust_root, + $this->return_to); + } else { + return true; + } + } + + /** + * Respond to this request. Return either an + * {@link Auth_OpenID_ServerResponse} or + * {@link Auth_OpenID_ServerError}. + * + * @param bool $allow Allow this user to claim this identity, and + * allow the consumer to have this information? + * + * @param string $server_url DEPRECATED. Passing $op_endpoint to + * the {@link Auth_OpenID_Server} constructor makes this optional. + * + * When an OpenID 1.x immediate mode request does not succeed, it + * gets back a URL where the request may be carried out in a + * not-so-immediate fashion. Pass my URL in here (the fully + * qualified address of this server's endpoint, i.e. + * http://example.com/server), and I will use it as a base for the + * URL for a new request. + * + * Optional for requests where {@link $immediate} is false or + * $allow is true. + * + * @param string $identity The OP-local identifier to answer with. + * Only for use when the relying party requested identifier + * selection. + * + * @param string $claimed_id The claimed identifier to answer + * with, for use with identifier selection in the case where the + * claimed identifier and the OP-local identifier differ, + * i.e. when the claimed_id uses delegation. + * + * If $identity is provided but this is not, $claimed_id will + * default to the value of $identity. When answering requests + * that did not ask for identifier selection, the response + * $claimed_id will default to that of the request. + * + * This parameter is new in OpenID 2.0. + * + * @return mixed + */ + function answer($allow, $server_url = null, $identity = null, + $claimed_id = null) + { + if (!$this->return_to) { + return new Auth_OpenID_NoReturnToError(); + } + + if (!$server_url) { + if ((!$this->message->isOpenID1()) && + (!$this->server->op_endpoint)) { + return new Auth_OpenID_ServerError(null, + "server should be constructed with op_endpoint to " . + "respond to OpenID 2.0 messages."); + } + + $server_url = $this->server->op_endpoint; + } + + if ($allow) { + $mode = 'id_res'; + } else if ($this->message->isOpenID1()) { + if ($this->immediate) { + $mode = 'id_res'; + } else { + $mode = 'cancel'; + } + } else { + if ($this->immediate) { + $mode = 'setup_needed'; + } else { + $mode = 'cancel'; + } + } + + if (!$this->trustRootValid()) { + return new Auth_OpenID_UntrustedReturnURL(null, + $this->return_to, + $this->trust_root); + } + + $response = new Auth_OpenID_ServerResponse($this); + + if ($claimed_id && + ($this->message->isOpenID1())) { + return new Auth_OpenID_ServerError(null, + "claimed_id is new in OpenID 2.0 and not " . + "available for ".$this->namespace); + } + + if ($identity && !$claimed_id) { + $claimed_id = $identity; + } + + if ($allow) { + + if ($this->identity == Auth_OpenID_IDENTIFIER_SELECT) { + if (!$identity) { + return new Auth_OpenID_ServerError(null, + "This request uses IdP-driven identifier selection. " . + "You must supply an identifier in the response."); + } + + $response_identity = $identity; + $response_claimed_id = $claimed_id; + + } else if ($this->identity) { + if ($identity && + ($this->identity != $identity)) { + $fmt = "Request was for %s, cannot reply with identity %s"; + return new Auth_OpenID_ServerError(null, + sprintf($fmt, $this->identity, $identity)); + } + + $response_identity = $this->identity; + $response_claimed_id = $this->claimed_id; + } else { + if ($identity) { + return new Auth_OpenID_ServerError(null, + "This request specified no identity and " . + "you supplied ".$identity); + } + + $response_identity = null; + } + + if (($this->message->isOpenID1()) && + ($response_identity === null)) { + return new Auth_OpenID_ServerError(null, + "Request was an OpenID 1 request, so response must " . + "include an identifier."); + } + + $response->fields->updateArgs(Auth_OpenID_OPENID_NS, + array('mode' => $mode, + 'return_to' => $this->return_to, + 'response_nonce' => Auth_OpenID_mkNonce())); + + if (!$this->message->isOpenID1()) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'op_endpoint', $server_url); + } + + if ($response_identity !== null) { + $response->fields->setArg( + Auth_OpenID_OPENID_NS, + 'identity', + $response_identity); + if ($this->message->isOpenID2()) { + $response->fields->setArg( + Auth_OpenID_OPENID_NS, + 'claimed_id', + $response_claimed_id); + } + } + + } else { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'mode', $mode); + + if ($this->immediate) { + if (($this->message->isOpenID1()) && + (!$server_url)) { + return new Auth_OpenID_ServerError(null, + 'setup_url is required for $allow=false \ + in OpenID 1.x immediate mode.'); + } + + $setup_request =& new Auth_OpenID_CheckIDRequest( + $this->identity, + $this->return_to, + $this->trust_root, + false, + $this->assoc_handle, + $this->server); + $setup_request->message = $this->message; + + $setup_url = $setup_request->encodeToURL($server_url); + + if ($setup_url === null) { + return new Auth_OpenID_NoReturnToError(); + } + + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'user_setup_url', + $setup_url); + } + } + + return $response; + } + + function encodeToURL($server_url) + { + if (!$this->return_to) { + return new Auth_OpenID_NoReturnToError(); + } + + // Imported from the alternate reality where these classes are + // used in both the client and server code, so Requests are + // Encodable too. That's right, code imported from alternate + // realities all for the love of you, id_res/user_setup_url. + + $q = array('mode' => $this->mode, + 'identity' => $this->identity, + 'claimed_id' => $this->claimed_id, + 'return_to' => $this->return_to); + + if ($this->trust_root) { + if ($this->message->isOpenID1()) { + $q['trust_root'] = $this->trust_root; + } else { + $q['realm'] = $this->trust_root; + } + } + + if ($this->assoc_handle) { + $q['assoc_handle'] = $this->assoc_handle; + } + + $response = new Auth_OpenID_Message( + $this->message->getOpenIDNamespace()); + $response->updateArgs(Auth_OpenID_OPENID_NS, $q); + return $response->toURL($server_url); + } + + function getCancelURL() + { + if (!$this->return_to) { + return new Auth_OpenID_NoReturnToError(); + } + + if ($this->immediate) { + return new Auth_OpenID_ServerError(null, + "Cancel is not an appropriate \ + response to immediate mode \ + requests."); + } + + $response = new Auth_OpenID_Message( + $this->message->getOpenIDNamespace()); + $response->setArg(Auth_OpenID_OPENID_NS, 'mode', 'cancel'); + return $response->toURL($this->return_to); + } +} + +/** + * This class encapsulates the response to an OpenID server request. + * + * @package OpenID + */ +class Auth_OpenID_ServerResponse { + + function Auth_OpenID_ServerResponse(&$request) + { + $this->request =& $request; + $this->fields = new Auth_OpenID_Message($this->request->namespace); + } + + function whichEncoding() + { + global $_Auth_OpenID_Request_Modes; + + if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) { + if ($this->fields->isOpenID2() && + (strlen($this->encodeToURL()) > + Auth_OpenID_OPENID1_URL_LIMIT)) { + return Auth_OpenID_ENCODE_HTML_FORM; + } else { + return Auth_OpenID_ENCODE_URL; + } + } else { + return Auth_OpenID_ENCODE_KVFORM; + } + } + + /* + * Returns the form markup for this response. + * + * @return str + */ + function toFormMarkup($form_tag_attrs=null) + { + return $this->fields->toFormMarkup($this->request->return_to, + $form_tag_attrs); + } + + /* + * Returns an HTML document containing the form markup for this + * response that autosubmits with javascript. + */ + function toHTML() + { + return Auth_OpenID::autoSubmitHTML($this->toFormMarkup()); + } + + /* + * Returns True if this response's encoding is ENCODE_HTML_FORM. + * Convenience method for server authors. + * + * @return bool + */ + function renderAsForm() + { + return $this->whichEncoding() == Auth_OpenID_ENCODE_HTML_FORM; + } + + + function encodeToURL() + { + return $this->fields->toURL($this->request->return_to); + } + + function addExtension($extension_response) + { + $extension_response->toMessage($this->fields); + } + + function needsSigning() + { + return $this->fields->getArg(Auth_OpenID_OPENID_NS, + 'mode') == 'id_res'; + } + + function encodeToKVForm() + { + return $this->fields->toKVForm(); + } +} + +/** + * A web-capable response object which you can use to generate a + * user-agent response. + * + * @package OpenID + */ +class Auth_OpenID_WebResponse { + var $code = AUTH_OPENID_HTTP_OK; + var $body = ""; + + function Auth_OpenID_WebResponse($code = null, $headers = null, + $body = null) + { + if ($code) { + $this->code = $code; + } + + if ($headers !== null) { + $this->headers = $headers; + } else { + $this->headers = array(); + } + + if ($body !== null) { + $this->body = $body; + } + } +} + +/** + * Responsible for the signature of query data and the verification of + * OpenID signature values. + * + * @package OpenID + */ +class Auth_OpenID_Signatory { + + // = 14 * 24 * 60 * 60; # 14 days, in seconds + var $SECRET_LIFETIME = 1209600; + + // keys have a bogus server URL in them because the filestore + // really does expect that key to be a URL. This seems a little + // silly for the server store, since I expect there to be only one + // server URL. + var $normal_key = 'http://localhost/|normal'; + var $dumb_key = 'http://localhost/|dumb'; + + /** + * Create a new signatory using a given store. + */ + function Auth_OpenID_Signatory(&$store) + { + // assert store is not None + $this->store =& $store; + } + + /** + * Verify, using a given association handle, a signature with + * signed key-value pairs from an HTTP request. + */ + function verify($assoc_handle, $message) + { + $assoc = $this->getAssociation($assoc_handle, true); + if (!$assoc) { + // oidutil.log("failed to get assoc with handle %r to verify sig %r" + // % (assoc_handle, sig)) + return false; + } + + return $assoc->checkMessageSignature($message); + } + + /** + * Given a response, sign the fields in the response's 'signed' + * list, and insert the signature into the response. + */ + function sign($response) + { + $signed_response = $response; + $assoc_handle = $response->request->assoc_handle; + + if ($assoc_handle) { + // normal mode + $assoc = $this->getAssociation($assoc_handle, false, false); + if (!$assoc || ($assoc->getExpiresIn() <= 0)) { + // fall back to dumb mode + $signed_response->fields->setArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle', $assoc_handle); + $assoc_type = ($assoc ? $assoc->assoc_type : 'HMAC-SHA1'); + + if ($assoc && ($assoc->getExpiresIn() <= 0)) { + $this->invalidate($assoc_handle, false); + } + + $assoc = $this->createAssociation(true, $assoc_type); + } + } else { + // dumb mode. + $assoc = $this->createAssociation(true); + } + + $signed_response->fields = $assoc->signMessage( + $signed_response->fields); + return $signed_response; + } + + /** + * Make a new association. + */ + function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1') + { + $secret = Auth_OpenID_CryptUtil::getBytes( + Auth_OpenID_getSecretSize($assoc_type)); + + $uniq = base64_encode(Auth_OpenID_CryptUtil::getBytes(4)); + $handle = sprintf('{%s}{%x}{%s}', $assoc_type, intval(time()), $uniq); + + $assoc = Auth_OpenID_Association::fromExpiresIn( + $this->SECRET_LIFETIME, $handle, $secret, $assoc_type); + + if ($dumb) { + $key = $this->dumb_key; + } else { + $key = $this->normal_key; + } + + $this->store->storeAssociation($key, $assoc); + return $assoc; + } + + /** + * Given an association handle, get the association from the + * store, or return a ServerError or null if something goes wrong. + */ + function getAssociation($assoc_handle, $dumb, $check_expiration=true) + { + if ($assoc_handle === null) { + return new Auth_OpenID_ServerError(null, + "assoc_handle must not be null"); + } + + if ($dumb) { + $key = $this->dumb_key; + } else { + $key = $this->normal_key; + } + + $assoc = $this->store->getAssociation($key, $assoc_handle); + + if (($assoc !== null) && ($assoc->getExpiresIn() <= 0)) { + if ($check_expiration) { + $this->store->removeAssociation($key, $assoc_handle); + $assoc = null; + } + } + + return $assoc; + } + + /** + * Invalidate a given association handle. + */ + function invalidate($assoc_handle, $dumb) + { + if ($dumb) { + $key = $this->dumb_key; + } else { + $key = $this->normal_key; + } + $this->store->removeAssociation($key, $assoc_handle); + } +} + +/** + * Encode an {@link Auth_OpenID_ServerResponse} to an + * {@link Auth_OpenID_WebResponse}. + * + * @package OpenID + */ +class Auth_OpenID_Encoder { + + var $responseFactory = 'Auth_OpenID_WebResponse'; + + /** + * Encode an {@link Auth_OpenID_ServerResponse} and return an + * {@link Auth_OpenID_WebResponse}. + */ + function encode(&$response) + { + $cls = $this->responseFactory; + + $encode_as = $response->whichEncoding(); + if ($encode_as == Auth_OpenID_ENCODE_KVFORM) { + $wr = new $cls(null, null, $response->encodeToKVForm()); + if (is_a($response, 'Auth_OpenID_ServerError')) { + $wr->code = AUTH_OPENID_HTTP_ERROR; + } + } else if ($encode_as == Auth_OpenID_ENCODE_URL) { + $location = $response->encodeToURL(); + $wr = new $cls(AUTH_OPENID_HTTP_REDIRECT, + array('location' => $location)); + } else if ($encode_as == Auth_OpenID_ENCODE_HTML_FORM) { + $wr = new $cls(AUTH_OPENID_HTTP_OK, array(), + $response->toFormMarkup()); + } else { + return new Auth_OpenID_EncodingError($response); + } + return $wr; + } +} + +/** + * An encoder which also takes care of signing fields when required. + * + * @package OpenID + */ +class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder { + + function Auth_OpenID_SigningEncoder(&$signatory) + { + $this->signatory =& $signatory; + } + + /** + * Sign an {@link Auth_OpenID_ServerResponse} and return an + * {@link Auth_OpenID_WebResponse}. + */ + function encode(&$response) + { + // the isinstance is a bit of a kludge... it means there isn't + // really an adapter to make the interfaces quite match. + if (!is_a($response, 'Auth_OpenID_ServerError') && + $response->needsSigning()) { + + if (!$this->signatory) { + return new Auth_OpenID_ServerError(null, + "Must have a store to sign request"); + } + + if ($response->fields->hasKey(Auth_OpenID_OPENID_NS, 'sig')) { + return new Auth_OpenID_AlreadySigned($response); + } + $response = $this->signatory->sign($response); + } + + return parent::encode($response); + } +} + +/** + * Decode an incoming query into an Auth_OpenID_Request. + * + * @package OpenID + */ +class Auth_OpenID_Decoder { + + function Auth_OpenID_Decoder(&$server) + { + $this->server =& $server; + + $this->handlers = array( + 'checkid_setup' => 'Auth_OpenID_CheckIDRequest', + 'checkid_immediate' => 'Auth_OpenID_CheckIDRequest', + 'check_authentication' => 'Auth_OpenID_CheckAuthRequest', + 'associate' => 'Auth_OpenID_AssociateRequest' + ); + } + + /** + * Given an HTTP query in an array (key-value pairs), decode it + * into an Auth_OpenID_Request object. + */ + function decode($query) + { + if (!$query) { + return null; + } + + $message = Auth_OpenID_Message::fromPostArgs($query); + + if ($message === null) { + /* + * It's useful to have a Message attached to a + * ProtocolError, so we override the bad ns value to build + * a Message out of it. Kinda kludgy, since it's made of + * lies, but the parts that aren't lies are more useful + * than a 'None'. + */ + $old_ns = $query['openid.ns']; + + $query['openid.ns'] = Auth_OpenID_OPENID2_NS; + $message = Auth_OpenID_Message::fromPostArgs($query); + return new Auth_OpenID_ServerError( + $message, + sprintf("Invalid OpenID namespace URI: %s", $old_ns)); + } + + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); + if (!$mode) { + return new Auth_OpenID_ServerError($message, + "No mode value in message"); + } + + if (Auth_OpenID::isFailure($mode)) { + return new Auth_OpenID_ServerError($message, + $mode->message); + } + + $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode, + $this->defaultDecoder($message)); + + if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) { + return call_user_func_array(array($handlerCls, 'fromMessage'), + array($message, $this->server)); + } else { + return $handlerCls; + } + } + + function defaultDecoder($message) + { + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); + + if (Auth_OpenID::isFailure($mode)) { + return new Auth_OpenID_ServerError($message, + $mode->message); + } + + return new Auth_OpenID_ServerError($message, + sprintf("Unrecognized OpenID mode %s", $mode)); + } +} + +/** + * An error that indicates an encoding problem occurred. + * + * @package OpenID + */ +class Auth_OpenID_EncodingError { + function Auth_OpenID_EncodingError(&$response) + { + $this->response =& $response; + } +} + +/** + * An error that indicates that a response was already signed. + * + * @package OpenID + */ +class Auth_OpenID_AlreadySigned extends Auth_OpenID_EncodingError { + // This response is already signed. +} + +/** + * An error that indicates that the given return_to is not under the + * given trust_root. + * + * @package OpenID + */ +class Auth_OpenID_UntrustedReturnURL extends Auth_OpenID_ServerError { + function Auth_OpenID_UntrustedReturnURL($message, $return_to, + $trust_root) + { + parent::Auth_OpenID_ServerError($message, "Untrusted return_to URL"); + $this->return_to = $return_to; + $this->trust_root = $trust_root; + } + + function toString() + { + return sprintf("return_to %s not under trust_root %s", + $this->return_to, $this->trust_root); + } +} + +/** + * I handle requests for an OpenID server. + * + * Some types of requests (those which are not checkid requests) may + * be handed to my {@link handleRequest} method, and I will take care + * of it and return a response. + * + * For your convenience, I also provide an interface to {@link + * Auth_OpenID_Decoder::decode()} and {@link + * Auth_OpenID_SigningEncoder::encode()} through my methods {@link + * decodeRequest} and {@link encodeResponse}. + * + * All my state is encapsulated in an {@link Auth_OpenID_OpenIDStore}. + * + * Example: + * + * <pre> $oserver = new Auth_OpenID_Server(Auth_OpenID_FileStore($data_path), + * "http://example.com/op"); + * $request = $oserver->decodeRequest(); + * if (in_array($request->mode, array('checkid_immediate', + * 'checkid_setup'))) { + * if ($app->isAuthorized($request->identity, $request->trust_root)) { + * $response = $request->answer(true); + * } else if ($request->immediate) { + * $response = $request->answer(false); + * } else { + * $app->showDecidePage($request); + * return; + * } + * } else { + * $response = $oserver->handleRequest($request); + * } + * + * $webresponse = $oserver->encode($response);</pre> + * + * @package OpenID + */ +class Auth_OpenID_Server { + function Auth_OpenID_Server(&$store, $op_endpoint=null) + { + $this->store =& $store; + $this->signatory =& new Auth_OpenID_Signatory($this->store); + $this->encoder =& new Auth_OpenID_SigningEncoder($this->signatory); + $this->decoder =& new Auth_OpenID_Decoder($this); + $this->op_endpoint = $op_endpoint; + $this->negotiator =& Auth_OpenID_getDefaultNegotiator(); + } + + /** + * Handle a request. Given an {@link Auth_OpenID_Request} object, + * call the appropriate {@link Auth_OpenID_Server} method to + * process the request and generate a response. + * + * @param Auth_OpenID_Request $request An {@link Auth_OpenID_Request} + * returned by {@link Auth_OpenID_Server::decodeRequest()}. + * + * @return Auth_OpenID_ServerResponse $response A response object + * capable of generating a user-agent reply. + */ + function handleRequest($request) + { + if (method_exists($this, "openid_" . $request->mode)) { + $handler = array($this, "openid_" . $request->mode); + return call_user_func($handler, $request); + } + return null; + } + + /** + * The callback for 'check_authentication' messages. + */ + function openid_check_authentication(&$request) + { + return $request->answer($this->signatory); + } + + /** + * The callback for 'associate' messages. + */ + function openid_associate(&$request) + { + $assoc_type = $request->assoc_type; + $session_type = $request->session->session_type; + if ($this->negotiator->isAllowed($assoc_type, $session_type)) { + $assoc = $this->signatory->createAssociation(false, + $assoc_type); + return $request->answer($assoc); + } else { + $message = sprintf('Association type %s is not supported with '. + 'session type %s', $assoc_type, $session_type); + list($preferred_assoc_type, $preferred_session_type) = + $this->negotiator->getAllowedType(); + return $request->answerUnsupported($message, + $preferred_assoc_type, + $preferred_session_type); + } + } + + /** + * Encodes as response in the appropriate format suitable for + * sending to the user agent. + */ + function encodeResponse(&$response) + { + return $this->encoder->encode($response); + } + + /** + * Decodes a query args array into the appropriate + * {@link Auth_OpenID_Request} object. + */ + function decodeRequest($query=null) + { + if ($query === null) { + $query = Auth_OpenID::getQuery(); + } + + return $this->decoder->decode($query); + } +} + +?> diff --git a/extlib/Auth/OpenID/ServerRequest.php b/extlib/Auth/OpenID/ServerRequest.php new file mode 100644 index 000000000..33a8556ce --- /dev/null +++ b/extlib/Auth/OpenID/ServerRequest.php @@ -0,0 +1,37 @@ +<?php +/** + * OpenID Server Request + * + * @see Auth_OpenID_Server + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Imports + */ +require_once "Auth/OpenID.php"; + +/** + * Object that holds the state of a request to the OpenID server + * + * With accessor functions to get at the internal request data. + * + * @see Auth_OpenID_Server + * @package OpenID + */ +class Auth_OpenID_ServerRequest { + function Auth_OpenID_ServerRequest() + { + $this->mode = null; + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/TrustRoot.php b/extlib/Auth/OpenID/TrustRoot.php new file mode 100644 index 000000000..4919a6065 --- /dev/null +++ b/extlib/Auth/OpenID/TrustRoot.php @@ -0,0 +1,462 @@ +<?php +/** + * Functions for dealing with OpenID trust roots + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'Auth/OpenID/Discover.php'; + +/** + * A regular expression that matches a domain ending in a top-level domains. + * Used in checking trust roots for sanity. + * + * @access private + */ +define('Auth_OpenID___TLDs', + '/\.(ac|ad|ae|aero|af|ag|ai|al|am|an|ao|aq|ar|arpa|as|asia' . + '|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|biz|bj|bm|bn|bo|br' . + '|bs|bt|bv|bw|by|bz|ca|cat|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co' . + '|com|coop|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg' . + '|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl' . + '|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie' . + '|il|im|in|info|int|io|iq|ir|is|it|je|jm|jo|jobs|jp|ke|kg|kh' . + '|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly' . + '|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mo|mobi|mp|mq|mr|ms|mt' . + '|mu|museum|mv|mw|mx|my|mz|na|name|nc|ne|net|nf|ng|ni|nl|no' . + '|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|pro|ps|pt' . + '|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl' . + '|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm' . + '|tn|to|tp|tr|travel|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve' . + '|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g' . + '|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d' . + '|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp' . + '|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)\.?$/'); + +define('Auth_OpenID___HostSegmentRe', + "/^(?:[-a-zA-Z0-9!$&'\\(\\)\\*+,;=._~]|%[a-zA-Z0-9]{2})*$/"); + +/** + * A wrapper for trust-root related functions + */ +class Auth_OpenID_TrustRoot { + /* + * Return a discovery URL for this realm. + * + * Return null if the realm could not be parsed or was not valid. + * + * @param return_to The relying party return URL of the OpenID + * authentication request + * + * @return The URL upon which relying party discovery should be + * run in order to verify the return_to URL + */ + function buildDiscoveryURL($realm) + { + $parsed = Auth_OpenID_TrustRoot::_parse($realm); + + if ($parsed === false) { + return false; + } + + if ($parsed['wildcard']) { + // Use "www." in place of the star + if ($parsed['host'][0] != '.') { + return false; + } + + $www_domain = 'www' . $parsed['host']; + + return sprintf('%s://%s%s', $parsed['scheme'], + $www_domain, $parsed['path']); + } else { + return $parsed['unparsed']; + } + } + + /** + * Parse a URL into its trust_root parts. + * + * @static + * + * @access private + * + * @param string $trust_root The url to parse + * + * @return mixed $parsed Either an associative array of trust root + * parts or false if parsing failed. + */ + function _parse($trust_root) + { + $trust_root = Auth_OpenID_urinorm($trust_root); + if ($trust_root === null) { + return false; + } + + if (preg_match("/:\/\/[^:]+(:\d+){2,}(\/|$)/", $trust_root)) { + return false; + } + + $parts = @parse_url($trust_root); + if ($parts === false) { + return false; + } + + $required_parts = array('scheme', 'host'); + $forbidden_parts = array('user', 'pass', 'fragment'); + $keys = array_keys($parts); + if (array_intersect($keys, $required_parts) != $required_parts) { + return false; + } + + if (array_intersect($keys, $forbidden_parts) != array()) { + return false; + } + + if (!preg_match(Auth_OpenID___HostSegmentRe, $parts['host'])) { + return false; + } + + $scheme = strtolower($parts['scheme']); + $allowed_schemes = array('http', 'https'); + if (!in_array($scheme, $allowed_schemes)) { + return false; + } + $parts['scheme'] = $scheme; + + $host = strtolower($parts['host']); + $hostparts = explode('*', $host); + switch (count($hostparts)) { + case 1: + $parts['wildcard'] = false; + break; + case 2: + if ($hostparts[0] || + ($hostparts[1] && substr($hostparts[1], 0, 1) != '.')) { + return false; + } + $host = $hostparts[1]; + $parts['wildcard'] = true; + break; + default: + return false; + } + if (strpos($host, ':') !== false) { + return false; + } + + $parts['host'] = $host; + + if (isset($parts['path'])) { + $path = strtolower($parts['path']); + if (substr($path, 0, 1) != '/') { + return false; + } + } else { + $path = '/'; + } + + $parts['path'] = $path; + if (!isset($parts['port'])) { + $parts['port'] = false; + } + + + $parts['unparsed'] = $trust_root; + + return $parts; + } + + /** + * Is this trust root sane? + * + * A trust root is sane if it is syntactically valid and it has a + * reasonable domain name. Specifically, the domain name must be + * more than one level below a standard TLD or more than two + * levels below a two-letter tld. + * + * For example, '*.com' is not a sane trust root, but '*.foo.com' + * is. '*.co.uk' is not sane, but '*.bbc.co.uk' is. + * + * This check is not always correct, but it attempts to err on the + * side of marking sane trust roots insane instead of marking + * insane trust roots sane. For example, 'kink.fm' is marked as + * insane even though it "should" (for some meaning of should) be + * marked sane. + * + * This function should be used when creating OpenID servers to + * alert the users of the server when a consumer attempts to get + * the user to accept a suspicious trust root. + * + * @static + * @param string $trust_root The trust root to check + * @return bool $sanity Whether the trust root looks OK + */ + function isSane($trust_root) + { + $parts = Auth_OpenID_TrustRoot::_parse($trust_root); + if ($parts === false) { + return false; + } + + // Localhost is a special case + if ($parts['host'] == 'localhost') { + return true; + } + + $host_parts = explode('.', $parts['host']); + if ($parts['wildcard']) { + // Remove the empty string from the beginning of the array + array_shift($host_parts); + } + + if ($host_parts && !$host_parts[count($host_parts) - 1]) { + array_pop($host_parts); + } + + if (!$host_parts) { + return false; + } + + // Don't allow adjacent dots + if (in_array('', $host_parts, true)) { + return false; + } + + // Get the top-level domain of the host. If it is not a valid TLD, + // it's not sane. + preg_match(Auth_OpenID___TLDs, $parts['host'], $matches); + if (!$matches) { + return false; + } + $tld = $matches[1]; + + if (count($host_parts) == 1) { + return false; + } + + if ($parts['wildcard']) { + // It's a 2-letter tld with a short second to last segment + // so there needs to be more than two segments specified + // (e.g. *.co.uk is insane) + $second_level = $host_parts[count($host_parts) - 2]; + if (strlen($tld) == 2 && strlen($second_level) <= 3) { + return count($host_parts) > 2; + } + } + + return true; + } + + /** + * Does this URL match the given trust root? + * + * Return whether the URL falls under the given trust root. This + * does not check whether the trust root is sane. If the URL or + * trust root do not parse, this function will return false. + * + * @param string $trust_root The trust root to match against + * + * @param string $url The URL to check + * + * @return bool $matches Whether the URL matches against the + * trust root + */ + function match($trust_root, $url) + { + $trust_root_parsed = Auth_OpenID_TrustRoot::_parse($trust_root); + $url_parsed = Auth_OpenID_TrustRoot::_parse($url); + if (!$trust_root_parsed || !$url_parsed) { + return false; + } + + // Check hosts matching + if ($url_parsed['wildcard']) { + return false; + } + if ($trust_root_parsed['wildcard']) { + $host_tail = $trust_root_parsed['host']; + $host = $url_parsed['host']; + if ($host_tail && + substr($host, -(strlen($host_tail))) != $host_tail && + substr($host_tail, 1) != $host) { + return false; + } + } else { + if ($trust_root_parsed['host'] != $url_parsed['host']) { + return false; + } + } + + // Check path and query matching + $base_path = $trust_root_parsed['path']; + $path = $url_parsed['path']; + if (!isset($trust_root_parsed['query'])) { + if ($base_path != $path) { + if (substr($path, 0, strlen($base_path)) != $base_path) { + return false; + } + if (substr($base_path, strlen($base_path) - 1, 1) != '/' && + substr($path, strlen($base_path), 1) != '/') { + return false; + } + } + } else { + $base_query = $trust_root_parsed['query']; + $query = @$url_parsed['query']; + $qplus = substr($query, 0, strlen($base_query) + 1); + $bqplus = $base_query . '&'; + if ($base_path != $path || + ($base_query != $query && $qplus != $bqplus)) { + return false; + } + } + + // The port and scheme need to match exactly + return ($trust_root_parsed['scheme'] == $url_parsed['scheme'] && + $url_parsed['port'] === $trust_root_parsed['port']); + } +} + +/* + * If the endpoint is a relying party OpenID return_to endpoint, + * return the endpoint URL. Otherwise, return None. + * + * This function is intended to be used as a filter for the Yadis + * filtering interface. + * + * @see: C{L{openid.yadis.services}} + * @see: C{L{openid.yadis.filters}} + * + * @param endpoint: An XRDS BasicServiceEndpoint, as returned by + * performing Yadis dicovery. + * + * @returns: The endpoint URL or None if the endpoint is not a + * relying party endpoint. + */ +function filter_extractReturnURL(&$endpoint) +{ + if ($endpoint->matchTypes(array(Auth_OpenID_RP_RETURN_TO_URL_TYPE))) { + return $endpoint; + } else { + return null; + } +} + +function &Auth_OpenID_extractReturnURL(&$endpoint_list) +{ + $result = array(); + + foreach ($endpoint_list as $endpoint) { + if (filter_extractReturnURL($endpoint)) { + $result[] = $endpoint; + } + } + + return $result; +} + +/* + * Is the return_to URL under one of the supplied allowed return_to + * URLs? + */ +function Auth_OpenID_returnToMatches($allowed_return_to_urls, $return_to) +{ + foreach ($allowed_return_to_urls as $allowed_return_to) { + // A return_to pattern works the same as a realm, except that + // it's not allowed to use a wildcard. We'll model this by + // parsing it as a realm, and not trying to match it if it has + // a wildcard. + + $return_realm = Auth_OpenID_TrustRoot::_parse($allowed_return_to); + if (// Parses as a trust root + ($return_realm !== false) && + // Does not have a wildcard + (!$return_realm['wildcard']) && + // Matches the return_to that we passed in with it + (Auth_OpenID_TrustRoot::match($allowed_return_to, $return_to))) { + return true; + } + } + + // No URL in the list matched + return false; +} + +/* + * Given a relying party discovery URL return a list of return_to + * URLs. + */ +function Auth_OpenID_getAllowedReturnURLs($relying_party_url, &$fetcher, + $discover_function=null) +{ + if ($discover_function === null) { + $discover_function = array('Auth_Yadis_Yadis', 'discover'); + } + + $xrds_parse_cb = array('Auth_OpenID_ServiceEndpoint', 'fromXRDS'); + + list($rp_url_after_redirects, $endpoints) = + Auth_Yadis_getServiceEndpoints($relying_party_url, $xrds_parse_cb, + $discover_function, $fetcher); + + if ($rp_url_after_redirects != $relying_party_url) { + // Verification caused a redirect + return false; + } + + call_user_func_array($discover_function, + array($relying_party_url, $fetcher)); + + $return_to_urls = array(); + $matching_endpoints = Auth_OpenID_extractReturnURL($endpoints); + + foreach ($matching_endpoints as $e) { + $return_to_urls[] = $e->server_url; + } + + return $return_to_urls; +} + +/* + * Verify that a return_to URL is valid for the given realm. + * + * This function builds a discovery URL, performs Yadis discovery on + * it, makes sure that the URL does not redirect, parses out the + * return_to URLs, and finally checks to see if the current return_to + * URL matches the return_to. + * + * @return true if the return_to URL is valid for the realm + */ +function Auth_OpenID_verifyReturnTo($realm_str, $return_to, &$fetcher, + $_vrfy='Auth_OpenID_getAllowedReturnURLs') +{ + $disco_url = Auth_OpenID_TrustRoot::buildDiscoveryURL($realm_str); + + if ($disco_url === false) { + return false; + } + + $allowable_urls = call_user_func_array($_vrfy, + array($disco_url, &$fetcher)); + + // The realm_str could not be parsed. + if ($allowable_urls === false) { + return false; + } + + if (Auth_OpenID_returnToMatches($allowable_urls, $return_to)) { + return true; + } else { + return false; + } +} + +?>
\ No newline at end of file diff --git a/extlib/Auth/OpenID/URINorm.php b/extlib/Auth/OpenID/URINorm.php new file mode 100644 index 000000000..f821d836a --- /dev/null +++ b/extlib/Auth/OpenID/URINorm.php @@ -0,0 +1,249 @@ +<?php + +/** + * URI normalization routines. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'Auth/Yadis/Misc.php'; + +// from appendix B of rfc 3986 (http://www.ietf.org/rfc/rfc3986.txt) +function Auth_OpenID_getURIPattern() +{ + return '&^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?&'; +} + +function Auth_OpenID_getAuthorityPattern() +{ + return '/^([^@]*@)?([^:]*)(:.*)?/'; +} + +function Auth_OpenID_getEncodedPattern() +{ + return '/%([0-9A-Fa-f]{2})/'; +} + +# gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" +# +# sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +# / "*" / "+" / "," / ";" / "=" +# +# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +function Auth_OpenID_getURLIllegalCharRE() +{ + return "/([^-A-Za-z0-9:\/\?#\[\]@\!\$&'\(\)\*\+,;=\._~\%])/"; +} + +function Auth_OpenID_getUnreserved() +{ + $_unreserved = array(); + for ($i = 0; $i < 256; $i++) { + $_unreserved[$i] = false; + } + + for ($i = ord('A'); $i <= ord('Z'); $i++) { + $_unreserved[$i] = true; + } + + for ($i = ord('0'); $i <= ord('9'); $i++) { + $_unreserved[$i] = true; + } + + for ($i = ord('a'); $i <= ord('z'); $i++) { + $_unreserved[$i] = true; + } + + $_unreserved[ord('-')] = true; + $_unreserved[ord('.')] = true; + $_unreserved[ord('_')] = true; + $_unreserved[ord('~')] = true; + + return $_unreserved; +} + +function Auth_OpenID_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_OpenID_pct_encoded_replace_unreserved($mo) +{ + $_unreserved = Auth_OpenID_getUnreserved(); + + $i = intval($mo[1], 16); + if ($_unreserved[$i]) { + return chr($i); + } else { + return strtoupper($mo[0]); + } + + return $mo[0]; +} + +function Auth_OpenID_pct_encoded_replace($mo) +{ + return chr(intval($mo[1], 16)); +} + +function Auth_OpenID_remove_dot_segments($path) +{ + $result_segments = array(); + + while ($path) { + if (Auth_Yadis_startswith($path, '../')) { + $path = substr($path, 3); + } else if (Auth_Yadis_startswith($path, './')) { + $path = substr($path, 2); + } else if (Auth_Yadis_startswith($path, '/./')) { + $path = substr($path, 2); + } else if ($path == '/.') { + $path = '/'; + } else if (Auth_Yadis_startswith($path, '/../')) { + $path = substr($path, 3); + if ($result_segments) { + array_pop($result_segments); + } + } else if ($path == '/..') { + $path = '/'; + if ($result_segments) { + array_pop($result_segments); + } + } else if (($path == '..') || + ($path == '.')) { + $path = ''; + } else { + $i = 0; + if ($path[0] == '/') { + $i = 1; + } + $i = strpos($path, '/', $i); + if ($i === false) { + $i = strlen($path); + } + $result_segments[] = substr($path, 0, $i); + $path = substr($path, $i); + } + } + + return implode('', $result_segments); +} + +function Auth_OpenID_urinorm($uri) +{ + $uri_matches = array(); + preg_match(Auth_OpenID_getURIPattern(), $uri, $uri_matches); + + if (count($uri_matches) < 9) { + for ($i = count($uri_matches); $i <= 9; $i++) { + $uri_matches[] = ''; + } + } + + $illegal_matches = array(); + preg_match(Auth_OpenID_getURLIllegalCharRE(), + $uri, $illegal_matches); + if ($illegal_matches) { + return null; + } + + $scheme = $uri_matches[2]; + if ($scheme) { + $scheme = strtolower($scheme); + } + + $scheme = $uri_matches[2]; + if ($scheme === '') { + // No scheme specified + return null; + } + + $scheme = strtolower($scheme); + if (!in_array($scheme, array('http', 'https'))) { + // Not an absolute HTTP or HTTPS URI + return null; + } + + $authority = $uri_matches[4]; + if ($authority === '') { + // Not an absolute URI + return null; + } + + $authority_matches = array(); + preg_match(Auth_OpenID_getAuthorityPattern(), + $authority, $authority_matches); + if (count($authority_matches) === 0) { + // URI does not have a valid authority + return null; + } + + if (count($authority_matches) < 4) { + for ($i = count($authority_matches); $i <= 4; $i++) { + $authority_matches[] = ''; + } + } + + list($_whole, $userinfo, $host, $port) = $authority_matches; + + if ($userinfo === null) { + $userinfo = ''; + } + + if (strpos($host, '%') !== -1) { + $host = strtolower($host); + $host = preg_replace_callback( + Auth_OpenID_getEncodedPattern(), + 'Auth_OpenID_pct_encoded_replace', $host); + // NO IDNA. + // $host = unicode($host, 'utf-8').encode('idna'); + } else { + $host = strtolower($host); + } + + if ($port) { + if (($port == ':') || + ($scheme == 'http' && $port == ':80') || + ($scheme == 'https' && $port == ':443')) { + $port = ''; + } + } else { + $port = ''; + } + + $authority = $userinfo . $host . $port; + + $path = $uri_matches[5]; + $path = preg_replace_callback( + Auth_OpenID_getEncodedPattern(), + 'Auth_OpenID_pct_encoded_replace_unreserved', $path); + + $path = Auth_OpenID_remove_dot_segments($path); + if (!$path) { + $path = '/'; + } + + $query = $uri_matches[6]; + if ($query === null) { + $query = ''; + } + + $fragment = $uri_matches[8]; + if ($fragment === null) { + $fragment = ''; + } + + return $scheme . '://' . $authority . $path . $query . $fragment; +} + +?> 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 @@ +<?php + +/** + * This module contains the HTTP fetcher interface + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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 @@ +<?php + +/** + * Yadis service manager to be used during yadis-driven authentication + * attempts. + * + * @package OpenID + */ + +/** + * The base session class used by the Auth_Yadis_Manager. This + * class wraps the default PHP session machinery and should be + * subclassed if your application doesn't use PHP sessioning. + * + * @package OpenID + */ +class Auth_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]); + } + + /** + * Return the contents of the session in array form. + */ + function contents() + { + return $_SESSION; + } +} + +/** + * A session helper class designed to translate between arrays and + * objects. Note that the class used must have a constructor that + * takes no parameters. This is not a general solution, but it works + * for dumb objects that just need to have attributes set. The idea + * is that you'll subclass this and override $this->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 <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 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 @@ +<?php + +/** + * Miscellaneous utility values and functions for OpenID and Yadis. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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 @@ +<?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 OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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 @@ +<?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 OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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 = + "<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b(?!:)[^>]*>.*?<\/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 + * <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) + { + $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 </head> 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 @@ +<?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 OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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 @@ +<?php + +/** + * XML-parsing classes to wrap the domxml and DOM extensions for PHP 4 + * and 5, respectively. + * + * @package OpenID + */ + +/** + * 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 Auth_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 OpenID + */ +class Auth_Yadis_XMLParser { + /** + * Initialize an instance of Auth_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 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 @@ +<?php + +/** + * This module contains the XRDS parsing code. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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 <Service> 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 <Service> 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 <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 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: + * + * <pre> $xrds = Auth_Yadis_XRDS::parseXRDS($xml_string);</pre> + * + * 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 <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 + * 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 @@ +<?php + +/** + * Routines for XRI resolution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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 @@ +<?php + +/** + * Code for using a proxy XRI resolver. + */ + +require_once 'Auth/Yadis/XRDS.php'; +require_once 'Auth/Yadis/XRI.php'; + +class Auth_Yadis_ProxyResolver { + function Auth_Yadis_ProxyResolver(&$fetcher, $proxy_url = null) + { + $this->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 @@ +<?php + +/** + * The core PHP Yadis implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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: + * + * <pre> require_once "Auth/Yadis/Yadis.php";</pre> + * + * To perform Yadis discovery, first call the "discover" method + * statically with a URI parameter: + * + * <pre> $http_response = array(); + * $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + * $yadis_object = Auth_Yadis_Yadis::discover($uri, + * $http_response, $fetcher);</pre> + * + * 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 + * + * <pre> $service_list = $yadis_object->services();</pre> + * + * 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(): + * + * <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 Auth_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 + * 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; + } +} + +?> diff --git a/extlib/DB/DataObject/Cast.php b/extlib/DB/DataObject/Cast.php new file mode 100644 index 000000000..616abb55e --- /dev/null +++ b/extlib/DB/DataObject/Cast.php @@ -0,0 +1,546 @@ +<?php +/** + * Prototype Castable Object.. for DataObject queries + * + * Storage for Data that may be cast into a variety of formats. + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB_DataObject + * @author Alan Knowles <alan@akbkhome.com> + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Cast.php,v 1.15 2005/07/07 05:30:53 alan_k Exp $ + * @link http://pear.php.net/package/DB_DataObject + */ + +/** +* +* Common usages: +* // blobs +* $data = DB_DataObject_Cast::blob($somefile); +* $data = DB_DataObject_Cast::string($somefile); +* $dataObject->someblobfield = $data +* +* // dates? +* $d1 = new DB_DataObject_Cast::date('12/12/2000'); +* $d2 = new DB_DataObject_Cast::date(2000,12,30); +* $d3 = new DB_DataObject_Cast::date($d1->year, $d1->month+30, $d1->day+30); +* +* // time, datetime.. ????????? +* +* // raw sql???? +* $data = DB_DataObject_Cast::sql('cast("123123",datetime)'); +* $data = DB_DataObject_Cast::sql('NULL'); +* +* // int's/string etc. are proably pretty pointless..!!!! +* +* +* inside DB_DataObject, +* if (is_a($v,'db_dataobject_class')) { +* $value .= $v->toString(DB_DATAOBJECT_INT,'mysql'); +* } +* +* +* +* + +*/ +class DB_DataObject_Cast { + + /** + * Type of data Stored in the object.. + * + * @var string (date|blob|.....?) + * @access public + */ + var $type; + + /** + * Data For date representation + * + * @var int day/month/year + * @access public + */ + var $day; + var $month; + var $year; + + + /** + * Generic Data.. + * + * @var string + * @access public + */ + + var $value; + + + + /** + * Blob consructor + * + * create a Cast object from some raw data.. (binary) + * + * + * @param string (with binary data!) + * + * @return object DB_DataObject_Cast + * @access public + */ + + function blob($value) { + $r = new DB_DataObject_Cast; + $r->type = 'blob'; + $r->value = $value; + return $r; + } + + + /** + * String consructor (actually use if for ints and everything else!!! + * + * create a Cast object from some string (not binary) + * + * + * @param string (with binary data!) + * + * @return object DB_DataObject_Cast + * @access public + */ + + function string($value) { + $r = new DB_DataObject_Cast; + $r->type = 'string'; + $r->value = $value; + return $r; + } + + /** + * SQL constructor (for raw SQL insert) + * + * create a Cast object from some sql + * + * @param string (with binary data!) + * + * @return object DB_DataObject_Cast + * @access public + */ + + function sql($value) + { + $r = new DB_DataObject_Cast; + $r->type = 'sql'; + $r->value = $value; + return $r; + } + + + /** + * Date Constructor + * + * create a Cast object from some string (not binary) + * NO VALIDATION DONE, although some crappy re-calcing done! + * + * @param vargs... accepts + * dd/mm + * dd/mm/yyyy + * yyyy-mm + * yyyy-mm-dd + * array(yyyy,dd) + * array(yyyy,dd,mm) + * + * + * + * @return object DB_DataObject_Cast + * @access public + */ + + function date() + { + $args = func_get_args(); + switch(count($args)) { + case 0: // no args = today! + $bits = explode('-',date('Y-m-d')); + break; + case 1: // one arg = a string + + if (strpos($args[0],'/') !== false) { + $bits = array_reverse(explode('/',$args[0])); + } else { + $bits = explode('-',$args[0]); + } + break; + default: // 2 or more.. + $bits = $args; + } + if (count($bits) == 1) { // if YYYY set day = 1st.. + $bits[] = 1; + } + + if (count($bits) == 2) { // if YYYY-DD set day = 1st.. + $bits[] = 1; + } + + // if year < 1970 we cant use system tools to check it... + // so we make a few best gueses.... + // basically do date calculations for the year 2000!!! + // fix me if anyone has more time... + if (($bits[0] < 1975) || ($bits[0] > 2030)) { + $oldyear = $bits[0]; + $bits = explode('-',date('Y-m-d',mktime(1,1,1,$bits[1],$bits[2],2000))); + $bits[0] = ($bits[0] - 2000) + $oldyear; + } else { + // now mktime + $bits = explode('-',date('Y-m-d',mktime(1,1,1,$bits[1],$bits[2],$bits[0]))); + } + $r = new DB_DataObject_Cast; + $r->type = 'date'; + list($r->year,$r->month,$r->day) = $bits; + return $r; + } + + + + /** + * Data For time representation ** does not handle timezones!! + * + * @var int hour/minute/second + * @access public + */ + var $hour; + var $minute; + var $second; + + + /** + * DateTime Constructor + * + * create a Cast object from a Date/Time + * Maybe should accept a Date object.! + * NO VALIDATION DONE, although some crappy re-calcing done! + * + * @param vargs... accepts + * noargs (now) + * yyyy-mm-dd HH:MM:SS (Iso) + * array(yyyy,mm,dd,HH,MM,SS) + * + * + * @return object DB_DataObject_Cast + * @access public + * @author therion 5 at hotmail + */ + + function dateTime() + { + $args = func_get_args(); + switch(count($args)) { + case 0: // no args = now! + $datetime = date('Y-m-d G:i:s', mktime()); + + case 1: + // continue on from 0 args. + if (!isset($datetime)) { + $datetime = $args[0]; + } + + $parts = explode(' ', $datetime); + $bits = explode('-', $parts[0]); + $bits = array_merge($bits, explode(':', $parts[1])); + break; + + default: // 2 or more.. + $bits = $args; + + } + + if (count($bits) != 6) { + // PEAR ERROR? + return false; + } + + $r = DB_DataObject_Cast::date($bits[0], $bits[1], $bits[2]); + if (!$r) { + return $r; // pass thru error (False) - doesnt happen at present! + } + // change the type! + $r->type = 'datetime'; + + // should we mathematically sort this out.. + // (or just assume that no-one's dumb enough to enter 26:90:90 as a time! + $r->hour = $bits[3]; + $r->minute = $bits[4]; + $r->second = $bits[5]; + return $r; + + } + + + + /** + * time Constructor + * + * create a Cast object from a Date/Time + * Maybe should accept a Date object.! + * NO VALIDATION DONE, and no-recalcing done! + * + * @param vargs... accepts + * noargs (now) + * HH:MM:SS (Iso) + * array(HH,MM,SS) + * + * + * @return object DB_DataObject_Cast + * @access public + * @author therion 5 at hotmail + */ + function time() + { + $args = func_get_args(); + switch (count($args)) { + case 0: // no args = now! + $time = date('G:i:s', mktime()); + + case 1: + // continue on from 0 args. + if (!isset($time)) { + $time = $args[0]; + } + $bits = explode(':', $time); + break; + + default: // 2 or more.. + $bits = $args; + + } + + if (count($bits) != 3) { + return false; + } + + // now take data from bits into object fields + $r = new DB_DataObject_Cast; + $r->type = 'time'; + $r->hour = $bits[0]; + $r->minute = $bits[1]; + $r->second = $bits[2]; + return $r; + + } + + + + /** + * get the string to use in the SQL statement for this... + * + * + * @param int $to Type (DB_DATAOBJECT_* + * @param object $db DB Connection Object + * + * + * @return string + * @access public + */ + + function toString($to=false,$db) + { + // if $this->type is not set, we are in serious trouble!!!! + // values for to: + $method = 'toStringFrom'.$this->type; + return $this->$method($to,$db); + } + + /** + * get the string to use in the SQL statement from a blob of binary data + * ** Suppots only blob->postgres::bytea + * + * @param int $to Type (DB_DATAOBJECT_* + * @param object $db DB Connection Object + * + * + * @return string + * @access public + */ + function toStringFromBlob($to,$db) + { + // first weed out invalid casts.. + // in blobs can only be cast to blobs.! + + // perhaps we should support TEXT fields??? + + if (!($to & DB_DATAOBJECT_BLOB)) { + return PEAR::raiseError('Invalid Cast from a DB_DataObject_Cast::blob to something other than a blob!'); + } + + switch ($db->dsn["phptype"]) { + case 'pgsql': + return "'".pg_escape_bytea($this->value)."'::bytea"; + + case 'mysql': + return "'".mysql_real_escape_string($this->value,$db->connection)."'"; + + case 'mysqli': + // this is funny - the parameter order is reversed ;) + return "'".mysqli_real_escape_string($db->connection, $this->value)."'"; + + + + default: + return PEAR::raiseError("DB_DataObject_Cast cant handle blobs for Database:{$db->dsn['phptype']} Yet"); + } + + } + + /** + * get the string to use in the SQL statement for a blob from a string! + * ** Suppots only string->postgres::bytea + * + * + * @param int $to Type (DB_DATAOBJECT_* + * @param object $db DB Connection Object + * + * + * @return string + * @access public + */ + function toStringFromString($to,$db) + { + // first weed out invalid casts.. + // in blobs can only be cast to blobs.! + + // perhaps we should support TEXT fields??? + // + + if (!($to & DB_DATAOBJECT_BLOB)) { + return PEAR::raiseError('Invalid Cast from a DB_DataObject_Cast::string to something other than a blob!'. + ' (why not just use native features)'); + } + + switch ($db->dsn['phptype']) { + case 'pgsql': + return "'".pg_escape_string($this->value)."'::bytea"; + + case 'mysql': + return "'".mysql_real_escape_string($this->value,$db->connection)."'"; + + + case 'mysqli': + return "'".mysqli_real_escape_string($db->connection, $this->value)."'"; + + + default: + return PEAR::raiseError("DB_DataObject_Cast cant handle blobs for Database:{$db->dsn['phptype']} Yet"); + } + + } + + + /** + * get the string to use in the SQL statement for a date + * + * + * + * @param int $to Type (DB_DATAOBJECT_* + * @param object $db DB Connection Object + * + * + * @return string + * @access public + */ + function toStringFromDate($to,$db) + { + // first weed out invalid casts.. + // in blobs can only be cast to blobs.! + // perhaps we should support TEXT fields??? + // + + if (($to !== false) && !($to & DB_DATAOBJECT_DATE)) { + return PEAR::raiseError('Invalid Cast from a DB_DataObject_Cast::string to something other than a date!'. + ' (why not just use native features)'); + } + return "'{$this->year}-{$this->month}-{$this->day}'"; + } + + /** + * get the string to use in the SQL statement for a datetime + * + * + * + * @param int $to Type (DB_DATAOBJECT_* + * @param object $db DB Connection Object + * + * + * @return string + * @access public + * @author therion 5 at hotmail + */ + + function toStringFromDateTime($to,$db) + { + // first weed out invalid casts.. + // in blobs can only be cast to blobs.! + // perhaps we should support TEXT fields??? + if (($to !== false) && + !($to & (DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME))) { + return PEAR::raiseError('Invalid Cast from a ' . + ' DB_DataObject_Cast::dateTime to something other than a datetime!' . + ' (try using native features)'); + } + return "'{$this->year}-{$this->month}-{$this->day} {$this->hour}:{$this->minute}:{$this->second}'"; + } + + /** + * get the string to use in the SQL statement for a time + * + * + * + * @param int $to Type (DB_DATAOBJECT_* + * @param object $db DB Connection Object + * + * + * @return string + * @access public + * @author therion 5 at hotmail + */ + + function toStringFromTime($to,$db) + { + // first weed out invalid casts.. + // in blobs can only be cast to blobs.! + // perhaps we should support TEXT fields??? + if (($to !== false) && !($to & DB_DATAOBJECT_TIME)) { + return PEAR::raiseError('Invalid Cast from a' . + ' DB_DataObject_Cast::time to something other than a time!'. + ' (try using native features)'); + } + return "'{$this->hour}:{$this->minute}:{$this->second}'"; + } + + /** + * get the string to use in the SQL statement for a raw sql statement. + * + * @param int $to Type (DB_DATAOBJECT_* + * @param object $db DB Connection Object + * + * + * @return string + * @access public + */ + function toStringFromSql($to,$db) + { + return $this->value; + } + + + + +} + diff --git a/extlib/DB/DataObject/Error.php b/extlib/DB/DataObject/Error.php new file mode 100644 index 000000000..05a741408 --- /dev/null +++ b/extlib/DB/DataObject/Error.php @@ -0,0 +1,53 @@ +<?php +/** + * DataObjects error handler, loaded on demand... + * + * DB_DataObject_Error is a quick wrapper around pear error, so you can distinguish the + * error code source. + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB_DataObject + * @author Alan Knowles <alan@akbkhome.com> + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Error.php,v 1.3 2005/03/23 02:35:35 alan_k Exp $ + * @link http://pear.php.net/package/DB_DataObject + */ + + +class DB_DataObject_Error extends PEAR_Error +{ + + /** + * DB_DataObject_Error constructor. + * + * @param mixed $code DB error code, or string with error message. + * @param integer $mode what "error mode" to operate in + * @param integer $level what error level to use for $mode & PEAR_ERROR_TRIGGER + * @param mixed $debuginfo additional debug info, such as the last query + * + * @access public + * + * @see PEAR_Error + */ + function DB_DataObject_Error($message = '', $code = DB_ERROR, $mode = PEAR_ERROR_RETURN, + $level = E_USER_NOTICE) + { + $this->PEAR_Error('DB_DataObject Error: ' . $message, $code, $mode, $level); + + } + + + // todo : - support code -> message handling, and translated error messages... + + + +} diff --git a/extlib/DB/DataObject/Generator.php b/extlib/DB/DataObject/Generator.php new file mode 100644 index 000000000..de16af692 --- /dev/null +++ b/extlib/DB/DataObject/Generator.php @@ -0,0 +1,1553 @@ +<?php +/** + * Generation tools for DB_DataObject + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB_DataObject + * @author Alan Knowles <alan@akbkhome.com> + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Generator.php,v 1.141 2008/01/30 02:29:39 alan_k Exp $ + * @link http://pear.php.net/package/DB_DataObject + */ + + /* + * Security Notes: + * This class uses eval to create classes on the fly. + * The table name and database name are used to check the database before writing the + * class definitions, we now check for quotes and semi-colon's in both variables + * so I cant see how it would be possible to generate code even if + * for some crazy reason you took the classname and table name from User Input. + * + * If you consider that wrong, or can prove it.. let me know! + */ + + /** + * + * Config _$ptions + * [DB_DataObject_Generator] + * ; optional default = DB/DataObject.php + * extends_location = + * ; optional default = DB_DataObject + * extends = + * ; alter the extends field when updating a class (defaults to only replacing DB_DataObject) + * generator_class_rewrite = ANY|specific_name // default is DB_DataObject + * + */ + +/** + * Needed classes + * We lazy load here, due to problems with the tests not setting up include path correctly. + * FIXME! + */ +class_exists('DB_DataObject') ? '' : require_once 'DB/DataObject.php'; +//require_once('Config.php'); + +/** + * Generator class + * + * @package DB_DataObject + */ +class DB_DataObject_Generator extends DB_DataObject +{ + /* =========================================================== */ + /* Utility functions - for building db config files */ + /* =========================================================== */ + + /** + * Array of table names + * + * @var array + * @access private + */ + var $tables; + + /** + * associative array table -> array of table row objects + * + * @var array + * @access private + */ + var $_definitions; + + /** + * active table being output + * + * @var string + * @access private + */ + var $table; // active tablename + + + /** + * The 'starter' = call this to start the process + * + * @access public + * @return none + */ + function start() + { + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver']; + + $databases = array(); + foreach($options as $k=>$v) { + if (substr($k,0,9) == 'database_') { + $databases[substr($k,9)] = $v; + } + } + + if (isset($options['database'])) { + if ($db_driver == 'DB') { + require_once 'DB.php'; + $dsn = DB::parseDSN($options['database']); + } else { + require_once 'MDB2.php'; + $dsn = MDB2::parseDSN($options['database']); + } + + if (!isset($database[$dsn['database']])) { + $databases[$dsn['database']] = $options['database']; + } + } + + foreach($databases as $databasename => $database) { + if (!$database) { + continue; + } + $this->debug("CREATING FOR $databasename\n"); + $class = get_class($this); + $t = new $class; + $t->_database_dsn = $database; + + + $t->_database = $databasename; + if ($db_driver == 'DB') { + require_once 'DB.php'; + $dsn = DB::parseDSN($database); + } else { + require_once 'MDB2.php'; + $dsn = MDB2::parseDSN($database); + } + + if (($dsn['phptype'] == 'sqlite') && is_file($databasename)) { + $t->_database = basename($t->_database); + } + $t->_createTableList(); + + foreach(get_class_methods($class) as $method) { + if (substr($method,0,8 ) != 'generate') { + continue; + } + $this->debug("calling $method"); + $t->$method(); + } + } + $this->debug("DONE\n\n"); + } + + /** + * Output File was config object, now just string + * Used to generate the Tables + * + * @var string outputbuffer for table definitions + * @access private + */ + var $_newConfig; + + /** + * Build a list of tables; + * and store it in $this->tables and $this->_definitions[tablename]; + * + * @access private + * @return none + */ + function _createTableList() + { + $this->_connect(); + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + + $__DB= &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5]; + + $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver']; + $is_MDB2 = ($db_driver != 'DB') ? true : false; + + if (is_a($__DB , 'PEAR_Error')) { + return PEAR::raiseError($__DB->toString(), null, PEAR_ERROR_DIE); + } + + if (!$is_MDB2) { + // try getting a list of schema tables first. (postgres) + $__DB->expectError(DB_ERROR_UNSUPPORTED); + $this->tables = $__DB->getListOf('schema.tables'); + $__DB->popExpect(); + } else { + /** + * set portability and some modules to fetch the informations + */ + $__DB->setOption('portability', MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE); + $__DB->loadModule('Manager'); + $__DB->loadModule('Reverse'); + } + + if ((empty($this->tables) || is_a($this->tables , 'PEAR_Error'))) { + //if that fails fall back to clasic tables list. + if (!$is_MDB2) { + // try getting a list of schema tables first. (postgres) + $__DB->expectError(DB_ERROR_UNSUPPORTED); + $this->tables = $__DB->getListOf('tables'); + $__DB->popExpect(); + } else { + $this->tables = $__DB->manager->listTables(); + $sequences = $__DB->manager->listSequences(); + foreach ($sequences as $k => $v) { + $this->tables[] = $__DB->getSequenceName($v); + } + } + } + + if (is_a($this->tables , 'PEAR_Error')) { + return PEAR::raiseError($this->tables->toString(), null, PEAR_ERROR_DIE); + } + + // build views as well if asked to. + if (!empty($options['build_views'])) { + if (!$is_MDB2) { + $views = $__DB->getListOf('views'); + } else { + $views = $__DB->manager->listViews(); + } + if (is_a($views,'PEAR_Error')) { + return PEAR::raiseError( + 'Error getting Views (check the PEAR bug database for the fix to DB), ' . + $views->toString(), + null, + PEAR_ERROR_DIE + ); + } + $this->tables = array_merge ($this->tables, $views); + } + + // declare a temporary table to be filled with matching tables names + $tmp_table = array(); + + + foreach($this->tables as $table) { + if (isset($options['generator_include_regex']) && + !preg_match($options['generator_include_regex'],$table)) { + continue; + } else if (isset($options['generator_exclude_regex']) && + preg_match($options['generator_exclude_regex'],$table)) { + continue; + } + // postgres strip the schema bit from the + if (!empty($options['generator_strip_schema'])) { + $bits = explode('.', $table,2); + $table = $bits[0]; + if (count($bits) > 1) { + $table = $bits[1]; + } + } + $quotedTable = !empty($options['quote_identifiers_tableinfo']) ? + $__DB->quoteIdentifier($table) : $table; + + if (!$is_MDB2) { + + $defs = $__DB->tableInfo($quotedTable); + } else { + $defs = $__DB->reverse->tableInfo($quotedTable); + // rename the length value, so it matches db's return. + foreach ($defs as $k => $v) { + if (!isset($defs[$k]['length'])) { + continue; + } + $defs[$k]['len'] = $defs[$k]['length']; + } + } + + if (is_a($defs,'PEAR_Error')) { + // running in debug mode should pick this up as a big warning.. + $this->raiseError('Error reading tableInfo, '. $defs->toString()); + continue; + } + // cast all definitions to objects - as we deal with that better. + + + + foreach($defs as $def) { + if (!is_array($def)) { + continue; + } + + $this->_definitions[$table][] = (object) $def; + + } + // we find a matching table, just store it into a temporary array + $tmp_table[] = $table; + + + } + // the temporary table array is now the right one (tables names matching + // with regex expressions have been removed) + $this->tables = $tmp_table; + //print_r($this->_definitions); + } + + /** + * Auto generation of table data. + * + * it will output to db_oo_{database} the table definitions + * + * @access private + * @return none + */ + function generateDefinitions() + { + $this->debug("Generating Definitions file: "); + if (!$this->tables) { + $this->debug("-- NO TABLES -- \n"); + return; + } + + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + + + //$this->_newConfig = new Config('IniFile'); + $this->_newConfig = ''; + foreach($this->tables as $this->table) { + $this->_generateDefinitionsTable(); + } + $this->_connect(); + // dont generate a schema if location is not set + // it's created on the fly! + if (empty($options['schema_location']) && empty($options["ini_{$this->_database}"]) ) { + return; + } + if (!empty($options['generator_no_ini'])) { // built in ini files.. + return; + } + $base = @$options['schema_location']; + if (isset($options["ini_{$this->_database}"])) { + $file = $options["ini_{$this->_database}"]; + } else { + $file = "{$base}/{$this->_database}.ini"; + } + + if (!file_exists(dirname($file))) { + require_once 'System.php'; + System::mkdir(array('-p','-m',0755,dirname($file))); + } + $this->debug("Writing ini as {$file}\n"); + //touch($file); + $tmpname = tempnam(session_save_path(),'DataObject_'); + //print_r($this->_newConfig); + $fh = fopen($tmpname,'w'); + fwrite($fh,$this->_newConfig); + fclose($fh); + $perms = file_exists($file) ? fileperms($file) : 0755; + // windows can fail doing this. - not a perfect solution but otherwise it's getting really kludgy.. + + if (!@rename($tmpname, $file)) { + unlink($file); + rename($tmpname, $file); + } + chmod($file,$perms); + //$ret = $this->_newConfig->writeInput($file,false); + + //if (PEAR::isError($ret) ) { + // return PEAR::raiseError($ret->message,null,PEAR_ERROR_DIE); + // } + } + + /** + * generate Foreign Keys (for links.ini) + * Currenly only works with mysql / mysqli + * to use, you must set option: generate_links=true + * + * @author Pascal Schöni + */ + function generateForeignKeys() + { + $options = PEAR::getStaticProperty('DB_DataObject','options'); + if (empty($options['generate_links'])) { + return false; + } + $__DB = &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5]; + if (!in_array($__DB->phptype, array('mysql','mysqli'))) { + echo "WARNING: cant handle non-mysql introspection for defaults."; + return; // cant handle non-mysql introspection for defaults. + } + + $DB = $this->getDatabaseConnection(); + + $fk = array(); + + foreach($this->tables as $this->table) { + $res =& $DB->query('SHOW CREATE TABLE ' . $this->table); + if (PEAR::isError($res)) { + die($res->getMessage()); + } + + $text = $res->fetchRow(DB_FETCHMODE_DEFAULT, 0); + $treffer = array(); + // Extract FOREIGN KEYS + preg_match_all( + "/FOREIGN KEY \(`(\w*)`\) REFERENCES `(\w*)` \(`(\w*)`\)/i", + $text[1], + $treffer, + PREG_SET_ORDER); + + if (count($treffer) < 1) { + continue; + } + for ($i = 0; $i < count($treffer); $i++) { + $fk[$this->table][$treffer[$i][1]] = $treffer[$i][2] . ":" . $treffer[$i][3]; + } + + } + + $links_ini = ""; + + foreach($fk as $table => $details) { + $links_ini .= "[$table]\n"; + foreach ($details as $col => $ref) { + $links_ini .= "$col = $ref\n"; + } + $links_ini .= "\n"; + } + + // dont generate a schema if location is not set + // it's created on the fly! + $options = PEAR::getStaticProperty('DB_DataObject','options'); + + if (empty($options['schema_location'])) { + return; + } + + + $file = "{$options['schema_location']}/{$this->_database}.links.ini"; + + if (!file_exists(dirname($file))) { + require_once 'System.php'; + System::mkdir(array('-p','-m',0755,dirname($file))); + } + + $this->debug("Writing ini as {$file}\n"); + + //touch($file); // not sure why this is needed? + $tmpname = tempnam(session_save_path(),'DataObject_'); + + $fh = fopen($tmpname,'w'); + fwrite($fh,$links_ini); + fclose($fh); + $perms = file_exists($file) ? fileperms($file) : 0755; + // windows can fail doing this. - not a perfect solution but otherwise it's getting really kludgy.. + if (!@rename($tmpname, $file)) { + unlink($file); + rename($tmpname, $file); + } + chmod($file, $perms); + } + + + /** + * The table geneation part + * + * @access private + * @return tabledef and keys array. + */ + function _generateDefinitionsTable() + { + global $_DB_DATAOBJECT; + + $defs = $this->_definitions[$this->table]; + $this->_newConfig .= "\n[{$this->table}]\n"; + $keys_out = "\n[{$this->table}__keys]\n"; + $keys_out_primary = ''; + $keys_out_secondary = ''; + if (@$_DB_DATAOBJECT['CONFIG']['debug'] > 2) { + echo "TABLE STRUCTURE FOR {$this->table}\n"; + print_r($defs); + } + $DB = $this->getDatabaseConnection(); + $dbtype = $DB->phptype; + + $ret = array( + 'table' => array(), + 'keys' => array(), + ); + + $ret_keys_primary = array(); + $ret_keys_secondary = array(); + + + + foreach($defs as $t) { + + $n=0; + $write_ini = true; + + + switch (strtoupper($t->type)) { + + case 'INT': + case 'INT2': // postgres + case 'INT4': // postgres + case 'INT8': // postgres + case 'SERIAL4': // postgres + case 'SERIAL8': // postgres + case 'INTEGER': + case 'TINYINT': + case 'SMALLINT': + case 'MEDIUMINT': + case 'BIGINT': + $type = DB_DATAOBJECT_INT; + if ($t->len == 1) { + $type += DB_DATAOBJECT_BOOL; + } + break; + + case 'REAL': + case 'DOUBLE': + case 'DOUBLE PRECISION': // double precision (firebird) + case 'FLOAT': + case 'FLOAT4': // real (postgres) + case 'FLOAT8': // double precision (postgres) + case 'DECIMAL': + case 'MONEY': // mssql and maybe others + case 'NUMERIC': + case 'NUMBER': // oci8 + $type = DB_DATAOBJECT_INT; // should really by FLOAT!!! / MONEY... + break; + + case 'YEAR': + $type = DB_DATAOBJECT_INT; + break; + + case 'BIT': + case 'BOOL': + case 'BOOLEAN': + + $type = DB_DATAOBJECT_BOOL; + // postgres needs to quote '0' + if ($dbtype == 'pgsql') { + $type += DB_DATAOBJECT_STR; + } + break; + + case 'STRING': + case 'CHAR': + case 'VARCHAR': + case 'VARCHAR2': + case 'TINYTEXT': + + case 'ENUM': + case 'SET': // not really but oh well + case 'TIMESTAMPTZ': // postgres + case 'BPCHAR': // postgres + case 'INTERVAL': // postgres (eg. '12 days') + + case 'CIDR': // postgres IP net spec + case 'INET': // postgres IP + case 'MACADDR': // postgress network Mac address. + + case 'INTEGER[]': // postgres type + case 'BOOLEAN[]': // postgres type + + $type = DB_DATAOBJECT_STR; + break; + + case 'TEXT': + case 'MEDIUMTEXT': + case 'LONGTEXT': + + $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_TXT; + break; + + + case 'DATE': + $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE; + break; + + case 'TIME': + $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_TIME; + break; + + + case 'DATETIME': + + $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME; + break; + + case 'TIMESTAMP': // do other databases use this??? + + $type = ($dbtype == 'mysql') ? + DB_DATAOBJECT_MYSQLTIMESTAMP : + DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME; + break; + + + case 'TINYBLOB': + case 'BLOB': /// these should really be ignored!!!??? + case 'MEDIUMBLOB': + case 'LONGBLOB': + case 'BYTEA': // postgres blob support.. + $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_BLOB; + break; + default: + echo "*****************************************************************\n". + "** WARNING UNKNOWN TYPE **\n". + "** Found column '{$t->name}', of type '{$t->type}' **\n". + "** Please submit a bug, describe what type you expect this **\n". + "** column to be **\n". + "** ---------POSSIBLE FIX / WORKAROUND -------------------------**\n". + "** Try using MDB2 as the backend - eg set the config option **\n". + "** db_driver = MDB2 **\n". + "*****************************************************************\n"; + $write_ini = false; + break; + } + + if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $t->name)) { + echo "*****************************************************************\n". + "** WARNING COLUMN NAME UNUSABLE **\n". + "** Found column '{$t->name}', of type '{$t->type}' **\n". + "** Since this column name can't be converted to a php variable **\n". + "** name, and the whole idea of mapping would result in a mess **\n". + "** This column has been ignored... **\n". + "*****************************************************************\n"; + continue; + } + + if (!strlen(trim($t->name))) { + continue; // is this a bug? + } + + if (preg_match('/not[ _]null/i',$t->flags)) { + $type += DB_DATAOBJECT_NOTNULL; + } + + + if (in_array($t->name,array('null','yes','no','true','false'))) { + echo "*****************************************************************\n". + "** WARNING **\n". + "** Found column '{$t->name}', which is invalid in an .ini file **\n". + "** This line will not be writen to the file - you will have **\n". + "** define the keys()/method manually. **\n". + "*****************************************************************\n"; + $write_ini = false; + } else { + $this->_newConfig .= "{$t->name} = $type\n"; + } + + $ret['table'][$t->name] = $type; + // i've no idea if this will work well on other databases? + // only use primary key or nextval(), cause the setFrom blocks you setting all key items... + // if no keys exist fall back to using unique + //echo "\n{$t->name} => {$t->flags}\n"; + if (preg_match("/(auto_increment|nextval\()/i",rawurldecode($t->flags)) + || (isset($t->autoincrement) && ($t->autoincrement === true))) { + + // native sequences = 2 + if ($write_ini) { + $keys_out_primary .= "{$t->name} = N\n"; + } + $ret_keys_primary[$t->name] = 'N'; + + } else if (preg_match("/(primary|unique)/i",$t->flags)) { + // keys.. = 1 + $key_type = 'K'; + if (!preg_match("/(primary)/i",$t->flags)) { + $key_type = 'U'; + } + + if ($write_ini) { + $keys_out_secondary .= "{$t->name} = {$key_type}\n"; + } + $ret_keys_secondary[$t->name] = $key_type; + } + + + } + + $this->_newConfig .= $keys_out . (empty($keys_out_primary) ? $keys_out_secondary : $keys_out_primary); + $ret['keys'] = empty($keys_out_primary) ? $ret_keys_secondary : $ret_keys_primary; + + if (@$_DB_DATAOBJECT['CONFIG']['debug'] > 2) { + print_r(array("dump for {$this->table}", $ret)); + } + + return $ret; + + + } + + /** + * Convert a table name into a class name -> override this if you want a different mapping + * + * @access public + * @return string class name; + */ + + + function getClassNameFromTableName($table) + { + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + $class_prefix = empty($options['class_prefix']) ? '' : $options['class_prefix']; + return $class_prefix.preg_replace('/[^A-Z0-9]/i','_',ucfirst(trim($this->table))); + } + + + /** + * Convert a table name into a file name -> override this if you want a different mapping + * + * @access public + * @return string file name; + */ + + + function getFileNameFromTableName($table) + { + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + $base = $options['class_location']; + if (strpos($base,'%s') !== false) { + $base = dirname($base); + } + if (!file_exists($base)) { + require_once 'System.php'; + System::mkdir(array('-p',$base)); + } + if (strpos($options['class_location'],'%s') !== false) { + $outfilename = sprintf($options['class_location'], + preg_replace('/[^A-Z0-9]/i','_',ucfirst($this->table))); + } else { + $outfilename = "{$base}/".preg_replace('/[^A-Z0-9]/i','_',ucfirst($this->table)).".php"; + } + return $outfilename; + + } + + + /** + * Convert a column name into a method name (usually prefixed by get/set/validateXXXXX) + * + * @access public + * @return string method name; + */ + + + function getMethodNameFromColumnName($col) + { + return ucfirst($col); + } + + + + + /* + * building the class files + * for each of the tables output a file! + */ + function generateClasses() + { + //echo "Generating Class files: \n"; + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + + + if ($extends = @$options['extends']) { + $this->_extends = $extends; + $this->_extendsFile = $options['extends_location']; + } + + foreach($this->tables as $this->table) { + $this->table = trim($this->table); + $this->classname = $this->getClassNameFromTableName($this->table); + $i = ''; + $outfilename = $this->getFileNameFromTableName($this->table); + + $oldcontents = ''; + if (file_exists($outfilename)) { + // file_get_contents??? + $oldcontents = implode('',file($outfilename)); + } + + $out = $this->_generateClassTable($oldcontents); + $this->debug( "writing $this->classname\n"); + $tmpname = tempnam(session_save_path(),'DataObject_'); + + $fh = fopen($tmpname, "w"); + fputs($fh,$out); + fclose($fh); + $perms = file_exists($outfilename) ? fileperms($outfilename) : 0755; + + // windows can fail doing this. - not a perfect solution but otherwise it's getting really kludgy.. + if (!@rename($tmpname, $outfilename)) { + unlink($outfilename); + rename($tmpname, $outfilename); + } + + chmod($outfilename, $perms); + } + //echo $out; + } + + /** + * class being extended (can be overridden by [DB_DataObject_Generator] extends=xxxx + * + * @var string + * @access private + */ + var $_extends = 'DB_DataObject'; + + /** + * line to use for require('DB/DataObject.php'); + * + * @var string + * @access private + */ + var $_extendsFile = "DB/DataObject.php"; + + /** + * class being generated + * + * @var string + * @access private + */ + var $_className; + + /** + * The table class geneation part - single file. + * + * @access private + * @return none + */ + function _generateClassTable($input = '') + { + // title = expand me! + $foot = ""; + $head = "<?php\n/**\n * Table Definition for {$this->table}\n"; + $head .= $this->derivedHookPageLevelDocBlock(); + $head .= " */\n"; + $head .= $this->derivedHookExtendsDocBlock(); + + + // requires + $head .= "require_once '{$this->_extendsFile}';\n\n"; + // add dummy class header in... + // class + $head .= $this->derivedHookClassDocBlock(); + $head .= "class {$this->classname} extends {$this->_extends} \n{"; + + $body = "\n ###START_AUTOCODE\n"; + $body .= " /* the code below is auto generated do not remove the above tag */\n\n"; + // table + $padding = (30 - strlen($this->table)); + $padding = ($padding < 2) ? 2 : $padding; + + $p = str_repeat(' ',$padding) ; + + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + + + $var = (substr(phpversion(),0,1) > 4) ? 'public' : 'var'; + $var = !empty($options['generator_var_keyword']) ? $options['generator_var_keyword'] : $var; + + + $body .= " {$var} \$__table = '{$this->table}'; {$p}// table name\n"; + + + // if we are using the option database_{databasename} = dsn + // then we should add var $_database = here + // as database names may not always match.. + + + + + if (isset($options["database_{$this->_database}"])) { + $body .= " {$var} \$_database = '{$this->_database}'; {$p}// database name (used with database_{*} config)\n"; + } + + + if (!empty($options['generator_novars'])) { + $var = '//'.$var; + } + + $defs = $this->_definitions[$this->table]; + + // show nice information! + $connections = array(); + $sets = array(); + foreach($defs as $t) { + if (!strlen(trim($t->name))) { + continue; + } + if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $t->name)) { + echo "*****************************************************************\n". + "** WARNING COLUMN NAME UNUSABLE **\n". + "** Found column '{$t->name}', of type '{$t->type}' **\n". + "** Since this column name can't be converted to a php variable **\n". + "** name, and the whole idea of mapping would result in a mess **\n". + "** This column has been ignored... **\n". + "*****************************************************************\n"; + continue; + } + + + $padding = (30 - strlen($t->name)); + if ($padding < 2) $padding =2; + $p = str_repeat(' ',$padding) ; + + $body .=" {$var} \${$t->name}; {$p}// {$t->type}({$t->len}) {$t->flags}\n"; + + // can not do set as PEAR::DB table info doesnt support it. + //if (substr($t->Type,0,3) == "set") + // $sets[$t->Field] = "array".substr($t->Type,3); + $body .= $this->derivedHookVar($t,$padding); + } + + // THIS IS TOTALLY BORKED old FC creation + // IT WILL BE REMOVED!!!!! in DataObjects 1.6 + // grep -r __clone * to find all it's uses + // and replace them with $x = clone($y); + // due to the change in the PHP5 clone design. + + if ( substr(phpversion(),0,1) < 5) { + $body .= "\n"; + $body .= " /* ZE2 compatibility trick*/\n"; + $body .= " function __clone() { return \$this;}\n"; + } + + // simple creation tools ! (static stuff!) + $body .= "\n"; + $body .= " /* Static get */\n"; + $body .= " function staticGet(\$k,\$v=NULL) { return DB_DataObject::staticGet('{$this->classname}',\$k,\$v); }\n"; + + // generate getter and setter methods + $body .= $this->_generateGetters($input); + $body .= $this->_generateSetters($input); + + /* + theoretically there is scope here to introduce 'list' methods + based up 'xxxx_up' column!!! for heiracitcal trees.. + */ + + // set methods + //foreach ($sets as $k=>$v) { + // $kk = strtoupper($k); + // $body .=" function getSets{$k}() { return {$v}; }\n"; + //} + + if (!empty($options['generator_no_ini'])) { + $def = $this->_generateDefinitionsTable(); // simplify this!? + $body .= $this->_generateTableFunction($def['table']); + $body .= $this->_generateKeysFunction($def['keys']); + $body .= $this->_generateSequenceKeyFunction($def); + $body .= $this->_generateDefaultsFunction($this->table, $def['table']); + } else if (!empty($options['generator_add_defaults'])) { + // I dont really like doing it this way (adding another option) + // but it helps on older projects. + $def = $this->_generateDefinitionsTable(); // simplify this!? + $body .= $this->_generateDefaultsFunction($this->table,$def['table']); + + } + $body .= $this->derivedHookFunctions($input); + + $body .= "\n /* the code above is auto generated do not remove the tag below */"; + $body .= "\n ###END_AUTOCODE\n"; + + + // stubs.. + + if (!empty($options['generator_add_validate_stubs'])) { + foreach($defs as $t) { + if (!strlen(trim($t->name))) { + continue; + } + $validate_fname = 'validate' . $this->getMethodNameFromColumnName($t->name); + // dont re-add it.. + if (preg_match('/\s+function\s+' . $validate_fname . '\s*\(/i', $input)) { + continue; + } + $body .= "\n function {$validate_fname}()\n {\n return false;\n }\n"; + } + } + + + + + $foot .= "}\n"; + $full = $head . $body . $foot; + + if (!$input) { + return $full; + } + if (!preg_match('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n)/s',$input)) { + return $full; + } + if (!preg_match('/(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s',$input)) { + return $full; + } + + + /* this will only replace extends DB_DataObject by default, + unless use set generator_class_rewrite to ANY or a name*/ + + $class_rewrite = 'DB_DataObject'; + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + if (empty($options['generator_class_rewrite']) || !($class_rewrite = $options['generator_class_rewrite'])) { + $class_rewrite = 'DB_DataObject'; + } + if ($class_rewrite == 'ANY') { + $class_rewrite = '[a-z_]+'; + } + + $input = preg_replace( + '/(\n|\r\n)class\s*[a-z0-9_]+\s*extends\s*' .$class_rewrite . '\s*(\n|\r\n)\{(\n|\r\n)/si', + "\nclass {$this->classname} extends {$this->_extends} \n{\n", + $input); + + $ret = preg_replace( + '/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', + $body,$input); + + if (!strlen($ret)) { + return PEAR::raiseError( + "PREG_REPLACE failed to replace body, - you probably need to set these in your php.ini\n". + "pcre.backtrack_limit=1000000\n". + "pcre.recursion_limit=1000000\n" + ,null, PEAR_ERROR_DIE); + } + + return $ret; + } + + /** + * hook to add extra methods to all classes + * + * called once for each class, use with $this->table and + * $this->_definitions[$this->table], to get data out of the current table, + * use it to add extra methods to the default classes. + * + * @access public + * @return string added to class eg. functions. + */ + function derivedHookFunctions($input = "") + { + // This is so derived generator classes can generate functions + // It MUST NOT be changed here!!! + return ""; + } + + /** + * hook for var lines + * called each time a var line is generated, override to add extra var + * lines + * + * @param object t containing type,len,flags etc. from tableInfo call + * @param int padding number of spaces + * @access public + * @return string added to class eg. functions. + */ + function derivedHookVar(&$t,$padding) + { + // This is so derived generator classes can generate variabels + // It MUST NOT be changed here!!! + return ""; + } + + /** + * hook to add extra page-level (in terms of phpDocumentor) DocBlock + * + * called once for each class, use it add extra page-level docs + * @access public + * @return string added to class eg. functions. + */ + function derivedHookPageLevelDocBlock() { + return ''; + } + + /** + * hook to add extra doc block (in terms of phpDocumentor) to extend string + * + * called once for each class, use it add extra comments to extends + * string (require_once...) + * @access public + * @return string added to class eg. functions. + */ + function derivedHookExtendsDocBlock() { + return ''; + } + + /** + * hook to add extra class level DocBlock (in terms of phpDocumentor) + * + * called once for each class, use it add extra comments to class + * string (require_once...) + * @access public + * @return string added to class eg. functions. + */ + function derivedHookClassDocBlock() { + return ''; + } + + /** + + /** + * getProxyFull - create a class definition on the fly and instantate it.. + * + * similar to generated files - but also evals the class definitoin code. + * + * + * @param string database name + * @param string table name of table to create proxy for. + * + * + * @return object Instance of class. or PEAR Error + * @access public + */ + function getProxyFull($database,$table) + { + + if ($err = $this->fillTableSchema($database,$table)) { + return $err; + } + + + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + $class_prefix = empty($options['class_prefix']) ? '' : $options['class_prefix']; + + if ($extends = @$options['extends']) { + $this->_extends = $extends; + $this->_extendsFile = $options['extends_location']; + } + $classname = $this->classname = $this->getClassNameFromTableName($this->table); + + $out = $this->_generateClassTable(); + //echo $out; + eval('?>'.$out); + return new $classname; + + } + + /** + * fillTableSchema - set the database schema on the fly + * + * + * + * @param string database name + * @param string table name of table to create schema info for + * + * @return none | PEAR::error() + * @access public + */ + function fillTableSchema($database,$table) + { + global $_DB_DATAOBJECT; + // a little bit of sanity testing. + if ((false !== strpos($database,"'")) || (false !== strpos($database,";"))) { + return PEAR::raiseError("Error: Database name contains a quote or semi-colon", null, PEAR_ERROR_DIE); + } + + $this->_database = $database; + + $this->_connect(); + $table = trim($table); + + // a little bit of sanity testing. + if ((false !== strpos($table,"'")) || (false !== strpos($table,";"))) { + return PEAR::raiseError("Error: Table contains a quote or semi-colon", null, PEAR_ERROR_DIE); + } + $__DB= &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5]; + + + $options = PEAR::getStaticProperty('DB_DataObject','options'); + $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver']; + $is_MDB2 = ($db_driver != 'DB') ? true : false; + + if (!$is_MDB2) { + // try getting a list of schema tables first. (postgres) + $__DB->expectError(DB_ERROR_UNSUPPORTED); + $this->tables = $__DB->getListOf('schema.tables'); + $__DB->popExpect(); + } else { + /** + * set portability and some modules to fetch the informations + */ + $__DB->setOption('portability', MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE); + $__DB->loadModule('Manager'); + $__DB->loadModule('Reverse'); + } + $quotedTable = !empty($options['quote_identifiers']) ? + $__DB->quoteIdentifier($table) : $table; + + if (!$is_MDB2) { + $defs = $__DB->tableInfo($quotedTable); + } else { + $defs = $__DB->reverse->tableInfo($quotedTable); + foreach ($defs as $k => $v) { + if (!isset($defs[$k]['length'])) { + continue; + } + $defs[$k]['len'] = $defs[$k]['length']; + } + } + + + + + if (PEAR::isError($defs)) { + return $defs; + } + if (@$_DB_DATAOBJECT['CONFIG']['debug'] > 2) { + $this->debug("getting def for $database/$table",'fillTable'); + $this->debug(print_r($defs,true),'defs'); + } + // cast all definitions to objects - as we deal with that better. + + + foreach($defs as $def) { + if (is_array($def)) { + $this->_definitions[$table][] = (object) $def; + } + } + + $this->table = trim($table); + $ret = $this->_generateDefinitionsTable(); + + $_DB_DATAOBJECT['INI'][$database][$table] = $ret['table']; + $_DB_DATAOBJECT['INI'][$database][$table.'__keys'] = $ret['keys']; + return false; + + } + + /** + * Generate getter methods for class definition + * + * @param string $input Existing class contents + * @return string + * @access public + */ + function _generateGetters($input) + { + + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + $getters = ''; + + // only generate if option is set to true + if (empty($options['generate_getters'])) { + return ''; + } + + // remove auto-generated code from input to be able to check if the method exists outside of the auto-code + $input = preg_replace('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', '', $input); + + $getters .= "\n\n"; + $defs = $this->_definitions[$this->table]; + + // loop through properties and create getter methods + foreach ($defs = $defs as $t) { + + // build mehtod name + $methodName = 'get' . $this->getMethodNameFromColumnName($t->name); + + if (!strlen(trim($t->name)) || preg_match("/function[\s]+[&]?$methodName\(/i", $input)) { + continue; + } + + $getters .= " /**\n"; + $getters .= " * Getter for \${$t->name}\n"; + $getters .= " *\n"; + $getters .= (stristr($t->flags, 'multiple_key')) ? " * @return object\n" + : " * @return {$t->type}\n"; + $getters .= " * @access public\n"; + $getters .= " */\n"; + $getters .= (substr(phpversion(),0,1) > 4) ? ' public ' + : ' '; + $getters .= "function $methodName() {\n"; + $getters .= " return \$this->{$t->name};\n"; + $getters .= " }\n\n"; + } + + + return $getters; + } + + + /** + * Generate setter methods for class definition + * + * @param string Existing class contents + * @return string + * @access public + */ + function _generateSetters($input) + { + + $options = &PEAR::getStaticProperty('DB_DataObject','options'); + $setters = ''; + + // only generate if option is set to true + if (empty($options['generate_setters'])) { + return ''; + } + + // remove auto-generated code from input to be able to check if the method exists outside of the auto-code + $input = preg_replace('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', '', $input); + + $setters .= "\n"; + $defs = $this->_definitions[$this->table]; + + // loop through properties and create setter methods + foreach ($defs = $defs as $t) { + + // build mehtod name + $methodName = 'set' . $this->getMethodNameFromColumnName($t->name); + + if (!strlen(trim($t->name)) || preg_match("/function[\s]+[&]?$methodName\(/i", $input)) { + continue; + } + + $setters .= " /**\n"; + $setters .= " * Setter for \${$t->name}\n"; + $setters .= " *\n"; + $setters .= " * @param mixed input value\n"; + $setters .= " * @access public\n"; + $setters .= " */\n"; + $setters .= (substr(phpversion(),0,1) > 4) ? ' public ' + : ' '; + $setters .= "function $methodName(\$value) {\n"; + $setters .= " \$this->{$t->name} = \$value;\n"; + $setters .= " }\n\n"; + } + + + return $setters; + } + /** + * Generate table Function - used when generator_no_ini is set. + * + * @param array table array. + * @return string + * @access public + */ + function _generateTableFunction($def) + { + $defines = explode(',','INT,STR,DATE,TIME,BOOL,TXT,BLOB,NOTNULL,MYSQLTIMESTAMP'); + + $ret = "\n" . + " function table()\n" . + " {\n" . + " return array(\n"; + + foreach($def as $k=>$v) { + $str = '0'; + foreach($defines as $dn) { + if ($v & constant('DB_DATAOBJECT_' . $dn)) { + $str .= ' + DB_DATAOBJECT_' . $dn; + } + } + if (strlen($str) > 1) { + $str = substr($str,3); // strip the 0 + + } + // hopefully addslashes is good enough here!!! + $ret .= ' \''.addslashes($k).'\' => ' . $str . ",\n"; + } + return $ret . " );\n" . + " }\n"; + + + + } + /** + * Generate keys Function - used generator_no_ini is set. + * + * @param array keys array. + * @return string + * @access public + */ + function _generateKeysFunction($def) + { + + $ret = "\n" . + " function keys()\n" . + " {\n" . + " return array("; + + foreach($def as $k=>$type) { + // hopefully addslashes is good enough here!!! + $ret .= '\''.addslashes($k).'\', '; + } + $ret = preg_replace('#, $#', '', $ret); + return $ret . ");\n" . + " }\n"; + + + + } + /** + * Generate sequenceKey Function - used generator_no_ini is set. + * + * @param array table and key definition. + * @return string + * @access public + */ + function _generateSequenceKeyFunction($def) + { + + //print_r($def); + // DB_DataObject::debugLevel(5); + global $_DB_DATAOBJECT; + // print_r($def); + + + $dbtype = $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['phptype']; + $realkeys = $def['keys']; + $keys = array_keys($realkeys); + $usekey = isset($keys[0]) ? $keys[0] : false; + $table = $def['table']; + + + $seqname = false; + + + + + $ar = array(false,false,false); + if ($usekey !== false) { + if (!empty($_DB_DATAOBJECT['CONFIG']['sequence_'.$this->__table])) { + $usekey = $_DB_DATAOBJECT['CONFIG']['sequence_'.$this->__table]; + if (strpos($usekey,':') !== false) { + list($usekey,$seqname) = explode(':',$usekey); + } + } + + if (in_array($dbtype , array( 'mysql', 'mysqli', 'mssql', 'ifx')) && + ($table[$usekey] & DB_DATAOBJECT_INT) && + isset($realkeys[$usekey]) && ($realkeys[$usekey] == 'N') + ) { + // use native sequence keys. + $ar = array($usekey,true,$seqname); + } else { + // use generated sequence keys.. + if ($table[$usekey] & DB_DATAOBJECT_INT) { + $ar = array($usekey,false,$seqname); + } + } + } + + + + + $ret = "\n" . + " function sequenceKey() // keyname, use native, native name\n" . + " {\n" . + " return array("; + foreach($ar as $v) { + switch (gettype($v)) { + case 'boolean': + $ret .= ($v ? 'true' : 'false') . ', '; + break; + + case 'string': + $ret .= "'" . $v . "', "; + break; + + default: // eak + $ret .= "null, "; + + } + } + $ret = preg_replace('#, $#', '', $ret); + return $ret . ");\n" . + " }\n"; + + } + /** + * Generate defaults Function - used generator_add_defaults or generator_no_ini is set. + * Only supports mysql and mysqli ... welcome ideas for more.. + * + * + * @param array table and key definition. + * @return string + * @access public + */ + function _generateDefaultsFunction($table,$defs) + { + $__DB= &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5]; + if (!in_array($__DB->phptype, array('mysql','mysqli'))) { + return; // cant handle non-mysql introspection for defaults. + } + $options = PEAR::getStaticProperty('DB_DataObject','options'); + $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver']; + $method = $db_driver == 'DB' ? 'getAll' : 'queryAll'; + $res = $__DB->$method('DESCRIBE ' . $table,DB_FETCHMODE_ASSOC); + $defaults = array(); + foreach($res as $ar) { + // this is initially very dumb... -> and it may mess up.. + $type = $defs[$ar['Field']]; + + switch (true) { + + case (is_null( $ar['Default'])): + $defaults[$ar['Field']] = 'null'; + break; + + case ($type & DB_DATAOBJECT_DATE): + case ($type & DB_DATAOBJECT_TIME): + case ($type & DB_DATAOBJECT_MYSQLTIMESTAMP): // not supported yet.. + break; + + case ($type & DB_DATAOBJECT_BOOL): + $defaults[$ar['Field']] = (int)(boolean) $ar['Default']; + break; + + + case ($type & DB_DATAOBJECT_STR): + $defaults[$ar['Field']] = "'" . addslashes($ar['Default']) . "'"; + break; + + + default: // hopefully eveything else... - numbers etc. + if (!strlen($ar['Default'])) { + continue; + } + if (is_numeric($ar['Default'])) { + $defaults[$ar['Field']] = $ar['Default']; + } + break; + + } + //var_dump(array($ar['Field'], $ar['Default'], $defaults[$ar['Field']])); + } + if (empty($defaults)) { + return; + } + + $ret = "\n" . + " function defaults() // column default values \n" . + " {\n" . + " return array(\n"; + foreach($defaults as $k=>$v) { + $ret .= ' \''.addslashes($k).'\' => ' . $v . ",\n"; + } + return $ret . " );\n" . + " }\n"; + + + + + } + + + + + +} diff --git a/extlib/DB/DataObject/createTables.php b/extlib/DB/DataObject/createTables.php new file mode 100755 index 000000000..c0659574e --- /dev/null +++ b/extlib/DB/DataObject/createTables.php @@ -0,0 +1,59 @@ +#!/usr/bin/php -q +<?php +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Alan Knowles <alan@akbkhome.com> +// +----------------------------------------------------------------------+ +// +// $Id: createTables.php,v 1.24 2006/01/13 01:27:55 alan_k Exp $ +// + +// since this version doesnt use overload, +// and I assume anyone using custom generators should add this.. + +define('DB_DATAOBJECT_NO_OVERLOAD',1); + +//require_once 'DB/DataObject/Generator.php'; +require_once 'DB/DataObject/Generator.php'; + +if (!ini_get('register_argc_argv')) { + PEAR::raiseError("\nERROR: You must turn register_argc_argv On in you php.ini file for this to work\neg.\n\nregister_argc_argv = On\n\n", null, PEAR_ERROR_DIE); + exit; +} + +if (!@$_SERVER['argv'][1]) { + PEAR::raiseError("\nERROR: createTable.php usage:\n\nC:\php\pear\DB\DataObjects\createTable.php example.ini\n\n", null, PEAR_ERROR_DIE); + exit; +} + +$config = parse_ini_file($_SERVER['argv'][1], true); +foreach($config as $class=>$values) { + $options = &PEAR::getStaticProperty($class,'options'); + $options = $values; +} + + +$options = &PEAR::getStaticProperty('DB_DataObject','options'); +if (empty($options)) { + PEAR::raiseError("\nERROR: could not read ini file\n\n", null, PEAR_ERROR_DIE); + exit; +} +set_time_limit(0); + +// use debug level from file if set.. +DB_DataObject::debugLevel(isset($options['debug']) ? $options['debug'] : 1); + +$generator = new DB_DataObject_Generator; +$generator->start(); + diff --git a/extlib/DB/common.php b/extlib/DB/common.php new file mode 100644 index 000000000..c51323d25 --- /dev/null +++ b/extlib/DB/common.php @@ -0,0 +1,2262 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * Contains the DB_common base class + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: common.php,v 1.144 2007/11/26 22:54:03 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the PEAR class so it can be extended from + */ +require_once 'PEAR.php'; + +/** + * DB_common is the base class from which each database driver class extends + * + * All common methods are declared here. If a given DBMS driver contains + * a particular method, that method will overload the one here. + * + * @category Database + * @package DB + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_common extends PEAR +{ + // {{{ properties + + /** + * The current default fetch mode + * @var integer + */ + var $fetchmode = DB_FETCHMODE_ORDERED; + + /** + * The name of the class into which results should be fetched when + * DB_FETCHMODE_OBJECT is in effect + * + * @var string + */ + var $fetchmode_object_class = 'stdClass'; + + /** + * Was a connection present when the object was serialized()? + * @var bool + * @see DB_common::__sleep(), DB_common::__wake() + */ + var $was_connected = null; + + /** + * The most recently executed query + * @var string + */ + var $last_query = ''; + + /** + * Run-time configuration options + * + * The 'optimize' option has been deprecated. Use the 'portability' + * option instead. + * + * @var array + * @see DB_common::setOption() + */ + var $options = array( + 'result_buffering' => 500, + 'persistent' => false, + 'ssl' => false, + 'debug' => 0, + 'seqname_format' => '%s_seq', + 'autofree' => false, + 'portability' => DB_PORTABILITY_NONE, + 'optimize' => 'performance', // Deprecated. Use 'portability'. + ); + + /** + * The parameters from the most recently executed query + * @var array + * @since Property available since Release 1.7.0 + */ + var $last_parameters = array(); + + /** + * The elements from each prepared statement + * @var array + */ + var $prepare_tokens = array(); + + /** + * The data types of the various elements in each prepared statement + * @var array + */ + var $prepare_types = array(); + + /** + * The prepared queries + * @var array + */ + var $prepared_queries = array(); + + /** + * Flag indicating that the last query was a manipulation query. + * @access protected + * @var boolean + */ + var $_last_query_manip = false; + + /** + * Flag indicating that the next query <em>must</em> be a manipulation + * query. + * @access protected + * @var boolean + */ + var $_next_query_manip = false; + + + // }}} + // {{{ DB_common + + /** + * This constructor calls <kbd>$this->PEAR('DB_Error')</kbd> + * + * @return void + */ + function DB_common() + { + $this->PEAR('DB_Error'); + } + + // }}} + // {{{ __sleep() + + /** + * Automatically indicates which properties should be saved + * when PHP's serialize() function is called + * + * @return array the array of properties names that should be saved + */ + function __sleep() + { + if ($this->connection) { + // Don't disconnect(), people use serialize() for many reasons + $this->was_connected = true; + } else { + $this->was_connected = false; + } + if (isset($this->autocommit)) { + return array('autocommit', + 'dbsyntax', + 'dsn', + 'features', + 'fetchmode', + 'fetchmode_object_class', + 'options', + 'was_connected', + ); + } else { + return array('dbsyntax', + 'dsn', + 'features', + 'fetchmode', + 'fetchmode_object_class', + 'options', + 'was_connected', + ); + } + } + + // }}} + // {{{ __wakeup() + + /** + * Automatically reconnects to the database when PHP's unserialize() + * function is called + * + * The reconnection attempt is only performed if the object was connected + * at the time PHP's serialize() function was run. + * + * @return void + */ + function __wakeup() + { + if ($this->was_connected) { + $this->connect($this->dsn, $this->options); + } + } + + // }}} + // {{{ __toString() + + /** + * Automatic string conversion for PHP 5 + * + * @return string a string describing the current PEAR DB object + * + * @since Method available since Release 1.7.0 + */ + function __toString() + { + $info = strtolower(get_class($this)); + $info .= ': (phptype=' . $this->phptype . + ', dbsyntax=' . $this->dbsyntax . + ')'; + if ($this->connection) { + $info .= ' [connected]'; + } + return $info; + } + + // }}} + // {{{ toString() + + /** + * DEPRECATED: String conversion method + * + * @return string a string describing the current PEAR DB object + * + * @deprecated Method deprecated in Release 1.7.0 + */ + function toString() + { + return $this->__toString(); + } + + // }}} + // {{{ quoteString() + + /** + * DEPRECATED: Quotes a string so it can be safely used within string + * delimiters in a query + * + * @param string $string the string to be quoted + * + * @return string the quoted string + * + * @see DB_common::quoteSmart(), DB_common::escapeSimple() + * @deprecated Method deprecated some time before Release 1.2 + */ + function quoteString($string) + { + $string = $this->quote($string); + if ($string{0} == "'") { + return substr($string, 1, -1); + } + return $string; + } + + // }}} + // {{{ quote() + + /** + * DEPRECATED: Quotes a string so it can be safely used in a query + * + * @param string $string the string to quote + * + * @return string the quoted string or the string <samp>NULL</samp> + * if the value submitted is <kbd>null</kbd>. + * + * @see DB_common::quoteSmart(), DB_common::escapeSimple() + * @deprecated Deprecated in release 1.6.0 + */ + function quote($string = null) + { + return ($string === null) ? 'NULL' + : "'" . str_replace("'", "''", $string) . "'"; + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quotes a string so it can be safely used as a table or column name + * + * Delimiting style depends on which database driver is being used. + * + * NOTE: just because you CAN use delimited identifiers doesn't mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. + * + * Portability is broken by using the following characters inside + * delimited identifiers: + * + backtick (<kbd>`</kbd>) -- due to MySQL + * + double quote (<kbd>"</kbd>) -- due to Oracle + * + brackets (<kbd>[</kbd> or <kbd>]</kbd>) -- due to Access + * + * Delimited identifiers are known to generally work correctly under + * the following drivers: + * + mssql + * + mysql + * + mysqli + * + oci8 + * + odbc(access) + * + odbc(db2) + * + pgsql + * + sqlite + * + sybase (must execute <kbd>set quoted_identifier on</kbd> sometime + * prior to use) + * + * InterBase doesn't seem to be able to use delimited identifiers + * via PHP 4. They work fine under PHP 5. + * + * @param string $str the identifier name to be quoted + * + * @return string the quoted identifier + * + * @since Method available since Release 1.6.0 + */ + function quoteIdentifier($str) + { + return '"' . str_replace('"', '""', $str) . '"'; + } + + // }}} + // {{{ quoteSmart() + + /** + * Formats input so it can be safely used in a query + * + * The output depends on the PHP data type of input and the database + * type being used. + * + * @param mixed $in the data to be formatted + * + * @return mixed the formatted data. The format depends on the input's + * PHP type: + * <ul> + * <li> + * <kbd>input</kbd> -> <samp>returns</samp> + * </li> + * <li> + * <kbd>null</kbd> -> the string <samp>NULL</samp> + * </li> + * <li> + * <kbd>integer</kbd> or <kbd>double</kbd> -> the unquoted number + * </li> + * <li> + * <kbd>bool</kbd> -> output depends on the driver in use + * Most drivers return integers: <samp>1</samp> if + * <kbd>true</kbd> or <samp>0</samp> if + * <kbd>false</kbd>. + * Some return strings: <samp>TRUE</samp> if + * <kbd>true</kbd> or <samp>FALSE</samp> if + * <kbd>false</kbd>. + * Finally one returns strings: <samp>T</samp> if + * <kbd>true</kbd> or <samp>F</samp> if + * <kbd>false</kbd>. Here is a list of each DBMS, + * the values returned and the suggested column type: + * <ul> + * <li> + * <kbd>dbase</kbd> -> <samp>T/F</samp> + * (<kbd>Logical</kbd>) + * </li> + * <li> + * <kbd>fbase</kbd> -> <samp>TRUE/FALSE</samp> + * (<kbd>BOOLEAN</kbd>) + * </li> + * <li> + * <kbd>ibase</kbd> -> <samp>1/0</samp> + * (<kbd>SMALLINT</kbd>) [1] + * </li> + * <li> + * <kbd>ifx</kbd> -> <samp>1/0</samp> + * (<kbd>SMALLINT</kbd>) [1] + * </li> + * <li> + * <kbd>msql</kbd> -> <samp>1/0</samp> + * (<kbd>INTEGER</kbd>) + * </li> + * <li> + * <kbd>mssql</kbd> -> <samp>1/0</samp> + * (<kbd>BIT</kbd>) + * </li> + * <li> + * <kbd>mysql</kbd> -> <samp>1/0</samp> + * (<kbd>TINYINT(1)</kbd>) + * </li> + * <li> + * <kbd>mysqli</kbd> -> <samp>1/0</samp> + * (<kbd>TINYINT(1)</kbd>) + * </li> + * <li> + * <kbd>oci8</kbd> -> <samp>1/0</samp> + * (<kbd>NUMBER(1)</kbd>) + * </li> + * <li> + * <kbd>odbc</kbd> -> <samp>1/0</samp> + * (<kbd>SMALLINT</kbd>) [1] + * </li> + * <li> + * <kbd>pgsql</kbd> -> <samp>TRUE/FALSE</samp> + * (<kbd>BOOLEAN</kbd>) + * </li> + * <li> + * <kbd>sqlite</kbd> -> <samp>1/0</samp> + * (<kbd>INTEGER</kbd>) + * </li> + * <li> + * <kbd>sybase</kbd> -> <samp>1/0</samp> + * (<kbd>TINYINT(1)</kbd>) + * </li> + * </ul> + * [1] Accommodate the lowest common denominator because not all + * versions of have <kbd>BOOLEAN</kbd>. + * </li> + * <li> + * other (including strings and numeric strings) -> + * the data with single quotes escaped by preceeding + * single quotes, backslashes are escaped by preceeding + * backslashes, then the whole string is encapsulated + * between single quotes + * </li> + * </ul> + * + * @see DB_common::escapeSimple() + * @since Method available since Release 1.6.0 + */ + function quoteSmart($in) + { + if (is_int($in)) { + return $in; + } elseif (is_float($in)) { + return $this->quoteFloat($in); + } elseif (is_bool($in)) { + return $this->quoteBoolean($in); + } elseif (is_null($in)) { + return 'NULL'; + } else { + if ($this->dbsyntax == 'access' + && preg_match('/^#.+#$/', $in)) + { + return $this->escapeSimple($in); + } + return "'" . $this->escapeSimple($in) . "'"; + } + } + + // }}} + // {{{ quoteBoolean() + + /** + * Formats a boolean value for use within a query in a locale-independent + * manner. + * + * @param boolean the boolean value to be quoted. + * @return string the quoted string. + * @see DB_common::quoteSmart() + * @since Method available since release 1.7.8. + */ + function quoteBoolean($boolean) { + return $boolean ? '1' : '0'; + } + + // }}} + // {{{ quoteFloat() + + /** + * Formats a float value for use within a query in a locale-independent + * manner. + * + * @param float the float value to be quoted. + * @return string the quoted string. + * @see DB_common::quoteSmart() + * @since Method available since release 1.7.8. + */ + function quoteFloat($float) { + return "'".$this->escapeSimple(str_replace(',', '.', strval(floatval($float))))."'"; + } + + // }}} + // {{{ escapeSimple() + + /** + * Escapes a string according to the current DBMS's standards + * + * In SQLite, this makes things safe for inserts/updates, but may + * cause problems when performing text comparisons against columns + * containing binary data. See the + * {@link http://php.net/sqlite_escape_string PHP manual} for more info. + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @see DB_common::quoteSmart() + * @since Method available since Release 1.6.0 + */ + function escapeSimple($str) + { + return str_replace("'", "''", $str); + } + + // }}} + // {{{ provides() + + /** + * Tells whether the present driver supports a given feature + * + * @param string $feature the feature you're curious about + * + * @return bool whether this driver supports $feature + */ + function provides($feature) + { + return $this->features[$feature]; + } + + // }}} + // {{{ setFetchMode() + + /** + * Sets the fetch mode that should be used by default for query results + * + * @param integer $fetchmode DB_FETCHMODE_ORDERED, DB_FETCHMODE_ASSOC + * or DB_FETCHMODE_OBJECT + * @param string $object_class the class name of the object to be returned + * by the fetch methods when the + * DB_FETCHMODE_OBJECT mode is selected. + * If no class is specified by default a cast + * to object from the assoc array row will be + * done. There is also the posibility to use + * and extend the 'DB_row' class. + * + * @see DB_FETCHMODE_ORDERED, DB_FETCHMODE_ASSOC, DB_FETCHMODE_OBJECT + */ + function setFetchMode($fetchmode, $object_class = 'stdClass') + { + switch ($fetchmode) { + case DB_FETCHMODE_OBJECT: + $this->fetchmode_object_class = $object_class; + case DB_FETCHMODE_ORDERED: + case DB_FETCHMODE_ASSOC: + $this->fetchmode = $fetchmode; + break; + default: + return $this->raiseError('invalid fetchmode mode'); + } + } + + // }}} + // {{{ setOption() + + /** + * Sets run-time configuration options for PEAR DB + * + * Options, their data types, default values and description: + * <ul> + * <li> + * <var>autofree</var> <kbd>boolean</kbd> = <samp>false</samp> + * <br />should results be freed automatically when there are no + * more rows? + * </li><li> + * <var>result_buffering</var> <kbd>integer</kbd> = <samp>500</samp> + * <br />how many rows of the result set should be buffered? + * <br />In mysql: mysql_unbuffered_query() is used instead of + * mysql_query() if this value is 0. (Release 1.7.0) + * <br />In oci8: this value is passed to ocisetprefetch(). + * (Release 1.7.0) + * </li><li> + * <var>debug</var> <kbd>integer</kbd> = <samp>0</samp> + * <br />debug level + * </li><li> + * <var>persistent</var> <kbd>boolean</kbd> = <samp>false</samp> + * <br />should the connection be persistent? + * </li><li> + * <var>portability</var> <kbd>integer</kbd> = <samp>DB_PORTABILITY_NONE</samp> + * <br />portability mode constant (see below) + * </li><li> + * <var>seqname_format</var> <kbd>string</kbd> = <samp>%s_seq</samp> + * <br />the sprintf() format string used on sequence names. This + * format is applied to sequence names passed to + * createSequence(), nextID() and dropSequence(). + * </li><li> + * <var>ssl</var> <kbd>boolean</kbd> = <samp>false</samp> + * <br />use ssl to connect? + * </li> + * </ul> + * + * ----------------------------------------- + * + * PORTABILITY MODES + * + * These modes are bitwised, so they can be combined using <kbd>|</kbd> + * and removed using <kbd>^</kbd>. See the examples section below on how + * to do this. + * + * <samp>DB_PORTABILITY_NONE</samp> + * turn off all portability features + * + * This mode gets automatically turned on if the deprecated + * <var>optimize</var> option gets set to <samp>performance</samp>. + * + * + * <samp>DB_PORTABILITY_LOWERCASE</samp> + * convert names of tables and fields to lower case when using + * <kbd>get*()</kbd>, <kbd>fetch*()</kbd> and <kbd>tableInfo()</kbd> + * + * This mode gets automatically turned on in the following databases + * if the deprecated option <var>optimize</var> gets set to + * <samp>portability</samp>: + * + oci8 + * + * + * <samp>DB_PORTABILITY_RTRIM</samp> + * right trim the data output by <kbd>get*()</kbd> <kbd>fetch*()</kbd> + * + * + * <samp>DB_PORTABILITY_DELETE_COUNT</samp> + * force reporting the number of rows deleted + * + * Some DBMS's don't count the number of rows deleted when performing + * simple <kbd>DELETE FROM tablename</kbd> queries. This portability + * mode tricks such DBMS's into telling the count by adding + * <samp>WHERE 1=1</samp> to the end of <kbd>DELETE</kbd> queries. + * + * This mode gets automatically turned on in the following databases + * if the deprecated option <var>optimize</var> gets set to + * <samp>portability</samp>: + * + fbsql + * + mysql + * + mysqli + * + sqlite + * + * + * <samp>DB_PORTABILITY_NUMROWS</samp> + * enable hack that makes <kbd>numRows()</kbd> work in Oracle + * + * This mode gets automatically turned on in the following databases + * if the deprecated option <var>optimize</var> gets set to + * <samp>portability</samp>: + * + oci8 + * + * + * <samp>DB_PORTABILITY_ERRORS</samp> + * makes certain error messages in certain drivers compatible + * with those from other DBMS's + * + * + mysql, mysqli: change unique/primary key constraints + * DB_ERROR_ALREADY_EXISTS -> DB_ERROR_CONSTRAINT + * + * + odbc(access): MS's ODBC driver reports 'no such field' as code + * 07001, which means 'too few parameters.' When this option is on + * that code gets mapped to DB_ERROR_NOSUCHFIELD. + * DB_ERROR_MISMATCH -> DB_ERROR_NOSUCHFIELD + * + * <samp>DB_PORTABILITY_NULL_TO_EMPTY</samp> + * convert null values to empty strings in data output by get*() and + * fetch*(). Needed because Oracle considers empty strings to be null, + * while most other DBMS's know the difference between empty and null. + * + * + * <samp>DB_PORTABILITY_ALL</samp> + * turn on all portability features + * + * ----------------------------------------- + * + * Example 1. Simple setOption() example + * <code> + * $db->setOption('autofree', true); + * </code> + * + * Example 2. Portability for lowercasing and trimming + * <code> + * $db->setOption('portability', + * DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_RTRIM); + * </code> + * + * Example 3. All portability options except trimming + * <code> + * $db->setOption('portability', + * DB_PORTABILITY_ALL ^ DB_PORTABILITY_RTRIM); + * </code> + * + * @param string $option option name + * @param mixed $value value for the option + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::$options + */ + function setOption($option, $value) + { + if (isset($this->options[$option])) { + $this->options[$option] = $value; + + /* + * Backwards compatibility check for the deprecated 'optimize' + * option. Done here in case settings change after connecting. + */ + if ($option == 'optimize') { + if ($value == 'portability') { + switch ($this->phptype) { + case 'oci8': + $this->options['portability'] = + DB_PORTABILITY_LOWERCASE | + DB_PORTABILITY_NUMROWS; + break; + case 'fbsql': + case 'mysql': + case 'mysqli': + case 'sqlite': + $this->options['portability'] = + DB_PORTABILITY_DELETE_COUNT; + break; + } + } else { + $this->options['portability'] = DB_PORTABILITY_NONE; + } + } + + return DB_OK; + } + return $this->raiseError("unknown option $option"); + } + + // }}} + // {{{ getOption() + + /** + * Returns the value of an option + * + * @param string $option the option name you're curious about + * + * @return mixed the option's value + */ + function getOption($option) + { + if (isset($this->options[$option])) { + return $this->options[$option]; + } + return $this->raiseError("unknown option $option"); + } + + // }}} + // {{{ prepare() + + /** + * Prepares a query for multiple execution with execute() + * + * Creates a query that can be run multiple times. Each time it is run, + * the placeholders, if any, will be replaced by the contents of + * execute()'s $data argument. + * + * Three types of placeholders can be used: + * + <kbd>?</kbd> scalar value (i.e. strings, integers). The system + * will automatically quote and escape the data. + * + <kbd>!</kbd> value is inserted 'as is' + * + <kbd>&</kbd> requires a file name. The file's contents get + * inserted into the query (i.e. saving binary + * data in a db) + * + * Example 1. + * <code> + * $sth = $db->prepare('INSERT INTO tbl (a, b, c) VALUES (?, !, &)'); + * $data = array( + * "John's text", + * "'it''s good'", + * 'filename.txt' + * ); + * $res = $db->execute($sth, $data); + * </code> + * + * Use backslashes to escape placeholder characters if you don't want + * them to be interpreted as placeholders: + * <pre> + * "UPDATE foo SET col=? WHERE col='over \& under'" + * </pre> + * + * With some database backends, this is emulated. + * + * {@internal ibase and oci8 have their own prepare() methods.}} + * + * @param string $query the query to be prepared + * + * @return mixed DB statement resource on success. A DB_Error object + * on failure. + * + * @see DB_common::execute() + */ + function prepare($query) + { + $tokens = preg_split('/((?<!\\\)[&?!])/', $query, -1, + PREG_SPLIT_DELIM_CAPTURE); + $token = 0; + $types = array(); + $newtokens = array(); + + foreach ($tokens as $val) { + switch ($val) { + case '?': + $types[$token++] = DB_PARAM_SCALAR; + break; + case '&': + $types[$token++] = DB_PARAM_OPAQUE; + break; + case '!': + $types[$token++] = DB_PARAM_MISC; + break; + default: + $newtokens[] = preg_replace('/\\\([&?!])/', "\\1", $val); + } + } + + $this->prepare_tokens[] = &$newtokens; + end($this->prepare_tokens); + + $k = key($this->prepare_tokens); + $this->prepare_types[$k] = $types; + $this->prepared_queries[$k] = implode(' ', $newtokens); + + return $k; + } + + // }}} + // {{{ autoPrepare() + + /** + * Automaticaly generates an insert or update query and pass it to prepare() + * + * @param string $table the table name + * @param array $table_fields the array of field names + * @param int $mode a type of query to make: + * DB_AUTOQUERY_INSERT or DB_AUTOQUERY_UPDATE + * @param string $where for update queries: the WHERE clause to + * append to the SQL statement. Don't + * include the "WHERE" keyword. + * + * @return resource the query handle + * + * @uses DB_common::prepare(), DB_common::buildManipSQL() + */ + function autoPrepare($table, $table_fields, $mode = DB_AUTOQUERY_INSERT, + $where = false) + { + $query = $this->buildManipSQL($table, $table_fields, $mode, $where); + if (DB::isError($query)) { + return $query; + } + return $this->prepare($query); + } + + // }}} + // {{{ autoExecute() + + /** + * Automaticaly generates an insert or update query and call prepare() + * and execute() with it + * + * @param string $table the table name + * @param array $fields_values the associative array where $key is a + * field name and $value its value + * @param int $mode a type of query to make: + * DB_AUTOQUERY_INSERT or DB_AUTOQUERY_UPDATE + * @param string $where for update queries: the WHERE clause to + * append to the SQL statement. Don't + * include the "WHERE" keyword. + * + * @return mixed a new DB_result object for successful SELECT queries + * or DB_OK for successul data manipulation queries. + * A DB_Error object on failure. + * + * @uses DB_common::autoPrepare(), DB_common::execute() + */ + function autoExecute($table, $fields_values, $mode = DB_AUTOQUERY_INSERT, + $where = false) + { + $sth = $this->autoPrepare($table, array_keys($fields_values), $mode, + $where); + if (DB::isError($sth)) { + return $sth; + } + $ret = $this->execute($sth, array_values($fields_values)); + $this->freePrepared($sth); + return $ret; + + } + + // }}} + // {{{ buildManipSQL() + + /** + * Produces an SQL query string for autoPrepare() + * + * Example: + * <pre> + * buildManipSQL('table_sql', array('field1', 'field2', 'field3'), + * DB_AUTOQUERY_INSERT); + * </pre> + * + * That returns + * <samp> + * INSERT INTO table_sql (field1,field2,field3) VALUES (?,?,?) + * </samp> + * + * NOTES: + * - This belongs more to a SQL Builder class, but this is a simple + * facility. + * - Be carefull! If you don't give a $where param with an UPDATE + * query, all the records of the table will be updated! + * + * @param string $table the table name + * @param array $table_fields the array of field names + * @param int $mode a type of query to make: + * DB_AUTOQUERY_INSERT or DB_AUTOQUERY_UPDATE + * @param string $where for update queries: the WHERE clause to + * append to the SQL statement. Don't + * include the "WHERE" keyword. + * + * @return string the sql query for autoPrepare() + */ + function buildManipSQL($table, $table_fields, $mode, $where = false) + { + if (count($table_fields) == 0) { + return $this->raiseError(DB_ERROR_NEED_MORE_DATA); + } + $first = true; + switch ($mode) { + case DB_AUTOQUERY_INSERT: + $values = ''; + $names = ''; + foreach ($table_fields as $value) { + if ($first) { + $first = false; + } else { + $names .= ','; + $values .= ','; + } + $names .= $value; + $values .= '?'; + } + return "INSERT INTO $table ($names) VALUES ($values)"; + case DB_AUTOQUERY_UPDATE: + $set = ''; + foreach ($table_fields as $value) { + if ($first) { + $first = false; + } else { + $set .= ','; + } + $set .= "$value = ?"; + } + $sql = "UPDATE $table SET $set"; + if ($where) { + $sql .= " WHERE $where"; + } + return $sql; + default: + return $this->raiseError(DB_ERROR_SYNTAX); + } + } + + // }}} + // {{{ execute() + + /** + * Executes a DB statement prepared with prepare() + * + * Example 1. + * <code> + * $sth = $db->prepare('INSERT INTO tbl (a, b, c) VALUES (?, !, &)'); + * $data = array( + * "John's text", + * "'it''s good'", + * 'filename.txt' + * ); + * $res = $db->execute($sth, $data); + * </code> + * + * @param resource $stmt a DB statement resource returned from prepare() + * @param mixed $data array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return mixed a new DB_result object for successful SELECT queries + * or DB_OK for successul data manipulation queries. + * A DB_Error object on failure. + * + * {@internal ibase and oci8 have their own execute() methods.}} + * + * @see DB_common::prepare() + */ + function &execute($stmt, $data = array()) + { + $realquery = $this->executeEmulateQuery($stmt, $data); + if (DB::isError($realquery)) { + return $realquery; + } + $result = $this->simpleQuery($realquery); + + if ($result === DB_OK || DB::isError($result)) { + return $result; + } else { + $tmp = new DB_result($this, $result); + return $tmp; + } + } + + // }}} + // {{{ executeEmulateQuery() + + /** + * Emulates executing prepared statements if the DBMS not support them + * + * @param resource $stmt a DB statement resource returned from execute() + * @param mixed $data array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return mixed a string containing the real query run when emulating + * prepare/execute. A DB_Error object on failure. + * + * @access protected + * @see DB_common::execute() + */ + function executeEmulateQuery($stmt, $data = array()) + { + $stmt = (int)$stmt; + $data = (array)$data; + $this->last_parameters = $data; + + if (count($this->prepare_types[$stmt]) != count($data)) { + $this->last_query = $this->prepared_queries[$stmt]; + return $this->raiseError(DB_ERROR_MISMATCH); + } + + $realquery = $this->prepare_tokens[$stmt][0]; + + $i = 0; + foreach ($data as $value) { + if ($this->prepare_types[$stmt][$i] == DB_PARAM_SCALAR) { + $realquery .= $this->quoteSmart($value); + } elseif ($this->prepare_types[$stmt][$i] == DB_PARAM_OPAQUE) { + $fp = @fopen($value, 'rb'); + if (!$fp) { + return $this->raiseError(DB_ERROR_ACCESS_VIOLATION); + } + $realquery .= $this->quoteSmart(fread($fp, filesize($value))); + fclose($fp); + } else { + $realquery .= $value; + } + + $realquery .= $this->prepare_tokens[$stmt][++$i]; + } + + return $realquery; + } + + // }}} + // {{{ executeMultiple() + + /** + * Performs several execute() calls on the same statement handle + * + * $data must be an array indexed numerically + * from 0, one execute call is done for every "row" in the array. + * + * If an error occurs during execute(), executeMultiple() does not + * execute the unfinished rows, but rather returns that error. + * + * @param resource $stmt query handle from prepare() + * @param array $data numeric array containing the + * data to insert into the query + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::prepare(), DB_common::execute() + */ + function executeMultiple($stmt, $data) + { + foreach ($data as $value) { + $res = $this->execute($stmt, $value); + if (DB::isError($res)) { + return $res; + } + } + return DB_OK; + } + + // }}} + // {{{ freePrepared() + + /** + * Frees the internal resources associated with a prepared query + * + * @param resource $stmt the prepared statement's PHP resource + * @param bool $free_resource should the PHP resource be freed too? + * Use false if you need to get data + * from the result set later. + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_common::prepare() + */ + function freePrepared($stmt, $free_resource = true) + { + $stmt = (int)$stmt; + if (isset($this->prepare_tokens[$stmt])) { + unset($this->prepare_tokens[$stmt]); + unset($this->prepare_types[$stmt]); + unset($this->prepared_queries[$stmt]); + return true; + } + return false; + } + + // }}} + // {{{ modifyQuery() + + /** + * Changes a query string for various DBMS specific reasons + * + * It is defined here to ensure all drivers have this method available. + * + * @param string $query the query string to modify + * + * @return string the modified query string + * + * @access protected + * @see DB_mysql::modifyQuery(), DB_oci8::modifyQuery(), + * DB_sqlite::modifyQuery() + */ + function modifyQuery($query) + { + return $query; + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * Adds LIMIT clauses to a query string according to current DBMS standards + * + * It is defined here to assure that all implementations + * have this method defined. + * + * @param string $query the query to modify + * @param int $from the row to start to fetching (0 = the first row) + * @param int $count the numbers of rows to fetch + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return string the query string with LIMIT clauses added + * + * @access protected + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + return $query; + } + + // }}} + // {{{ query() + + /** + * Sends a query to the database server + * + * The query string can be either a normal statement to be sent directly + * to the server OR if <var>$params</var> are passed the query can have + * placeholders and it will be passed through prepare() and execute(). + * + * @param string $query the SQL query or the statement to prepare + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return mixed a new DB_result object for successful SELECT queries + * or DB_OK for successul data manipulation queries. + * A DB_Error object on failure. + * + * @see DB_result, DB_common::prepare(), DB_common::execute() + */ + function &query($query, $params = array()) + { + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + if (DB::isError($sth)) { + return $sth; + } + $ret = $this->execute($sth, $params); + $this->freePrepared($sth, false); + return $ret; + } else { + $this->last_parameters = array(); + $result = $this->simpleQuery($query); + if ($result === DB_OK || DB::isError($result)) { + return $result; + } else { + $tmp = new DB_result($this, $result); + return $tmp; + } + } + } + + // }}} + // {{{ limitQuery() + + /** + * Generates and executes a LIMIT query + * + * @param string $query the query + * @param intr $from the row to start to fetching (0 = the first row) + * @param int $count the numbers of rows to fetch + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return mixed a new DB_result object for successful SELECT queries + * or DB_OK for successul data manipulation queries. + * A DB_Error object on failure. + */ + function &limitQuery($query, $from, $count, $params = array()) + { + $query = $this->modifyLimitQuery($query, $from, $count, $params); + if (DB::isError($query)){ + return $query; + } + $result = $this->query($query, $params); + if (is_a($result, 'DB_result')) { + $result->setOption('limit_from', $from); + $result->setOption('limit_count', $count); + } + return $result; + } + + // }}} + // {{{ getOne() + + /** + * Fetches the first column of the first row from a query result + * + * Takes care of doing the query and freeing the results when finished. + * + * @param string $query the SQL query + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return mixed the returned value of the query. + * A DB_Error object on failure. + */ + function &getOne($query, $params = array()) + { + $params = (array)$params; + // modifyLimitQuery() would be nice here, but it causes BC issues + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + if (DB::isError($sth)) { + return $sth; + } + $res = $this->execute($sth, $params); + $this->freePrepared($sth); + } else { + $res = $this->query($query); + } + + if (DB::isError($res)) { + return $res; + } + + $err = $res->fetchInto($row, DB_FETCHMODE_ORDERED); + $res->free(); + + if ($err !== DB_OK) { + return $err; + } + + return $row[0]; + } + + // }}} + // {{{ getRow() + + /** + * Fetches the first row of data returned from a query result + * + * Takes care of doing the query and freeing the results when finished. + * + * @param string $query the SQL query + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * @param int $fetchmode the fetch mode to use + * + * @return array the first row of results as an array. + * A DB_Error object on failure. + */ + function &getRow($query, $params = array(), + $fetchmode = DB_FETCHMODE_DEFAULT) + { + // compat check, the params and fetchmode parameters used to + // have the opposite order + if (!is_array($params)) { + if (is_array($fetchmode)) { + if ($params === null) { + $tmp = DB_FETCHMODE_DEFAULT; + } else { + $tmp = $params; + } + $params = $fetchmode; + $fetchmode = $tmp; + } elseif ($params !== null) { + $fetchmode = $params; + $params = array(); + } + } + // modifyLimitQuery() would be nice here, but it causes BC issues + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + if (DB::isError($sth)) { + return $sth; + } + $res = $this->execute($sth, $params); + $this->freePrepared($sth); + } else { + $res = $this->query($query); + } + + if (DB::isError($res)) { + return $res; + } + + $err = $res->fetchInto($row, $fetchmode); + + $res->free(); + + if ($err !== DB_OK) { + return $err; + } + + return $row; + } + + // }}} + // {{{ getCol() + + /** + * Fetches a single column from a query result and returns it as an + * indexed array + * + * @param string $query the SQL query + * @param mixed $col which column to return (integer [column number, + * starting at 0] or string [column name]) + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return array the results as an array. A DB_Error object on failure. + * + * @see DB_common::query() + */ + function &getCol($query, $col = 0, $params = array()) + { + $params = (array)$params; + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + + if (DB::isError($sth)) { + return $sth; + } + + $res = $this->execute($sth, $params); + $this->freePrepared($sth); + } else { + $res = $this->query($query); + } + + if (DB::isError($res)) { + return $res; + } + + $fetchmode = is_int($col) ? DB_FETCHMODE_ORDERED : DB_FETCHMODE_ASSOC; + + if (!is_array($row = $res->fetchRow($fetchmode))) { + $ret = array(); + } else { + if (!array_key_exists($col, $row)) { + $ret = $this->raiseError(DB_ERROR_NOSUCHFIELD); + } else { + $ret = array($row[$col]); + while (is_array($row = $res->fetchRow($fetchmode))) { + $ret[] = $row[$col]; + } + } + } + + $res->free(); + + if (DB::isError($row)) { + $ret = $row; + } + + return $ret; + } + + // }}} + // {{{ getAssoc() + + /** + * Fetches an entire query result and returns it as an + * associative array using the first column as the key + * + * If the result set contains more than two columns, the value + * will be an array of the values from column 2-n. If the result + * set contains only two columns, the returned value will be a + * scalar with the value of the second column (unless forced to an + * array with the $force_array parameter). A DB error code is + * returned on errors. If the result set contains fewer than two + * columns, a DB_ERROR_TRUNCATED error is returned. + * + * For example, if the table "mytable" contains: + * + * <pre> + * ID TEXT DATE + * -------------------------------- + * 1 'one' 944679408 + * 2 'two' 944679408 + * 3 'three' 944679408 + * </pre> + * + * Then the call getAssoc('SELECT id,text FROM mytable') returns: + * <pre> + * array( + * '1' => 'one', + * '2' => 'two', + * '3' => 'three', + * ) + * </pre> + * + * ...while the call getAssoc('SELECT id,text,date FROM mytable') returns: + * <pre> + * array( + * '1' => array('one', '944679408'), + * '2' => array('two', '944679408'), + * '3' => array('three', '944679408') + * ) + * </pre> + * + * If the more than one row occurs with the same value in the + * first column, the last row overwrites all previous ones by + * default. Use the $group parameter if you don't want to + * overwrite like this. Example: + * + * <pre> + * getAssoc('SELECT category,id,name FROM mytable', false, null, + * DB_FETCHMODE_ASSOC, true) returns: + * + * array( + * '1' => array(array('id' => '4', 'name' => 'number four'), + * array('id' => '6', 'name' => 'number six') + * ), + * '9' => array(array('id' => '4', 'name' => 'number four'), + * array('id' => '6', 'name' => 'number six') + * ) + * ) + * </pre> + * + * Keep in mind that database functions in PHP usually return string + * values for results regardless of the database's internal type. + * + * @param string $query the SQL query + * @param bool $force_array used only when the query returns + * exactly two columns. If true, the values + * of the returned array will be one-element + * arrays instead of scalars. + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of + * items passed must match quantity of + * placeholders in query: meaning 1 + * placeholder for non-array parameters or + * 1 placeholder per array element. + * @param int $fetchmode the fetch mode to use + * @param bool $group if true, the values of the returned array + * is wrapped in another array. If the same + * key value (in the first column) repeats + * itself, the values will be appended to + * this array instead of overwriting the + * existing values. + * + * @return array the associative array containing the query results. + * A DB_Error object on failure. + */ + function &getAssoc($query, $force_array = false, $params = array(), + $fetchmode = DB_FETCHMODE_DEFAULT, $group = false) + { + $params = (array)$params; + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + + if (DB::isError($sth)) { + return $sth; + } + + $res = $this->execute($sth, $params); + $this->freePrepared($sth); + } else { + $res = $this->query($query); + } + + if (DB::isError($res)) { + return $res; + } + if ($fetchmode == DB_FETCHMODE_DEFAULT) { + $fetchmode = $this->fetchmode; + } + $cols = $res->numCols(); + + if ($cols < 2) { + $tmp = $this->raiseError(DB_ERROR_TRUNCATED); + return $tmp; + } + + $results = array(); + + if ($cols > 2 || $force_array) { + // return array values + // XXX this part can be optimized + if ($fetchmode == DB_FETCHMODE_ASSOC) { + while (is_array($row = $res->fetchRow(DB_FETCHMODE_ASSOC))) { + reset($row); + $key = current($row); + unset($row[key($row)]); + if ($group) { + $results[$key][] = $row; + } else { + $results[$key] = $row; + } + } + } elseif ($fetchmode == DB_FETCHMODE_OBJECT) { + while ($row = $res->fetchRow(DB_FETCHMODE_OBJECT)) { + $arr = get_object_vars($row); + $key = current($arr); + if ($group) { + $results[$key][] = $row; + } else { + $results[$key] = $row; + } + } + } else { + while (is_array($row = $res->fetchRow(DB_FETCHMODE_ORDERED))) { + // we shift away the first element to get + // indices running from 0 again + $key = array_shift($row); + if ($group) { + $results[$key][] = $row; + } else { + $results[$key] = $row; + } + } + } + if (DB::isError($row)) { + $results = $row; + } + } else { + // return scalar values + while (is_array($row = $res->fetchRow(DB_FETCHMODE_ORDERED))) { + if ($group) { + $results[$row[0]][] = $row[1]; + } else { + $results[$row[0]] = $row[1]; + } + } + if (DB::isError($row)) { + $results = $row; + } + } + + $res->free(); + + return $results; + } + + // }}} + // {{{ getAll() + + /** + * Fetches all of the rows from a query result + * + * @param string $query the SQL query + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of + * items passed must match quantity of + * placeholders in query: meaning 1 + * placeholder for non-array parameters or + * 1 placeholder per array element. + * @param int $fetchmode the fetch mode to use: + * + DB_FETCHMODE_ORDERED + * + DB_FETCHMODE_ASSOC + * + DB_FETCHMODE_ORDERED | DB_FETCHMODE_FLIPPED + * + DB_FETCHMODE_ASSOC | DB_FETCHMODE_FLIPPED + * + * @return array the nested array. A DB_Error object on failure. + */ + function &getAll($query, $params = array(), + $fetchmode = DB_FETCHMODE_DEFAULT) + { + // compat check, the params and fetchmode parameters used to + // have the opposite order + if (!is_array($params)) { + if (is_array($fetchmode)) { + if ($params === null) { + $tmp = DB_FETCHMODE_DEFAULT; + } else { + $tmp = $params; + } + $params = $fetchmode; + $fetchmode = $tmp; + } elseif ($params !== null) { + $fetchmode = $params; + $params = array(); + } + } + + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + + if (DB::isError($sth)) { + return $sth; + } + + $res = $this->execute($sth, $params); + $this->freePrepared($sth); + } else { + $res = $this->query($query); + } + + if ($res === DB_OK || DB::isError($res)) { + return $res; + } + + $results = array(); + while (DB_OK === $res->fetchInto($row, $fetchmode)) { + if ($fetchmode & DB_FETCHMODE_FLIPPED) { + foreach ($row as $key => $val) { + $results[$key][] = $val; + } + } else { + $results[] = $row; + } + } + + $res->free(); + + if (DB::isError($row)) { + $tmp = $this->raiseError($row); + return $tmp; + } + return $results; + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff = false) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ numRows() + + /** + * Determines the number of rows in a query result + * + * @param resource $result the query result idenifier produced by PHP + * + * @return int the number of rows. A DB_Error object on failure. + */ + function numRows($result) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ getSequenceName() + + /** + * Generates the name used inside the database for a sequence + * + * The createSequence() docblock contains notes about storing sequence + * names. + * + * @param string $sqn the sequence's public name + * + * @return string the sequence's name in the backend + * + * @access protected + * @see DB_common::createSequence(), DB_common::dropSequence(), + * DB_common::nextID(), DB_common::setOption() + */ + function getSequenceName($sqn) + { + return sprintf($this->getOption('seqname_format'), + preg_replace('/[^a-z0-9_.]/i', '_', $sqn)); + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::dropSequence(), + * DB_common::getSequenceName() + */ + function nextId($seq_name, $ondemand = true) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ createSequence() + + /** + * Creates a new sequence + * + * The name of a given sequence is determined by passing the string + * provided in the <var>$seq_name</var> argument through PHP's sprintf() + * function using the value from the <var>seqname_format</var> option as + * the sprintf()'s format argument. + * + * <var>seqname_format</var> is set via setOption(). + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_common::nextID() + */ + function createSequence($seq_name) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_common::nextID() + */ + function dropSequence($seq_name) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ raiseError() + + /** + * Communicates an error and invoke error callbacks, etc + * + * Basically a wrapper for PEAR::raiseError without the message string. + * + * @param mixed integer error code, or a PEAR error object (all + * other parameters are ignored if this parameter is + * an object + * @param int error mode, see PEAR_Error docs + * @param mixed if error mode is PEAR_ERROR_TRIGGER, this is the + * error level (E_USER_NOTICE etc). If error mode is + * PEAR_ERROR_CALLBACK, this is the callback function, + * either as a function name, or as an array of an + * object and method name. For other error modes this + * parameter is ignored. + * @param string extra debug information. Defaults to the last + * query and native error code. + * @param mixed native error code, integer or string depending the + * backend + * @param mixed dummy parameter for E_STRICT compatibility with + * PEAR::raiseError + * @param mixed dummy parameter for E_STRICT compatibility with + * PEAR::raiseError + * + * @return object the PEAR_Error object + * + * @see PEAR_Error + */ + function &raiseError($code = DB_ERROR, $mode = null, $options = null, + $userinfo = null, $nativecode = null, $dummy1 = null, + $dummy2 = null) + { + // The error is yet a DB error object + if (is_object($code)) { + // because we the static PEAR::raiseError, our global + // handler should be used if it is set + if ($mode === null && !empty($this->_default_error_mode)) { + $mode = $this->_default_error_mode; + $options = $this->_default_error_options; + } + $tmp = PEAR::raiseError($code, null, $mode, $options, + null, null, true); + return $tmp; + } + + if ($userinfo === null) { + $userinfo = $this->last_query; + } + + if ($nativecode) { + $userinfo .= ' [nativecode=' . trim($nativecode) . ']'; + } else { + $userinfo .= ' [DB Error: ' . DB::errorMessage($code) . ']'; + } + + $tmp = PEAR::raiseError(null, $code, $mode, $options, $userinfo, + 'DB_Error', true); + return $tmp; + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error code produced by the last query + * + * @return mixed the DBMS' error code. A DB_Error object on failure. + */ + function errorNative() + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ errorCode() + + /** + * Maps native error codes to DB's portable ones + * + * Uses the <var>$errorcode_map</var> property defined in each driver. + * + * @param string|int $nativecode the error code returned by the DBMS + * + * @return int the portable DB error code. Return DB_ERROR if the + * current driver doesn't have a mapping for the + * $nativecode submitted. + */ + function errorCode($nativecode) + { + if (isset($this->errorcode_map[$nativecode])) { + return $this->errorcode_map[$nativecode]; + } + // Fall back to DB_ERROR if there was no mapping. + return DB_ERROR; + } + + // }}} + // {{{ errorMessage() + + /** + * Maps a DB error code to a textual message + * + * @param integer $dbcode the DB error code + * + * @return string the error message corresponding to the error code + * submitted. FALSE if the error code is unknown. + * + * @see DB::errorMessage() + */ + function errorMessage($dbcode) + { + return DB::errorMessage($this->errorcode_map[$dbcode]); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * The format of the resulting array depends on which <var>$mode</var> + * you select. The sample output below is based on this query: + * <pre> + * SELECT tblFoo.fldID, tblFoo.fldPhone, tblBar.fldId + * FROM tblFoo + * JOIN tblBar ON tblFoo.fldId = tblBar.fldId + * </pre> + * + * <ul> + * <li> + * + * <kbd>null</kbd> (default) + * <pre> + * [0] => Array ( + * [table] => tblFoo + * [name] => fldId + * [type] => int + * [len] => 11 + * [flags] => primary_key not_null + * ) + * [1] => Array ( + * [table] => tblFoo + * [name] => fldPhone + * [type] => string + * [len] => 20 + * [flags] => + * ) + * [2] => Array ( + * [table] => tblBar + * [name] => fldId + * [type] => int + * [len] => 11 + * [flags] => primary_key not_null + * ) + * </pre> + * + * </li><li> + * + * <kbd>DB_TABLEINFO_ORDER</kbd> + * + * <p>In addition to the information found in the default output, + * a notation of the number of columns is provided by the + * <samp>num_fields</samp> element while the <samp>order</samp> + * element provides an array with the column names as the keys and + * their location index number (corresponding to the keys in the + * the default output) as the values.</p> + * + * <p>If a result set has identical field names, the last one is + * used.</p> + * + * <pre> + * [num_fields] => 3 + * [order] => Array ( + * [fldId] => 2 + * [fldTrans] => 1 + * ) + * </pre> + * + * </li><li> + * + * <kbd>DB_TABLEINFO_ORDERTABLE</kbd> + * + * <p>Similar to <kbd>DB_TABLEINFO_ORDER</kbd> but adds more + * dimensions to the array in which the table names are keys and + * the field names are sub-keys. This is helpful for queries that + * join tables which have identical field names.</p> + * + * <pre> + * [num_fields] => 3 + * [ordertable] => Array ( + * [tblFoo] => Array ( + * [fldId] => 0 + * [fldPhone] => 1 + * ) + * [tblBar] => Array ( + * [fldId] => 2 + * ) + * ) + * </pre> + * + * </li> + * </ul> + * + * The <samp>flags</samp> element contains a space separated list + * of extra information about the field. This data is inconsistent + * between DBMS's due to the way each DBMS works. + * + <samp>primary_key</samp> + * + <samp>unique_key</samp> + * + <samp>multiple_key</samp> + * + <samp>not_null</samp> + * + * Most DBMS's only provide the <samp>table</samp> and <samp>flags</samp> + * elements if <var>$result</var> is a table name. The following DBMS's + * provide full information from queries: + * + fbsql + * + mysql + * + * If the 'portability' option has <samp>DB_PORTABILITY_LOWERCASE</samp> + * turned on, the names of tables and fields will be lowercased. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode either unused or one of the tableInfo modes: + * <kbd>DB_TABLEINFO_ORDERTABLE</kbd>, + * <kbd>DB_TABLEINFO_ORDER</kbd> or + * <kbd>DB_TABLEINFO_FULL</kbd> (which does both). + * These are bitwise, so the first two can be + * combined using <kbd>|</kbd>. + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::setOption() + */ + function tableInfo($result, $mode = null) + { + /* + * If the DB_<driver> class has a tableInfo() method, that one + * overrides this one. But, if the driver doesn't have one, + * this method runs and tells users about that fact. + */ + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ getTables() + + /** + * Lists the tables in the current database + * + * @return array the list of tables. A DB_Error object on failure. + * + * @deprecated Method deprecated some time before Release 1.2 + */ + function getTables() + { + return $this->getListOf('tables'); + } + + // }}} + // {{{ getListOf() + + /** + * Lists internal database information + * + * @param string $type type of information being sought. + * Common items being sought are: + * tables, databases, users, views, functions + * Each DBMS's has its own capabilities. + * + * @return array an array listing the items sought. + * A DB DB_Error object on failure. + */ + function getListOf($type) + { + $sql = $this->getSpecialQuery($type); + if ($sql === null) { + $this->last_query = ''; + return $this->raiseError(DB_ERROR_UNSUPPORTED); + } elseif (is_int($sql) || DB::isError($sql)) { + // Previous error + return $this->raiseError($sql); + } elseif (is_array($sql)) { + // Already the result + return $sql; + } + // Launch this query + return $this->getCol($sql); + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + return $this->raiseError(DB_ERROR_UNSUPPORTED); + } + + // }}} + // {{{ nextQueryIsManip() + + /** + * Sets (or unsets) a flag indicating that the next query will be a + * manipulation query, regardless of the usual DB::isManip() heuristics. + * + * @param boolean true to set the flag overriding the isManip() behaviour, + * false to clear it and fall back onto isManip() + * + * @return void + * + * @access public + */ + function nextQueryIsManip($manip) + { + $this->_next_query_manip = $manip; + } + + // }}} + // {{{ _checkManip() + + /** + * Checks if the given query is a manipulation query. This also takes into + * account the _next_query_manip flag and sets the _last_query_manip flag + * (and resets _next_query_manip) according to the result. + * + * @param string The query to check. + * + * @return boolean true if the query is a manipulation query, false + * otherwise + * + * @access protected + */ + function _checkManip($query) + { + if ($this->_next_query_manip || DB::isManip($query)) { + $this->_last_query_manip = true; + } else { + $this->_last_query_manip = false; + } + $this->_next_query_manip = false; + return $this->_last_query_manip; + $manip = $this->_next_query_manip; + } + + // }}} + // {{{ _rtrimArrayValues() + + /** + * Right-trims all strings in an array + * + * @param array $array the array to be trimmed (passed by reference) + * + * @return void + * + * @access protected + */ + function _rtrimArrayValues(&$array) + { + foreach ($array as $key => $value) { + if (is_string($value)) { + $array[$key] = rtrim($value); + } + } + } + + // }}} + // {{{ _convertNullArrayValuesToEmpty() + + /** + * Converts all null values in an array to empty strings + * + * @param array $array the array to be de-nullified (passed by reference) + * + * @return void + * + * @access protected + */ + function _convertNullArrayValuesToEmpty(&$array) + { + foreach ($array as $key => $value) { + if (is_null($value)) { + $array[$key] = ''; + } + } + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/dbase.php b/extlib/DB/dbase.php new file mode 100644 index 000000000..67afc897d --- /dev/null +++ b/extlib/DB/dbase.php @@ -0,0 +1,510 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's dbase extension + * for interacting with dBase databases + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: dbase.php,v 1.45 2007/09/21 13:40:41 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's dbase extension + * for interacting with dBase databases + * + * These methods overload the ones declared in DB_common. + * + * @category Database + * @package DB + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_dbase extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'dbase'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'dbase'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => false, + 'new_link' => false, + 'numrows' => true, + 'pconnect' => false, + 'prepare' => false, + 'ssl' => false, + 'transactions' => false, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * A means of emulating result resources + * @var array + */ + var $res_row = array(); + + /** + * The quantity of results so far + * + * For emulating result resources. + * + * @var integer + */ + var $result = 0; + + /** + * Maps dbase data type id's to human readable strings + * + * The human readable values are based on the output of PHP's + * dbase_get_header_info() function. + * + * @var array + * @since Property available since Release 1.7.0 + */ + var $types = array( + 'C' => 'character', + 'D' => 'date', + 'L' => 'boolean', + 'M' => 'memo', + 'N' => 'number', + ); + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_dbase() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database and create it if it doesn't exist + * + * Don't call this method directly. Use DB::connect() instead. + * + * PEAR DB's dbase driver supports the following extra DSN options: + * + mode An integer specifying the read/write mode to use + * (0 = read only, 1 = write only, 2 = read/write). + * Available since PEAR DB 1.7.0. + * + fields An array of arrays that PHP's dbase_create() function needs + * to create a new database. This information is used if the + * dBase file specified in the "database" segment of the DSN + * does not exist. For more info, see the PHP manual's + * {@link http://php.net/dbase_create dbase_create()} page. + * Available since PEAR DB 1.7.0. + * + * Example of how to connect and establish a new dBase file if necessary: + * <code> + * require_once 'DB.php'; + * + * $dsn = array( + * 'phptype' => 'dbase', + * 'database' => '/path/and/name/of/dbase/file', + * 'mode' => 2, + * 'fields' => array( + * array('a', 'N', 5, 0), + * array('b', 'C', 40), + * array('c', 'C', 255), + * array('d', 'C', 20), + * ), + * ); + * $options = array( + * 'debug' => 2, + * 'portability' => DB_PORTABILITY_ALL, + * ); + * + * $db = DB::connect($dsn, $options); + * if (PEAR::isError($db)) { + * die($db->getMessage()); + * } + * </code> + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('dbase')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + /* + * Turn track_errors on for entire script since $php_errormsg + * is the only way to find errors from the dbase extension. + */ + @ini_set('track_errors', 1); + $php_errormsg = ''; + + if (!file_exists($dsn['database'])) { + $this->dsn['mode'] = 2; + if (empty($dsn['fields']) || !is_array($dsn['fields'])) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + 'the dbase file does not exist and ' + . 'it could not be created because ' + . 'the "fields" element of the DSN ' + . 'is not properly set'); + } + $this->connection = @dbase_create($dsn['database'], + $dsn['fields']); + if (!$this->connection) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + 'the dbase file does not exist and ' + . 'the attempt to create it failed: ' + . $php_errormsg); + } + } else { + if (!isset($this->dsn['mode'])) { + $this->dsn['mode'] = 0; + } + $this->connection = @dbase_open($dsn['database'], + $this->dsn['mode']); + if (!$this->connection) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $php_errormsg); + } + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @dbase_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ &query() + + function &query($query = null) + { + // emulate result resources + $this->res_row[(int)$this->result] = 0; + $tmp = new DB_result($this, $this->result++); + return $tmp; + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if ($rownum === null) { + $rownum = $this->res_row[(int)$result]++; + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @dbase_get_record_with_names($this->connection, $rownum); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @dbase_get_record($this->connection, $rownum); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set. + * + * This method is a no-op for dbase, as there aren't result resources in + * the same sense as most other database backends. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return true; + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($foo) + { + return @dbase_numfields($this->connection); + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows() + */ + function numRows($foo) + { + return @dbase_numrecords($this->connection); + } + + // }}} + // {{{ quoteBoolean() + + /** + * Formats a boolean value for use within a query in a locale-independent + * manner. + * + * @param boolean the boolean value to be quoted. + * @return string the quoted string. + * @see DB_common::quoteSmart() + * @since Method available since release 1.7.8. + */ + function quoteBoolean($boolean) { + return $boolean ? 'T' : 'F'; + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about the current database + * + * @param mixed $result THIS IS UNUSED IN DBASE. The current database + * is examined regardless of what is provided here. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + * @since Method available since Release 1.7.0 + */ + function tableInfo($result = null, $mode = null) + { + if (function_exists('dbase_get_header_info')) { + $id = @dbase_get_header_info($this->connection); + if (!$id && $php_errormsg) { + return $this->raiseError(DB_ERROR, + null, null, null, + $php_errormsg); + } + } else { + /* + * This segment for PHP 4 is loosely based on code by + * Hadi Rusiah <deegos@yahoo.com> in the comments on + * the dBase reference page in the PHP manual. + */ + $db = @fopen($this->dsn['database'], 'r'); + if (!$db) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $php_errormsg); + } + + $id = array(); + $i = 0; + + $line = fread($db, 32); + while (!feof($db)) { + $line = fread($db, 32); + if (substr($line, 0, 1) == chr(13)) { + break; + } else { + $pos = strpos(substr($line, 0, 10), chr(0)); + $pos = ($pos == 0 ? 10 : $pos); + $id[$i] = array( + 'name' => substr($line, 0, $pos), + 'type' => $this->types[substr($line, 11, 1)], + 'length' => ord(substr($line, 16, 1)), + 'precision' => ord(substr($line, 17, 1)), + ); + } + $i++; + } + + fclose($db); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $res = array(); + $count = count($id); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + $res[$i] = array( + 'table' => $this->dsn['database'], + 'name' => $case_func($id[$i]['name']), + 'type' => $id[$i]['type'], + 'len' => $id[$i]['length'], + 'flags' => '' + ); + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + return $res; + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/fbsql.php b/extlib/DB/fbsql.php new file mode 100644 index 000000000..4de4078f7 --- /dev/null +++ b/extlib/DB/fbsql.php @@ -0,0 +1,769 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's fbsql extension + * for interacting with FrontBase databases + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Frank M. Kromann <frank@frontbase.com> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: fbsql.php,v 1.88 2007/07/06 05:19:21 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's fbsql extension + * for interacting with FrontBase databases + * + * These methods overload the ones declared in DB_common. + * + * @category Database + * @package DB + * @author Frank M. Kromann <frank@frontbase.com> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + * @since Class functional since Release 1.7.0 + */ +class DB_fbsql extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'fbsql'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'fbsql'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => 'alter', + 'new_link' => false, + 'numrows' => true, + 'pconnect' => true, + 'prepare' => false, + 'ssl' => false, + 'transactions' => true, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + 22 => DB_ERROR_SYNTAX, + 85 => DB_ERROR_ALREADY_EXISTS, + 108 => DB_ERROR_SYNTAX, + 116 => DB_ERROR_NOSUCHTABLE, + 124 => DB_ERROR_VALUE_COUNT_ON_ROW, + 215 => DB_ERROR_NOSUCHFIELD, + 217 => DB_ERROR_INVALID_NUMBER, + 226 => DB_ERROR_NOSUCHFIELD, + 231 => DB_ERROR_INVALID, + 239 => DB_ERROR_TRUNCATED, + 251 => DB_ERROR_SYNTAX, + 266 => DB_ERROR_NOT_FOUND, + 357 => DB_ERROR_CONSTRAINT_NOT_NULL, + 358 => DB_ERROR_CONSTRAINT, + 360 => DB_ERROR_CONSTRAINT, + 361 => DB_ERROR_CONSTRAINT, + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_fbsql() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('fbsql')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + $params = array( + $dsn['hostspec'] ? $dsn['hostspec'] : 'localhost', + $dsn['username'] ? $dsn['username'] : null, + $dsn['password'] ? $dsn['password'] : null, + ); + + $connect_function = $persistent ? 'fbsql_pconnect' : 'fbsql_connect'; + + $ini = ini_get('track_errors'); + $php_errormsg = ''; + if ($ini) { + $this->connection = @call_user_func_array($connect_function, + $params); + } else { + @ini_set('track_errors', 1); + $this->connection = @call_user_func_array($connect_function, + $params); + @ini_set('track_errors', $ini); + } + + if (!$this->connection) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $php_errormsg); + } + + if ($dsn['database']) { + if (!@fbsql_select_db($dsn['database'], $this->connection)) { + return $this->fbsqlRaiseError(); + } + } + + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @fbsql_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $this->last_query = $query; + $query = $this->modifyQuery($query); + $result = @fbsql_query("$query;", $this->connection); + if (!$result) { + return $this->fbsqlRaiseError(); + } + // Determine which queries that should return data, and which + // should return an error code only. + if ($this->_checkManip($query)) { + return DB_OK; + } + return $result; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal fbsql result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return @fbsql_next_result($result); + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if ($rownum !== null) { + if (!@fbsql_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @fbsql_fetch_array($result, FBSQL_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @fbsql_fetch_row($result); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return is_resource($result) ? fbsql_free_result($result) : false; + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff=false) + { + if ($onoff) { + $this->query("SET COMMIT TRUE"); + } else { + $this->query("SET COMMIT FALSE"); + } + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + @fbsql_commit($this->connection); + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + @fbsql_rollback($this->connection); + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @fbsql_num_fields($result); + if (!$cols) { + return $this->fbsqlRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows() + */ + function numRows($result) + { + $rows = @fbsql_num_rows($result); + if ($rows === null) { + return $this->fbsqlRaiseError(); + } + return $rows; + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + if ($this->_last_query_manip) { + $result = @fbsql_affected_rows($this->connection); + } else { + $result = 0; + } + return $result; + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_fbsql::createSequence(), DB_fbsql::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + do { + $repeat = 0; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query('SELECT UNIQUE FROM ' . $seqname); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) { + $repeat = 1; + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $result; + } + } else { + $repeat = 0; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->fbsqlRaiseError(); + } + $result->fetchInto($tmp, DB_FETCHMODE_ORDERED); + return $tmp[0]; + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_fbsql::nextID(), DB_fbsql::dropSequence() + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + $res = $this->query('CREATE TABLE ' . $seqname + . ' (id INTEGER NOT NULL,' + . ' PRIMARY KEY(id))'); + if ($res) { + $res = $this->query('SET UNIQUE = 0 FOR ' . $seqname); + } + return $res; + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_fbsql::nextID(), DB_fbsql::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name) + . ' RESTRICT'); + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * Adds LIMIT clauses to a query string according to current DBMS standards + * + * @param string $query the query to modify + * @param int $from the row to start to fetching (0 = the first row) + * @param int $count the numbers of rows to fetch + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return string the query string with LIMIT clauses added + * + * @access protected + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + if (DB::isManip($query) || $this->_next_query_manip) { + return preg_replace('/^([\s(])*SELECT/i', + "\\1SELECT TOP($count)", $query); + } else { + return preg_replace('/([\s(])*SELECT/i', + "\\1SELECT TOP($from, $count)", $query); + } + } + + // }}} + // {{{ quoteBoolean() + + /** + * Formats a boolean value for use within a query in a locale-independent + * manner. + * + * @param boolean the boolean value to be quoted. + * @return string the quoted string. + * @see DB_common::quoteSmart() + * @since Method available since release 1.7.8. + */ + function quoteBoolean($boolean) { + return $boolean ? 'TRUE' : 'FALSE'; + } + + // }}} + // {{{ quoteFloat() + + /** + * Formats a float value for use within a query in a locale-independent + * manner. + * + * @param float the float value to be quoted. + * @return string the quoted string. + * @see DB_common::quoteSmart() + * @since Method available since release 1.7.8. + */ + function quoteFloat($float) { + return $this->escapeSimple(str_replace(',', '.', strval(floatval($float)))); + } + + // }}} + // {{{ fbsqlRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_fbsql::errorNative(), DB_common::errorCode() + */ + function fbsqlRaiseError($errno = null) + { + if ($errno === null) { + $errno = $this->errorCode(fbsql_errno($this->connection)); + } + return $this->raiseError($errno, null, null, null, + @fbsql_error($this->connection)); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error code produced by the last query + * + * @return int the DBMS' error code + */ + function errorNative() + { + return @fbsql_errno($this->connection); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @fbsql_list_fields($this->dsn['database'], + $result, $this->connection); + $got_string = true; + } elseif (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->fbsqlRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @fbsql_num_fields($id); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + $res[$i] = array( + 'table' => $case_func(@fbsql_field_table($id, $i)), + 'name' => $case_func(@fbsql_field_name($id, $i)), + 'type' => @fbsql_field_type($id, $i), + 'len' => @fbsql_field_len($id, $i), + 'flags' => @fbsql_field_flags($id, $i), + ); + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + // free the result only if we were called on a table + if ($got_string) { + @fbsql_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'SELECT "table_name" FROM information_schema.tables' + . ' t0, information_schema.schemata t1' + . ' WHERE t0.schema_pk=t1.schema_pk AND' + . ' "table_type" = \'BASE TABLE\'' + . ' AND "schema_name" = current_schema'; + case 'views': + return 'SELECT "table_name" FROM information_schema.tables' + . ' t0, information_schema.schemata t1' + . ' WHERE t0.schema_pk=t1.schema_pk AND' + . ' "table_type" = \'VIEW\'' + . ' AND "schema_name" = current_schema'; + case 'users': + return 'SELECT "user_name" from information_schema.users'; + case 'functions': + return 'SELECT "routine_name" FROM' + . ' information_schema.psm_routines' + . ' t0, information_schema.schemata t1' + . ' WHERE t0.schema_pk=t1.schema_pk' + . ' AND "routine_kind"=\'FUNCTION\'' + . ' AND "schema_name" = current_schema'; + case 'procedures': + return 'SELECT "routine_name" FROM' + . ' information_schema.psm_routines' + . ' t0, information_schema.schemata t1' + . ' WHERE t0.schema_pk=t1.schema_pk' + . ' AND "routine_kind"=\'PROCEDURE\'' + . ' AND "schema_name" = current_schema'; + default: + return null; + } + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/ibase.php b/extlib/DB/ibase.php new file mode 100644 index 000000000..ee19c5589 --- /dev/null +++ b/extlib/DB/ibase.php @@ -0,0 +1,1082 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's interbase extension + * for interacting with Interbase and Firebird databases + * + * While this class works with PHP 4, PHP's InterBase extension is + * unstable in PHP 4. Use PHP 5. + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Sterling Hughes <sterling@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: ibase.php,v 1.116 2007/09/21 13:40:41 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's interbase extension + * for interacting with Interbase and Firebird databases + * + * These methods overload the ones declared in DB_common. + * + * While this class works with PHP 4, PHP's InterBase extension is + * unstable in PHP 4. Use PHP 5. + * + * NOTICE: limitQuery() only works for Firebird. + * + * @category Database + * @package DB + * @author Sterling Hughes <sterling@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + * @since Class became stable in Release 1.7.0 + */ +class DB_ibase extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'ibase'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'ibase'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * NOTE: only firebird supports limit. + * + * @var array + */ + var $features = array( + 'limit' => false, + 'new_link' => false, + 'numrows' => 'emulate', + 'pconnect' => true, + 'prepare' => true, + 'ssl' => false, + 'transactions' => true, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + -104 => DB_ERROR_SYNTAX, + -150 => DB_ERROR_ACCESS_VIOLATION, + -151 => DB_ERROR_ACCESS_VIOLATION, + -155 => DB_ERROR_NOSUCHTABLE, + -157 => DB_ERROR_NOSUCHFIELD, + -158 => DB_ERROR_VALUE_COUNT_ON_ROW, + -170 => DB_ERROR_MISMATCH, + -171 => DB_ERROR_MISMATCH, + -172 => DB_ERROR_INVALID, + // -204 => // Covers too many errors, need to use regex on msg + -205 => DB_ERROR_NOSUCHFIELD, + -206 => DB_ERROR_NOSUCHFIELD, + -208 => DB_ERROR_INVALID, + -219 => DB_ERROR_NOSUCHTABLE, + -297 => DB_ERROR_CONSTRAINT, + -303 => DB_ERROR_INVALID, + -413 => DB_ERROR_INVALID_NUMBER, + -530 => DB_ERROR_CONSTRAINT, + -551 => DB_ERROR_ACCESS_VIOLATION, + -552 => DB_ERROR_ACCESS_VIOLATION, + // -607 => // Covers too many errors, need to use regex on msg + -625 => DB_ERROR_CONSTRAINT_NOT_NULL, + -803 => DB_ERROR_CONSTRAINT, + -804 => DB_ERROR_VALUE_COUNT_ON_ROW, + // -902 => // Covers too many errors, need to use regex on msg + -904 => DB_ERROR_CONNECT_FAILED, + -922 => DB_ERROR_NOSUCHDB, + -923 => DB_ERROR_CONNECT_FAILED, + -924 => DB_ERROR_CONNECT_FAILED + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * The number of rows affected by a data manipulation query + * @var integer + * @access private + */ + var $affected = 0; + + /** + * Should data manipulation queries be committed automatically? + * @var bool + * @access private + */ + var $autocommit = true; + + /** + * The prepared statement handle from the most recently executed statement + * + * {@internal Mainly here because the InterBase/Firebird API is only + * able to retrieve data from result sets if the statemnt handle is + * still in scope.}} + * + * @var resource + */ + var $last_stmt; + + /** + * Is the given prepared statement a data manipulation query? + * @var array + * @access private + */ + var $manip_query = array(); + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_ibase() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * PEAR DB's ibase driver supports the following extra DSN options: + * + buffers The number of database buffers to allocate for the + * server-side cache. + * + charset The default character set for a database. + * + dialect The default SQL dialect for any statement + * executed within a connection. Defaults to the + * highest one supported by client libraries. + * Functional only with InterBase 6 and up. + * + role Functional only with InterBase 5 and up. + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('interbase')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + if ($this->dbsyntax == 'firebird') { + $this->features['limit'] = 'alter'; + } + + $params = array( + $dsn['hostspec'] + ? ($dsn['hostspec'] . ':' . $dsn['database']) + : $dsn['database'], + $dsn['username'] ? $dsn['username'] : null, + $dsn['password'] ? $dsn['password'] : null, + isset($dsn['charset']) ? $dsn['charset'] : null, + isset($dsn['buffers']) ? $dsn['buffers'] : null, + isset($dsn['dialect']) ? $dsn['dialect'] : null, + isset($dsn['role']) ? $dsn['role'] : null, + ); + + $connect_function = $persistent ? 'ibase_pconnect' : 'ibase_connect'; + + $this->connection = @call_user_func_array($connect_function, $params); + if (!$this->connection) { + return $this->ibaseRaiseError(DB_ERROR_CONNECT_FAILED); + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @ibase_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $ismanip = $this->_checkManip($query); + $this->last_query = $query; + $query = $this->modifyQuery($query); + $result = @ibase_query($this->connection, $query); + + if (!$result) { + return $this->ibaseRaiseError(); + } + if ($this->autocommit && $ismanip) { + @ibase_commit($this->connection); + } + if ($ismanip) { + $this->affected = $result; + return DB_OK; + } else { + $this->affected = 0; + return $result; + } + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * Adds LIMIT clauses to a query string according to current DBMS standards + * + * Only works with Firebird. + * + * @param string $query the query to modify + * @param int $from the row to start to fetching (0 = the first row) + * @param int $count the numbers of rows to fetch + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return string the query string with LIMIT clauses added + * + * @access protected + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + if ($this->dsn['dbsyntax'] == 'firebird') { + $query = preg_replace('/^([\s(])*SELECT/i', + "SELECT FIRST $count SKIP $from", $query); + } + return $query; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal ibase result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if ($rownum !== null) { + return $this->ibaseRaiseError(DB_ERROR_NOT_CAPABLE); + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + if (function_exists('ibase_fetch_assoc')) { + $arr = @ibase_fetch_assoc($result); + } else { + $arr = get_object_vars(ibase_fetch_object($result)); + } + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @ibase_fetch_row($result); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return is_resource($result) ? ibase_free_result($result) : false; + } + + // }}} + // {{{ freeQuery() + + function freeQuery($query) + { + return is_resource($query) ? ibase_free_query($query) : false; + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + if (is_integer($this->affected)) { + return $this->affected; + } + return $this->ibaseRaiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @ibase_num_fields($result); + if (!$cols) { + return $this->ibaseRaiseError(); + } + return $cols; + } + + // }}} + // {{{ prepare() + + /** + * Prepares a query for multiple execution with execute(). + * + * prepare() requires a generic query as string like <code> + * INSERT INTO numbers VALUES (?, ?, ?) + * </code>. The <kbd>?</kbd> characters are placeholders. + * + * Three types of placeholders can be used: + * + <kbd>?</kbd> a quoted scalar value, i.e. strings, integers + * + <kbd>!</kbd> value is inserted 'as is' + * + <kbd>&</kbd> requires a file name. The file's contents get + * inserted into the query (i.e. saving binary + * data in a db) + * + * Use backslashes to escape placeholder characters if you don't want + * them to be interpreted as placeholders. Example: <code> + * "UPDATE foo SET col=? WHERE col='over \& under'" + * </code> + * + * @param string $query query to be prepared + * @return mixed DB statement resource on success. DB_Error on failure. + */ + function prepare($query) + { + $tokens = preg_split('/((?<!\\\)[&?!])/', $query, -1, + PREG_SPLIT_DELIM_CAPTURE); + $token = 0; + $types = array(); + $newquery = ''; + + foreach ($tokens as $key => $val) { + switch ($val) { + case '?': + $types[$token++] = DB_PARAM_SCALAR; + break; + case '&': + $types[$token++] = DB_PARAM_OPAQUE; + break; + case '!': + $types[$token++] = DB_PARAM_MISC; + break; + default: + $tokens[$key] = preg_replace('/\\\([&?!])/', "\\1", $val); + $newquery .= $tokens[$key] . '?'; + } + } + + $newquery = substr($newquery, 0, -1); + $this->last_query = $query; + $newquery = $this->modifyQuery($newquery); + $stmt = @ibase_prepare($this->connection, $newquery); + + if ($stmt === false) { + $stmt = $this->ibaseRaiseError(); + } else { + $this->prepare_types[(int)$stmt] = $types; + $this->manip_query[(int)$stmt] = DB::isManip($query); + } + + return $stmt; + } + + // }}} + // {{{ execute() + + /** + * Executes a DB statement prepared with prepare(). + * + * @param resource $stmt a DB statement resource returned from prepare() + * @param mixed $data array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 for non-array items or the + * quantity of elements in the array. + * @return object a new DB_Result or a DB_Error when fail + * @see DB_ibase::prepare() + * @access public + */ + function &execute($stmt, $data = array()) + { + $data = (array)$data; + $this->last_parameters = $data; + + $types = $this->prepare_types[(int)$stmt]; + if (count($types) != count($data)) { + $tmp = $this->raiseError(DB_ERROR_MISMATCH); + return $tmp; + } + + $i = 0; + foreach ($data as $key => $value) { + if ($types[$i] == DB_PARAM_MISC) { + /* + * ibase doesn't seem to have the ability to pass a + * parameter along unchanged, so strip off quotes from start + * and end, plus turn two single quotes to one single quote, + * in order to avoid the quotes getting escaped by + * ibase and ending up in the database. + */ + $data[$key] = preg_replace("/^'(.*)'$/", "\\1", $data[$key]); + $data[$key] = str_replace("''", "'", $data[$key]); + } elseif ($types[$i] == DB_PARAM_OPAQUE) { + $fp = @fopen($data[$key], 'rb'); + if (!$fp) { + $tmp = $this->raiseError(DB_ERROR_ACCESS_VIOLATION); + return $tmp; + } + $data[$key] = fread($fp, filesize($data[$key])); + fclose($fp); + } + $i++; + } + + array_unshift($data, $stmt); + + $res = call_user_func_array('ibase_execute', $data); + if (!$res) { + $tmp = $this->ibaseRaiseError(); + return $tmp; + } + /* XXX need this? + if ($this->autocommit && $this->manip_query[(int)$stmt]) { + @ibase_commit($this->connection); + }*/ + $this->last_stmt = $stmt; + if ($this->manip_query[(int)$stmt] || $this->_next_query_manip) { + $this->_last_query_manip = true; + $this->_next_query_manip = false; + $tmp = DB_OK; + } else { + $this->_last_query_manip = false; + $tmp = new DB_result($this, $res); + } + return $tmp; + } + + /** + * Frees the internal resources associated with a prepared query + * + * @param resource $stmt the prepared statement's PHP resource + * @param bool $free_resource should the PHP resource be freed too? + * Use false if you need to get data + * from the result set later. + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_ibase::prepare() + */ + function freePrepared($stmt, $free_resource = true) + { + if (!is_resource($stmt)) { + return false; + } + if ($free_resource) { + @ibase_free_query($stmt); + } + unset($this->prepare_tokens[(int)$stmt]); + unset($this->prepare_types[(int)$stmt]); + unset($this->manip_query[(int)$stmt]); + return true; + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff = false) + { + $this->autocommit = $onoff ? 1 : 0; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + return @ibase_commit($this->connection); + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + return @ibase_rollback($this->connection); + } + + // }}} + // {{{ transactionInit() + + function transactionInit($trans_args = 0) + { + return $trans_args + ? @ibase_trans($trans_args, $this->connection) + : @ibase_trans(); + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_ibase::createSequence(), DB_ibase::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $sqn = strtoupper($this->getSequenceName($seq_name)); + $repeat = 0; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("SELECT GEN_ID(${sqn}, 1) " + . 'FROM RDB$GENERATORS ' + . "WHERE RDB\$GENERATOR_NAME='${sqn}'"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result)) { + $repeat = 1; + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $result; + } + } else { + $repeat = 0; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $arr = $result->fetchRow(DB_FETCHMODE_ORDERED); + $result->free(); + return $arr[0]; + } + + // }}} + // {{{ createSequence() + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_ibase::nextID(), DB_ibase::dropSequence() + */ + function createSequence($seq_name) + { + $sqn = strtoupper($this->getSequenceName($seq_name)); + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("CREATE GENERATOR ${sqn}"); + $this->popErrorHandling(); + + return $result; + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_ibase::nextID(), DB_ibase::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DELETE FROM RDB$GENERATORS ' + . "WHERE RDB\$GENERATOR_NAME='" + . strtoupper($this->getSequenceName($seq_name)) + . "'"); + } + + // }}} + // {{{ _ibaseFieldFlags() + + /** + * Get the column's flags + * + * Supports "primary_key", "unique_key", "not_null", "default", + * "computed" and "blob". + * + * @param string $field_name the name of the field + * @param string $table_name the name of the table + * + * @return string the flags + * + * @access private + */ + function _ibaseFieldFlags($field_name, $table_name) + { + $sql = 'SELECT R.RDB$CONSTRAINT_TYPE CTYPE' + .' FROM RDB$INDEX_SEGMENTS I' + .' JOIN RDB$RELATION_CONSTRAINTS R ON I.RDB$INDEX_NAME=R.RDB$INDEX_NAME' + .' WHERE I.RDB$FIELD_NAME=\'' . $field_name . '\'' + .' AND UPPER(R.RDB$RELATION_NAME)=\'' . strtoupper($table_name) . '\''; + + $result = @ibase_query($this->connection, $sql); + if (!$result) { + return $this->ibaseRaiseError(); + } + + $flags = ''; + if ($obj = @ibase_fetch_object($result)) { + @ibase_free_result($result); + if (isset($obj->CTYPE) && trim($obj->CTYPE) == 'PRIMARY KEY') { + $flags .= 'primary_key '; + } + if (isset($obj->CTYPE) && trim($obj->CTYPE) == 'UNIQUE') { + $flags .= 'unique_key '; + } + } + + $sql = 'SELECT R.RDB$NULL_FLAG AS NFLAG,' + .' R.RDB$DEFAULT_SOURCE AS DSOURCE,' + .' F.RDB$FIELD_TYPE AS FTYPE,' + .' F.RDB$COMPUTED_SOURCE AS CSOURCE' + .' FROM RDB$RELATION_FIELDS R ' + .' JOIN RDB$FIELDS F ON R.RDB$FIELD_SOURCE=F.RDB$FIELD_NAME' + .' WHERE UPPER(R.RDB$RELATION_NAME)=\'' . strtoupper($table_name) . '\'' + .' AND R.RDB$FIELD_NAME=\'' . $field_name . '\''; + + $result = @ibase_query($this->connection, $sql); + if (!$result) { + return $this->ibaseRaiseError(); + } + if ($obj = @ibase_fetch_object($result)) { + @ibase_free_result($result); + if (isset($obj->NFLAG)) { + $flags .= 'not_null '; + } + if (isset($obj->DSOURCE)) { + $flags .= 'default '; + } + if (isset($obj->CSOURCE)) { + $flags .= 'computed '; + } + if (isset($obj->FTYPE) && $obj->FTYPE == 261) { + $flags .= 'blob '; + } + } + + return trim($flags); + } + + // }}} + // {{{ ibaseRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_ibase::errorNative(), DB_ibase::errorCode() + */ + function &ibaseRaiseError($errno = null) + { + if ($errno === null) { + $errno = $this->errorCode($this->errorNative()); + } + $tmp = $this->raiseError($errno, null, null, null, @ibase_errmsg()); + return $tmp; + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error code produced by the last query + * + * @return int the DBMS' error code. NULL if there is no error code. + * + * @since Method available since Release 1.7.0 + */ + function errorNative() + { + if (function_exists('ibase_errcode')) { + return @ibase_errcode(); + } + if (preg_match('/^Dynamic SQL Error SQL error code = ([0-9-]+)/i', + @ibase_errmsg(), $m)) { + return (int)$m[1]; + } + return null; + } + + // }}} + // {{{ errorCode() + + /** + * Maps native error codes to DB's portable ones + * + * @param int $nativecode the error code returned by the DBMS + * + * @return int the portable DB error code. Return DB_ERROR if the + * current driver doesn't have a mapping for the + * $nativecode submitted. + * + * @since Method available since Release 1.7.0 + */ + function errorCode($nativecode = null) + { + if (isset($this->errorcode_map[$nativecode])) { + return $this->errorcode_map[$nativecode]; + } + + static $error_regexps; + if (!isset($error_regexps)) { + $error_regexps = array( + '/generator .* is not defined/' + => DB_ERROR_SYNTAX, // for compat. w ibase_errcode() + '/table.*(not exist|not found|unknown)/i' + => DB_ERROR_NOSUCHTABLE, + '/table .* already exists/i' + => DB_ERROR_ALREADY_EXISTS, + '/unsuccessful metadata update .* failed attempt to store duplicate value/i' + => DB_ERROR_ALREADY_EXISTS, + '/unsuccessful metadata update .* not found/i' + => DB_ERROR_NOT_FOUND, + '/validation error for column .* value "\*\*\* null/i' + => DB_ERROR_CONSTRAINT_NOT_NULL, + '/violation of [\w ]+ constraint/i' + => DB_ERROR_CONSTRAINT, + '/conversion error from string/i' + => DB_ERROR_INVALID_NUMBER, + '/no permission for/i' + => DB_ERROR_ACCESS_VIOLATION, + '/arithmetic exception, numeric overflow, or string truncation/i' + => DB_ERROR_INVALID, + '/feature is not supported/i' + => DB_ERROR_NOT_CAPABLE, + ); + } + + $errormsg = @ibase_errmsg(); + foreach ($error_regexps as $regexp => $code) { + if (preg_match($regexp, $errormsg)) { + return $code; + } + } + return DB_ERROR; + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * NOTE: only supports 'table' and 'flags' if <var>$result</var> + * is a table name. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @ibase_query($this->connection, + "SELECT * FROM $result WHERE 1=0"); + $got_string = true; + } elseif (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->ibaseRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @ibase_num_fields($id); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + $info = @ibase_field_info($id, $i); + $res[$i] = array( + 'table' => $got_string ? $case_func($result) : '', + 'name' => $case_func($info['name']), + 'type' => $info['type'], + 'len' => $info['length'], + 'flags' => ($got_string) + ? $this->_ibaseFieldFlags($info['name'], $result) + : '', + ); + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + // free the result only if we were called on a table + if ($got_string) { + @ibase_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'SELECT DISTINCT R.RDB$RELATION_NAME FROM ' + . 'RDB$RELATION_FIELDS R WHERE R.RDB$SYSTEM_FLAG=0'; + case 'views': + return 'SELECT DISTINCT RDB$VIEW_NAME from RDB$VIEW_RELATIONS'; + case 'users': + return 'SELECT DISTINCT RDB$USER FROM RDB$USER_PRIVILEGES'; + default: + return null; + } + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/ifx.php b/extlib/DB/ifx.php new file mode 100644 index 000000000..baa6f2867 --- /dev/null +++ b/extlib/DB/ifx.php @@ -0,0 +1,683 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's ifx extension + * for interacting with Informix databases + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Tomas V.V.Cox <cox@idecnet.com> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: ifx.php,v 1.75 2007/07/06 05:19:21 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's ifx extension + * for interacting with Informix databases + * + * These methods overload the ones declared in DB_common. + * + * More info on Informix errors can be found at: + * http://www.informix.com/answers/english/ierrors.htm + * + * TODO: + * - set needed env Informix vars on connect + * - implement native prepare/execute + * + * @category Database + * @package DB + * @author Tomas V.V.Cox <cox@idecnet.com> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_ifx extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'ifx'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'ifx'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => 'emulate', + 'new_link' => false, + 'numrows' => 'emulate', + 'pconnect' => true, + 'prepare' => false, + 'ssl' => false, + 'transactions' => true, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + '-201' => DB_ERROR_SYNTAX, + '-206' => DB_ERROR_NOSUCHTABLE, + '-217' => DB_ERROR_NOSUCHFIELD, + '-236' => DB_ERROR_VALUE_COUNT_ON_ROW, + '-239' => DB_ERROR_CONSTRAINT, + '-253' => DB_ERROR_SYNTAX, + '-268' => DB_ERROR_CONSTRAINT, + '-292' => DB_ERROR_CONSTRAINT_NOT_NULL, + '-310' => DB_ERROR_ALREADY_EXISTS, + '-316' => DB_ERROR_ALREADY_EXISTS, + '-319' => DB_ERROR_NOT_FOUND, + '-329' => DB_ERROR_NODBSELECTED, + '-346' => DB_ERROR_CONSTRAINT, + '-386' => DB_ERROR_CONSTRAINT_NOT_NULL, + '-391' => DB_ERROR_CONSTRAINT_NOT_NULL, + '-554' => DB_ERROR_SYNTAX, + '-691' => DB_ERROR_CONSTRAINT, + '-692' => DB_ERROR_CONSTRAINT, + '-703' => DB_ERROR_CONSTRAINT_NOT_NULL, + '-1202' => DB_ERROR_DIVZERO, + '-1204' => DB_ERROR_INVALID_DATE, + '-1205' => DB_ERROR_INVALID_DATE, + '-1206' => DB_ERROR_INVALID_DATE, + '-1209' => DB_ERROR_INVALID_DATE, + '-1210' => DB_ERROR_INVALID_DATE, + '-1212' => DB_ERROR_INVALID_DATE, + '-1213' => DB_ERROR_INVALID_NUMBER, + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * Should data manipulation queries be committed automatically? + * @var bool + * @access private + */ + var $autocommit = true; + + /** + * The quantity of transactions begun + * + * {@internal While this is private, it can't actually be designated + * private in PHP 5 because it is directly accessed in the test suite.}} + * + * @var integer + * @access private + */ + var $transaction_opcount = 0; + + /** + * The number of rows affected by a data manipulation query + * @var integer + * @access private + */ + var $affected = 0; + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_ifx() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('informix') && + !PEAR::loadExtension('Informix')) + { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + $dbhost = $dsn['hostspec'] ? '@' . $dsn['hostspec'] : ''; + $dbname = $dsn['database'] ? $dsn['database'] . $dbhost : ''; + $user = $dsn['username'] ? $dsn['username'] : ''; + $pw = $dsn['password'] ? $dsn['password'] : ''; + + $connect_function = $persistent ? 'ifx_pconnect' : 'ifx_connect'; + + $this->connection = @$connect_function($dbname, $user, $pw); + if (!is_resource($this->connection)) { + return $this->ifxRaiseError(DB_ERROR_CONNECT_FAILED); + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @ifx_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $ismanip = $this->_checkManip($query); + $this->last_query = $query; + $this->affected = null; + if (preg_match('/(SELECT|EXECUTE)/i', $query)) { //TESTME: Use !DB::isManip()? + // the scroll is needed for fetching absolute row numbers + // in a select query result + $result = @ifx_query($query, $this->connection, IFX_SCROLL); + } else { + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @ifx_query('BEGIN WORK', $this->connection); + if (!$result) { + return $this->ifxRaiseError(); + } + } + $this->transaction_opcount++; + } + $result = @ifx_query($query, $this->connection); + } + if (!$result) { + return $this->ifxRaiseError(); + } + $this->affected = @ifx_affected_rows($result); + // Determine which queries should return data, and which + // should return an error code only. + if (preg_match('/(SELECT|EXECUTE)/i', $query)) { + return $result; + } + // XXX Testme: free results inside a transaction + // may cause to stop it and commit the work? + + // Result has to be freed even with a insert or update + @ifx_free_result($result); + + return DB_OK; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal ifx result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + if ($this->_last_query_manip) { + return $this->affected; + } else { + return 0; + } + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if (($rownum !== null) && ($rownum < 0)) { + return null; + } + if ($rownum === null) { + /* + * Even though fetch_row() should return the next row if + * $rownum is null, it doesn't in all cases. Bug 598. + */ + $rownum = 'NEXT'; + } else { + // Index starts at row 1, unlike most DBMS's starting at 0. + $rownum++; + } + if (!$arr = @ifx_fetch_row($result, $rownum)) { + return null; + } + if ($fetchmode !== DB_FETCHMODE_ASSOC) { + $i=0; + $order = array(); + foreach ($arr as $val) { + $order[$i++] = $val; + } + $arr = $order; + } elseif ($fetchmode == DB_FETCHMODE_ASSOC && + $this->options['portability'] & DB_PORTABILITY_LOWERCASE) + { + $arr = array_change_key_case($arr, CASE_LOWER); + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + if (!$cols = @ifx_num_fields($result)) { + return $this->ifxRaiseError(); + } + return $cols; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return is_resource($result) ? ifx_free_result($result) : false; + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff = true) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + $result = @ifx_query('COMMIT WORK', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->ifxRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + $result = @ifx_query('ROLLBACK WORK', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->ifxRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ ifxRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_ifx::errorNative(), DB_ifx::errorCode() + */ + function ifxRaiseError($errno = null) + { + if ($errno === null) { + $errno = $this->errorCode(ifx_error()); + } + return $this->raiseError($errno, null, null, null, + $this->errorNative()); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error code and message produced by the last query + * + * @return string the DBMS' error code and message + */ + function errorNative() + { + return @ifx_error() . ' ' . @ifx_errormsg(); + } + + // }}} + // {{{ errorCode() + + /** + * Maps native error codes to DB's portable ones. + * + * Requires that the DB implementation's constructor fills + * in the <var>$errorcode_map</var> property. + * + * @param string $nativecode error code returned by the database + * @return int a portable DB error code, or DB_ERROR if this DB + * implementation has no mapping for the given error code. + */ + function errorCode($nativecode) + { + if (ereg('SQLCODE=(.*)]', $nativecode, $match)) { + $code = $match[1]; + if (isset($this->errorcode_map[$code])) { + return $this->errorcode_map[$code]; + } + } + return DB_ERROR; + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * NOTE: only supports 'table' if <var>$result</var> is a table name. + * + * If analyzing a query result and the result has duplicate field names, + * an error will be raised saying + * <samp>can't distinguish duplicate field names</samp>. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + * @since Method available since Release 1.6.0 + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @ifx_query("SELECT * FROM $result WHERE 1=0", + $this->connection); + $got_string = true; + } elseif (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->ifxRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + $flds = @ifx_fieldproperties($id); + $count = @ifx_num_fields($id); + + if (count($flds) != $count) { + return $this->raiseError("can't distinguish duplicate field names"); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $i = 0; + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + foreach ($flds as $key => $value) { + $props = explode(';', $value); + $res[$i] = array( + 'table' => $got_string ? $case_func($result) : '', + 'name' => $case_func($key), + 'type' => $props[0], + 'len' => $props[1], + 'flags' => $props[4] == 'N' ? 'not_null' : '', + ); + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + $i++; + } + + // free the result only if we were called on a table + if ($got_string) { + @ifx_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'SELECT tabname FROM systables WHERE tabid >= 100'; + default: + return null; + } + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/msql.php b/extlib/DB/msql.php new file mode 100644 index 000000000..34854f472 --- /dev/null +++ b/extlib/DB/msql.php @@ -0,0 +1,831 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's msql extension + * for interacting with Mini SQL databases + * + * PHP's mSQL extension did weird things with NULL values prior to PHP + * 4.3.11 and 5.0.4. Make sure your version of PHP meets or exceeds + * those versions. + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: msql.php,v 1.64 2007/09/21 13:40:41 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's msql extension + * for interacting with Mini SQL databases + * + * These methods overload the ones declared in DB_common. + * + * PHP's mSQL extension did weird things with NULL values prior to PHP + * 4.3.11 and 5.0.4. Make sure your version of PHP meets or exceeds + * those versions. + * + * @category Database + * @package DB + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + * @since Class not functional until Release 1.7.0 + */ +class DB_msql extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'msql'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'msql'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => 'emulate', + 'new_link' => false, + 'numrows' => true, + 'pconnect' => true, + 'prepare' => false, + 'ssl' => false, + 'transactions' => false, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * The query result resource created by PHP + * + * Used to make affectedRows() work. Only contains the result for + * data manipulation queries. Contains false for other queries. + * + * @var resource + * @access private + */ + var $_result; + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_msql() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * Example of how to connect: + * <code> + * require_once 'DB.php'; + * + * // $dsn = 'msql://hostname/dbname'; // use a TCP connection + * $dsn = 'msql:///dbname'; // use a socket + * $options = array( + * 'portability' => DB_PORTABILITY_ALL, + * ); + * + * $db = DB::connect($dsn, $options); + * if (PEAR::isError($db)) { + * die($db->getMessage()); + * } + * </code> + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('msql')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + $params = array(); + if ($dsn['hostspec']) { + $params[] = $dsn['port'] + ? $dsn['hostspec'] . ',' . $dsn['port'] + : $dsn['hostspec']; + } + + $connect_function = $persistent ? 'msql_pconnect' : 'msql_connect'; + + $ini = ini_get('track_errors'); + $php_errormsg = ''; + if ($ini) { + $this->connection = @call_user_func_array($connect_function, + $params); + } else { + @ini_set('track_errors', 1); + $this->connection = @call_user_func_array($connect_function, + $params); + @ini_set('track_errors', $ini); + } + + if (!$this->connection) { + if (($err = @msql_error()) != '') { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $err); + } else { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $php_errormsg); + } + } + + if (!@msql_select_db($dsn['database'], $this->connection)) { + return $this->msqlRaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @msql_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $this->last_query = $query; + $query = $this->modifyQuery($query); + $result = @msql_query($query, $this->connection); + if (!$result) { + return $this->msqlRaiseError(); + } + // Determine which queries that should return data, and which + // should return an error code only. + if ($this->_checkManip($query)) { + $this->_result = $result; + return DB_OK; + } else { + $this->_result = false; + return $result; + } + } + + + // }}} + // {{{ nextResult() + + /** + * Move the internal msql result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * PHP's mSQL extension did weird things with NULL values prior to PHP + * 4.3.11 and 5.0.4. Make sure your version of PHP meets or exceeds + * those versions. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if ($rownum !== null) { + if (!@msql_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @msql_fetch_array($result, MSQL_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @msql_fetch_row($result); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return is_resource($result) ? msql_free_result($result) : false; + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @msql_num_fields($result); + if (!$cols) { + return $this->msqlRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows() + */ + function numRows($result) + { + $rows = @msql_num_rows($result); + if ($rows === false) { + return $this->msqlRaiseError(); + } + return $rows; + } + + // }}} + // {{{ affected() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + if (!$this->_result) { + return 0; + } + return msql_affected_rows($this->_result); + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_msql::createSequence(), DB_msql::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + $repeat = false; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("SELECT _seq FROM ${seqname}"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) { + $repeat = true; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->createSequence($seq_name); + $this->popErrorHandling(); + if (DB::isError($result)) { + return $this->raiseError($result); + } + } else { + $repeat = false; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $arr = $result->fetchRow(DB_FETCHMODE_ORDERED); + $result->free(); + return $arr[0]; + } + + // }}} + // {{{ createSequence() + + /** + * Creates a new sequence + * + * Also creates a new table to associate the sequence with. Uses + * a separate table to ensure portability with other drivers. + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_msql::nextID(), DB_msql::dropSequence() + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + $res = $this->query('CREATE TABLE ' . $seqname + . ' (id INTEGER NOT NULL)'); + if (DB::isError($res)) { + return $res; + } + $res = $this->query("CREATE SEQUENCE ON ${seqname}"); + return $res; + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_msql::nextID(), DB_msql::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ quoteIdentifier() + + /** + * mSQL does not support delimited identifiers + * + * @param string $str the identifier name to be quoted + * + * @return object a DB_Error object + * + * @see DB_common::quoteIdentifier() + * @since Method available since Release 1.7.0 + */ + function quoteIdentifier($str) + { + return $this->raiseError(DB_ERROR_UNSUPPORTED); + } + + // }}} + // {{{ quoteFloat() + + /** + * Formats a float value for use within a query in a locale-independent + * manner. + * + * @param float the float value to be quoted. + * @return string the quoted string. + * @see DB_common::quoteSmart() + * @since Method available since release 1.7.8. + */ + function quoteFloat($float) { + return $this->escapeSimple(str_replace(',', '.', strval(floatval($float)))); + } + + // }}} + // {{{ escapeSimple() + + /** + * Escapes a string according to the current DBMS's standards + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @see DB_common::quoteSmart() + * @since Method available since Release 1.7.0 + */ + function escapeSimple($str) + { + return addslashes($str); + } + + // }}} + // {{{ msqlRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_msql::errorNative(), DB_msql::errorCode() + */ + function msqlRaiseError($errno = null) + { + $native = $this->errorNative(); + if ($errno === null) { + $errno = $this->errorCode($native); + } + return $this->raiseError($errno, null, null, null, $native); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error message produced by the last query + * + * @return string the DBMS' error message + */ + function errorNative() + { + return @msql_error(); + } + + // }}} + // {{{ errorCode() + + /** + * Determines PEAR::DB error code from the database's text error message + * + * @param string $errormsg the error message returned from the database + * + * @return integer the error number from a DB_ERROR* constant + */ + function errorCode($errormsg) + { + static $error_regexps; + + // PHP 5.2+ prepends the function name to $php_errormsg, so we need + // this hack to work around it, per bug #9599. + $errormsg = preg_replace('/^msql[a-z_]+\(\): /', '', $errormsg); + + if (!isset($error_regexps)) { + $error_regexps = array( + '/^Access to database denied/i' + => DB_ERROR_ACCESS_VIOLATION, + '/^Bad index name/i' + => DB_ERROR_ALREADY_EXISTS, + '/^Bad order field/i' + => DB_ERROR_SYNTAX, + '/^Bad type for comparison/i' + => DB_ERROR_SYNTAX, + '/^Can\'t perform LIKE on/i' + => DB_ERROR_SYNTAX, + '/^Can\'t use TEXT fields in LIKE comparison/i' + => DB_ERROR_SYNTAX, + '/^Couldn\'t create temporary table/i' + => DB_ERROR_CANNOT_CREATE, + '/^Error creating table file/i' + => DB_ERROR_CANNOT_CREATE, + '/^Field .* cannot be null$/i' + => DB_ERROR_CONSTRAINT_NOT_NULL, + '/^Index (field|condition) .* cannot be null$/i' + => DB_ERROR_SYNTAX, + '/^Invalid date format/i' + => DB_ERROR_INVALID_DATE, + '/^Invalid time format/i' + => DB_ERROR_INVALID, + '/^Literal value for .* is wrong type$/i' + => DB_ERROR_INVALID_NUMBER, + '/^No Database Selected/i' + => DB_ERROR_NODBSELECTED, + '/^No value specified for field/i' + => DB_ERROR_VALUE_COUNT_ON_ROW, + '/^Non unique value for unique index/i' + => DB_ERROR_CONSTRAINT, + '/^Out of memory for temporary table/i' + => DB_ERROR_CANNOT_CREATE, + '/^Permission denied/i' + => DB_ERROR_ACCESS_VIOLATION, + '/^Reference to un-selected table/i' + => DB_ERROR_SYNTAX, + '/^syntax error/i' + => DB_ERROR_SYNTAX, + '/^Table .* exists$/i' + => DB_ERROR_ALREADY_EXISTS, + '/^Unknown database/i' + => DB_ERROR_NOSUCHDB, + '/^Unknown field/i' + => DB_ERROR_NOSUCHFIELD, + '/^Unknown (index|system variable)/i' + => DB_ERROR_NOT_FOUND, + '/^Unknown table/i' + => DB_ERROR_NOSUCHTABLE, + '/^Unqualified field/i' + => DB_ERROR_SYNTAX, + ); + } + + foreach ($error_regexps as $regexp => $code) { + if (preg_match($regexp, $errormsg)) { + return $code; + } + } + return DB_ERROR; + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::setOption() + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @msql_query("SELECT * FROM $result", + $this->connection); + $got_string = true; + } elseif (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->raiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @msql_num_fields($id); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + $tmp = @msql_fetch_field($id); + + $flags = ''; + if ($tmp->not_null) { + $flags .= 'not_null '; + } + if ($tmp->unique) { + $flags .= 'unique_key '; + } + $flags = trim($flags); + + $res[$i] = array( + 'table' => $case_func($tmp->table), + 'name' => $case_func($tmp->name), + 'type' => $tmp->type, + 'len' => msql_field_len($id, $i), + 'flags' => $flags, + ); + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + // free the result only if we were called on a table + if ($got_string) { + @msql_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtain a list of a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return array the array containing the list of objects requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'databases': + $id = @msql_list_dbs($this->connection); + break; + case 'tables': + $id = @msql_list_tables($this->dsn['database'], + $this->connection); + break; + default: + return null; + } + if (!$id) { + return $this->msqlRaiseError(); + } + $out = array(); + while ($row = @msql_fetch_row($id)) { + $out[] = $row[0]; + } + return $out; + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/mssql.php b/extlib/DB/mssql.php new file mode 100644 index 000000000..511a2b686 --- /dev/null +++ b/extlib/DB/mssql.php @@ -0,0 +1,963 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's mssql extension + * for interacting with Microsoft SQL Server databases + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Sterling Hughes <sterling@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: mssql.php,v 1.92 2007/09/21 13:40:41 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's mssql extension + * for interacting with Microsoft SQL Server databases + * + * These methods overload the ones declared in DB_common. + * + * DB's mssql driver is only for Microsfoft SQL Server databases. + * + * If you're connecting to a Sybase database, you MUST specify "sybase" + * as the "phptype" in the DSN. + * + * This class only works correctly if you have compiled PHP using + * --with-mssql=[dir_to_FreeTDS]. + * + * @category Database + * @package DB + * @author Sterling Hughes <sterling@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_mssql extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'mssql'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'mssql'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => 'emulate', + 'new_link' => false, + 'numrows' => true, + 'pconnect' => true, + 'prepare' => false, + 'ssl' => false, + 'transactions' => true, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + // XXX Add here error codes ie: 'S100E' => DB_ERROR_SYNTAX + var $errorcode_map = array( + 102 => DB_ERROR_SYNTAX, + 110 => DB_ERROR_VALUE_COUNT_ON_ROW, + 155 => DB_ERROR_NOSUCHFIELD, + 156 => DB_ERROR_SYNTAX, + 170 => DB_ERROR_SYNTAX, + 207 => DB_ERROR_NOSUCHFIELD, + 208 => DB_ERROR_NOSUCHTABLE, + 245 => DB_ERROR_INVALID_NUMBER, + 319 => DB_ERROR_SYNTAX, + 321 => DB_ERROR_NOSUCHFIELD, + 325 => DB_ERROR_SYNTAX, + 336 => DB_ERROR_SYNTAX, + 515 => DB_ERROR_CONSTRAINT_NOT_NULL, + 547 => DB_ERROR_CONSTRAINT, + 1018 => DB_ERROR_SYNTAX, + 1035 => DB_ERROR_SYNTAX, + 1913 => DB_ERROR_ALREADY_EXISTS, + 2209 => DB_ERROR_SYNTAX, + 2223 => DB_ERROR_SYNTAX, + 2248 => DB_ERROR_SYNTAX, + 2256 => DB_ERROR_SYNTAX, + 2257 => DB_ERROR_SYNTAX, + 2627 => DB_ERROR_CONSTRAINT, + 2714 => DB_ERROR_ALREADY_EXISTS, + 3607 => DB_ERROR_DIVZERO, + 3701 => DB_ERROR_NOSUCHTABLE, + 7630 => DB_ERROR_SYNTAX, + 8134 => DB_ERROR_DIVZERO, + 9303 => DB_ERROR_SYNTAX, + 9317 => DB_ERROR_SYNTAX, + 9318 => DB_ERROR_SYNTAX, + 9331 => DB_ERROR_SYNTAX, + 9332 => DB_ERROR_SYNTAX, + 15253 => DB_ERROR_SYNTAX, + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * Should data manipulation queries be committed automatically? + * @var bool + * @access private + */ + var $autocommit = true; + + /** + * The quantity of transactions begun + * + * {@internal While this is private, it can't actually be designated + * private in PHP 5 because it is directly accessed in the test suite.}} + * + * @var integer + * @access private + */ + var $transaction_opcount = 0; + + /** + * The database specified in the DSN + * + * It's a fix to allow calls to different databases in the same script. + * + * @var string + * @access private + */ + var $_db = null; + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_mssql() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('mssql') && !PEAR::loadExtension('sybase') + && !PEAR::loadExtension('sybase_ct')) + { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + $params = array( + $dsn['hostspec'] ? $dsn['hostspec'] : 'localhost', + $dsn['username'] ? $dsn['username'] : null, + $dsn['password'] ? $dsn['password'] : null, + ); + if ($dsn['port']) { + $params[0] .= ((substr(PHP_OS, 0, 3) == 'WIN') ? ',' : ':') + . $dsn['port']; + } + + $connect_function = $persistent ? 'mssql_pconnect' : 'mssql_connect'; + + $this->connection = @call_user_func_array($connect_function, $params); + + if (!$this->connection) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + @mssql_get_last_message()); + } + if ($dsn['database']) { + if (!@mssql_select_db($dsn['database'], $this->connection)) { + return $this->raiseError(DB_ERROR_NODBSELECTED, + null, null, null, + @mssql_get_last_message()); + } + $this->_db = $dsn['database']; + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @mssql_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $ismanip = $this->_checkManip($query); + $this->last_query = $query; + if (!@mssql_select_db($this->_db, $this->connection)) { + return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED); + } + $query = $this->modifyQuery($query); + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @mssql_query('BEGIN TRAN', $this->connection); + if (!$result) { + return $this->mssqlRaiseError(); + } + } + $this->transaction_opcount++; + } + $result = @mssql_query($query, $this->connection); + if (!$result) { + return $this->mssqlRaiseError(); + } + // Determine which queries that should return data, and which + // should return an error code only. + return $ismanip ? DB_OK : $result; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal mssql result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return @mssql_next_result($result); + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if ($rownum !== null) { + if (!@mssql_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @mssql_fetch_assoc($result); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @mssql_fetch_row($result); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return is_resource($result) ? mssql_free_result($result) : false; + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @mssql_num_fields($result); + if (!$cols) { + return $this->mssqlRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows() + */ + function numRows($result) + { + $rows = @mssql_num_rows($result); + if ($rows === false) { + return $this->mssqlRaiseError(); + } + return $rows; + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff = false) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + if (!@mssql_select_db($this->_db, $this->connection)) { + return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED); + } + $result = @mssql_query('COMMIT TRAN', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mssqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + if (!@mssql_select_db($this->_db, $this->connection)) { + return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED); + } + $result = @mssql_query('ROLLBACK TRAN', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mssqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + if ($this->_last_query_manip) { + $res = @mssql_query('select @@rowcount', $this->connection); + if (!$res) { + return $this->mssqlRaiseError(); + } + $ar = @mssql_fetch_row($res); + if (!$ar) { + $result = 0; + } else { + @mssql_free_result($res); + $result = $ar[0]; + } + } else { + $result = 0; + } + return $result; + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_mssql::createSequence(), DB_mssql::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + if (!@mssql_select_db($this->_db, $this->connection)) { + return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED); + } + $repeat = 0; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("INSERT INTO $seqname (vapor) VALUES (0)"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result) && + ($result->getCode() == DB_ERROR || $result->getCode() == DB_ERROR_NOSUCHTABLE)) + { + $repeat = 1; + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $this->raiseError($result); + } + } elseif (!DB::isError($result)) { + $result = $this->query("SELECT IDENT_CURRENT('$seqname')"); + if (DB::isError($result)) { + /* Fallback code for MS SQL Server 7.0, which doesn't have + * IDENT_CURRENT. This is *not* safe for concurrent + * requests, and really, if you're using it, you're in a + * world of hurt. Nevertheless, it's here to ensure BC. See + * bug #181 for the gory details.*/ + $result = $this->query("SELECT @@IDENTITY FROM $seqname"); + } + $repeat = 0; + } else { + $repeat = false; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $result = $result->fetchRow(DB_FETCHMODE_ORDERED); + return $result[0]; + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_mssql::nextID(), DB_mssql::dropSequence() + */ + function createSequence($seq_name) + { + return $this->query('CREATE TABLE ' + . $this->getSequenceName($seq_name) + . ' ([id] [int] IDENTITY (1, 1) NOT NULL,' + . ' [vapor] [int] NULL)'); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_mssql::nextID(), DB_mssql::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quotes a string so it can be safely used as a table or column name + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + * + * @see DB_common::quoteIdentifier() + * @since Method available since Release 1.6.0 + */ + function quoteIdentifier($str) + { + return '[' . str_replace(']', ']]', $str) . ']'; + } + + // }}} + // {{{ mssqlRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_mssql::errorNative(), DB_mssql::errorCode() + */ + function mssqlRaiseError($code = null) + { + $message = @mssql_get_last_message(); + if (!$code) { + $code = $this->errorNative(); + } + return $this->raiseError($this->errorCode($code, $message), + null, null, null, "$code - $message"); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error code produced by the last query + * + * @return int the DBMS' error code + */ + function errorNative() + { + $res = @mssql_query('select @@ERROR as ErrorCode', $this->connection); + if (!$res) { + return DB_ERROR; + } + $row = @mssql_fetch_row($res); + return $row[0]; + } + + // }}} + // {{{ errorCode() + + /** + * Determines PEAR::DB error code from mssql's native codes. + * + * If <var>$nativecode</var> isn't known yet, it will be looked up. + * + * @param mixed $nativecode mssql error code, if known + * @return integer an error number from a DB error constant + * @see errorNative() + */ + function errorCode($nativecode = null, $msg = '') + { + if (!$nativecode) { + $nativecode = $this->errorNative(); + } + if (isset($this->errorcode_map[$nativecode])) { + if ($nativecode == 3701 + && preg_match('/Cannot drop the index/i', $msg)) + { + return DB_ERROR_NOT_FOUND; + } + return $this->errorcode_map[$nativecode]; + } else { + return DB_ERROR; + } + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * NOTE: only supports 'table' and 'flags' if <var>$result</var> + * is a table name. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + if (!@mssql_select_db($this->_db, $this->connection)) { + return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED); + } + $id = @mssql_query("SELECT * FROM $result WHERE 1=0", + $this->connection); + $got_string = true; + } elseif (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->mssqlRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @mssql_num_fields($id); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + if ($got_string) { + $flags = $this->_mssql_field_flags($result, + @mssql_field_name($id, $i)); + if (DB::isError($flags)) { + return $flags; + } + } else { + $flags = ''; + } + + $res[$i] = array( + 'table' => $got_string ? $case_func($result) : '', + 'name' => $case_func(@mssql_field_name($id, $i)), + 'type' => @mssql_field_type($id, $i), + 'len' => @mssql_field_length($id, $i), + 'flags' => $flags, + ); + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + // free the result only if we were called on a table + if ($got_string) { + @mssql_free_result($id); + } + return $res; + } + + // }}} + // {{{ _mssql_field_flags() + + /** + * Get a column's flags + * + * Supports "not_null", "primary_key", + * "auto_increment" (mssql identity), "timestamp" (mssql timestamp), + * "unique_key" (mssql unique index, unique check or primary_key) and + * "multiple_key" (multikey index) + * + * mssql timestamp is NOT similar to the mysql timestamp so this is maybe + * not useful at all - is the behaviour of mysql_field_flags that primary + * keys are alway unique? is the interpretation of multiple_key correct? + * + * @param string $table the table name + * @param string $column the field name + * + * @return string the flags + * + * @access private + * @author Joern Barthel <j_barthel@web.de> + */ + function _mssql_field_flags($table, $column) + { + static $tableName = null; + static $flags = array(); + + if ($table != $tableName) { + + $flags = array(); + $tableName = $table; + + // get unique and primary keys + $res = $this->getAll("EXEC SP_HELPINDEX $table", DB_FETCHMODE_ASSOC); + if (DB::isError($res)) { + return $res; + } + + foreach ($res as $val) { + $keys = explode(', ', $val['index_keys']); + + if (sizeof($keys) > 1) { + foreach ($keys as $key) { + $this->_add_flag($flags[$key], 'multiple_key'); + } + } + + if (strpos($val['index_description'], 'primary key')) { + foreach ($keys as $key) { + $this->_add_flag($flags[$key], 'primary_key'); + } + } elseif (strpos($val['index_description'], 'unique')) { + foreach ($keys as $key) { + $this->_add_flag($flags[$key], 'unique_key'); + } + } + } + + // get auto_increment, not_null and timestamp + $res = $this->getAll("EXEC SP_COLUMNS $table", DB_FETCHMODE_ASSOC); + if (DB::isError($res)) { + return $res; + } + + foreach ($res as $val) { + $val = array_change_key_case($val, CASE_LOWER); + if ($val['nullable'] == '0') { + $this->_add_flag($flags[$val['column_name']], 'not_null'); + } + if (strpos($val['type_name'], 'identity')) { + $this->_add_flag($flags[$val['column_name']], 'auto_increment'); + } + if (strpos($val['type_name'], 'timestamp')) { + $this->_add_flag($flags[$val['column_name']], 'timestamp'); + } + } + } + + if (array_key_exists($column, $flags)) { + return(implode(' ', $flags[$column])); + } + return ''; + } + + // }}} + // {{{ _add_flag() + + /** + * Adds a string to the flags array if the flag is not yet in there + * - if there is no flag present the array is created + * + * @param array &$array the reference to the flag-array + * @param string $value the flag value + * + * @return void + * + * @access private + * @author Joern Barthel <j_barthel@web.de> + */ + function _add_flag(&$array, $value) + { + if (!is_array($array)) { + $array = array($value); + } elseif (!in_array($value, $array)) { + array_push($array, $value); + } + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return "SELECT name FROM sysobjects WHERE type = 'U'" + . ' ORDER BY name'; + case 'views': + return "SELECT name FROM sysobjects WHERE type = 'V'"; + default: + return null; + } + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/mysql.php b/extlib/DB/mysql.php new file mode 100644 index 000000000..c67254520 --- /dev/null +++ b/extlib/DB/mysql.php @@ -0,0 +1,1045 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's mysql extension + * for interacting with MySQL databases + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Stig Bakken <ssb@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: mysql.php,v 1.126 2007/09/21 13:32:52 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's mysql extension + * for interacting with MySQL databases + * + * These methods overload the ones declared in DB_common. + * + * @category Database + * @package DB + * @author Stig Bakken <ssb@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_mysql extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'mysql'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'mysql'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => 'alter', + 'new_link' => '4.2.0', + 'numrows' => true, + 'pconnect' => true, + 'prepare' => false, + 'ssl' => false, + 'transactions' => true, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + 1004 => DB_ERROR_CANNOT_CREATE, + 1005 => DB_ERROR_CANNOT_CREATE, + 1006 => DB_ERROR_CANNOT_CREATE, + 1007 => DB_ERROR_ALREADY_EXISTS, + 1008 => DB_ERROR_CANNOT_DROP, + 1022 => DB_ERROR_ALREADY_EXISTS, + 1044 => DB_ERROR_ACCESS_VIOLATION, + 1046 => DB_ERROR_NODBSELECTED, + 1048 => DB_ERROR_CONSTRAINT, + 1049 => DB_ERROR_NOSUCHDB, + 1050 => DB_ERROR_ALREADY_EXISTS, + 1051 => DB_ERROR_NOSUCHTABLE, + 1054 => DB_ERROR_NOSUCHFIELD, + 1061 => DB_ERROR_ALREADY_EXISTS, + 1062 => DB_ERROR_ALREADY_EXISTS, + 1064 => DB_ERROR_SYNTAX, + 1091 => DB_ERROR_NOT_FOUND, + 1100 => DB_ERROR_NOT_LOCKED, + 1136 => DB_ERROR_VALUE_COUNT_ON_ROW, + 1142 => DB_ERROR_ACCESS_VIOLATION, + 1146 => DB_ERROR_NOSUCHTABLE, + 1216 => DB_ERROR_CONSTRAINT, + 1217 => DB_ERROR_CONSTRAINT, + 1356 => DB_ERROR_DIVZERO, + 1451 => DB_ERROR_CONSTRAINT, + 1452 => DB_ERROR_CONSTRAINT, + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * Should data manipulation queries be committed automatically? + * @var bool + * @access private + */ + var $autocommit = true; + + /** + * The quantity of transactions begun + * + * {@internal While this is private, it can't actually be designated + * private in PHP 5 because it is directly accessed in the test suite.}} + * + * @var integer + * @access private + */ + var $transaction_opcount = 0; + + /** + * The database specified in the DSN + * + * It's a fix to allow calls to different databases in the same script. + * + * @var string + * @access private + */ + var $_db = ''; + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_mysql() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * PEAR DB's mysql driver supports the following extra DSN options: + * + new_link If set to true, causes subsequent calls to connect() + * to return a new connection link instead of the + * existing one. WARNING: this is not portable to + * other DBMS's. Available since PEAR DB 1.7.0. + * + client_flags Any combination of MYSQL_CLIENT_* constants. + * Only used if PHP is at version 4.3.0 or greater. + * Available since PEAR DB 1.7.0. + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('mysql')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + $params = array(); + if ($dsn['protocol'] && $dsn['protocol'] == 'unix') { + $params[0] = ':' . $dsn['socket']; + } else { + $params[0] = $dsn['hostspec'] ? $dsn['hostspec'] + : 'localhost'; + if ($dsn['port']) { + $params[0] .= ':' . $dsn['port']; + } + } + $params[] = $dsn['username'] ? $dsn['username'] : null; + $params[] = $dsn['password'] ? $dsn['password'] : null; + + if (!$persistent) { + if (isset($dsn['new_link']) + && ($dsn['new_link'] == 'true' || $dsn['new_link'] === true)) + { + $params[] = true; + } else { + $params[] = false; + } + } + if (version_compare(phpversion(), '4.3.0', '>=')) { + $params[] = isset($dsn['client_flags']) + ? $dsn['client_flags'] : null; + } + + $connect_function = $persistent ? 'mysql_pconnect' : 'mysql_connect'; + + $ini = ini_get('track_errors'); + $php_errormsg = ''; + if ($ini) { + $this->connection = @call_user_func_array($connect_function, + $params); + } else { + @ini_set('track_errors', 1); + $this->connection = @call_user_func_array($connect_function, + $params); + @ini_set('track_errors', $ini); + } + + if (!$this->connection) { + if (($err = @mysql_error()) != '') { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $err); + } else { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $php_errormsg); + } + } + + if ($dsn['database']) { + if (!@mysql_select_db($dsn['database'], $this->connection)) { + return $this->mysqlRaiseError(); + } + $this->_db = $dsn['database']; + } + + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @mysql_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * Generally uses mysql_query(). If you want to use + * mysql_unbuffered_query() set the "result_buffering" option to 0 using + * setOptions(). This option was added in Release 1.7.0. + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $ismanip = $this->_checkManip($query); + $this->last_query = $query; + $query = $this->modifyQuery($query); + if ($this->_db) { + if (!@mysql_select_db($this->_db, $this->connection)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @mysql_query('SET AUTOCOMMIT=0', $this->connection); + $result = @mysql_query('BEGIN', $this->connection); + if (!$result) { + return $this->mysqlRaiseError(); + } + } + $this->transaction_opcount++; + } + if (!$this->options['result_buffering']) { + $result = @mysql_unbuffered_query($query, $this->connection); + } else { + $result = @mysql_query($query, $this->connection); + } + if (!$result) { + return $this->mysqlRaiseError(); + } + if (is_resource($result)) { + return $result; + } + return DB_OK; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal mysql result pointer to the next available result + * + * This method has not been implemented yet. + * + * @param a valid sql result resource + * + * @return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if ($rownum !== null) { + if (!@mysql_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @mysql_fetch_array($result, MYSQL_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @mysql_fetch_row($result); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + /* + * Even though this DBMS already trims output, we do this because + * a field might have intentional whitespace at the end that + * gets removed by DB_PORTABILITY_RTRIM under another driver. + */ + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return is_resource($result) ? mysql_free_result($result) : false; + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @mysql_num_fields($result); + if (!$cols) { + return $this->mysqlRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows() + */ + function numRows($result) + { + $rows = @mysql_num_rows($result); + if ($rows === null) { + return $this->mysqlRaiseError(); + } + return $rows; + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff = false) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + if ($this->_db) { + if (!@mysql_select_db($this->_db, $this->connection)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + $result = @mysql_query('COMMIT', $this->connection); + $result = @mysql_query('SET AUTOCOMMIT=1', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mysqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + if ($this->_db) { + if (!@mysql_select_db($this->_db, $this->connection)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + $result = @mysql_query('ROLLBACK', $this->connection); + $result = @mysql_query('SET AUTOCOMMIT=1', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mysqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + if ($this->_last_query_manip) { + return @mysql_affected_rows($this->connection); + } else { + return 0; + } + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_mysql::createSequence(), DB_mysql::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + do { + $repeat = 0; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("UPDATE ${seqname} ". + 'SET id=LAST_INSERT_ID(id+1)'); + $this->popErrorHandling(); + if ($result === DB_OK) { + // COMMON CASE + $id = @mysql_insert_id($this->connection); + if ($id != 0) { + return $id; + } + // EMPTY SEQ TABLE + // Sequence table must be empty for some reason, so fill + // it and return 1 and obtain a user-level lock + $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + if ($result == 0) { + // Failed to get the lock + return $this->mysqlRaiseError(DB_ERROR_NOT_LOCKED); + } + + // add the default value + $result = $this->query("REPLACE INTO ${seqname} (id) VALUES (0)"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + + // Release the lock + $result = $this->getOne('SELECT RELEASE_LOCK(' + . "'${seqname}_lock')"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + // We know what the result will be, so no need to try again + return 1; + + } elseif ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) + { + // ONDEMAND TABLE CREATION + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $this->raiseError($result); + } else { + $repeat = 1; + } + + } elseif (DB::isError($result) && + $result->getCode() == DB_ERROR_ALREADY_EXISTS) + { + // BACKWARDS COMPAT + // see _BCsequence() comment + $result = $this->_BCsequence($seqname); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $repeat = 1; + } + } while ($repeat); + + return $this->raiseError($result); + } + + // }}} + // {{{ createSequence() + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_mysql::nextID(), DB_mysql::dropSequence() + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + $res = $this->query('CREATE TABLE ' . $seqname + . ' (id INTEGER UNSIGNED AUTO_INCREMENT NOT NULL,' + . ' PRIMARY KEY(id))'); + if (DB::isError($res)) { + return $res; + } + // insert yields value 1, nextId call will generate ID 2 + $res = $this->query("INSERT INTO ${seqname} (id) VALUES (0)"); + if (DB::isError($res)) { + return $res; + } + // so reset to zero + return $this->query("UPDATE ${seqname} SET id = 0"); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_mysql::nextID(), DB_mysql::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ _BCsequence() + + /** + * Backwards compatibility with old sequence emulation implementation + * (clean up the dupes) + * + * @param string $seqname the sequence name to clean up + * + * @return bool true on success. A DB_Error object on failure. + * + * @access private + */ + function _BCsequence($seqname) + { + // Obtain a user-level lock... this will release any previous + // application locks, but unlike LOCK TABLES, it does not abort + // the current transaction and is much less frequently used. + $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)"); + if (DB::isError($result)) { + return $result; + } + if ($result == 0) { + // Failed to get the lock, can't do the conversion, bail + // with a DB_ERROR_NOT_LOCKED error + return $this->mysqlRaiseError(DB_ERROR_NOT_LOCKED); + } + + $highest_id = $this->getOne("SELECT MAX(id) FROM ${seqname}"); + if (DB::isError($highest_id)) { + return $highest_id; + } + // This should kill all rows except the highest + // We should probably do something if $highest_id isn't + // numeric, but I'm at a loss as how to handle that... + $result = $this->query('DELETE FROM ' . $seqname + . " WHERE id <> $highest_id"); + if (DB::isError($result)) { + return $result; + } + + // If another thread has been waiting for this lock, + // it will go thru the above procedure, but will have no + // real effect + $result = $this->getOne("SELECT RELEASE_LOCK('${seqname}_lock')"); + if (DB::isError($result)) { + return $result; + } + return true; + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quotes a string so it can be safely used as a table or column name + * (WARNING: using names that require this is a REALLY BAD IDEA) + * + * WARNING: Older versions of MySQL can't handle the backtick + * character (<kbd>`</kbd>) in table or column names. + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + * + * @see DB_common::quoteIdentifier() + * @since Method available since Release 1.6.0 + */ + function quoteIdentifier($str) + { + return '`' . str_replace('`', '``', $str) . '`'; + } + + // }}} + // {{{ quote() + + /** + * @deprecated Deprecated in release 1.6.0 + */ + function quote($str) + { + return $this->quoteSmart($str); + } + + // }}} + // {{{ escapeSimple() + + /** + * Escapes a string according to the current DBMS's standards + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @see DB_common::quoteSmart() + * @since Method available since Release 1.6.0 + */ + function escapeSimple($str) + { + if (function_exists('mysql_real_escape_string')) { + return @mysql_real_escape_string($str, $this->connection); + } else { + return @mysql_escape_string($str); + } + } + + // }}} + // {{{ modifyQuery() + + /** + * Changes a query string for various DBMS specific reasons + * + * This little hack lets you know how many rows were deleted + * when running a "DELETE FROM table" query. Only implemented + * if the DB_PORTABILITY_DELETE_COUNT portability option is on. + * + * @param string $query the query string to modify + * + * @return string the modified query string + * + * @access protected + * @see DB_common::setOption() + */ + function modifyQuery($query) + { + if ($this->options['portability'] & DB_PORTABILITY_DELETE_COUNT) { + // "DELETE FROM table" gives 0 affected rows in MySQL. + // This little hack lets you know how many rows were deleted. + if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $query)) { + $query = preg_replace('/^\s*DELETE\s+FROM\s+(\S+)\s*$/', + 'DELETE FROM \1 WHERE 1=1', $query); + } + } + return $query; + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * Adds LIMIT clauses to a query string according to current DBMS standards + * + * @param string $query the query to modify + * @param int $from the row to start to fetching (0 = the first row) + * @param int $count the numbers of rows to fetch + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return string the query string with LIMIT clauses added + * + * @access protected + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + if (DB::isManip($query) || $this->_next_query_manip) { + return $query . " LIMIT $count"; + } else { + return $query . " LIMIT $from, $count"; + } + } + + // }}} + // {{{ mysqlRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_mysql::errorNative(), DB_common::errorCode() + */ + function mysqlRaiseError($errno = null) + { + if ($errno === null) { + if ($this->options['portability'] & DB_PORTABILITY_ERRORS) { + $this->errorcode_map[1022] = DB_ERROR_CONSTRAINT; + $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT_NOT_NULL; + $this->errorcode_map[1062] = DB_ERROR_CONSTRAINT; + } else { + // Doing this in case mode changes during runtime. + $this->errorcode_map[1022] = DB_ERROR_ALREADY_EXISTS; + $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT; + $this->errorcode_map[1062] = DB_ERROR_ALREADY_EXISTS; + } + $errno = $this->errorCode(mysql_errno($this->connection)); + } + return $this->raiseError($errno, null, null, null, + @mysql_errno($this->connection) . ' ** ' . + @mysql_error($this->connection)); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error code produced by the last query + * + * @return int the DBMS' error code + */ + function errorNative() + { + return @mysql_errno($this->connection); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + // Fix for bug #11580. + if ($this->_db) { + if (!@mysql_select_db($this->_db, $this->connection)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @mysql_query("SELECT * FROM $result LIMIT 0", + $this->connection); + $got_string = true; + } elseif (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->mysqlRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @mysql_num_fields($id); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + $res[$i] = array( + 'table' => $case_func(@mysql_field_table($id, $i)), + 'name' => $case_func(@mysql_field_name($id, $i)), + 'type' => @mysql_field_type($id, $i), + 'len' => @mysql_field_len($id, $i), + 'flags' => @mysql_field_flags($id, $i), + ); + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + // free the result only if we were called on a table + if ($got_string) { + @mysql_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'SHOW TABLES'; + case 'users': + return 'SELECT DISTINCT User FROM mysql.user'; + case 'databases': + return 'SHOW DATABASES'; + default: + return null; + } + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/mysqli.php b/extlib/DB/mysqli.php new file mode 100644 index 000000000..c6941b170 --- /dev/null +++ b/extlib/DB/mysqli.php @@ -0,0 +1,1092 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's mysqli extension + * for interacting with MySQL databases + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: mysqli.php,v 1.82 2007/09/21 13:40:41 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's mysqli extension + * for interacting with MySQL databases + * + * This is for MySQL versions 4.1 and above. Requires PHP 5. + * + * Note that persistent connections no longer exist. + * + * These methods overload the ones declared in DB_common. + * + * @category Database + * @package DB + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + * @since Class functional since Release 1.6.3 + */ +class DB_mysqli extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'mysqli'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'mysqli'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => 'alter', + 'new_link' => false, + 'numrows' => true, + 'pconnect' => false, + 'prepare' => false, + 'ssl' => true, + 'transactions' => true, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + 1004 => DB_ERROR_CANNOT_CREATE, + 1005 => DB_ERROR_CANNOT_CREATE, + 1006 => DB_ERROR_CANNOT_CREATE, + 1007 => DB_ERROR_ALREADY_EXISTS, + 1008 => DB_ERROR_CANNOT_DROP, + 1022 => DB_ERROR_ALREADY_EXISTS, + 1044 => DB_ERROR_ACCESS_VIOLATION, + 1046 => DB_ERROR_NODBSELECTED, + 1048 => DB_ERROR_CONSTRAINT, + 1049 => DB_ERROR_NOSUCHDB, + 1050 => DB_ERROR_ALREADY_EXISTS, + 1051 => DB_ERROR_NOSUCHTABLE, + 1054 => DB_ERROR_NOSUCHFIELD, + 1061 => DB_ERROR_ALREADY_EXISTS, + 1062 => DB_ERROR_ALREADY_EXISTS, + 1064 => DB_ERROR_SYNTAX, + 1091 => DB_ERROR_NOT_FOUND, + 1100 => DB_ERROR_NOT_LOCKED, + 1136 => DB_ERROR_VALUE_COUNT_ON_ROW, + 1142 => DB_ERROR_ACCESS_VIOLATION, + 1146 => DB_ERROR_NOSUCHTABLE, + 1216 => DB_ERROR_CONSTRAINT, + 1217 => DB_ERROR_CONSTRAINT, + 1356 => DB_ERROR_DIVZERO, + 1451 => DB_ERROR_CONSTRAINT, + 1452 => DB_ERROR_CONSTRAINT, + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * Should data manipulation queries be committed automatically? + * @var bool + * @access private + */ + var $autocommit = true; + + /** + * The quantity of transactions begun + * + * {@internal While this is private, it can't actually be designated + * private in PHP 5 because it is directly accessed in the test suite.}} + * + * @var integer + * @access private + */ + var $transaction_opcount = 0; + + /** + * The database specified in the DSN + * + * It's a fix to allow calls to different databases in the same script. + * + * @var string + * @access private + */ + var $_db = ''; + + /** + * Array for converting MYSQLI_*_FLAG constants to text values + * @var array + * @access public + * @since Property available since Release 1.6.5 + */ + var $mysqli_flags = array( + MYSQLI_NOT_NULL_FLAG => 'not_null', + MYSQLI_PRI_KEY_FLAG => 'primary_key', + MYSQLI_UNIQUE_KEY_FLAG => 'unique_key', + MYSQLI_MULTIPLE_KEY_FLAG => 'multiple_key', + MYSQLI_BLOB_FLAG => 'blob', + MYSQLI_UNSIGNED_FLAG => 'unsigned', + MYSQLI_ZEROFILL_FLAG => 'zerofill', + MYSQLI_AUTO_INCREMENT_FLAG => 'auto_increment', + MYSQLI_TIMESTAMP_FLAG => 'timestamp', + MYSQLI_SET_FLAG => 'set', + // MYSQLI_NUM_FLAG => 'numeric', // unnecessary + // MYSQLI_PART_KEY_FLAG => 'multiple_key', // duplicatvie + MYSQLI_GROUP_FLAG => 'group_by' + ); + + /** + * Array for converting MYSQLI_TYPE_* constants to text values + * @var array + * @access public + * @since Property available since Release 1.6.5 + */ + var $mysqli_types = array( + MYSQLI_TYPE_DECIMAL => 'decimal', + MYSQLI_TYPE_TINY => 'tinyint', + MYSQLI_TYPE_SHORT => 'int', + MYSQLI_TYPE_LONG => 'int', + MYSQLI_TYPE_FLOAT => 'float', + MYSQLI_TYPE_DOUBLE => 'double', + // MYSQLI_TYPE_NULL => 'DEFAULT NULL', // let flags handle it + MYSQLI_TYPE_TIMESTAMP => 'timestamp', + MYSQLI_TYPE_LONGLONG => 'bigint', + MYSQLI_TYPE_INT24 => 'mediumint', + MYSQLI_TYPE_DATE => 'date', + MYSQLI_TYPE_TIME => 'time', + MYSQLI_TYPE_DATETIME => 'datetime', + MYSQLI_TYPE_YEAR => 'year', + MYSQLI_TYPE_NEWDATE => 'date', + MYSQLI_TYPE_ENUM => 'enum', + MYSQLI_TYPE_SET => 'set', + MYSQLI_TYPE_TINY_BLOB => 'tinyblob', + MYSQLI_TYPE_MEDIUM_BLOB => 'mediumblob', + MYSQLI_TYPE_LONG_BLOB => 'longblob', + MYSQLI_TYPE_BLOB => 'blob', + MYSQLI_TYPE_VAR_STRING => 'varchar', + MYSQLI_TYPE_STRING => 'char', + MYSQLI_TYPE_GEOMETRY => 'geometry', + /* These constants are conditionally compiled in ext/mysqli, so we'll + * define them by number rather than constant. */ + 16 => 'bit', + 246 => 'decimal', + ); + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_mysqli() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * PEAR DB's mysqli driver supports the following extra DSN options: + * + When the 'ssl' $option passed to DB::connect() is true: + * + key The path to the key file. + * + cert The path to the certificate file. + * + ca The path to the certificate authority file. + * + capath The path to a directory that contains trusted SSL + * CA certificates in pem format. + * + cipher The list of allowable ciphers for SSL encryption. + * + * Example of how to connect using SSL: + * <code> + * require_once 'DB.php'; + * + * $dsn = array( + * 'phptype' => 'mysqli', + * 'username' => 'someuser', + * 'password' => 'apasswd', + * 'hostspec' => 'localhost', + * 'database' => 'thedb', + * 'key' => 'client-key.pem', + * 'cert' => 'client-cert.pem', + * 'ca' => 'cacert.pem', + * 'capath' => '/path/to/ca/dir', + * 'cipher' => 'AES', + * ); + * + * $options = array( + * 'ssl' => true, + * ); + * + * $db = DB::connect($dsn, $options); + * if (PEAR::isError($db)) { + * die($db->getMessage()); + * } + * </code> + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('mysqli')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + $ini = ini_get('track_errors'); + @ini_set('track_errors', 1); + $php_errormsg = ''; + + if (((int) $this->getOption('ssl')) === 1) { + $init = mysqli_init(); + mysqli_ssl_set( + $init, + empty($dsn['key']) ? null : $dsn['key'], + empty($dsn['cert']) ? null : $dsn['cert'], + empty($dsn['ca']) ? null : $dsn['ca'], + empty($dsn['capath']) ? null : $dsn['capath'], + empty($dsn['cipher']) ? null : $dsn['cipher'] + ); + if ($this->connection = @mysqli_real_connect( + $init, + $dsn['hostspec'], + $dsn['username'], + $dsn['password'], + $dsn['database'], + $dsn['port'], + $dsn['socket'])) + { + $this->connection = $init; + } + } else { + $this->connection = @mysqli_connect( + $dsn['hostspec'], + $dsn['username'], + $dsn['password'], + $dsn['database'], + $dsn['port'], + $dsn['socket'] + ); + } + + @ini_set('track_errors', $ini); + + if (!$this->connection) { + if (($err = @mysqli_connect_error()) != '') { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $err); + } else { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $php_errormsg); + } + } + + if ($dsn['database']) { + $this->_db = $dsn['database']; + } + + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @mysqli_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $ismanip = $this->_checkManip($query); + $this->last_query = $query; + $query = $this->modifyQuery($query); + if ($this->_db) { + if (!@mysqli_select_db($this->connection, $this->_db)) { + return $this->mysqliRaiseError(DB_ERROR_NODBSELECTED); + } + } + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @mysqli_query($this->connection, 'SET AUTOCOMMIT=0'); + $result = @mysqli_query($this->connection, 'BEGIN'); + if (!$result) { + return $this->mysqliRaiseError(); + } + } + $this->transaction_opcount++; + } + $result = @mysqli_query($this->connection, $query); + if (!$result) { + return $this->mysqliRaiseError(); + } + if (is_object($result)) { + return $result; + } + return DB_OK; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal mysql result pointer to the next available result. + * + * This method has not been implemented yet. + * + * @param resource $result a valid sql result resource + * @return false + * @access public + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if ($rownum !== null) { + if (!@mysqli_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @mysqli_fetch_array($result, MYSQLI_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @mysqli_fetch_row($result); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + /* + * Even though this DBMS already trims output, we do this because + * a field might have intentional whitespace at the end that + * gets removed by DB_PORTABILITY_RTRIM under another driver. + */ + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return is_resource($result) ? mysqli_free_result($result) : false; + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @mysqli_num_fields($result); + if (!$cols) { + return $this->mysqliRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows() + */ + function numRows($result) + { + $rows = @mysqli_num_rows($result); + if ($rows === null) { + return $this->mysqliRaiseError(); + } + return $rows; + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff = false) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + if ($this->_db) { + if (!@mysqli_select_db($this->connection, $this->_db)) { + return $this->mysqliRaiseError(DB_ERROR_NODBSELECTED); + } + } + $result = @mysqli_query($this->connection, 'COMMIT'); + $result = @mysqli_query($this->connection, 'SET AUTOCOMMIT=1'); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mysqliRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + if ($this->_db) { + if (!@mysqli_select_db($this->connection, $this->_db)) { + return $this->mysqliRaiseError(DB_ERROR_NODBSELECTED); + } + } + $result = @mysqli_query($this->connection, 'ROLLBACK'); + $result = @mysqli_query($this->connection, 'SET AUTOCOMMIT=1'); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mysqliRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + if ($this->_last_query_manip) { + return @mysqli_affected_rows($this->connection); + } else { + return 0; + } + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_mysqli::createSequence(), DB_mysqli::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + do { + $repeat = 0; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query('UPDATE ' . $seqname + . ' SET id = LAST_INSERT_ID(id + 1)'); + $this->popErrorHandling(); + if ($result === DB_OK) { + // COMMON CASE + $id = @mysqli_insert_id($this->connection); + if ($id != 0) { + return $id; + } + + // EMPTY SEQ TABLE + // Sequence table must be empty for some reason, + // so fill it and return 1 + // Obtain a user-level lock + $result = $this->getOne('SELECT GET_LOCK(' + . "'${seqname}_lock', 10)"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + if ($result == 0) { + return $this->mysqliRaiseError(DB_ERROR_NOT_LOCKED); + } + + // add the default value + $result = $this->query('REPLACE INTO ' . $seqname + . ' (id) VALUES (0)'); + if (DB::isError($result)) { + return $this->raiseError($result); + } + + // Release the lock + $result = $this->getOne('SELECT RELEASE_LOCK(' + . "'${seqname}_lock')"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + // We know what the result will be, so no need to try again + return 1; + + } elseif ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) + { + // ONDEMAND TABLE CREATION + $result = $this->createSequence($seq_name); + + // Since createSequence initializes the ID to be 1, + // we do not need to retrieve the ID again (or we will get 2) + if (DB::isError($result)) { + return $this->raiseError($result); + } else { + // First ID of a newly created sequence is 1 + return 1; + } + + } elseif (DB::isError($result) && + $result->getCode() == DB_ERROR_ALREADY_EXISTS) + { + // BACKWARDS COMPAT + // see _BCsequence() comment + $result = $this->_BCsequence($seqname); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $repeat = 1; + } + } while ($repeat); + + return $this->raiseError($result); + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_mysqli::nextID(), DB_mysqli::dropSequence() + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + $res = $this->query('CREATE TABLE ' . $seqname + . ' (id INTEGER UNSIGNED AUTO_INCREMENT NOT NULL,' + . ' PRIMARY KEY(id))'); + if (DB::isError($res)) { + return $res; + } + // insert yields value 1, nextId call will generate ID 2 + return $this->query("INSERT INTO ${seqname} (id) VALUES (0)"); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_mysql::nextID(), DB_mysql::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ _BCsequence() + + /** + * Backwards compatibility with old sequence emulation implementation + * (clean up the dupes) + * + * @param string $seqname the sequence name to clean up + * + * @return bool true on success. A DB_Error object on failure. + * + * @access private + */ + function _BCsequence($seqname) + { + // Obtain a user-level lock... this will release any previous + // application locks, but unlike LOCK TABLES, it does not abort + // the current transaction and is much less frequently used. + $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)"); + if (DB::isError($result)) { + return $result; + } + if ($result == 0) { + // Failed to get the lock, can't do the conversion, bail + // with a DB_ERROR_NOT_LOCKED error + return $this->mysqliRaiseError(DB_ERROR_NOT_LOCKED); + } + + $highest_id = $this->getOne("SELECT MAX(id) FROM ${seqname}"); + if (DB::isError($highest_id)) { + return $highest_id; + } + + // This should kill all rows except the highest + // We should probably do something if $highest_id isn't + // numeric, but I'm at a loss as how to handle that... + $result = $this->query('DELETE FROM ' . $seqname + . " WHERE id <> $highest_id"); + if (DB::isError($result)) { + return $result; + } + + // If another thread has been waiting for this lock, + // it will go thru the above procedure, but will have no + // real effect + $result = $this->getOne("SELECT RELEASE_LOCK('${seqname}_lock')"); + if (DB::isError($result)) { + return $result; + } + return true; + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quotes a string so it can be safely used as a table or column name + * (WARNING: using names that require this is a REALLY BAD IDEA) + * + * WARNING: Older versions of MySQL can't handle the backtick + * character (<kbd>`</kbd>) in table or column names. + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + * + * @see DB_common::quoteIdentifier() + * @since Method available since Release 1.6.0 + */ + function quoteIdentifier($str) + { + return '`' . str_replace('`', '``', $str) . '`'; + } + + // }}} + // {{{ escapeSimple() + + /** + * Escapes a string according to the current DBMS's standards + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @see DB_common::quoteSmart() + * @since Method available since Release 1.6.0 + */ + function escapeSimple($str) + { + return @mysqli_real_escape_string($this->connection, $str); + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * Adds LIMIT clauses to a query string according to current DBMS standards + * + * @param string $query the query to modify + * @param int $from the row to start to fetching (0 = the first row) + * @param int $count the numbers of rows to fetch + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return string the query string with LIMIT clauses added + * + * @access protected + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + if (DB::isManip($query) || $this->_next_query_manip) { + return $query . " LIMIT $count"; + } else { + return $query . " LIMIT $from, $count"; + } + } + + // }}} + // {{{ mysqliRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_mysqli::errorNative(), DB_common::errorCode() + */ + function mysqliRaiseError($errno = null) + { + if ($errno === null) { + if ($this->options['portability'] & DB_PORTABILITY_ERRORS) { + $this->errorcode_map[1022] = DB_ERROR_CONSTRAINT; + $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT_NOT_NULL; + $this->errorcode_map[1062] = DB_ERROR_CONSTRAINT; + } else { + // Doing this in case mode changes during runtime. + $this->errorcode_map[1022] = DB_ERROR_ALREADY_EXISTS; + $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT; + $this->errorcode_map[1062] = DB_ERROR_ALREADY_EXISTS; + } + $errno = $this->errorCode(mysqli_errno($this->connection)); + } + return $this->raiseError($errno, null, null, null, + @mysqli_errno($this->connection) . ' ** ' . + @mysqli_error($this->connection)); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error code produced by the last query + * + * @return int the DBMS' error code + */ + function errorNative() + { + return @mysqli_errno($this->connection); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::setOption() + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + // Fix for bug #11580. + if ($this->_db) { + if (!@mysqli_select_db($this->connection, $this->_db)) { + return $this->mysqliRaiseError(DB_ERROR_NODBSELECTED); + } + } + + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @mysqli_query($this->connection, + "SELECT * FROM $result LIMIT 0"); + $got_string = true; + } elseif (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_a($id, 'mysqli_result')) { + return $this->mysqliRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @mysqli_num_fields($id); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + $tmp = @mysqli_fetch_field($id); + + $flags = ''; + foreach ($this->mysqli_flags as $const => $means) { + if ($tmp->flags & $const) { + $flags .= $means . ' '; + } + } + if ($tmp->def) { + $flags .= 'default_' . rawurlencode($tmp->def); + } + $flags = trim($flags); + + $res[$i] = array( + 'table' => $case_func($tmp->table), + 'name' => $case_func($tmp->name), + 'type' => isset($this->mysqli_types[$tmp->type]) + ? $this->mysqli_types[$tmp->type] + : 'unknown', + // http://bugs.php.net/?id=36579 + 'len' => $tmp->length, + 'flags' => $flags, + ); + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + // free the result only if we were called on a table + if ($got_string) { + @mysqli_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'SHOW TABLES'; + case 'users': + return 'SELECT DISTINCT User FROM mysql.user'; + case 'databases': + return 'SHOW DATABASES'; + default: + return null; + } + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/oci8.php b/extlib/DB/oci8.php new file mode 100644 index 000000000..d30794871 --- /dev/null +++ b/extlib/DB/oci8.php @@ -0,0 +1,1156 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's oci8 extension + * for interacting with Oracle databases + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author James L. Pine <jlp@valinux.com> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: oci8.php,v 1.116 2007/11/28 02:22:39 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's oci8 extension + * for interacting with Oracle databases + * + * Definitely works with versions 8 and 9 of Oracle. + * + * These methods overload the ones declared in DB_common. + * + * Be aware... OCIError() only appears to return anything when given a + * statement, so functions return the generic DB_ERROR instead of more + * useful errors that have to do with feedback from the database. + * + * @category Database + * @package DB + * @author James L. Pine <jlp@valinux.com> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_oci8 extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'oci8'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'oci8'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => 'alter', + 'new_link' => '5.0.0', + 'numrows' => 'subquery', + 'pconnect' => true, + 'prepare' => true, + 'ssl' => false, + 'transactions' => true, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + 1 => DB_ERROR_CONSTRAINT, + 900 => DB_ERROR_SYNTAX, + 904 => DB_ERROR_NOSUCHFIELD, + 913 => DB_ERROR_VALUE_COUNT_ON_ROW, + 921 => DB_ERROR_SYNTAX, + 923 => DB_ERROR_SYNTAX, + 942 => DB_ERROR_NOSUCHTABLE, + 955 => DB_ERROR_ALREADY_EXISTS, + 1400 => DB_ERROR_CONSTRAINT_NOT_NULL, + 1401 => DB_ERROR_INVALID, + 1407 => DB_ERROR_CONSTRAINT_NOT_NULL, + 1418 => DB_ERROR_NOT_FOUND, + 1476 => DB_ERROR_DIVZERO, + 1722 => DB_ERROR_INVALID_NUMBER, + 2289 => DB_ERROR_NOSUCHTABLE, + 2291 => DB_ERROR_CONSTRAINT, + 2292 => DB_ERROR_CONSTRAINT, + 2449 => DB_ERROR_CONSTRAINT, + 12899 => DB_ERROR_INVALID, + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * Should data manipulation queries be committed automatically? + * @var bool + * @access private + */ + var $autocommit = true; + + /** + * Stores the $data passed to execute() in the oci8 driver + * + * Gets reset to array() when simpleQuery() is run. + * + * Needed in case user wants to call numRows() after prepare/execute + * was used. + * + * @var array + * @access private + */ + var $_data = array(); + + /** + * The result or statement handle from the most recently executed query + * @var resource + */ + var $last_stmt; + + /** + * Is the given prepared statement a data manipulation query? + * @var array + * @access private + */ + var $manip_query = array(); + + /** + * Store of prepared SQL queries. + * @var array + * @access private + */ + var $_prepared_queries = array(); + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_oci8() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * If PHP is at version 5.0.0 or greater: + * + Generally, oci_connect() or oci_pconnect() are used. + * + But if the new_link DSN option is set to true, oci_new_connect() + * is used. + * + * When using PHP version 4.x, OCILogon() or OCIPLogon() are used. + * + * PEAR DB's oci8 driver supports the following extra DSN options: + * + charset The character set to be used on the connection. + * Only used if PHP is at version 5.0.0 or greater + * and the Oracle server is at 9.2 or greater. + * Available since PEAR DB 1.7.0. + * + new_link If set to true, causes subsequent calls to + * connect() to return a new connection link + * instead of the existing one. WARNING: this is + * not portable to other DBMS's. + * Available since PEAR DB 1.7.0. + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('oci8')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + // Backwards compatibility with DB < 1.7.0 + if (empty($dsn['database']) && !empty($dsn['hostspec'])) { + $db = $dsn['hostspec']; + } else { + $db = $dsn['database']; + } + + if (function_exists('oci_connect')) { + if (isset($dsn['new_link']) + && ($dsn['new_link'] == 'true' || $dsn['new_link'] === true)) + { + $connect_function = 'oci_new_connect'; + } else { + $connect_function = $persistent ? 'oci_pconnect' + : 'oci_connect'; + } + if (isset($this->dsn['port']) && $this->dsn['port']) { + $db = '//'.$db.':'.$this->dsn['port']; + } + + $char = empty($dsn['charset']) ? null : $dsn['charset']; + $this->connection = @$connect_function($dsn['username'], + $dsn['password'], + $db, + $char); + $error = OCIError(); + if (!empty($error) && $error['code'] == 12541) { + // Couldn't find TNS listener. Try direct connection. + $this->connection = @$connect_function($dsn['username'], + $dsn['password'], + null, + $char); + } + } else { + $connect_function = $persistent ? 'OCIPLogon' : 'OCILogon'; + if ($db) { + $this->connection = @$connect_function($dsn['username'], + $dsn['password'], + $db); + } elseif ($dsn['username'] || $dsn['password']) { + $this->connection = @$connect_function($dsn['username'], + $dsn['password']); + } + } + + if (!$this->connection) { + $error = OCIError(); + $error = (is_array($error)) ? $error['message'] : null; + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $error); + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + if (function_exists('oci_close')) { + $ret = @oci_close($this->connection); + } else { + $ret = @OCILogOff($this->connection); + } + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * To determine how many rows of a result set get buffered using + * ocisetprefetch(), see the "result_buffering" option in setOptions(). + * This option was added in Release 1.7.0. + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $this->_data = array(); + $this->last_parameters = array(); + $this->last_query = $query; + $query = $this->modifyQuery($query); + $result = @OCIParse($this->connection, $query); + if (!$result) { + return $this->oci8RaiseError(); + } + if ($this->autocommit) { + $success = @OCIExecute($result,OCI_COMMIT_ON_SUCCESS); + } else { + $success = @OCIExecute($result,OCI_DEFAULT); + } + if (!$success) { + return $this->oci8RaiseError($result); + } + $this->last_stmt = $result; + if ($this->_checkManip($query)) { + return DB_OK; + } else { + @ocisetprefetch($result, $this->options['result_buffering']); + return $result; + } + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal oracle result pointer to the next available result + * + * @param a valid oci8 result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if ($rownum !== null) { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $moredata = @OCIFetchInto($result,$arr,OCI_ASSOC+OCI_RETURN_NULLS+OCI_RETURN_LOBS); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && + $moredata) + { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $moredata = OCIFetchInto($result,$arr,OCI_RETURN_NULLS+OCI_RETURN_LOBS); + } + if (!$moredata) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return is_resource($result) ? OCIFreeStatement($result) : false; + } + + /** + * Frees the internal resources associated with a prepared query + * + * @param resource $stmt the prepared statement's resource + * @param bool $free_resource should the PHP resource be freed too? + * Use false if you need to get data + * from the result set later. + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_oci8::prepare() + */ + function freePrepared($stmt, $free_resource = true) + { + if (!is_resource($stmt)) { + return false; + } + if ($free_resource) { + @ocifreestatement($stmt); + } + if (isset($this->prepare_types[(int)$stmt])) { + unset($this->prepare_types[(int)$stmt]); + unset($this->manip_query[(int)$stmt]); + } else { + return false; + } + return true; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * Only works if the DB_PORTABILITY_NUMROWS portability option + * is turned on. + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows(), DB_common::setOption() + */ + function numRows($result) + { + // emulate numRows for Oracle. yuck. + if ($this->options['portability'] & DB_PORTABILITY_NUMROWS && + $result === $this->last_stmt) + { + $countquery = 'SELECT COUNT(*) FROM ('.$this->last_query.')'; + $save_query = $this->last_query; + $save_stmt = $this->last_stmt; + + $count = $this->query($countquery); + + // Restore the last query and statement. + $this->last_query = $save_query; + $this->last_stmt = $save_stmt; + + if (DB::isError($count) || + DB::isError($row = $count->fetchRow(DB_FETCHMODE_ORDERED))) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + return $row[0]; + } + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @OCINumCols($result); + if (!$cols) { + return $this->oci8RaiseError($result); + } + return $cols; + } + + // }}} + // {{{ prepare() + + /** + * Prepares a query for multiple execution with execute(). + * + * With oci8, this is emulated. + * + * prepare() requires a generic query as string like <code> + * INSERT INTO numbers VALUES (?, ?, ?) + * </code>. The <kbd>?</kbd> characters are placeholders. + * + * Three types of placeholders can be used: + * + <kbd>?</kbd> a quoted scalar value, i.e. strings, integers + * + <kbd>!</kbd> value is inserted 'as is' + * + <kbd>&</kbd> requires a file name. The file's contents get + * inserted into the query (i.e. saving binary + * data in a db) + * + * Use backslashes to escape placeholder characters if you don't want + * them to be interpreted as placeholders. Example: <code> + * "UPDATE foo SET col=? WHERE col='over \& under'" + * </code> + * + * @param string $query the query to be prepared + * + * @return mixed DB statement resource on success. DB_Error on failure. + * + * @see DB_oci8::execute() + */ + function prepare($query) + { + $tokens = preg_split('/((?<!\\\)[&?!])/', $query, -1, + PREG_SPLIT_DELIM_CAPTURE); + $binds = count($tokens) - 1; + $token = 0; + $types = array(); + $newquery = ''; + + foreach ($tokens as $key => $val) { + switch ($val) { + case '?': + $types[$token++] = DB_PARAM_SCALAR; + unset($tokens[$key]); + break; + case '&': + $types[$token++] = DB_PARAM_OPAQUE; + unset($tokens[$key]); + break; + case '!': + $types[$token++] = DB_PARAM_MISC; + unset($tokens[$key]); + break; + default: + $tokens[$key] = preg_replace('/\\\([&?!])/', "\\1", $val); + if ($key != $binds) { + $newquery .= $tokens[$key] . ':bind' . $token; + } else { + $newquery .= $tokens[$key]; + } + } + } + + $this->last_query = $query; + $newquery = $this->modifyQuery($newquery); + if (!$stmt = @OCIParse($this->connection, $newquery)) { + return $this->oci8RaiseError(); + } + $this->prepare_types[(int)$stmt] = $types; + $this->manip_query[(int)$stmt] = DB::isManip($query); + $this->_prepared_queries[(int)$stmt] = $newquery; + return $stmt; + } + + // }}} + // {{{ execute() + + /** + * Executes a DB statement prepared with prepare(). + * + * To determine how many rows of a result set get buffered using + * ocisetprefetch(), see the "result_buffering" option in setOptions(). + * This option was added in Release 1.7.0. + * + * @param resource $stmt a DB statement resource returned from prepare() + * @param mixed $data array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 for non-array items or the + * quantity of elements in the array. + * + * @return mixed returns an oic8 result resource for successful SELECT + * queries, DB_OK for other successful queries. + * A DB error object is returned on failure. + * + * @see DB_oci8::prepare() + */ + function &execute($stmt, $data = array()) + { + $data = (array)$data; + $this->last_parameters = $data; + $this->last_query = $this->_prepared_queries[(int)$stmt]; + $this->_data = $data; + + $types = $this->prepare_types[(int)$stmt]; + if (count($types) != count($data)) { + $tmp = $this->raiseError(DB_ERROR_MISMATCH); + return $tmp; + } + + $i = 0; + foreach ($data as $key => $value) { + if ($types[$i] == DB_PARAM_MISC) { + /* + * Oracle doesn't seem to have the ability to pass a + * parameter along unchanged, so strip off quotes from start + * and end, plus turn two single quotes to one single quote, + * in order to avoid the quotes getting escaped by + * Oracle and ending up in the database. + */ + $data[$key] = preg_replace("/^'(.*)'$/", "\\1", $data[$key]); + $data[$key] = str_replace("''", "'", $data[$key]); + } elseif ($types[$i] == DB_PARAM_OPAQUE) { + $fp = @fopen($data[$key], 'rb'); + if (!$fp) { + $tmp = $this->raiseError(DB_ERROR_ACCESS_VIOLATION); + return $tmp; + } + $data[$key] = fread($fp, filesize($data[$key])); + fclose($fp); + } elseif ($types[$i] == DB_PARAM_SCALAR) { + // Floats have to be converted to a locale-neutral + // representation. + if (is_float($data[$key])) { + $data[$key] = $this->quoteFloat($data[$key]); + } + } + if (!@OCIBindByName($stmt, ':bind' . $i, $data[$key], -1)) { + $tmp = $this->oci8RaiseError($stmt); + return $tmp; + } + $this->last_query = preg_replace("/:bind$i/",$this->quoteSmart($data[$key]),$this->last_query,1); + $i++; + } + if ($this->autocommit) { + $success = @OCIExecute($stmt, OCI_COMMIT_ON_SUCCESS); + } else { + $success = @OCIExecute($stmt, OCI_DEFAULT); + } + if (!$success) { + $tmp = $this->oci8RaiseError($stmt); + return $tmp; + } + $this->last_stmt = $stmt; + if ($this->manip_query[(int)$stmt] || $this->_next_query_manip) { + $this->_last_query_manip = true; + $this->_next_query_manip = false; + $tmp = DB_OK; + } else { + $this->_last_query_manip = false; + @ocisetprefetch($stmt, $this->options['result_buffering']); + $tmp = new DB_result($this, $stmt); + } + return $tmp; + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff = false) + { + $this->autocommit = (bool)$onoff;; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + $result = @OCICommit($this->connection); + if (!$result) { + return $this->oci8RaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + $result = @OCIRollback($this->connection); + if (!$result) { + return $this->oci8RaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + if ($this->last_stmt === false) { + return $this->oci8RaiseError(); + } + $result = @OCIRowCount($this->last_stmt); + if ($result === false) { + return $this->oci8RaiseError($this->last_stmt); + } + return $result; + } + + // }}} + // {{{ modifyQuery() + + /** + * Changes a query string for various DBMS specific reasons + * + * "SELECT 2+2" must be "SELECT 2+2 FROM dual" in Oracle. + * + * @param string $query the query string to modify + * + * @return string the modified query string + * + * @access protected + */ + function modifyQuery($query) + { + if (preg_match('/^\s*SELECT/i', $query) && + !preg_match('/\sFROM\s/i', $query)) { + $query .= ' FROM dual'; + } + return $query; + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * Adds LIMIT clauses to a query string according to current DBMS standards + * + * @param string $query the query to modify + * @param int $from the row to start to fetching (0 = the first row) + * @param int $count the numbers of rows to fetch + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return string the query string with LIMIT clauses added + * + * @access protected + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + // Let Oracle return the name of the columns instead of + // coding a "home" SQL parser + + if (count($params)) { + $result = $this->prepare("SELECT * FROM ($query) " + . 'WHERE NULL = NULL'); + $tmp = $this->execute($result, $params); + } else { + $q_fields = "SELECT * FROM ($query) WHERE NULL = NULL"; + + if (!$result = @OCIParse($this->connection, $q_fields)) { + $this->last_query = $q_fields; + return $this->oci8RaiseError(); + } + if (!@OCIExecute($result, OCI_DEFAULT)) { + $this->last_query = $q_fields; + return $this->oci8RaiseError($result); + } + } + + $ncols = OCINumCols($result); + $cols = array(); + for ( $i = 1; $i <= $ncols; $i++ ) { + $cols[] = '"' . OCIColumnName($result, $i) . '"'; + } + $fields = implode(', ', $cols); + // XXX Test that (tip by John Lim) + //if (preg_match('/^\s*SELECT\s+/is', $query, $match)) { + // // Introduce the FIRST_ROWS Oracle query optimizer + // $query = substr($query, strlen($match[0]), strlen($query)); + // $query = "SELECT /* +FIRST_ROWS */ " . $query; + //} + + // Construct the query + // more at: http://marc.theaimsgroup.com/?l=php-db&m=99831958101212&w=2 + // Perhaps this could be optimized with the use of Unions + $query = "SELECT $fields FROM". + " (SELECT rownum as linenum, $fields FROM". + " ($query)". + ' WHERE rownum <= '. ($from + $count) . + ') WHERE linenum >= ' . ++$from; + return $query; + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_oci8::createSequence(), DB_oci8::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + $repeat = 0; + do { + $this->expectError(DB_ERROR_NOSUCHTABLE); + $result = $this->query("SELECT ${seqname}.nextval FROM dual"); + $this->popExpect(); + if ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) { + $repeat = 1; + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $this->raiseError($result); + } + } else { + $repeat = 0; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $arr = $result->fetchRow(DB_FETCHMODE_ORDERED); + return $arr[0]; + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_oci8::nextID(), DB_oci8::dropSequence() + */ + function createSequence($seq_name) + { + return $this->query('CREATE SEQUENCE ' + . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_oci8::nextID(), DB_oci8::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DROP SEQUENCE ' + . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ oci8RaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_oci8::errorNative(), DB_oci8::errorCode() + */ + function oci8RaiseError($errno = null) + { + if ($errno === null) { + $error = @OCIError($this->connection); + return $this->raiseError($this->errorCode($error['code']), + null, null, null, $error['message']); + } elseif (is_resource($errno)) { + $error = @OCIError($errno); + return $this->raiseError($this->errorCode($error['code']), + null, null, null, $error['message']); + } + return $this->raiseError($this->errorCode($errno)); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error code produced by the last query + * + * @return int the DBMS' error code. FALSE if the code could not be + * determined + */ + function errorNative() + { + if (is_resource($this->last_stmt)) { + $error = @OCIError($this->last_stmt); + } else { + $error = @OCIError($this->connection); + } + if (is_array($error)) { + return $error['code']; + } + return false; + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * NOTE: only supports 'table' and 'flags' if <var>$result</var> + * is a table name. + * + * NOTE: flags won't contain index information. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $res = array(); + + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $result = strtoupper($result); + $q_fields = 'SELECT column_name, data_type, data_length, ' + . 'nullable ' + . 'FROM user_tab_columns ' + . "WHERE table_name='$result' ORDER BY column_id"; + + $this->last_query = $q_fields; + + if (!$stmt = @OCIParse($this->connection, $q_fields)) { + return $this->oci8RaiseError(DB_ERROR_NEED_MORE_DATA); + } + if (!@OCIExecute($stmt, OCI_DEFAULT)) { + return $this->oci8RaiseError($stmt); + } + + $i = 0; + while (@OCIFetch($stmt)) { + $res[$i] = array( + 'table' => $case_func($result), + 'name' => $case_func(@OCIResult($stmt, 1)), + 'type' => @OCIResult($stmt, 2), + 'len' => @OCIResult($stmt, 3), + 'flags' => (@OCIResult($stmt, 4) == 'N') ? 'not_null' : '', + ); + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + $i++; + } + + if ($mode) { + $res['num_fields'] = $i; + } + @OCIFreeStatement($stmt); + + } else { + if (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $result = $result->result; + } + + $res = array(); + + if ($result === $this->last_stmt) { + $count = @OCINumCols($result); + if ($mode) { + $res['num_fields'] = $count; + } + for ($i = 0; $i < $count; $i++) { + $res[$i] = array( + 'table' => '', + 'name' => $case_func(@OCIColumnName($result, $i+1)), + 'type' => @OCIColumnType($result, $i+1), + 'len' => @OCIColumnSize($result, $i+1), + 'flags' => '', + ); + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + } else { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'SELECT table_name FROM user_tables'; + case 'synonyms': + return 'SELECT synonym_name FROM user_synonyms'; + case 'views': + return 'SELECT view_name FROM user_views'; + default: + return null; + } + } + + // }}} + // {{{ quoteFloat() + + /** + * Formats a float value for use within a query in a locale-independent + * manner. + * + * @param float the float value to be quoted. + * @return string the quoted string. + * @see DB_common::quoteSmart() + * @since Method available since release 1.7.8. + */ + function quoteFloat($float) { + return $this->escapeSimple(str_replace(',', '.', strval(floatval($float)))); + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/odbc.php b/extlib/DB/odbc.php new file mode 100644 index 000000000..eba43659a --- /dev/null +++ b/extlib/DB/odbc.php @@ -0,0 +1,883 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's odbc extension + * for interacting with databases via ODBC connections + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Stig Bakken <ssb@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: odbc.php,v 1.81 2007/07/06 05:19:21 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's odbc extension + * for interacting with databases via ODBC connections + * + * These methods overload the ones declared in DB_common. + * + * More info on ODBC errors could be found here: + * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/trblsql/tr_err_odbc_5stz.asp + * + * @category Database + * @package DB + * @author Stig Bakken <ssb@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_odbc extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'odbc'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'sql92'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * NOTE: The feature set of the following drivers are different than + * the default: + * + solid: 'transactions' = true + * + navision: 'limit' = false + * + * @var array + */ + var $features = array( + 'limit' => 'emulate', + 'new_link' => false, + 'numrows' => true, + 'pconnect' => true, + 'prepare' => false, + 'ssl' => false, + 'transactions' => false, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + '01004' => DB_ERROR_TRUNCATED, + '07001' => DB_ERROR_MISMATCH, + '21S01' => DB_ERROR_VALUE_COUNT_ON_ROW, + '21S02' => DB_ERROR_MISMATCH, + '22001' => DB_ERROR_INVALID, + '22003' => DB_ERROR_INVALID_NUMBER, + '22005' => DB_ERROR_INVALID_NUMBER, + '22008' => DB_ERROR_INVALID_DATE, + '22012' => DB_ERROR_DIVZERO, + '23000' => DB_ERROR_CONSTRAINT, + '23502' => DB_ERROR_CONSTRAINT_NOT_NULL, + '23503' => DB_ERROR_CONSTRAINT, + '23504' => DB_ERROR_CONSTRAINT, + '23505' => DB_ERROR_CONSTRAINT, + '24000' => DB_ERROR_INVALID, + '34000' => DB_ERROR_INVALID, + '37000' => DB_ERROR_SYNTAX, + '42000' => DB_ERROR_SYNTAX, + '42601' => DB_ERROR_SYNTAX, + 'IM001' => DB_ERROR_UNSUPPORTED, + 'S0000' => DB_ERROR_NOSUCHTABLE, + 'S0001' => DB_ERROR_ALREADY_EXISTS, + 'S0002' => DB_ERROR_NOSUCHTABLE, + 'S0011' => DB_ERROR_ALREADY_EXISTS, + 'S0012' => DB_ERROR_NOT_FOUND, + 'S0021' => DB_ERROR_ALREADY_EXISTS, + 'S0022' => DB_ERROR_NOSUCHFIELD, + 'S1009' => DB_ERROR_INVALID, + 'S1090' => DB_ERROR_INVALID, + 'S1C00' => DB_ERROR_NOT_CAPABLE, + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * The number of rows affected by a data manipulation query + * @var integer + * @access private + */ + var $affected = 0; + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_odbc() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * PEAR DB's odbc driver supports the following extra DSN options: + * + cursor The type of cursor to be used for this connection. + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('odbc')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + switch ($this->dbsyntax) { + case 'access': + case 'db2': + case 'solid': + $this->features['transactions'] = true; + break; + case 'navision': + $this->features['limit'] = false; + } + + /* + * This is hear for backwards compatibility. Should have been using + * 'database' all along, but prior to 1.6.0RC3 'hostspec' was used. + */ + if ($dsn['database']) { + $odbcdsn = $dsn['database']; + } elseif ($dsn['hostspec']) { + $odbcdsn = $dsn['hostspec']; + } else { + $odbcdsn = 'localhost'; + } + + $connect_function = $persistent ? 'odbc_pconnect' : 'odbc_connect'; + + if (empty($dsn['cursor'])) { + $this->connection = @$connect_function($odbcdsn, $dsn['username'], + $dsn['password']); + } else { + $this->connection = @$connect_function($odbcdsn, $dsn['username'], + $dsn['password'], + $dsn['cursor']); + } + + if (!is_resource($this->connection)) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $this->errorNative()); + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $err = @odbc_close($this->connection); + $this->connection = null; + return $err; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $this->last_query = $query; + $query = $this->modifyQuery($query); + $result = @odbc_exec($this->connection, $query); + if (!$result) { + return $this->odbcRaiseError(); // XXX ERRORMSG + } + // Determine which queries that should return data, and which + // should return an error code only. + if ($this->_checkManip($query)) { + $this->affected = $result; // For affectedRows() + return DB_OK; + } + $this->affected = 0; + return $result; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal odbc result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return @odbc_next_result($result); + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + $arr = array(); + if ($rownum !== null) { + $rownum++; // ODBC first row is 1 + if (version_compare(phpversion(), '4.2.0', 'ge')) { + $cols = @odbc_fetch_into($result, $arr, $rownum); + } else { + $cols = @odbc_fetch_into($result, $rownum, $arr); + } + } else { + $cols = @odbc_fetch_into($result, $arr); + } + if (!$cols) { + return null; + } + if ($fetchmode !== DB_FETCHMODE_ORDERED) { + for ($i = 0; $i < count($arr); $i++) { + $colName = @odbc_field_name($result, $i+1); + $a[$colName] = $arr[$i]; + } + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $a = array_change_key_case($a, CASE_LOWER); + } + $arr = $a; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return is_resource($result) ? odbc_free_result($result) : false; + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @odbc_num_fields($result); + if (!$cols) { + return $this->odbcRaiseError(); + } + return $cols; + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + if (empty($this->affected)) { // In case of SELECT stms + return 0; + } + $nrows = @odbc_num_rows($this->affected); + if ($nrows == -1) { + return $this->odbcRaiseError(); + } + return $nrows; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * Not all ODBC drivers support this functionality. If they don't + * a DB_Error object for DB_ERROR_UNSUPPORTED is returned. + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows() + */ + function numRows($result) + { + $nrows = @odbc_num_rows($result); + if ($nrows == -1) { + return $this->odbcRaiseError(DB_ERROR_UNSUPPORTED); + } + if ($nrows === false) { + return $this->odbcRaiseError(); + } + return $nrows; + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quotes a string so it can be safely used as a table or column name + * + * Use 'mssql' as the dbsyntax in the DB DSN only if you've unchecked + * "Use ANSI quoted identifiers" when setting up the ODBC data source. + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + * + * @see DB_common::quoteIdentifier() + * @since Method available since Release 1.6.0 + */ + function quoteIdentifier($str) + { + switch ($this->dsn['dbsyntax']) { + case 'access': + return '[' . $str . ']'; + case 'mssql': + case 'sybase': + return '[' . str_replace(']', ']]', $str) . ']'; + case 'mysql': + case 'mysqli': + return '`' . $str . '`'; + default: + return '"' . str_replace('"', '""', $str) . '"'; + } + } + + // }}} + // {{{ quote() + + /** + * @deprecated Deprecated in release 1.6.0 + * @internal + */ + function quote($str) + { + return $this->quoteSmart($str); + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_odbc::createSequence(), DB_odbc::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + $repeat = 0; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("update ${seqname} set id = id + 1"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) { + $repeat = 1; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->createSequence($seq_name); + $this->popErrorHandling(); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $result = $this->query("insert into ${seqname} (id) values(0)"); + } else { + $repeat = 0; + } + } while ($repeat); + + if (DB::isError($result)) { + return $this->raiseError($result); + } + + $result = $this->query("select id from ${seqname}"); + if (DB::isError($result)) { + return $result; + } + + $row = $result->fetchRow(DB_FETCHMODE_ORDERED); + if (DB::isError($row || !$row)) { + return $row; + } + + return $row[0]; + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_odbc::nextID(), DB_odbc::dropSequence() + */ + function createSequence($seq_name) + { + return $this->query('CREATE TABLE ' + . $this->getSequenceName($seq_name) + . ' (id integer NOT NULL,' + . ' PRIMARY KEY(id))'); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_odbc::nextID(), DB_odbc::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff = false) + { + if (!@odbc_autocommit($this->connection, $onoff)) { + return $this->odbcRaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + if (!@odbc_commit($this->connection)) { + return $this->odbcRaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + if (!@odbc_rollback($this->connection)) { + return $this->odbcRaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ odbcRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_odbc::errorNative(), DB_common::errorCode() + */ + function odbcRaiseError($errno = null) + { + if ($errno === null) { + switch ($this->dbsyntax) { + case 'access': + if ($this->options['portability'] & DB_PORTABILITY_ERRORS) { + $this->errorcode_map['07001'] = DB_ERROR_NOSUCHFIELD; + } else { + // Doing this in case mode changes during runtime. + $this->errorcode_map['07001'] = DB_ERROR_MISMATCH; + } + + $native_code = odbc_error($this->connection); + + // S1000 is for "General Error." Let's be more specific. + if ($native_code == 'S1000') { + $errormsg = odbc_errormsg($this->connection); + static $error_regexps; + if (!isset($error_regexps)) { + $error_regexps = array( + '/includes related records.$/i' => DB_ERROR_CONSTRAINT, + '/cannot contain a Null value/i' => DB_ERROR_CONSTRAINT_NOT_NULL, + ); + } + foreach ($error_regexps as $regexp => $code) { + if (preg_match($regexp, $errormsg)) { + return $this->raiseError($code, + null, null, null, + $native_code . ' ' . $errormsg); + } + } + $errno = DB_ERROR; + } else { + $errno = $this->errorCode($native_code); + } + break; + default: + $errno = $this->errorCode(odbc_error($this->connection)); + } + } + return $this->raiseError($errno, null, null, null, + $this->errorNative()); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error code and message produced by the last query + * + * @return string the DBMS' error code and message + */ + function errorNative() + { + if (!is_resource($this->connection)) { + return @odbc_error() . ' ' . @odbc_errormsg(); + } + return @odbc_error($this->connection) . ' ' . @odbc_errormsg($this->connection); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + * @since Method available since Release 1.7.0 + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @odbc_exec($this->connection, "SELECT * FROM $result"); + if (!$id) { + return $this->odbcRaiseError(); + } + $got_string = true; + } elseif (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->odbcRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @odbc_num_fields($id); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + $col = $i + 1; + $res[$i] = array( + 'table' => $got_string ? $case_func($result) : '', + 'name' => $case_func(@odbc_field_name($id, $col)), + 'type' => @odbc_field_type($id, $col), + 'len' => @odbc_field_len($id, $col), + 'flags' => '', + ); + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + // free the result only if we were called on a table + if ($got_string) { + @odbc_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * Thanks to symbol1@gmail.com and Philippe.Jausions@11abacus.com. + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the list of objects requested + * + * @access protected + * @see DB_common::getListOf() + * @since Method available since Release 1.7.0 + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'databases': + if (!function_exists('odbc_data_source')) { + return null; + } + $res = @odbc_data_source($this->connection, SQL_FETCH_FIRST); + if (is_array($res)) { + $out = array($res['server']); + while($res = @odbc_data_source($this->connection, + SQL_FETCH_NEXT)) + { + $out[] = $res['server']; + } + return $out; + } else { + return $this->odbcRaiseError(); + } + break; + case 'tables': + case 'schema.tables': + $keep = 'TABLE'; + break; + case 'views': + $keep = 'VIEW'; + break; + default: + return null; + } + + /* + * Removing non-conforming items in the while loop rather than + * in the odbc_tables() call because some backends choke on this: + * odbc_tables($this->connection, '', '', '', 'TABLE') + */ + $res = @odbc_tables($this->connection); + if (!$res) { + return $this->odbcRaiseError(); + } + $out = array(); + while ($row = odbc_fetch_array($res)) { + if ($row['TABLE_TYPE'] != $keep) { + continue; + } + if ($type == 'schema.tables') { + $out[] = $row['TABLE_SCHEM'] . '.' . $row['TABLE_NAME']; + } else { + $out[] = $row['TABLE_NAME']; + } + } + return $out; + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/pgsql.php b/extlib/DB/pgsql.php new file mode 100644 index 000000000..6030bb4c1 --- /dev/null +++ b/extlib/DB/pgsql.php @@ -0,0 +1,1135 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's pgsql extension + * for interacting with PostgreSQL databases + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Rui Hirokawa <hirokawa@php.net> + * @author Stig Bakken <ssb@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: pgsql.php,v 1.139 2007/11/28 02:19:44 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's pgsql extension + * for interacting with PostgreSQL databases + * + * These methods overload the ones declared in DB_common. + * + * @category Database + * @package DB + * @author Rui Hirokawa <hirokawa@php.net> + * @author Stig Bakken <ssb@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_pgsql extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'pgsql'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'pgsql'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => 'alter', + 'new_link' => '4.3.0', + 'numrows' => true, + 'pconnect' => true, + 'prepare' => false, + 'ssl' => true, + 'transactions' => true, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * Should data manipulation queries be committed automatically? + * @var bool + * @access private + */ + var $autocommit = true; + + /** + * The quantity of transactions begun + * + * {@internal While this is private, it can't actually be designated + * private in PHP 5 because it is directly accessed in the test suite.}} + * + * @var integer + * @access private + */ + var $transaction_opcount = 0; + + /** + * The number of rows affected by a data manipulation query + * @var integer + */ + var $affected = 0; + + /** + * The current row being looked at in fetchInto() + * @var array + * @access private + */ + var $row = array(); + + /** + * The number of rows in a given result set + * @var array + * @access private + */ + var $_num_rows = array(); + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_pgsql() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * PEAR DB's pgsql driver supports the following extra DSN options: + * + connect_timeout How many seconds to wait for a connection to + * be established. Available since PEAR DB 1.7.0. + * + new_link If set to true, causes subsequent calls to + * connect() to return a new connection link + * instead of the existing one. WARNING: this is + * not portable to other DBMS's. Available only + * if PHP is >= 4.3.0 and PEAR DB is >= 1.7.0. + * + options Command line options to be sent to the server. + * Available since PEAR DB 1.6.4. + * + service Specifies a service name in pg_service.conf that + * holds additional connection parameters. + * Available since PEAR DB 1.7.0. + * + sslmode How should SSL be used when connecting? Values: + * disable, allow, prefer or require. + * Available since PEAR DB 1.7.0. + * + tty This was used to specify where to send server + * debug output. Available since PEAR DB 1.6.4. + * + * Example of connecting to a new link via a socket: + * <code> + * require_once 'DB.php'; + * + * $dsn = 'pgsql://user:pass@unix(/tmp)/dbname?new_link=true'; + * $options = array( + * 'portability' => DB_PORTABILITY_ALL, + * ); + * + * $db = DB::connect($dsn, $options); + * if (PEAR::isError($db)) { + * die($db->getMessage()); + * } + * </code> + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @link http://www.postgresql.org/docs/current/static/libpq.html#LIBPQ-CONNECT + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('pgsql')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + $protocol = $dsn['protocol'] ? $dsn['protocol'] : 'tcp'; + + $params = array(''); + if ($protocol == 'tcp') { + if ($dsn['hostspec']) { + $params[0] .= 'host=' . $dsn['hostspec']; + } + if ($dsn['port']) { + $params[0] .= ' port=' . $dsn['port']; + } + } elseif ($protocol == 'unix') { + // Allow for pg socket in non-standard locations. + if ($dsn['socket']) { + $params[0] .= 'host=' . $dsn['socket']; + } + if ($dsn['port']) { + $params[0] .= ' port=' . $dsn['port']; + } + } + if ($dsn['database']) { + $params[0] .= ' dbname=\'' . addslashes($dsn['database']) . '\''; + } + if ($dsn['username']) { + $params[0] .= ' user=\'' . addslashes($dsn['username']) . '\''; + } + if ($dsn['password']) { + $params[0] .= ' password=\'' . addslashes($dsn['password']) . '\''; + } + if (!empty($dsn['options'])) { + $params[0] .= ' options=' . $dsn['options']; + } + if (!empty($dsn['tty'])) { + $params[0] .= ' tty=' . $dsn['tty']; + } + if (!empty($dsn['connect_timeout'])) { + $params[0] .= ' connect_timeout=' . $dsn['connect_timeout']; + } + if (!empty($dsn['sslmode'])) { + $params[0] .= ' sslmode=' . $dsn['sslmode']; + } + if (!empty($dsn['service'])) { + $params[0] .= ' service=' . $dsn['service']; + } + + if (isset($dsn['new_link']) + && ($dsn['new_link'] == 'true' || $dsn['new_link'] === true)) + { + if (version_compare(phpversion(), '4.3.0', '>=')) { + $params[] = PGSQL_CONNECT_FORCE_NEW; + } + } + + $connect_function = $persistent ? 'pg_pconnect' : 'pg_connect'; + + $ini = ini_get('track_errors'); + $php_errormsg = ''; + if ($ini) { + $this->connection = @call_user_func_array($connect_function, + $params); + } else { + @ini_set('track_errors', 1); + $this->connection = @call_user_func_array($connect_function, + $params); + @ini_set('track_errors', $ini); + } + + if (!$this->connection) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + $php_errormsg); + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @pg_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $ismanip = $this->_checkManip($query); + $this->last_query = $query; + $query = $this->modifyQuery($query); + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @pg_exec($this->connection, 'begin;'); + if (!$result) { + return $this->pgsqlRaiseError(); + } + } + $this->transaction_opcount++; + } + $result = @pg_exec($this->connection, $query); + if (!$result) { + return $this->pgsqlRaiseError(); + } + + /* + * Determine whether queries produce affected rows, result or nothing. + * + * This logic was introduced in version 1.1 of the file by ssb, + * though the regex has been modified slightly since then. + * + * PostgreSQL commands: + * ABORT, ALTER, BEGIN, CLOSE, CLUSTER, COMMIT, COPY, + * CREATE, DECLARE, DELETE, DROP TABLE, EXPLAIN, FETCH, + * GRANT, INSERT, LISTEN, LOAD, LOCK, MOVE, NOTIFY, RESET, + * REVOKE, ROLLBACK, SELECT, SELECT INTO, SET, SHOW, + * UNLISTEN, UPDATE, VACUUM + */ + if ($ismanip) { + $this->affected = @pg_affected_rows($result); + return DB_OK; + } elseif (preg_match('/^\s*\(*\s*(SELECT|EXPLAIN|FETCH|SHOW)\s/si', + $query)) + { + $this->row[(int)$result] = 0; // reset the row counter. + $numrows = $this->numRows($result); + if (is_object($numrows)) { + return $numrows; + } + $this->_num_rows[(int)$result] = $numrows; + $this->affected = 0; + return $result; + } else { + $this->affected = 0; + return DB_OK; + } + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal pgsql result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + $result_int = (int)$result; + $rownum = ($rownum !== null) ? $rownum : $this->row[$result_int]; + if ($rownum >= $this->_num_rows[$result_int]) { + return null; + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @pg_fetch_array($result, $rownum, PGSQL_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @pg_fetch_row($result, $rownum); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + $this->row[$result_int] = ++$rownum; + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + if (is_resource($result)) { + unset($this->row[(int)$result]); + unset($this->_num_rows[(int)$result]); + $this->affected = 0; + return @pg_freeresult($result); + } + return false; + } + + // }}} + // {{{ quote() + + /** + * @deprecated Deprecated in release 1.6.0 + * @internal + */ + function quote($str) + { + return $this->quoteSmart($str); + } + + // }}} + // {{{ quoteBoolean() + + /** + * Formats a boolean value for use within a query in a locale-independent + * manner. + * + * @param boolean the boolean value to be quoted. + * @return string the quoted string. + * @see DB_common::quoteSmart() + * @since Method available since release 1.7.8. + */ + function quoteBoolean($boolean) { + return $boolean ? 'TRUE' : 'FALSE'; + } + + // }}} + // {{{ escapeSimple() + + /** + * Escapes a string according to the current DBMS's standards + * + * {@internal PostgreSQL treats a backslash as an escape character, + * so they are escaped as well. + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @see DB_common::quoteSmart() + * @since Method available since Release 1.6.0 + */ + function escapeSimple($str) + { + if (function_exists('pg_escape_string')) { + /* This fixes an undocumented BC break in PHP 5.2.0 which changed + * the prototype of pg_escape_string. I'm not thrilled about having + * to sniff the PHP version, quite frankly, but it's the only way + * to deal with the problem. Revision 1.331.2.13.2.10 on + * php-src/ext/pgsql/pgsql.c (PHP_5_2 branch) is to blame, for the + * record. */ + if (version_compare(PHP_VERSION, '5.2.0', '>=')) { + return pg_escape_string($this->connection, $str); + } else { + return pg_escape_string($str); + } + } else { + return str_replace("'", "''", str_replace('\\', '\\\\', $str)); + } + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @pg_numfields($result); + if (!$cols) { + return $this->pgsqlRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows() + */ + function numRows($result) + { + $rows = @pg_numrows($result); + if ($rows === null) { + return $this->pgsqlRaiseError(); + } + return $rows; + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff = false) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + // (disabled) hack to shut up error messages from libpq.a + //@fclose(@fopen("php://stderr", "w")); + $result = @pg_exec($this->connection, 'end;'); + $this->transaction_opcount = 0; + if (!$result) { + return $this->pgsqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + $result = @pg_exec($this->connection, 'abort;'); + $this->transaction_opcount = 0; + if (!$result) { + return $this->pgsqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + return $this->affected; + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_pgsql::createSequence(), DB_pgsql::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + $repeat = false; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("SELECT NEXTVAL('${seqname}')"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) { + $repeat = true; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->createSequence($seq_name); + $this->popErrorHandling(); + if (DB::isError($result)) { + return $this->raiseError($result); + } + } else { + $repeat = false; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $arr = $result->fetchRow(DB_FETCHMODE_ORDERED); + $result->free(); + return $arr[0]; + } + + // }}} + // {{{ createSequence() + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_pgsql::nextID(), DB_pgsql::dropSequence() + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + $result = $this->query("CREATE SEQUENCE ${seqname}"); + return $result; + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_pgsql::nextID(), DB_pgsql::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DROP SEQUENCE ' + . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * Adds LIMIT clauses to a query string according to current DBMS standards + * + * @param string $query the query to modify + * @param int $from the row to start to fetching (0 = the first row) + * @param int $count the numbers of rows to fetch + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return string the query string with LIMIT clauses added + * + * @access protected + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + return "$query LIMIT $count OFFSET $from"; + } + + // }}} + // {{{ pgsqlRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_pgsql::errorNative(), DB_pgsql::errorCode() + */ + function pgsqlRaiseError($errno = null) + { + $native = $this->errorNative(); + if (!$native) { + $native = 'Database connection has been lost.'; + $errno = DB_ERROR_CONNECT_FAILED; + } + if ($errno === null) { + $errno = $this->errorCode($native); + } + return $this->raiseError($errno, null, null, null, $native); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error message produced by the last query + * + * {@internal Error messages are used instead of error codes + * in order to support older versions of PostgreSQL.}} + * + * @return string the DBMS' error message + */ + function errorNative() + { + return @pg_errormessage($this->connection); + } + + // }}} + // {{{ errorCode() + + /** + * Determines PEAR::DB error code from the database's text error message. + * + * @param string $errormsg error message returned from the database + * @return integer an error number from a DB error constant + */ + function errorCode($errormsg) + { + static $error_regexps; + if (!isset($error_regexps)) { + $error_regexps = array( + '/column .* (of relation .*)?does not exist/i' + => DB_ERROR_NOSUCHFIELD, + '/(relation|sequence|table).*does not exist|class .* not found/i' + => DB_ERROR_NOSUCHTABLE, + '/index .* does not exist/' + => DB_ERROR_NOT_FOUND, + '/relation .* already exists/i' + => DB_ERROR_ALREADY_EXISTS, + '/(divide|division) by zero$/i' + => DB_ERROR_DIVZERO, + '/pg_atoi: error in .*: can\'t parse /i' + => DB_ERROR_INVALID_NUMBER, + '/invalid input syntax for( type)? (integer|numeric)/i' + => DB_ERROR_INVALID_NUMBER, + '/value .* is out of range for type \w*int/i' + => DB_ERROR_INVALID_NUMBER, + '/integer out of range/i' + => DB_ERROR_INVALID_NUMBER, + '/value too long for type character/i' + => DB_ERROR_INVALID, + '/attribute .* not found|relation .* does not have attribute/i' + => DB_ERROR_NOSUCHFIELD, + '/column .* specified in USING clause does not exist in (left|right) table/i' + => DB_ERROR_NOSUCHFIELD, + '/parser: parse error at or near/i' + => DB_ERROR_SYNTAX, + '/syntax error at/' + => DB_ERROR_SYNTAX, + '/column reference .* is ambiguous/i' + => DB_ERROR_SYNTAX, + '/permission denied/' + => DB_ERROR_ACCESS_VIOLATION, + '/violates not-null constraint/' + => DB_ERROR_CONSTRAINT_NOT_NULL, + '/violates [\w ]+ constraint/' + => DB_ERROR_CONSTRAINT, + '/referential integrity violation/' + => DB_ERROR_CONSTRAINT, + '/more expressions than target columns/i' + => DB_ERROR_VALUE_COUNT_ON_ROW, + ); + } + foreach ($error_regexps as $regexp => $code) { + if (preg_match($regexp, $errormsg)) { + return $code; + } + } + // Fall back to DB_ERROR if there was no mapping. + return DB_ERROR; + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * NOTE: only supports 'table' and 'flags' if <var>$result</var> + * is a table name. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @pg_exec($this->connection, "SELECT * FROM $result LIMIT 0"); + $got_string = true; + } elseif (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->pgsqlRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @pg_numfields($id); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + $res[$i] = array( + 'table' => $got_string ? $case_func($result) : '', + 'name' => $case_func(@pg_fieldname($id, $i)), + 'type' => @pg_fieldtype($id, $i), + 'len' => @pg_fieldsize($id, $i), + 'flags' => $got_string + ? $this->_pgFieldFlags($id, $i, $result) + : '', + ); + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + // free the result only if we were called on a table + if ($got_string) { + @pg_freeresult($id); + } + return $res; + } + + // }}} + // {{{ _pgFieldFlags() + + /** + * Get a column's flags + * + * Supports "not_null", "default_value", "primary_key", "unique_key" + * and "multiple_key". The default value is passed through + * rawurlencode() in case there are spaces in it. + * + * @param int $resource the PostgreSQL result identifier + * @param int $num_field the field number + * + * @return string the flags + * + * @access private + */ + function _pgFieldFlags($resource, $num_field, $table_name) + { + $field_name = @pg_fieldname($resource, $num_field); + + // Check if there's a schema in $table_name and update things + // accordingly. + $from = 'pg_attribute f, pg_class tab, pg_type typ'; + if (strpos($table_name, '.') !== false) { + $from .= ', pg_namespace nsp'; + list($schema, $table) = explode('.', $table_name); + $tableWhere = "tab.relname = '$table' AND tab.relnamespace = nsp.oid AND nsp.nspname = '$schema'"; + } else { + $tableWhere = "tab.relname = '$table_name'"; + } + + $result = @pg_exec($this->connection, "SELECT f.attnotnull, f.atthasdef + FROM $from + WHERE tab.relname = typ.typname + AND typ.typrelid = f.attrelid + AND f.attname = '$field_name' + AND $tableWhere"); + if (@pg_numrows($result) > 0) { + $row = @pg_fetch_row($result, 0); + $flags = ($row[0] == 't') ? 'not_null ' : ''; + + if ($row[1] == 't') { + $result = @pg_exec($this->connection, "SELECT a.adsrc + FROM $from, pg_attrdef a + WHERE tab.relname = typ.typname AND typ.typrelid = f.attrelid + AND f.attrelid = a.adrelid AND f.attname = '$field_name' + AND $tableWhere AND f.attnum = a.adnum"); + $row = @pg_fetch_row($result, 0); + $num = preg_replace("/'(.*)'::\w+/", "\\1", $row[0]); + $flags .= 'default_' . rawurlencode($num) . ' '; + } + } else { + $flags = ''; + } + $result = @pg_exec($this->connection, "SELECT i.indisunique, i.indisprimary, i.indkey + FROM $from, pg_index i + WHERE tab.relname = typ.typname + AND typ.typrelid = f.attrelid + AND f.attrelid = i.indrelid + AND f.attname = '$field_name' + AND $tableWhere"); + $count = @pg_numrows($result); + + for ($i = 0; $i < $count ; $i++) { + $row = @pg_fetch_row($result, $i); + $keys = explode(' ', $row[2]); + + if (in_array($num_field + 1, $keys)) { + $flags .= ($row[0] == 't' && $row[1] == 'f') ? 'unique_key ' : ''; + $flags .= ($row[1] == 't') ? 'primary_key ' : ''; + if (count($keys) > 1) + $flags .= 'multiple_key '; + } + } + + return trim($flags); + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'SELECT c.relname AS "Name"' + . ' FROM pg_class c, pg_user u' + . ' WHERE c.relowner = u.usesysid' + . " AND c.relkind = 'r'" + . ' AND NOT EXISTS' + . ' (SELECT 1 FROM pg_views' + . ' WHERE viewname = c.relname)' + . " AND c.relname !~ '^(pg_|sql_)'" + . ' UNION' + . ' SELECT c.relname AS "Name"' + . ' FROM pg_class c' + . " WHERE c.relkind = 'r'" + . ' AND NOT EXISTS' + . ' (SELECT 1 FROM pg_views' + . ' WHERE viewname = c.relname)' + . ' AND NOT EXISTS' + . ' (SELECT 1 FROM pg_user' + . ' WHERE usesysid = c.relowner)' + . " AND c.relname !~ '^pg_'"; + case 'schema.tables': + return "SELECT schemaname || '.' || tablename" + . ' AS "Name"' + . ' FROM pg_catalog.pg_tables' + . ' WHERE schemaname NOT IN' + . " ('pg_catalog', 'information_schema', 'pg_toast')"; + case 'schema.views': + return "SELECT schemaname || '.' || viewname from pg_views WHERE schemaname" + . " NOT IN ('information_schema', 'pg_catalog')"; + case 'views': + // Table cols: viewname | viewowner | definition + return 'SELECT viewname from pg_views WHERE schemaname' + . " NOT IN ('information_schema', 'pg_catalog')"; + case 'users': + // cols: usename |usesysid|usecreatedb|usetrace|usesuper|usecatupd|passwd |valuntil + return 'SELECT usename FROM pg_user'; + case 'databases': + return 'SELECT datname FROM pg_database'; + case 'functions': + case 'procedures': + return 'SELECT proname FROM pg_proc WHERE proowner <> 1'; + default: + return null; + } + } + + // }}} + // {{{ _checkManip() + + /** + * Checks if the given query is a manipulation query. This also takes into + * account the _next_query_manip flag and sets the _last_query_manip flag + * (and resets _next_query_manip) according to the result. + * + * @param string The query to check. + * + * @return boolean true if the query is a manipulation query, false + * otherwise + * + * @access protected + */ + function _checkManip($query) + { + return (preg_match('/^\s*(SAVEPOINT|RELEASE)\s+/i', $query) + || parent::_checkManip($query)); + } + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/sqlite.php b/extlib/DB/sqlite.php new file mode 100644 index 000000000..5c4b396e5 --- /dev/null +++ b/extlib/DB/sqlite.php @@ -0,0 +1,960 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's sqlite extension + * for interacting with SQLite databases + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Urs Gehrig <urs@circle.ch> + * @author Mika Tuupola <tuupola@appelsiini.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 3.0 + * @version CVS: $Id: sqlite.php,v 1.118 2007/11/26 22:57:18 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's sqlite extension + * for interacting with SQLite databases + * + * These methods overload the ones declared in DB_common. + * + * NOTICE: This driver needs PHP's track_errors ini setting to be on. + * It is automatically turned on when connecting to the database. + * Make sure your scripts don't turn it off. + * + * @category Database + * @package DB + * @author Urs Gehrig <urs@circle.ch> + * @author Mika Tuupola <tuupola@appelsiini.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_sqlite extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'sqlite'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'sqlite'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => 'alter', + 'new_link' => false, + 'numrows' => true, + 'pconnect' => true, + 'prepare' => false, + 'ssl' => false, + 'transactions' => false, + ); + + /** + * A mapping of native error codes to DB error codes + * + * {@internal Error codes according to sqlite_exec. See the online + * manual at http://sqlite.org/c_interface.html for info. + * This error handling based on sqlite_exec is not yet implemented.}} + * + * @var array + */ + var $errorcode_map = array( + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * SQLite data types + * + * @link http://www.sqlite.org/datatypes.html + * + * @var array + */ + var $keywords = array ( + 'BLOB' => '', + 'BOOLEAN' => '', + 'CHARACTER' => '', + 'CLOB' => '', + 'FLOAT' => '', + 'INTEGER' => '', + 'KEY' => '', + 'NATIONAL' => '', + 'NUMERIC' => '', + 'NVARCHAR' => '', + 'PRIMARY' => '', + 'TEXT' => '', + 'TIMESTAMP' => '', + 'UNIQUE' => '', + 'VARCHAR' => '', + 'VARYING' => '', + ); + + /** + * The most recent error message from $php_errormsg + * @var string + * @access private + */ + var $_lasterror = ''; + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_sqlite() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * PEAR DB's sqlite driver supports the following extra DSN options: + * + mode The permissions for the database file, in four digit + * chmod octal format (eg "0600"). + * + * Example of connecting to a database in read-only mode: + * <code> + * require_once 'DB.php'; + * + * $dsn = 'sqlite:///path/and/name/of/db/file?mode=0400'; + * $options = array( + * 'portability' => DB_PORTABILITY_ALL, + * ); + * + * $db = DB::connect($dsn, $options); + * if (PEAR::isError($db)) { + * die($db->getMessage()); + * } + * </code> + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('sqlite')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + if (!$dsn['database']) { + return $this->sqliteRaiseError(DB_ERROR_ACCESS_VIOLATION); + } + + if ($dsn['database'] !== ':memory:') { + if (!file_exists($dsn['database'])) { + if (!touch($dsn['database'])) { + return $this->sqliteRaiseError(DB_ERROR_NOT_FOUND); + } + if (!isset($dsn['mode']) || + !is_numeric($dsn['mode'])) + { + $mode = 0644; + } else { + $mode = octdec($dsn['mode']); + } + if (!chmod($dsn['database'], $mode)) { + return $this->sqliteRaiseError(DB_ERROR_NOT_FOUND); + } + if (!file_exists($dsn['database'])) { + return $this->sqliteRaiseError(DB_ERROR_NOT_FOUND); + } + } + if (!is_file($dsn['database'])) { + return $this->sqliteRaiseError(DB_ERROR_INVALID); + } + if (!is_readable($dsn['database'])) { + return $this->sqliteRaiseError(DB_ERROR_ACCESS_VIOLATION); + } + } + + $connect_function = $persistent ? 'sqlite_popen' : 'sqlite_open'; + + // track_errors must remain on for simpleQuery() + @ini_set('track_errors', 1); + $php_errormsg = ''; + + if (!$this->connection = @$connect_function($dsn['database'])) { + return $this->raiseError(DB_ERROR_NODBSELECTED, + null, null, null, + $php_errormsg); + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @sqlite_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * NOTICE: This method needs PHP's track_errors ini setting to be on. + * It is automatically turned on when connecting to the database. + * Make sure your scripts don't turn it off. + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $ismanip = $this->_checkManip($query); + $this->last_query = $query; + $query = $this->modifyQuery($query); + + $php_errormsg = ''; + + $result = @sqlite_query($query, $this->connection); + $this->_lasterror = $php_errormsg ? $php_errormsg : ''; + + $this->result = $result; + if (!$this->result) { + return $this->sqliteRaiseError(null); + } + + // sqlite_query() seems to allways return a resource + // so cant use that. Using $ismanip instead + if (!$ismanip) { + $numRows = $this->numRows($result); + if (is_object($numRows)) { + // we've got PEAR_Error + return $numRows; + } + return $result; + } + return DB_OK; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal sqlite result pointer to the next available result + * + * @param resource $result the valid sqlite result resource + * + * @return bool true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if ($rownum !== null) { + if (!@sqlite_seek($this->result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @sqlite_fetch_array($result, SQLITE_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + + /* Remove extraneous " characters from the fields in the result. + * Fixes bug #11716. */ + if (is_array($arr) && count($arr) > 0) { + $strippedArr = array(); + foreach ($arr as $field => $value) { + $strippedArr[trim($field, '"')] = $value; + } + $arr = $strippedArr; + } + } else { + $arr = @sqlite_fetch_array($result, SQLITE_NUM); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + /* + * Even though this DBMS already trims output, we do this because + * a field might have intentional whitespace at the end that + * gets removed by DB_PORTABILITY_RTRIM under another driver. + */ + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult(&$result) + { + // XXX No native free? + if (!is_resource($result)) { + return false; + } + $result = null; + return true; + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @sqlite_num_fields($result); + if (!$cols) { + return $this->sqliteRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows() + */ + function numRows($result) + { + $rows = @sqlite_num_rows($result); + if ($rows === null) { + return $this->sqliteRaiseError(); + } + return $rows; + } + + // }}} + // {{{ affected() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + return @sqlite_changes($this->connection); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_sqlite::nextID(), DB_sqlite::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name)); + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_sqlite::nextID(), DB_sqlite::dropSequence() + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + $query = 'CREATE TABLE ' . $seqname . + ' (id INTEGER UNSIGNED PRIMARY KEY) '; + $result = $this->query($query); + if (DB::isError($result)) { + return($result); + } + $query = "CREATE TRIGGER ${seqname}_cleanup AFTER INSERT ON $seqname + BEGIN + DELETE FROM $seqname WHERE id<LAST_INSERT_ROWID(); + END "; + $result = $this->query($query); + if (DB::isError($result)) { + return($result); + } + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_sqlite::createSequence(), DB_sqlite::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + + do { + $repeat = 0; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("INSERT INTO $seqname (id) VALUES (NULL)"); + $this->popErrorHandling(); + if ($result === DB_OK) { + $id = @sqlite_last_insert_rowid($this->connection); + if ($id != 0) { + return $id; + } + } elseif ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) + { + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $this->raiseError($result); + } else { + $repeat = 1; + } + } + } while ($repeat); + + return $this->raiseError($result); + } + + // }}} + // {{{ getDbFileStats() + + /** + * Get the file stats for the current database + * + * Possible arguments are dev, ino, mode, nlink, uid, gid, rdev, size, + * atime, mtime, ctime, blksize, blocks or a numeric key between + * 0 and 12. + * + * @param string $arg the array key for stats() + * + * @return mixed an array on an unspecified key, integer on a passed + * arg and false at a stats error + */ + function getDbFileStats($arg = '') + { + $stats = stat($this->dsn['database']); + if ($stats == false) { + return false; + } + if (is_array($stats)) { + if (is_numeric($arg)) { + if (((int)$arg <= 12) & ((int)$arg >= 0)) { + return false; + } + return $stats[$arg ]; + } + if (array_key_exists(trim($arg), $stats)) { + return $stats[$arg ]; + } + } + return $stats; + } + + // }}} + // {{{ escapeSimple() + + /** + * Escapes a string according to the current DBMS's standards + * + * In SQLite, this makes things safe for inserts/updates, but may + * cause problems when performing text comparisons against columns + * containing binary data. See the + * {@link http://php.net/sqlite_escape_string PHP manual} for more info. + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @since Method available since Release 1.6.1 + * @see DB_common::escapeSimple() + */ + function escapeSimple($str) + { + return @sqlite_escape_string($str); + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * Adds LIMIT clauses to a query string according to current DBMS standards + * + * @param string $query the query to modify + * @param int $from the row to start to fetching (0 = the first row) + * @param int $count the numbers of rows to fetch + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return string the query string with LIMIT clauses added + * + * @access protected + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + return "$query LIMIT $count OFFSET $from"; + } + + // }}} + // {{{ modifyQuery() + + /** + * Changes a query string for various DBMS specific reasons + * + * This little hack lets you know how many rows were deleted + * when running a "DELETE FROM table" query. Only implemented + * if the DB_PORTABILITY_DELETE_COUNT portability option is on. + * + * @param string $query the query string to modify + * + * @return string the modified query string + * + * @access protected + * @see DB_common::setOption() + */ + function modifyQuery($query) + { + if ($this->options['portability'] & DB_PORTABILITY_DELETE_COUNT) { + if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $query)) { + $query = preg_replace('/^\s*DELETE\s+FROM\s+(\S+)\s*$/', + 'DELETE FROM \1 WHERE 1=1', $query); + } + } + return $query; + } + + // }}} + // {{{ sqliteRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_sqlite::errorNative(), DB_sqlite::errorCode() + */ + function sqliteRaiseError($errno = null) + { + $native = $this->errorNative(); + if ($errno === null) { + $errno = $this->errorCode($native); + } + + $errorcode = @sqlite_last_error($this->connection); + $userinfo = "$errorcode ** $this->last_query"; + + return $this->raiseError($errno, null, null, $userinfo, $native); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error message produced by the last query + * + * {@internal This is used to retrieve more meaningfull error messages + * because sqlite_last_error() does not provide adequate info.}} + * + * @return string the DBMS' error message + */ + function errorNative() + { + return $this->_lasterror; + } + + // }}} + // {{{ errorCode() + + /** + * Determines PEAR::DB error code from the database's text error message + * + * @param string $errormsg the error message returned from the database + * + * @return integer the DB error number + */ + function errorCode($errormsg) + { + static $error_regexps; + + // PHP 5.2+ prepends the function name to $php_errormsg, so we need + // this hack to work around it, per bug #9599. + $errormsg = preg_replace('/^sqlite[a-z_]+\(\): /', '', $errormsg); + + if (!isset($error_regexps)) { + $error_regexps = array( + '/^no such table:/' => DB_ERROR_NOSUCHTABLE, + '/^no such index:/' => DB_ERROR_NOT_FOUND, + '/^(table|index) .* already exists$/' => DB_ERROR_ALREADY_EXISTS, + '/PRIMARY KEY must be unique/i' => DB_ERROR_CONSTRAINT, + '/is not unique/' => DB_ERROR_CONSTRAINT, + '/columns .* are not unique/i' => DB_ERROR_CONSTRAINT, + '/uniqueness constraint failed/' => DB_ERROR_CONSTRAINT, + '/may not be NULL/' => DB_ERROR_CONSTRAINT_NOT_NULL, + '/^no such column:/' => DB_ERROR_NOSUCHFIELD, + '/no column named/' => DB_ERROR_NOSUCHFIELD, + '/column not present in both tables/i' => DB_ERROR_NOSUCHFIELD, + '/^near ".*": syntax error$/' => DB_ERROR_SYNTAX, + '/[0-9]+ values for [0-9]+ columns/i' => DB_ERROR_VALUE_COUNT_ON_ROW, + ); + } + foreach ($error_regexps as $regexp => $code) { + if (preg_match($regexp, $errormsg)) { + return $code; + } + } + // Fall back to DB_ERROR if there was no mapping. + return DB_ERROR; + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table + * + * @param string $result a string containing the name of a table + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + * @since Method available since Release 1.7.0 + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @sqlite_array_query($this->connection, + "PRAGMA table_info('$result');", + SQLITE_ASSOC); + $got_string = true; + } else { + $this->last_query = ''; + return $this->raiseError(DB_ERROR_NOT_CAPABLE, null, null, null, + 'This DBMS can not obtain tableInfo' . + ' from result sets'); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = count($id); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + if (strpos($id[$i]['type'], '(') !== false) { + $bits = explode('(', $id[$i]['type']); + $type = $bits[0]; + $len = rtrim($bits[1],')'); + } else { + $type = $id[$i]['type']; + $len = 0; + } + + $flags = ''; + if ($id[$i]['pk']) { + $flags .= 'primary_key '; + } + if ($id[$i]['notnull']) { + $flags .= 'not_null '; + } + if ($id[$i]['dflt_value'] !== null) { + $flags .= 'default_' . rawurlencode($id[$i]['dflt_value']); + } + $flags = trim($flags); + + $res[$i] = array( + 'table' => $case_func($result), + 'name' => $case_func($id[$i]['name']), + 'type' => $type, + 'len' => $len, + 'flags' => $flags, + ); + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * @param array $args SQLITE DRIVER ONLY: a private array of arguments + * used by the getSpecialQuery(). Do not use + * this directly. + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type, $args = array()) + { + if (!is_array($args)) { + return $this->raiseError('no key specified', null, null, null, + 'Argument has to be an array.'); + } + + switch ($type) { + case 'master': + return 'SELECT * FROM sqlite_master;'; + case 'tables': + return "SELECT name FROM sqlite_master WHERE type='table' " + . 'UNION ALL SELECT name FROM sqlite_temp_master ' + . "WHERE type='table' ORDER BY name;"; + case 'schema': + return 'SELECT sql FROM (SELECT * FROM sqlite_master ' + . 'UNION ALL SELECT * FROM sqlite_temp_master) ' + . "WHERE type!='meta' " + . 'ORDER BY tbl_name, type DESC, name;'; + case 'schemax': + case 'schema_x': + /* + * Use like: + * $res = $db->query($db->getSpecialQuery('schema_x', + * array('table' => 'table3'))); + */ + return 'SELECT sql FROM (SELECT * FROM sqlite_master ' + . 'UNION ALL SELECT * FROM sqlite_temp_master) ' + . "WHERE tbl_name LIKE '{$args['table']}' " + . "AND type!='meta' " + . 'ORDER BY type DESC, name;'; + case 'alter': + /* + * SQLite does not support ALTER TABLE; this is a helper query + * to handle this. 'table' represents the table name, 'rows' + * the news rows to create, 'save' the row(s) to keep _with_ + * the data. + * + * Use like: + * $args = array( + * 'table' => $table, + * 'rows' => "id INTEGER PRIMARY KEY, firstname TEXT, surname TEXT, datetime TEXT", + * 'save' => "NULL, titel, content, datetime" + * ); + * $res = $db->query( $db->getSpecialQuery('alter', $args)); + */ + $rows = strtr($args['rows'], $this->keywords); + + $q = array( + 'BEGIN TRANSACTION', + "CREATE TEMPORARY TABLE {$args['table']}_backup ({$args['rows']})", + "INSERT INTO {$args['table']}_backup SELECT {$args['save']} FROM {$args['table']}", + "DROP TABLE {$args['table']}", + "CREATE TABLE {$args['table']} ({$args['rows']})", + "INSERT INTO {$args['table']} SELECT {$rows} FROM {$args['table']}_backup", + "DROP TABLE {$args['table']}_backup", + 'COMMIT', + ); + + /* + * This is a dirty hack, since the above query will not get + * executed with a single query call so here the query method + * will be called directly and return a select instead. + */ + foreach ($q as $query) { + $this->query($query); + } + return "SELECT * FROM {$args['table']};"; + default: + return null; + } + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/storage.php b/extlib/DB/storage.php new file mode 100644 index 000000000..ffa2d9447 --- /dev/null +++ b/extlib/DB/storage.php @@ -0,0 +1,506 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * Provides an object interface to a table row + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Stig Bakken <stig@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: storage.php,v 1.24 2007/08/12 05:27:25 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB class so it can be extended from + */ +require_once 'DB.php'; + +/** + * Provides an object interface to a table row + * + * It lets you add, delete and change rows using objects rather than SQL + * statements. + * + * @category Database + * @package DB + * @author Stig Bakken <stig@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_storage extends PEAR +{ + // {{{ properties + + /** the name of the table (or view, if the backend database supports + updates in views) we hold data from */ + var $_table = null; + + /** which column(s) in the table contains primary keys, can be a + string for single-column primary keys, or an array of strings + for multiple-column primary keys */ + var $_keycolumn = null; + + /** DB connection handle used for all transactions */ + var $_dbh = null; + + /** an assoc with the names of database fields stored as properties + in this object */ + var $_properties = array(); + + /** an assoc with the names of the properties in this object that + have been changed since they were fetched from the database */ + var $_changes = array(); + + /** flag that decides if data in this object can be changed. + objects that don't have their table's key column in their + property lists will be flagged as read-only. */ + var $_readonly = false; + + /** function or method that implements a validator for fields that + are set, this validator function returns true if the field is + valid, false if not */ + var $_validator = null; + + // }}} + // {{{ constructor + + /** + * Constructor + * + * @param $table string the name of the database table + * + * @param $keycolumn mixed string with name of key column, or array of + * strings if the table has a primary key of more than one column + * + * @param $dbh object database connection object + * + * @param $validator mixed function or method used to validate + * each new value, called with three parameters: the name of the + * field/column that is changing, a reference to the new value and + * a reference to this object + * + */ + function DB_storage($table, $keycolumn, &$dbh, $validator = null) + { + $this->PEAR('DB_Error'); + $this->_table = $table; + $this->_keycolumn = $keycolumn; + $this->_dbh = $dbh; + $this->_readonly = false; + $this->_validator = $validator; + } + + // }}} + // {{{ _makeWhere() + + /** + * Utility method to build a "WHERE" clause to locate ourselves in + * the table. + * + * XXX future improvement: use rowids? + * + * @access private + */ + function _makeWhere($keyval = null) + { + if (is_array($this->_keycolumn)) { + if ($keyval === null) { + for ($i = 0; $i < sizeof($this->_keycolumn); $i++) { + $keyval[] = $this->{$this->_keycolumn[$i]}; + } + } + $whereclause = ''; + for ($i = 0; $i < sizeof($this->_keycolumn); $i++) { + if ($i > 0) { + $whereclause .= ' AND '; + } + $whereclause .= $this->_keycolumn[$i]; + if (is_null($keyval[$i])) { + // there's not much point in having a NULL key, + // but we support it anyway + $whereclause .= ' IS NULL'; + } else { + $whereclause .= ' = ' . $this->_dbh->quote($keyval[$i]); + } + } + } else { + if ($keyval === null) { + $keyval = @$this->{$this->_keycolumn}; + } + $whereclause = $this->_keycolumn; + if (is_null($keyval)) { + // there's not much point in having a NULL key, + // but we support it anyway + $whereclause .= ' IS NULL'; + } else { + $whereclause .= ' = ' . $this->_dbh->quote($keyval); + } + } + return $whereclause; + } + + // }}} + // {{{ setup() + + /** + * Method used to initialize a DB_storage object from the + * configured table. + * + * @param $keyval mixed the key[s] of the row to fetch (string or array) + * + * @return int DB_OK on success, a DB error if not + */ + function setup($keyval) + { + $whereclause = $this->_makeWhere($keyval); + $query = 'SELECT * FROM ' . $this->_table . ' WHERE ' . $whereclause; + $sth = $this->_dbh->query($query); + if (DB::isError($sth)) { + return $sth; + } + $row = $sth->fetchRow(DB_FETCHMODE_ASSOC); + if (DB::isError($row)) { + return $row; + } + if (!$row) { + return $this->raiseError(null, DB_ERROR_NOT_FOUND, null, null, + $query, null, true); + } + foreach ($row as $key => $value) { + $this->_properties[$key] = true; + $this->$key = $value; + } + return DB_OK; + } + + // }}} + // {{{ insert() + + /** + * Create a new (empty) row in the configured table for this + * object. + */ + function insert($newpk) + { + if (is_array($this->_keycolumn)) { + $primarykey = $this->_keycolumn; + } else { + $primarykey = array($this->_keycolumn); + } + settype($newpk, "array"); + for ($i = 0; $i < sizeof($primarykey); $i++) { + $pkvals[] = $this->_dbh->quote($newpk[$i]); + } + + $sth = $this->_dbh->query("INSERT INTO $this->_table (" . + implode(",", $primarykey) . ") VALUES(" . + implode(",", $pkvals) . ")"); + if (DB::isError($sth)) { + return $sth; + } + if (sizeof($newpk) == 1) { + $newpk = $newpk[0]; + } + $this->setup($newpk); + } + + // }}} + // {{{ toString() + + /** + * Output a simple description of this DB_storage object. + * @return string object description + */ + function toString() + { + $info = strtolower(get_class($this)); + $info .= " (table="; + $info .= $this->_table; + $info .= ", keycolumn="; + if (is_array($this->_keycolumn)) { + $info .= "(" . implode(",", $this->_keycolumn) . ")"; + } else { + $info .= $this->_keycolumn; + } + $info .= ", dbh="; + if (is_object($this->_dbh)) { + $info .= $this->_dbh->toString(); + } else { + $info .= "null"; + } + $info .= ")"; + if (sizeof($this->_properties)) { + $info .= " [loaded, key="; + $keyname = $this->_keycolumn; + if (is_array($keyname)) { + $info .= "("; + for ($i = 0; $i < sizeof($keyname); $i++) { + if ($i > 0) { + $info .= ","; + } + $info .= $this->$keyname[$i]; + } + $info .= ")"; + } else { + $info .= $this->$keyname; + } + $info .= "]"; + } + if (sizeof($this->_changes)) { + $info .= " [modified]"; + } + return $info; + } + + // }}} + // {{{ dump() + + /** + * Dump the contents of this object to "standard output". + */ + function dump() + { + foreach ($this->_properties as $prop => $foo) { + print "$prop = "; + print htmlentities($this->$prop); + print "<br />\n"; + } + } + + // }}} + // {{{ &create() + + /** + * Static method used to create new DB storage objects. + * @param $data assoc. array where the keys are the names + * of properties/columns + * @return object a new instance of DB_storage or a subclass of it + */ + function &create($table, &$data) + { + $classname = strtolower(get_class($this)); + $obj = new $classname($table); + foreach ($data as $name => $value) { + $obj->_properties[$name] = true; + $obj->$name = &$value; + } + return $obj; + } + + // }}} + // {{{ loadFromQuery() + + /** + * Loads data into this object from the given query. If this + * object already contains table data, changes will be saved and + * the object re-initialized first. + * + * @param $query SQL query + * + * @param $params parameter list in case you want to use + * prepare/execute mode + * + * @return int DB_OK on success, DB_WARNING_READ_ONLY if the + * returned object is read-only (because the object's specified + * key column was not found among the columns returned by $query), + * or another DB error code in case of errors. + */ +// XXX commented out for now +/* + function loadFromQuery($query, $params = null) + { + if (sizeof($this->_properties)) { + if (sizeof($this->_changes)) { + $this->store(); + $this->_changes = array(); + } + $this->_properties = array(); + } + $rowdata = $this->_dbh->getRow($query, DB_FETCHMODE_ASSOC, $params); + if (DB::isError($rowdata)) { + return $rowdata; + } + reset($rowdata); + $found_keycolumn = false; + while (list($key, $value) = each($rowdata)) { + if ($key == $this->_keycolumn) { + $found_keycolumn = true; + } + $this->_properties[$key] = true; + $this->$key = &$value; + unset($value); // have to unset, or all properties will + // refer to the same value + } + if (!$found_keycolumn) { + $this->_readonly = true; + return DB_WARNING_READ_ONLY; + } + return DB_OK; + } + */ + + // }}} + // {{{ set() + + /** + * Modify an attriute value. + */ + function set($property, $newvalue) + { + // only change if $property is known and object is not + // read-only + if ($this->_readonly) { + return $this->raiseError(null, DB_WARNING_READ_ONLY, null, + null, null, null, true); + } + if (@isset($this->_properties[$property])) { + if (empty($this->_validator)) { + $valid = true; + } else { + $valid = @call_user_func($this->_validator, + $this->_table, + $property, + $newvalue, + $this->$property, + $this); + } + if ($valid) { + $this->$property = $newvalue; + if (empty($this->_changes[$property])) { + $this->_changes[$property] = 0; + } else { + $this->_changes[$property]++; + } + } else { + return $this->raiseError(null, DB_ERROR_INVALID, null, + null, "invalid field: $property", + null, true); + } + return true; + } + return $this->raiseError(null, DB_ERROR_NOSUCHFIELD, null, + null, "unknown field: $property", + null, true); + } + + // }}} + // {{{ &get() + + /** + * Fetch an attribute value. + * + * @param string attribute name + * + * @return attribute contents, or null if the attribute name is + * unknown + */ + function &get($property) + { + // only return if $property is known + if (isset($this->_properties[$property])) { + return $this->$property; + } + $tmp = null; + return $tmp; + } + + // }}} + // {{{ _DB_storage() + + /** + * Destructor, calls DB_storage::store() if there are changes + * that are to be kept. + */ + function _DB_storage() + { + if (sizeof($this->_changes)) { + $this->store(); + } + $this->_properties = array(); + $this->_changes = array(); + $this->_table = null; + } + + // }}} + // {{{ store() + + /** + * Stores changes to this object in the database. + * + * @return DB_OK or a DB error + */ + function store() + { + $params = array(); + $vars = array(); + foreach ($this->_changes as $name => $foo) { + $params[] = &$this->$name; + $vars[] = $name . ' = ?'; + } + if ($vars) { + $query = 'UPDATE ' . $this->_table . ' SET ' . + implode(', ', $vars) . ' WHERE ' . + $this->_makeWhere(); + $stmt = $this->_dbh->prepare($query); + $res = $this->_dbh->execute($stmt, $params); + if (DB::isError($res)) { + return $res; + } + $this->_changes = array(); + } + return DB_OK; + } + + // }}} + // {{{ remove() + + /** + * Remove the row represented by this object from the database. + * + * @return mixed DB_OK or a DB error + */ + function remove() + { + if ($this->_readonly) { + return $this->raiseError(null, DB_WARNING_READ_ONLY, null, + null, null, null, true); + } + $query = 'DELETE FROM ' . $this->_table .' WHERE '. + $this->_makeWhere(); + $res = $this->_dbh->query($query); + if (DB::isError($res)) { + return $res; + } + foreach ($this->_properties as $prop => $foo) { + unset($this->$prop); + } + $this->_properties = array(); + $this->_changes = array(); + return DB_OK; + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/DB/sybase.php b/extlib/DB/sybase.php new file mode 100644 index 000000000..3befbf6ea --- /dev/null +++ b/extlib/DB/sybase.php @@ -0,0 +1,942 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * The PEAR DB driver for PHP's sybase extension + * for interacting with Sybase databases + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Database + * @package DB + * @author Sterling Hughes <sterling@php.net> + * @author Antônio Carlos Venâncio Júnior <floripa@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: sybase.php,v 1.87 2007/09/21 13:40:42 aharvey Exp $ + * @link http://pear.php.net/package/DB + */ + +/** + * Obtain the DB_common class so it can be extended from + */ +require_once 'DB/common.php'; + +/** + * The methods PEAR DB uses to interact with PHP's sybase extension + * for interacting with Sybase databases + * + * These methods overload the ones declared in DB_common. + * + * WARNING: This driver may fail with multiple connections under the + * same user/pass/host and different databases. + * + * @category Database + * @package DB + * @author Sterling Hughes <sterling@php.net> + * @author Antônio Carlos Venâncio Júnior <floripa@php.net> + * @author Daniel Convissor <danielc@php.net> + * @copyright 1997-2007 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.14RC1 + * @link http://pear.php.net/package/DB + */ +class DB_sybase extends DB_common +{ + // {{{ properties + + /** + * The DB driver type (mysql, oci8, odbc, etc.) + * @var string + */ + var $phptype = 'sybase'; + + /** + * The database syntax variant to be used (db2, access, etc.), if any + * @var string + */ + var $dbsyntax = 'sybase'; + + /** + * The capabilities of this DB implementation + * + * The 'new_link' element contains the PHP version that first provided + * new_link support for this DBMS. Contains false if it's unsupported. + * + * Meaning of the 'limit' element: + * + 'emulate' = emulate with fetch row by number + * + 'alter' = alter the query + * + false = skip rows + * + * @var array + */ + var $features = array( + 'limit' => 'emulate', + 'new_link' => false, + 'numrows' => true, + 'pconnect' => true, + 'prepare' => false, + 'ssl' => false, + 'transactions' => true, + ); + + /** + * A mapping of native error codes to DB error codes + * @var array + */ + var $errorcode_map = array( + ); + + /** + * The raw database connection created by PHP + * @var resource + */ + var $connection; + + /** + * The DSN information for connecting to a database + * @var array + */ + var $dsn = array(); + + + /** + * Should data manipulation queries be committed automatically? + * @var bool + * @access private + */ + var $autocommit = true; + + /** + * The quantity of transactions begun + * + * {@internal While this is private, it can't actually be designated + * private in PHP 5 because it is directly accessed in the test suite.}} + * + * @var integer + * @access private + */ + var $transaction_opcount = 0; + + /** + * The database specified in the DSN + * + * It's a fix to allow calls to different databases in the same script. + * + * @var string + * @access private + */ + var $_db = ''; + + + // }}} + // {{{ constructor + + /** + * This constructor calls <kbd>$this->DB_common()</kbd> + * + * @return void + */ + function DB_sybase() + { + $this->DB_common(); + } + + // }}} + // {{{ connect() + + /** + * Connect to the database server, log in and open the database + * + * Don't call this method directly. Use DB::connect() instead. + * + * PEAR DB's sybase driver supports the following extra DSN options: + * + appname The application name to use on this connection. + * Available since PEAR DB 1.7.0. + * + charset The character set to use on this connection. + * Available since PEAR DB 1.7.0. + * + * @param array $dsn the data source name + * @param bool $persistent should the connection be persistent? + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function connect($dsn, $persistent = false) + { + if (!PEAR::loadExtension('sybase') && + !PEAR::loadExtension('sybase_ct')) + { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsn; + if ($dsn['dbsyntax']) { + $this->dbsyntax = $dsn['dbsyntax']; + } + + $dsn['hostspec'] = $dsn['hostspec'] ? $dsn['hostspec'] : 'localhost'; + $dsn['password'] = !empty($dsn['password']) ? $dsn['password'] : false; + $dsn['charset'] = isset($dsn['charset']) ? $dsn['charset'] : false; + $dsn['appname'] = isset($dsn['appname']) ? $dsn['appname'] : false; + + $connect_function = $persistent ? 'sybase_pconnect' : 'sybase_connect'; + + if ($dsn['username']) { + $this->connection = @$connect_function($dsn['hostspec'], + $dsn['username'], + $dsn['password'], + $dsn['charset'], + $dsn['appname']); + } else { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + 'The DSN did not contain a username.'); + } + + if (!$this->connection) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, + null, null, null, + @sybase_get_last_message()); + } + + if ($dsn['database']) { + if (!@sybase_select_db($dsn['database'], $this->connection)) { + return $this->raiseError(DB_ERROR_NODBSELECTED, + null, null, null, + @sybase_get_last_message()); + } + $this->_db = $dsn['database']; + } + + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects from the database server + * + * @return bool TRUE on success, FALSE on failure + */ + function disconnect() + { + $ret = @sybase_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Sends a query to the database server + * + * @param string the SQL query string + * + * @return mixed + a PHP result resrouce for successful SELECT queries + * + the DB_OK constant for other successful queries + * + a DB_Error object on failure + */ + function simpleQuery($query) + { + $ismanip = $this->_checkManip($query); + $this->last_query = $query; + if ($this->_db && !@sybase_select_db($this->_db, $this->connection)) { + return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED); + } + $query = $this->modifyQuery($query); + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @sybase_query('BEGIN TRANSACTION', $this->connection); + if (!$result) { + return $this->sybaseRaiseError(); + } + } + $this->transaction_opcount++; + } + $result = @sybase_query($query, $this->connection); + if (!$result) { + return $this->sybaseRaiseError(); + } + if (is_resource($result)) { + return $result; + } + // Determine which queries that should return data, and which + // should return an error code only. + return $ismanip ? DB_OK : $result; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal sybase result pointer to the next available result + * + * @param a valid sybase result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Places a row from the result set into the given array + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * This method is not meant to be called directly. Use + * DB_result::fetchInto() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result the query result resource + * @param array $arr the referenced array to put the data in + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch (0 = first row) + * + * @return mixed DB_OK on success, NULL when the end of a result set is + * reached or on failure + * + * @see DB_result::fetchInto() + */ + function fetchInto($result, &$arr, $fetchmode, $rownum = null) + { + if ($rownum !== null) { + if (!@sybase_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + if (function_exists('sybase_fetch_assoc')) { + $arr = @sybase_fetch_assoc($result); + } else { + if ($arr = @sybase_fetch_array($result)) { + foreach ($arr as $key => $value) { + if (is_int($key)) { + unset($arr[$key]); + } + } + } + } + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @sybase_fetch_row($result); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Deletes the result set and frees the memory occupied by the result set + * + * This method is not meant to be called directly. Use + * DB_result::free() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return bool TRUE on success, FALSE if $result is invalid + * + * @see DB_result::free() + */ + function freeResult($result) + { + return is_resource($result) ? sybase_free_result($result) : false; + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numCols() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of columns. A DB_Error object on failure. + * + * @see DB_result::numCols() + */ + function numCols($result) + { + $cols = @sybase_num_fields($result); + if (!$cols) { + return $this->sybaseRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows in a result set + * + * This method is not meant to be called directly. Use + * DB_result::numRows() instead. It can't be declared "protected" + * because DB_result is a separate object. + * + * @param resource $result PHP's query result resource + * + * @return int the number of rows. A DB_Error object on failure. + * + * @see DB_result::numRows() + */ + function numRows($result) + { + $rows = @sybase_num_rows($result); + if ($rows === false) { + return $this->sybaseRaiseError(); + } + return $rows; + } + + // }}} + // {{{ affectedRows() + + /** + * Determines the number of rows affected by a data maniuplation query + * + * 0 is returned for queries that don't manipulate data. + * + * @return int the number of rows. A DB_Error object on failure. + */ + function affectedRows() + { + if ($this->_last_query_manip) { + $result = @sybase_affected_rows($this->connection); + } else { + $result = 0; + } + return $result; + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. + * A DB_Error object on failure. + * + * @see DB_common::nextID(), DB_common::getSequenceName(), + * DB_sybase::createSequence(), DB_sybase::dropSequence() + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + if ($this->_db && !@sybase_select_db($this->_db, $this->connection)) { + return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED); + } + $repeat = 0; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("INSERT INTO $seqname (vapor) VALUES (0)"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result) && + ($result->getCode() == DB_ERROR || $result->getCode() == DB_ERROR_NOSUCHTABLE)) + { + $repeat = 1; + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $this->raiseError($result); + } + } elseif (!DB::isError($result)) { + $result = $this->query("SELECT @@IDENTITY FROM $seqname"); + $repeat = 0; + } else { + $repeat = false; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $result = $result->fetchRow(DB_FETCHMODE_ORDERED); + return $result[0]; + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_sybase::nextID(), DB_sybase::dropSequence() + */ + function createSequence($seq_name) + { + return $this->query('CREATE TABLE ' + . $this->getSequenceName($seq_name) + . ' (id numeric(10, 0) IDENTITY NOT NULL,' + . ' vapor int NULL)'); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. A DB_Error object on failure. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_sybase::nextID(), DB_sybase::createSequence() + */ + function dropSequence($seq_name) + { + return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ quoteFloat() + + /** + * Formats a float value for use within a query in a locale-independent + * manner. + * + * @param float the float value to be quoted. + * @return string the quoted string. + * @see DB_common::quoteSmart() + * @since Method available since release 1.7.8. + */ + function quoteFloat($float) { + return $this->escapeSimple(str_replace(',', '.', strval(floatval($float)))); + } + + // }}} + // {{{ autoCommit() + + /** + * Enables or disables automatic commits + * + * @param bool $onoff true turns it on, false turns it off + * + * @return int DB_OK on success. A DB_Error object if the driver + * doesn't support auto-committing transactions. + */ + function autoCommit($onoff = false) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commits the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + if ($this->_db && !@sybase_select_db($this->_db, $this->connection)) { + return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED); + } + $result = @sybase_query('COMMIT', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->sybaseRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Reverts the current transaction + * + * @return int DB_OK on success. A DB_Error object on failure. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + if ($this->_db && !@sybase_select_db($this->_db, $this->connection)) { + return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED); + } + $result = @sybase_query('ROLLBACK', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->sybaseRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ sybaseRaiseError() + + /** + * Produces a DB_Error object regarding the current problem + * + * @param int $errno if the error is being manually raised pass a + * DB_ERROR* constant here. If this isn't passed + * the error information gathered from the DBMS. + * + * @return object the DB_Error object + * + * @see DB_common::raiseError(), + * DB_sybase::errorNative(), DB_sybase::errorCode() + */ + function sybaseRaiseError($errno = null) + { + $native = $this->errorNative(); + if ($errno === null) { + $errno = $this->errorCode($native); + } + return $this->raiseError($errno, null, null, null, $native); + } + + // }}} + // {{{ errorNative() + + /** + * Gets the DBMS' native error message produced by the last query + * + * @return string the DBMS' error message + */ + function errorNative() + { + return @sybase_get_last_message(); + } + + // }}} + // {{{ errorCode() + + /** + * Determines PEAR::DB error code from the database's text error message. + * + * @param string $errormsg error message returned from the database + * @return integer an error number from a DB error constant + */ + function errorCode($errormsg) + { + static $error_regexps; + + // PHP 5.2+ prepends the function name to $php_errormsg, so we need + // this hack to work around it, per bug #9599. + $errormsg = preg_replace('/^sybase[a-z_]+\(\): /', '', $errormsg); + + if (!isset($error_regexps)) { + $error_regexps = array( + '/Incorrect syntax near/' + => DB_ERROR_SYNTAX, + '/^Unclosed quote before the character string [\"\'].*[\"\']\./' + => DB_ERROR_SYNTAX, + '/Implicit conversion (from datatype|of NUMERIC value)/i' + => DB_ERROR_INVALID_NUMBER, + '/Cannot drop the table [\"\'].+[\"\'], because it doesn\'t exist in the system catalogs\./' + => DB_ERROR_NOSUCHTABLE, + '/Only the owner of object [\"\'].+[\"\'] or a user with System Administrator \(SA\) role can run this command\./' + => DB_ERROR_ACCESS_VIOLATION, + '/^.+ permission denied on object .+, database .+, owner .+/' + => DB_ERROR_ACCESS_VIOLATION, + '/^.* permission denied, database .+, owner .+/' + => DB_ERROR_ACCESS_VIOLATION, + '/[^.*] not found\./' + => DB_ERROR_NOSUCHTABLE, + '/There is already an object named/' + => DB_ERROR_ALREADY_EXISTS, + '/Invalid column name/' + => DB_ERROR_NOSUCHFIELD, + '/does not allow null values/' + => DB_ERROR_CONSTRAINT_NOT_NULL, + '/Command has been aborted/' + => DB_ERROR_CONSTRAINT, + '/^Cannot drop the index .* because it doesn\'t exist/i' + => DB_ERROR_NOT_FOUND, + '/^There is already an index/i' + => DB_ERROR_ALREADY_EXISTS, + '/^There are fewer columns in the INSERT statement than values specified/i' + => DB_ERROR_VALUE_COUNT_ON_ROW, + '/Divide by zero/i' + => DB_ERROR_DIVZERO, + ); + } + + foreach ($error_regexps as $regexp => $code) { + if (preg_match($regexp, $errormsg)) { + return $code; + } + } + return DB_ERROR; + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * NOTE: only supports 'table' and 'flags' if <var>$result</var> + * is a table name. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode a valid tableInfo mode + * + * @return array an associative array with the information requested. + * A DB_Error object on failure. + * + * @see DB_common::tableInfo() + * @since Method available since Release 1.6.0 + */ + function tableInfo($result, $mode = null) + { + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + if ($this->_db && !@sybase_select_db($this->_db, $this->connection)) { + return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED); + } + $id = @sybase_query("SELECT * FROM $result WHERE 1=0", + $this->connection); + $got_string = true; + } elseif (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->sybaseRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @sybase_num_fields($id); + $res = array(); + + if ($mode) { + $res['num_fields'] = $count; + } + + for ($i = 0; $i < $count; $i++) { + $f = @sybase_fetch_field($id, $i); + // column_source is often blank + $res[$i] = array( + 'table' => $got_string + ? $case_func($result) + : $case_func($f->column_source), + 'name' => $case_func($f->name), + 'type' => $f->type, + 'len' => $f->max_length, + 'flags' => '', + ); + if ($res[$i]['table']) { + $res[$i]['flags'] = $this->_sybase_field_flags( + $res[$i]['table'], $res[$i]['name']); + } + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + // free the result only if we were called on a table + if ($got_string) { + @sybase_free_result($id); + } + return $res; + } + + // }}} + // {{{ _sybase_field_flags() + + /** + * Get the flags for a field + * + * Currently supports: + * + <samp>unique_key</samp> (unique index, unique check or primary_key) + * + <samp>multiple_key</samp> (multi-key index) + * + * @param string $table the table name + * @param string $column the field name + * + * @return string space delimited string of flags. Empty string if none. + * + * @access private + */ + function _sybase_field_flags($table, $column) + { + static $tableName = null; + static $flags = array(); + + if ($table != $tableName) { + $flags = array(); + $tableName = $table; + + /* We're running sp_helpindex directly because it doesn't exist in + * older versions of ASE -- unfortunately, we can't just use + * DB::isError() because the user may be using callback error + * handling. */ + $res = @sybase_query("sp_helpindex $table", $this->connection); + + if ($res === false || $res === true) { + // Fake a valid response for BC reasons. + return ''; + } + + while (($val = sybase_fetch_assoc($res)) !== false) { + if (!isset($val['index_keys'])) { + /* No useful information returned. Break and be done with + * it, which preserves the pre-1.7.9 behaviour. */ + break; + } + + $keys = explode(', ', trim($val['index_keys'])); + + if (sizeof($keys) > 1) { + foreach ($keys as $key) { + $this->_add_flag($flags[$key], 'multiple_key'); + } + } + + if (strpos($val['index_description'], 'unique')) { + foreach ($keys as $key) { + $this->_add_flag($flags[$key], 'unique_key'); + } + } + } + + sybase_free_result($res); + + } + + if (array_key_exists($column, $flags)) { + return(implode(' ', $flags[$column])); + } + + return ''; + } + + // }}} + // {{{ _add_flag() + + /** + * Adds a string to the flags array if the flag is not yet in there + * - if there is no flag present the array is created + * + * @param array $array reference of flags array to add a value to + * @param mixed $value value to add to the flag array + * + * @return void + * + * @access private + */ + function _add_flag(&$array, $value) + { + if (!is_array($array)) { + $array = array($value); + } elseif (!in_array($value, $array)) { + array_push($array, $value); + } + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Obtains the query string needed for listing a given type of objects + * + * @param string $type the kind of objects you want to retrieve + * + * @return string the SQL query string or null if the driver doesn't + * support the object type requested + * + * @access protected + * @see DB_common::getListOf() + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return "SELECT name FROM sysobjects WHERE type = 'U'" + . ' ORDER BY name'; + case 'views': + return "SELECT name FROM sysobjects WHERE type = 'V'"; + default: + return null; + } + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/extlib/Mail/RFC822.php b/extlib/Mail/RFC822.php new file mode 100644 index 000000000..8714df2e2 --- /dev/null +++ b/extlib/Mail/RFC822.php @@ -0,0 +1,940 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2001-2002, Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Authors: Richard Heyes <richard@phpguru.org> | +// | Chuck Hagenbuch <chuck@horde.org> | +// +-----------------------------------------------------------------------+ + +/** + * RFC 822 Email address list validation Utility + * + * What is it? + * + * This class will take an address string, and parse it into it's consituent + * parts, be that either addresses, groups, or combinations. Nested groups + * are not supported. The structure it returns is pretty straight forward, + * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use + * print_r() to view the structure. + * + * How do I use it? + * + * $address_string = 'My Group: "Richard" <richard@localhost> (A comment), ted@example.com (Ted Bloggs), Barney;'; + * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true) + * print_r($structure); + * + * @author Richard Heyes <richard@phpguru.org> + * @author Chuck Hagenbuch <chuck@horde.org> + * @version $Revision: 1.24 $ + * @license BSD + * @package Mail + */ +class Mail_RFC822 { + + /** + * The address being parsed by the RFC822 object. + * @var string $address + */ + var $address = ''; + + /** + * The default domain to use for unqualified addresses. + * @var string $default_domain + */ + var $default_domain = 'localhost'; + + /** + * Should we return a nested array showing groups, or flatten everything? + * @var boolean $nestGroups + */ + var $nestGroups = true; + + /** + * Whether or not to validate atoms for non-ascii characters. + * @var boolean $validate + */ + var $validate = true; + + /** + * The array of raw addresses built up as we parse. + * @var array $addresses + */ + var $addresses = array(); + + /** + * The final array of parsed address information that we build up. + * @var array $structure + */ + var $structure = array(); + + /** + * The current error message, if any. + * @var string $error + */ + var $error = null; + + /** + * An internal counter/pointer. + * @var integer $index + */ + var $index = null; + + /** + * The number of groups that have been found in the address list. + * @var integer $num_groups + * @access public + */ + var $num_groups = 0; + + /** + * A variable so that we can tell whether or not we're inside a + * Mail_RFC822 object. + * @var boolean $mailRFC822 + */ + var $mailRFC822 = true; + + /** + * A limit after which processing stops + * @var int $limit + */ + var $limit = null; + + /** + * Sets up the object. The address must either be set here or when + * calling parseAddressList(). One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return object Mail_RFC822 A new Mail_RFC822 object. + */ + function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + } + + /** + * Starts the whole process. The address must either be set here + * or when creating the object. One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return array A structured array of addresses. + */ + function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (!isset($this) || !isset($this->mailRFC822)) { + $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit); + return $obj->parseAddressList(); + } + + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + + $this->structure = array(); + $this->addresses = array(); + $this->error = null; + $this->index = null; + + // Unfold any long lines in $this->address. + $this->address = preg_replace('/\r?\n/', "\r\n", $this->address); + $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address); + + while ($this->address = $this->_splitAddresses($this->address)); + + if ($this->address === false || isset($this->error)) { + require_once 'PEAR.php'; + return PEAR::raiseError($this->error); + } + + // Validate each address individually. If we encounter an invalid + // address, stop iterating and return an error immediately. + foreach ($this->addresses as $address) { + $valid = $this->_validateAddress($address); + + if ($valid === false || isset($this->error)) { + require_once 'PEAR.php'; + return PEAR::raiseError($this->error); + } + + if (!$this->nestGroups) { + $this->structure = array_merge($this->structure, $valid); + } else { + $this->structure[] = $valid; + } + } + + return $this->structure; + } + + /** + * Splits an address into separate addresses. + * + * @access private + * @param string $address The addresses to split. + * @return boolean Success or failure. + */ + function _splitAddresses($address) + { + if (!empty($this->limit) && count($this->addresses) == $this->limit) { + return ''; + } + + if ($this->_isGroup($address) && !isset($this->error)) { + $split_char = ';'; + $is_group = true; + } elseif (!isset($this->error)) { + $split_char = ','; + $is_group = false; + } elseif (isset($this->error)) { + return false; + } + + // Split the string based on the above ten or so lines. + $parts = explode($split_char, $address); + $string = $this->_splitCheck($parts, $split_char); + + // If a group... + if ($is_group) { + // If $string does not contain a colon outside of + // brackets/quotes etc then something's fubar. + + // First check there's a colon at all: + if (strpos($string, ':') === false) { + $this->error = 'Invalid address: ' . $string; + return false; + } + + // Now check it's outside of brackets/quotes: + if (!$this->_splitCheck(explode(':', $string), ':')) { + return false; + } + + // We must have a group at this point, so increase the counter: + $this->num_groups++; + } + + // $string now contains the first full address/group. + // Add to the addresses array. + $this->addresses[] = array( + 'address' => trim($string), + 'group' => $is_group + ); + + // Remove the now stored address from the initial line, the +1 + // is to account for the explode character. + $address = trim(substr($address, strlen($string) + 1)); + + // If the next char is a comma and this was a group, then + // there are more addresses, otherwise, if there are any more + // chars, then there is another address. + if ($is_group && substr($address, 0, 1) == ','){ + $address = trim(substr($address, 1)); + return $address; + + } elseif (strlen($address) > 0) { + return $address; + + } else { + return ''; + } + + // If you got here then something's off + return false; + } + + /** + * Checks for a group at the start of the string. + * + * @access private + * @param string $address The address to check. + * @return boolean Whether or not there is a group at the start of the string. + */ + function _isGroup($address) + { + // First comma not in quotes, angles or escaped: + $parts = explode(',', $address); + $string = $this->_splitCheck($parts, ','); + + // Now we have the first address, we can reliably check for a + // group by searching for a colon that's not escaped or in + // quotes or angle brackets. + if (count($parts = explode(':', $string)) > 1) { + $string2 = $this->_splitCheck($parts, ':'); + return ($string2 !== $string); + } else { + return false; + } + } + + /** + * A common function that will check an exploded string. + * + * @access private + * @param array $parts The exloded string. + * @param string $char The char that was exploded on. + * @return mixed False if the string contains unclosed quotes/brackets, or the string on success. + */ + function _splitCheck($parts, $char) + { + $string = $parts[0]; + + for ($i = 0; $i < count($parts); $i++) { + if ($this->_hasUnclosedQuotes($string) + || $this->_hasUnclosedBrackets($string, '<>') + || $this->_hasUnclosedBrackets($string, '[]') + || $this->_hasUnclosedBrackets($string, '()') + || substr($string, -1) == '\\') { + if (isset($parts[$i + 1])) { + $string = $string . $char . $parts[$i + 1]; + } else { + $this->error = 'Invalid address spec. Unclosed bracket or quotes'; + return false; + } + } else { + $this->index = $i; + break; + } + } + + return $string; + } + + /** + * Checks if a string has unclosed quotes or not. + * + * @access private + * @param string $string The string to check. + * @return boolean True if there are unclosed quotes inside the string, + * false otherwise. + */ + function _hasUnclosedQuotes($string) + { + $string = trim($string); + $iMax = strlen($string); + $in_quote = false; + $i = $slashes = 0; + + for (; $i < $iMax; ++$i) { + switch ($string[$i]) { + case '\\': + ++$slashes; + break; + + case '"': + if ($slashes % 2 == 0) { + $in_quote = !$in_quote; + } + // Fall through to default action below. + + default: + $slashes = 0; + break; + } + } + + return $in_quote; + } + + /** + * Checks if a string has an unclosed brackets or not. IMPORTANT: + * This function handles both angle brackets and square brackets; + * + * @access private + * @param string $string The string to check. + * @param string $chars The characters to check for. + * @return boolean True if there are unclosed brackets inside the string, false otherwise. + */ + function _hasUnclosedBrackets($string, $chars) + { + $num_angle_start = substr_count($string, $chars[0]); + $num_angle_end = substr_count($string, $chars[1]); + + $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]); + $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]); + + if ($num_angle_start < $num_angle_end) { + $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')'; + return false; + } else { + return ($num_angle_start > $num_angle_end); + } + } + + /** + * Sub function that is used only by hasUnclosedBrackets(). + * + * @access private + * @param string $string The string to check. + * @param integer &$num The number of occurences. + * @param string $char The character to count. + * @return integer The number of occurences of $char in $string, adjusted for backslashes. + */ + function _hasUnclosedBracketsSub($string, &$num, $char) + { + $parts = explode($char, $string); + for ($i = 0; $i < count($parts); $i++){ + if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) + $num--; + if (isset($parts[$i + 1])) + $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1]; + } + + return $num; + } + + /** + * Function to begin checking the address. + * + * @access private + * @param string $address The address to validate. + * @return mixed False on failure, or a structured array of address information on success. + */ + function _validateAddress($address) + { + $is_group = false; + $addresses = array(); + + if ($address['group']) { + $is_group = true; + + // Get the group part of the name + $parts = explode(':', $address['address']); + $groupname = $this->_splitCheck($parts, ':'); + $structure = array(); + + // And validate the group part of the name. + if (!$this->_validatePhrase($groupname)){ + $this->error = 'Group name did not validate.'; + return false; + } else { + // Don't include groups if we are not nesting + // them. This avoids returning invalid addresses. + if ($this->nestGroups) { + $structure = new stdClass; + $structure->groupname = $groupname; + } + } + + $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':'))); + } + + // If a group then split on comma and put into an array. + // Otherwise, Just put the whole address in an array. + if ($is_group) { + while (strlen($address['address']) > 0) { + $parts = explode(',', $address['address']); + $addresses[] = $this->_splitCheck($parts, ','); + $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ','))); + } + } else { + $addresses[] = $address['address']; + } + + // Check that $addresses is set, if address like this: + // Groupname:; + // Then errors were appearing. + if (!count($addresses)){ + $this->error = 'Empty group.'; + return false; + } + + // Trim the whitespace from all of the address strings. + array_map('trim', $addresses); + + // Validate each mailbox. + // Format could be one of: name <geezer@domain.com> + // geezer@domain.com + // geezer + // ... or any other format valid by RFC 822. + for ($i = 0; $i < count($addresses); $i++) { + if (!$this->validateMailbox($addresses[$i])) { + if (empty($this->error)) { + $this->error = 'Validation failed for: ' . $addresses[$i]; + } + return false; + } + } + + // Nested format + if ($this->nestGroups) { + if ($is_group) { + $structure->addresses = $addresses; + } else { + $structure = $addresses[0]; + } + + // Flat format + } else { + if ($is_group) { + $structure = array_merge($structure, $addresses); + } else { + $structure = $addresses; + } + } + + return $structure; + } + + /** + * Function to validate a phrase. + * + * @access private + * @param string $phrase The phrase to check. + * @return boolean Success or failure. + */ + function _validatePhrase($phrase) + { + // Splits on one or more Tab or space. + $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY); + + $phrase_parts = array(); + while (count($parts) > 0){ + $phrase_parts[] = $this->_splitCheck($parts, ' '); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($parts); + } + + foreach ($phrase_parts as $part) { + // If quoted string: + if (substr($part, 0, 1) == '"') { + if (!$this->_validateQuotedString($part)) { + return false; + } + continue; + } + + // Otherwise it's an atom: + if (!$this->_validateAtom($part)) return false; + } + + return true; + } + + /** + * Function to validate an atom which from rfc822 is: + * atom = 1*<any CHAR except specials, SPACE and CTLs> + * + * If validation ($this->validate) has been turned off, then + * validateAtom() doesn't actually check anything. This is so that you + * can split a list of addresses up before encoding personal names + * (umlauts, etc.), for example. + * + * @access private + * @param string $atom The string to check. + * @return boolean Success or failure. + */ + function _validateAtom($atom) + { + if (!$this->validate) { + // Validation has been turned off; assume the atom is okay. + return true; + } + + // Check for any char from ASCII 0 - ASCII 127 + if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) { + return false; + } + + // Check for specials: + if (preg_match('/[][()<>@,;\\:". ]/', $atom)) { + return false; + } + + // Check for control characters (ASCII 0-31): + if (preg_match('/[\\x00-\\x1F]+/', $atom)) { + return false; + } + + return true; + } + + /** + * Function to validate quoted string, which is: + * quoted-string = <"> *(qtext/quoted-pair) <"> + * + * @access private + * @param string $qstring The string to check + * @return boolean Success or failure. + */ + function _validateQuotedString($qstring) + { + // Leading and trailing " + $qstring = substr($qstring, 1, -1); + + // Perform check, removing quoted characters first. + return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring)); + } + + /** + * Function to validate a mailbox, which is: + * mailbox = addr-spec ; simple address + * / phrase route-addr ; name and route-addr + * + * @access public + * @param string &$mailbox The string to check. + * @return boolean Success or failure. + */ + function validateMailbox(&$mailbox) + { + // A couple of defaults. + $phrase = ''; + $comment = ''; + $comments = array(); + + // Catch any RFC822 comments and store them separately. + $_mailbox = $mailbox; + while (strlen(trim($_mailbox)) > 0) { + $parts = explode('(', $_mailbox); + $before_comment = $this->_splitCheck($parts, '('); + if ($before_comment != $_mailbox) { + // First char should be a (. + $comment = substr(str_replace($before_comment, '', $_mailbox), 1); + $parts = explode(')', $comment); + $comment = $this->_splitCheck($parts, ')'); + $comments[] = $comment; + + // +1 is for the trailing ) + $_mailbox = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1); + } else { + break; + } + } + + foreach ($comments as $comment) { + $mailbox = str_replace("($comment)", '', $mailbox); + } + + $mailbox = trim($mailbox); + + // Check for name + route-addr + if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') { + $parts = explode('<', $mailbox); + $name = $this->_splitCheck($parts, '<'); + + $phrase = trim($name); + $route_addr = trim(substr($mailbox, strlen($name.'<'), -1)); + + if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) { + return false; + } + + // Only got addr-spec + } else { + // First snip angle brackets if present. + if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') { + $addr_spec = substr($mailbox, 1, -1); + } else { + $addr_spec = $mailbox; + } + + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + // Construct the object that will be returned. + $mbox = new stdClass(); + + // Add the phrase (even if empty) and comments + $mbox->personal = $phrase; + $mbox->comment = isset($comments) ? $comments : array(); + + if (isset($route_addr)) { + $mbox->mailbox = $route_addr['local_part']; + $mbox->host = $route_addr['domain']; + $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : ''; + } else { + $mbox->mailbox = $addr_spec['local_part']; + $mbox->host = $addr_spec['domain']; + } + + $mailbox = $mbox; + return true; + } + + /** + * This function validates a route-addr which is: + * route-addr = "<" [route] addr-spec ">" + * + * Angle brackets have already been removed at the point of + * getting to this function. + * + * @access private + * @param string $route_addr The string to check. + * @return mixed False on failure, or an array containing validated address/route information on success. + */ + function _validateRouteAddr($route_addr) + { + // Check for colon. + if (strpos($route_addr, ':') !== false) { + $parts = explode(':', $route_addr); + $route = $this->_splitCheck($parts, ':'); + } else { + $route = $route_addr; + } + + // If $route is same as $route_addr then the colon was in + // quotes or brackets or, of course, non existent. + if ($route === $route_addr){ + unset($route); + $addr_spec = $route_addr; + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } else { + // Validate route part. + if (($route = $this->_validateRoute($route)) === false) { + return false; + } + + $addr_spec = substr($route_addr, strlen($route . ':')); + + // Validate addr-spec part. + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + if (isset($route)) { + $return['adl'] = $route; + } else { + $return['adl'] = ''; + } + + $return = array_merge($return, $addr_spec); + return $return; + } + + /** + * Function to validate a route, which is: + * route = 1#("@" domain) ":" + * + * @access private + * @param string $route The string to check. + * @return mixed False on failure, or the validated $route on success. + */ + function _validateRoute($route) + { + // Split on comma. + $domains = explode(',', trim($route)); + + foreach ($domains as $domain) { + $domain = str_replace('@', '', trim($domain)); + if (!$this->_validateDomain($domain)) return false; + } + + return $route; + } + + /** + * Function to validate a domain, though this is not quite what + * you expect of a strict internet domain. + * + * domain = sub-domain *("." sub-domain) + * + * @access private + * @param string $domain The string to check. + * @return mixed False on failure, or the validated domain on success. + */ + function _validateDomain($domain) + { + // Note the different use of $subdomains and $sub_domains + $subdomains = explode('.', $domain); + + while (count($subdomains) > 0) { + $sub_domains[] = $this->_splitCheck($subdomains, '.'); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($subdomains); + } + + foreach ($sub_domains as $sub_domain) { + if (!$this->_validateSubdomain(trim($sub_domain))) + return false; + } + + // Managed to get here, so return input. + return $domain; + } + + /** + * Function to validate a subdomain: + * subdomain = domain-ref / domain-literal + * + * @access private + * @param string $subdomain The string to check. + * @return boolean Success or failure. + */ + function _validateSubdomain($subdomain) + { + if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){ + if (!$this->_validateDliteral($arr[1])) return false; + } else { + if (!$this->_validateAtom($subdomain)) return false; + } + + // Got here, so return successful. + return true; + } + + /** + * Function to validate a domain literal: + * domain-literal = "[" *(dtext / quoted-pair) "]" + * + * @access private + * @param string $dliteral The string to check. + * @return boolean Success or failure. + */ + function _validateDliteral($dliteral) + { + return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\'; + } + + /** + * Function to validate an addr-spec. + * + * addr-spec = local-part "@" domain + * + * @access private + * @param string $addr_spec The string to check. + * @return mixed False on failure, or the validated addr-spec on success. + */ + function _validateAddrSpec($addr_spec) + { + $addr_spec = trim($addr_spec); + + // Split on @ sign if there is one. + if (strpos($addr_spec, '@') !== false) { + $parts = explode('@', $addr_spec); + $local_part = $this->_splitCheck($parts, '@'); + $domain = substr($addr_spec, strlen($local_part . '@')); + + // No @ sign so assume the default domain. + } else { + $local_part = $addr_spec; + $domain = $this->default_domain; + } + + if (($local_part = $this->_validateLocalPart($local_part)) === false) return false; + if (($domain = $this->_validateDomain($domain)) === false) return false; + + // Got here so return successful. + return array('local_part' => $local_part, 'domain' => $domain); + } + + /** + * Function to validate the local part of an address: + * local-part = word *("." word) + * + * @access private + * @param string $local_part + * @return mixed False on failure, or the validated local part on success. + */ + function _validateLocalPart($local_part) + { + $parts = explode('.', $local_part); + $words = array(); + + // Split the local_part into words. + while (count($parts) > 0){ + $words[] = $this->_splitCheck($parts, '.'); + for ($i = 0; $i < $this->index + 1; $i++) { + array_shift($parts); + } + } + + // Validate each word. + foreach ($words as $word) { + // If this word contains an unquoted space, it is invalid. (6.2.4) + if (strpos($word, ' ') && $word[0] !== '"') + { + return false; + } + + if ($this->_validatePhrase(trim($word)) === false) return false; + } + + // Managed to get here, so return the input. + return $local_part; + } + + /** + * Returns an approximate count of how many addresses are in the + * given string. This is APPROXIMATE as it only splits based on a + * comma which has no preceding backslash. Could be useful as + * large amounts of addresses will end up producing *large* + * structures when used with parseAddressList(). + * + * @param string $data Addresses to count + * @return int Approximate count + */ + function approximateCount($data) + { + return count(preg_split('/(?<!\\\\),/', $data)); + } + + /** + * This is a email validating function separate to the rest of the + * class. It simply validates whether an email is of the common + * internet form: <user>@<domain>. This can be sufficient for most + * people. Optional stricter mode can be utilised which restricts + * mailbox characters allowed to alphanumeric, full stop, hyphen + * and underscore. + * + * @param string $data Address to check + * @param boolean $strict Optional stricter mode + * @return mixed False if it fails, an indexed array + * username/domain if it matches + */ + function isValidInetAddress($data, $strict = false) + { + $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'; + if (preg_match($regex, trim($data), $matches)) { + return array($matches[1], $matches[2]); + } else { + return false; + } + } + +} diff --git a/extlib/Mail/mail.php b/extlib/Mail/mail.php new file mode 100644 index 000000000..b13d69565 --- /dev/null +++ b/extlib/Mail/mail.php @@ -0,0 +1,143 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Chuck Hagenbuch <chuck@horde.org> | +// +----------------------------------------------------------------------+ +// +// $Id: mail.php,v 1.20 2007/10/06 17:00:00 chagenbu Exp $ + +/** + * internal PHP-mail() implementation of the PEAR Mail:: interface. + * @package Mail + * @version $Revision: 1.20 $ + */ +class Mail_mail extends Mail { + + /** + * Any arguments to pass to the mail() function. + * @var string + */ + var $_params = ''; + + /** + * Constructor. + * + * Instantiates a new Mail_mail:: object based on the parameters + * passed in. + * + * @param array $params Extra arguments for the mail() function. + */ + function Mail_mail($params = null) + { + // The other mail implementations accept parameters as arrays. + // In the interest of being consistent, explode an array into + // a string of parameter arguments. + if (is_array($params)) { + $this->_params = join(' ', $params); + } else { + $this->_params = $params; + } + + /* Because the mail() function may pass headers as command + * line arguments, we can't guarantee the use of the standard + * "\r\n" separator. Instead, we use the system's native line + * separator. */ + if (defined('PHP_EOL')) { + $this->sep = PHP_EOL; + } else { + $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; + } + } + + /** + * Implements Mail_mail::send() function using php's built-in mail() + * command. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * + * @access public + */ + function send($recipients, $headers, $body) + { + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + // If we're passed an array of recipients, implode it. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // Get the Subject out of the headers array so that we can + // pass it as a seperate argument to mail(). + $subject = ''; + if (isset($headers['Subject'])) { + $subject = $headers['Subject']; + unset($headers['Subject']); + } + + // Also remove the To: header. The mail() function will add its own + // To: header based on the contents of $recipients. + unset($headers['To']); + + // Flatten the headers out. + $headerElements = $this->prepareHeaders($headers); + if (is_a($headerElements, 'PEAR_Error')) { + return $headerElements; + } + list(, $text_headers) = $headerElements; + + // We only use mail()'s optional fifth parameter if the additional + // parameters have been provided and we're not running in safe mode. + if (empty($this->_params) || ini_get('safe_mode')) { + $result = mail($recipients, $subject, $body, $text_headers); + } else { + $result = mail($recipients, $subject, $body, $text_headers, + $this->_params); + } + + // If the mail() function returned failure, we need to create a + // PEAR_Error object and return it instead of the boolean result. + if ($result === false) { + $result = PEAR::raiseError('mail() returned failure'); + } + + return $result; + } + +} diff --git a/extlib/Mail/mock.php b/extlib/Mail/mock.php new file mode 100644 index 000000000..971dae6a0 --- /dev/null +++ b/extlib/Mail/mock.php @@ -0,0 +1,119 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Chuck Hagenbuch <chuck@horde.org> | +// +----------------------------------------------------------------------+ +// +// $Id: mock.php,v 1.1 2007/12/08 17:57:54 chagenbu Exp $ +// + +/** + * Mock implementation of the PEAR Mail:: interface for testing. + * @access public + * @package Mail + * @version $Revision: 1.1 $ + */ +class Mail_mock extends Mail { + + /** + * Array of messages that have been sent with the mock. + * + * @var array + * @access public + */ + var $sentMessages = array(); + + /** + * Callback before sending mail. + * + * @var callback + */ + var $_preSendCallback; + + /** + * Callback after sending mai. + * + * @var callback + */ + var $_postSendCallback; + + /** + * Constructor. + * + * Instantiates a new Mail_mock:: object based on the parameters + * passed in. It looks for the following parameters, both optional: + * preSendCallback Called before an email would be sent. + * postSendCallback Called after an email would have been sent. + * + * @param array Hash containing any parameters. + * @access public + */ + function Mail_mock($params) + { + if (isset($params['preSendCallback']) && + is_callable($params['preSendCallback'])) { + $this->_preSendCallback = $params['preSendCallback']; + } + + if (isset($params['postSendCallback']) && + is_callable($params['postSendCallback'])) { + $this->_postSendCallback = $params['postSendCallback']; + } + } + + /** + * Implements Mail_mock::send() function. Silently discards all + * mail. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + if ($this->_preSendCallback) { + call_user_func_array($this->_preSendCallback, + array(&$this, $recipients, $headers, $body)); + } + + $entry = array('recipients' => $recipients, 'headers' => $headers, 'body' => $body); + $this->sentMessages[] = $entry; + + if ($this->_postSendCallback) { + call_user_func_array($this->_postSendCallback, + array(&$this, $recipients, $headers, $body)); + } + + return true; + } + +} diff --git a/extlib/Mail/null.php b/extlib/Mail/null.php new file mode 100644 index 000000000..982bfa45b --- /dev/null +++ b/extlib/Mail/null.php @@ -0,0 +1,60 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Phil Kernick <philk@rotfl.com.au> | +// +----------------------------------------------------------------------+ +// +// $Id: null.php,v 1.2 2004/04/06 05:19:03 jon Exp $ +// + +/** + * Null implementation of the PEAR Mail:: interface. + * @access public + * @package Mail + * @version $Revision: 1.2 $ + */ +class Mail_null extends Mail { + + /** + * Implements Mail_null::send() function. Silently discards all + * mail. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + return true; + } + +} diff --git a/extlib/Mail/sendmail.php b/extlib/Mail/sendmail.php new file mode 100644 index 000000000..cd248e61d --- /dev/null +++ b/extlib/Mail/sendmail.php @@ -0,0 +1,170 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Chuck Hagenbuch <chuck@horde.org> | +// +----------------------------------------------------------------------+ + +/** + * Sendmail implementation of the PEAR Mail:: interface. + * @access public + * @package Mail + * @version $Revision: 1.19 $ + */ +class Mail_sendmail extends Mail { + + /** + * The location of the sendmail or sendmail wrapper binary on the + * filesystem. + * @var string + */ + var $sendmail_path = '/usr/sbin/sendmail'; + + /** + * Any extra command-line parameters to pass to the sendmail or + * sendmail wrapper binary. + * @var string + */ + var $sendmail_args = '-i'; + + /** + * Constructor. + * + * Instantiates a new Mail_sendmail:: object based on the parameters + * passed in. It looks for the following parameters: + * sendmail_path The location of the sendmail binary on the + * filesystem. Defaults to '/usr/sbin/sendmail'. + * + * sendmail_args Any extra parameters to pass to the sendmail + * or sendmail wrapper binary. + * + * If a parameter is present in the $params array, it replaces the + * default. + * + * @param array $params Hash containing any parameters different from the + * defaults. + * @access public + */ + function Mail_sendmail($params) + { + if (isset($params['sendmail_path'])) { + $this->sendmail_path = $params['sendmail_path']; + } + if (isset($params['sendmail_args'])) { + $this->sendmail_args = $params['sendmail_args']; + } + + /* + * Because we need to pass message headers to the sendmail program on + * the commandline, we can't guarantee the use of the standard "\r\n" + * separator. Instead, we use the system's native line separator. + */ + if (defined('PHP_EOL')) { + $this->sep = PHP_EOL; + } else { + $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; + } + } + + /** + * Implements Mail::send() function using the sendmail + * command-line binary. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + $recipients = $this->parseRecipients($recipients); + if (is_a($recipients, 'PEAR_Error')) { + return $recipients; + } + $recipients = escapeShellCmd(implode(' ', $recipients)); + + $headerElements = $this->prepareHeaders($headers); + if (is_a($headerElements, 'PEAR_Error')) { + return $headerElements; + } + list($from, $text_headers) = $headerElements; + + /* Since few MTAs are going to allow this header to be forged + * unless it's in the MAIL FROM: exchange, we'll use + * Return-Path instead of From: if it's set. */ + if (!empty($headers['Return-Path'])) { + $from = $headers['Return-Path']; + } + + if (!isset($from)) { + return PEAR::raiseError('No from address given.'); + } elseif (strpos($from, ' ') !== false || + strpos($from, ';') !== false || + strpos($from, '&') !== false || + strpos($from, '`') !== false) { + return PEAR::raiseError('From address specified with dangerous characters.'); + } + + $from = escapeShellCmd($from); + $mail = @popen($this->sendmail_path . (!empty($this->sendmail_args) ? ' ' . $this->sendmail_args : '') . " -f$from -- $recipients", 'w'); + if (!$mail) { + return PEAR::raiseError('Failed to open sendmail [' . $this->sendmail_path . '] for execution.'); + } + + // Write the headers following by two newlines: one to end the headers + // section and a second to separate the headers block from the body. + fputs($mail, $text_headers . $this->sep . $this->sep); + + fputs($mail, $body); + $result = pclose($mail); + if (version_compare(phpversion(), '4.2.3') == -1) { + // With older php versions, we need to shift the pclose + // result to get the exit code. + $result = $result >> 8 & 0xFF; + } + + if ($result != 0) { + return PEAR::raiseError('sendmail returned error code ' . $result, + $result); + } + + return true; + } + +} diff --git a/extlib/Mail/smtp.php b/extlib/Mail/smtp.php new file mode 100644 index 000000000..baf3a962b --- /dev/null +++ b/extlib/Mail/smtp.php @@ -0,0 +1,407 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Authors: Chuck Hagenbuch <chuck@horde.org> | +// | Jon Parise <jon@php.net> | +// +----------------------------------------------------------------------+ + +/** Error: Failed to create a Net_SMTP object */ +define('PEAR_MAIL_SMTP_ERROR_CREATE', 10000); + +/** Error: Failed to connect to SMTP server */ +define('PEAR_MAIL_SMTP_ERROR_CONNECT', 10001); + +/** Error: SMTP authentication failure */ +define('PEAR_MAIL_SMTP_ERROR_AUTH', 10002); + +/** Error: No From: address has been provided */ +define('PEAR_MAIL_SMTP_ERROR_FROM', 10003); + +/** Error: Failed to set sender */ +define('PEAR_MAIL_SMTP_ERROR_SENDER', 10004); + +/** Error: Failed to add recipient */ +define('PEAR_MAIL_SMTP_ERROR_RECIPIENT', 10005); + +/** Error: Failed to send data */ +define('PEAR_MAIL_SMTP_ERROR_DATA', 10006); + +/** + * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class. + * @access public + * @package Mail + * @version $Revision: 1.33 $ + */ +class Mail_smtp extends Mail { + + /** + * SMTP connection object. + * + * @var object + * @access private + */ + var $_smtp = null; + + /** + * The list of service extension parameters to pass to the Net_SMTP + * mailFrom() command. + * @var array + */ + var $_extparams = array(); + + /** + * The SMTP host to connect to. + * @var string + */ + var $host = 'localhost'; + + /** + * The port the SMTP server is on. + * @var integer + */ + var $port = 25; + + /** + * Should SMTP authentication be used? + * + * This value may be set to true, false or the name of a specific + * authentication method. + * + * If the value is set to true, the Net_SMTP package will attempt to use + * the best authentication method advertised by the remote SMTP server. + * + * @var mixed + */ + var $auth = false; + + /** + * The username to use if the SMTP server requires authentication. + * @var string + */ + var $username = ''; + + /** + * The password to use if the SMTP server requires authentication. + * @var string + */ + var $password = ''; + + /** + * Hostname or domain that will be sent to the remote SMTP server in the + * HELO / EHLO message. + * + * @var string + */ + var $localhost = 'localhost'; + + /** + * SMTP connection timeout value. NULL indicates no timeout. + * + * @var integer + */ + var $timeout = null; + + /** + * Turn on Net_SMTP debugging? + * + * @var boolean $debug + */ + var $debug = false; + + /** + * Indicates whether or not the SMTP connection should persist over + * multiple calls to the send() method. + * + * @var boolean + */ + var $persist = false; + + /** + * Use SMTP command pipelining (specified in RFC 2920) if the SMTP server + * supports it. This speeds up delivery over high-latency connections. By + * default, use the default value supplied by Net_SMTP. + * @var bool + */ + var $pipelining; + + /** + * Constructor. + * + * Instantiates a new Mail_smtp:: object based on the parameters + * passed in. It looks for the following parameters: + * host The server to connect to. Defaults to localhost. + * port The port to connect to. Defaults to 25. + * auth SMTP authentication. Defaults to none. + * username The username to use for SMTP auth. No default. + * password The password to use for SMTP auth. No default. + * localhost The local hostname / domain. Defaults to localhost. + * timeout The SMTP connection timeout. Defaults to none. + * verp Whether to use VERP or not. Defaults to false. + * DEPRECATED as of 1.2.0 (use setMailParams()). + * debug Activate SMTP debug mode? Defaults to false. + * persist Should the SMTP connection persist? + * pipelining Use SMTP command pipelining + * + * If a parameter is present in the $params array, it replaces the + * default. + * + * @param array Hash containing any parameters different from the + * defaults. + * @access public + */ + function Mail_smtp($params) + { + if (isset($params['host'])) $this->host = $params['host']; + if (isset($params['port'])) $this->port = $params['port']; + if (isset($params['auth'])) $this->auth = $params['auth']; + if (isset($params['username'])) $this->username = $params['username']; + if (isset($params['password'])) $this->password = $params['password']; + if (isset($params['localhost'])) $this->localhost = $params['localhost']; + if (isset($params['timeout'])) $this->timeout = $params['timeout']; + if (isset($params['debug'])) $this->debug = (bool)$params['debug']; + if (isset($params['persist'])) $this->persist = (bool)$params['persist']; + if (isset($params['pipelining'])) $this->pipelining = (bool)$params['pipelining']; + + // Deprecated options + if (isset($params['verp'])) { + $this->addServiceExtensionParameter('XVERP', is_bool($params['verp']) ? null : $params['verp']); + } + + register_shutdown_function(array(&$this, '_Mail_smtp')); + } + + /** + * Destructor implementation to ensure that we disconnect from any + * potentially-alive persistent SMTP connections. + */ + function _Mail_smtp() + { + $this->disconnect(); + } + + /** + * Implements Mail::send() function using SMTP. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (e.g., 'Subject'), and the array value + * is the header value (e.g., 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * MIME parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + /* If we don't already have an SMTP object, create one. */ + $result = &$this->getSMTPObject(); + if (PEAR::isError($result)) { + return $result; + } + + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + + $this->_sanitizeHeaders($headers); + + $headerElements = $this->prepareHeaders($headers); + if (is_a($headerElements, 'PEAR_Error')) { + $this->_smtp->rset(); + return $headerElements; + } + list($from, $textHeaders) = $headerElements; + + /* Since few MTAs are going to allow this header to be forged + * unless it's in the MAIL FROM: exchange, we'll use + * Return-Path instead of From: if it's set. */ + if (!empty($headers['Return-Path'])) { + $from = $headers['Return-Path']; + } + + if (!isset($from)) { + $this->_smtp->rset(); + return PEAR::raiseError('No From: address has been provided', + PEAR_MAIL_SMTP_ERROR_FROM); + } + + $params = null; + if (!empty($this->_extparams)) { + foreach ($this->_extparams as $key => $val) { + $params .= ' ' . $key . (is_null($val) ? '' : '=' . $val); + } + } + if (PEAR::isError($res = $this->_smtp->mailFrom($from, ltrim($params)))) { + $error = $this->_error("Failed to set sender: $from", $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_SENDER); + } + + $recipients = $this->parseRecipients($recipients); + if (is_a($recipients, 'PEAR_Error')) { + $this->_smtp->rset(); + return $recipients; + } + + foreach ($recipients as $recipient) { + $res = $this->_smtp->rcptTo($recipient); + if (is_a($res, 'PEAR_Error')) { + $error = $this->_error("Failed to add recipient: $recipient", $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_RECIPIENT); + } + } + + /* Send the message's headers and the body as SMTP data. */ + $res = $this->_smtp->data($textHeaders . "\r\n\r\n" . $body); + if (is_a($res, 'PEAR_Error')) { + $error = $this->_error('Failed to send data', $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_DATA); + } + + /* If persistent connections are disabled, destroy our SMTP object. */ + if ($this->persist === false) { + $this->disconnect(); + } + + return true; + } + + /** + * Connect to the SMTP server by instantiating a Net_SMTP object. + * + * @return mixed Returns a reference to the Net_SMTP object on success, or + * a PEAR_Error containing a descriptive error message on + * failure. + * + * @since 1.2.0 + * @access public + */ + function &getSMTPObject() + { + if (is_object($this->_smtp) !== false) { + return $this->_smtp; + } + + include_once 'Net/SMTP.php'; + $this->_smtp = &new Net_SMTP($this->host, + $this->port, + $this->localhost); + + /* If we still don't have an SMTP object at this point, fail. */ + if (is_object($this->_smtp) === false) { + return PEAR::raiseError('Failed to create a Net_SMTP object', + PEAR_MAIL_SMTP_ERROR_CREATE); + } + + /* Configure the SMTP connection. */ + if ($this->debug) { + $this->_smtp->setDebug(true); + } + + /* Attempt to connect to the configured SMTP server. */ + if (PEAR::isError($res = $this->_smtp->connect($this->timeout))) { + $error = $this->_error('Failed to connect to ' . + $this->host . ':' . $this->port, + $res); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_CONNECT); + } + + /* Attempt to authenticate if authentication has been enabled. */ + if ($this->auth) { + $method = is_string($this->auth) ? $this->auth : ''; + + if (PEAR::isError($res = $this->_smtp->auth($this->username, + $this->password, + $method))) { + $error = $this->_error("$method authentication failure", + $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_AUTH); + } + } + + return $this->_smtp; + } + + /** + * Add parameter associated with a SMTP service extension. + * + * @param string Extension keyword. + * @param string Any value the keyword needs. + * + * @since 1.2.0 + * @access public + */ + function addServiceExtensionParameter($keyword, $value = null) + { + $this->_extparams[$keyword] = $value; + } + + /** + * Disconnect and destroy the current SMTP connection. + * + * @return boolean True if the SMTP connection no longer exists. + * + * @since 1.1.9 + * @access public + */ + function disconnect() + { + /* If we have an SMTP object, disconnect and destroy it. */ + if (is_object($this->_smtp) && $this->_smtp->disconnect()) { + $this->_smtp = null; + } + + /* We are disconnected if we no longer have an SMTP object. */ + return ($this->_smtp === null); + } + + /** + * Build a standardized string describing the current SMTP error. + * + * @param string $text Custom string describing the error context. + * @param object $error Reference to the current PEAR_Error object. + * + * @return string A string describing the current SMTP error. + * + * @since 1.1.7 + * @access private + */ + function _error($text, &$error) + { + /* Split the SMTP response into a code and a response string. */ + list($code, $response) = $this->_smtp->getResponse(); + + /* Build our standardized error string. */ + return $text + . ' [SMTP: ' . $error->getMessage() + . " (code: $code, response: $response)]"; + } + +} diff --git a/extlib/Mail/smtpmx.php b/extlib/Mail/smtpmx.php new file mode 100644 index 000000000..9d2dccfb1 --- /dev/null +++ b/extlib/Mail/smtpmx.php @@ -0,0 +1,478 @@ +<?PHP +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * SMTP MX + * + * SMTP MX implementation of the PEAR Mail interface. Requires the Net_SMTP class. + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Mail + * @package Mail_smtpmx + * @author gERD Schaufelberger <gerd@php-tools.net> + * @copyright 1997-2005 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: smtpmx.php,v 1.2 2007/10/06 17:00:00 chagenbu Exp $ + * @see Mail + */ + +require_once 'Net/SMTP.php'; + +/** + * SMTP MX implementation of the PEAR Mail interface. Requires the Net_SMTP class. + * + * + * @access public + * @author gERD Schaufelberger <gerd@php-tools.net> + * @package Mail + * @version $Revision: 1.2 $ + */ +class Mail_smtpmx extends Mail { + + /** + * SMTP connection object. + * + * @var object + * @access private + */ + var $_smtp = null; + + /** + * The port the SMTP server is on. + * @var integer + * @see getservicebyname() + */ + var $port = 25; + + /** + * Hostname or domain that will be sent to the remote SMTP server in the + * HELO / EHLO message. + * + * @var string + * @see posix_uname() + */ + var $mailname = 'localhost'; + + /** + * SMTP connection timeout value. NULL indicates no timeout. + * + * @var integer + */ + var $timeout = 10; + + /** + * use either PEAR:Net_DNS or getmxrr + * + * @var boolean + */ + var $withNetDns = true; + + /** + * PEAR:Net_DNS_Resolver + * + * @var object + */ + var $resolver; + + /** + * Whether to use VERP or not. If not a boolean, the string value + * will be used as the VERP separators. + * + * @var mixed boolean or string + */ + var $verp = false; + + /** + * Whether to use VRFY or not. + * + * @var boolean $vrfy + */ + var $vrfy = false; + + /** + * Switch to test mode - don't send emails for real + * + * @var boolean $debug + */ + var $test = false; + + /** + * Turn on Net_SMTP debugging? + * + * @var boolean $peardebug + */ + var $debug = false; + + /** + * internal error codes + * + * translate internal error identifier to PEAR-Error codes and human + * readable messages. + * + * @var boolean $debug + * @todo as I need unique error-codes to identify what exactly went wrond + * I did not use intergers as it should be. Instead I added a "namespace" + * for each code. This avoids conflicts with error codes from different + * classes. How can I use unique error codes and stay conform with PEAR? + */ + var $errorCode = array( + 'not_connected' => array( + 'code' => 1, + 'msg' => 'Could not connect to any mail server ({HOST}) at port {PORT} to send mail to {RCPT}.' + ), + 'failed_vrfy_rcpt' => array( + 'code' => 2, + 'msg' => 'Recipient "{RCPT}" could not be veryfied.' + ), + 'failed_set_from' => array( + 'code' => 3, + 'msg' => 'Failed to set sender: {FROM}.' + ), + 'failed_set_rcpt' => array( + 'code' => 4, + 'msg' => 'Failed to set recipient: {RCPT}.' + ), + 'failed_send_data' => array( + 'code' => 5, + 'msg' => 'Failed to send mail to: {RCPT}.' + ), + 'no_from' => array( + 'code' => 5, + 'msg' => 'No from address has be provided.' + ), + 'send_data' => array( + 'code' => 7, + 'msg' => 'Failed to create Net_SMTP object.' + ), + 'no_mx' => array( + 'code' => 8, + 'msg' => 'No MX-record for {RCPT} found.' + ), + 'no_resolver' => array( + 'code' => 9, + 'msg' => 'Could not start resolver! Install PEAR:Net_DNS or switch off "netdns"' + ), + 'failed_rset' => array( + 'code' => 10, + 'msg' => 'RSET command failed, SMTP-connection corrupt.' + ), + ); + + /** + * Constructor. + * + * Instantiates a new Mail_smtp:: object based on the parameters + * passed in. It looks for the following parameters: + * mailname The name of the local mail system (a valid hostname which matches the reverse lookup) + * port smtp-port - the default comes from getservicebyname() and should work fine + * timeout The SMTP connection timeout. Defaults to 30 seconds. + * vrfy Whether to use VRFY or not. Defaults to false. + * verp Whether to use VERP or not. Defaults to false. + * test Activate test mode? Defaults to false. + * debug Activate SMTP and Net_DNS debug mode? Defaults to false. + * netdns whether to use PEAR:Net_DNS or the PHP build in function getmxrr, default is true + * + * If a parameter is present in the $params array, it replaces the + * default. + * + * @access public + * @param array Hash containing any parameters different from the + * defaults. + * @see _Mail_smtpmx() + */ + function __construct($params) + { + if (isset($params['mailname'])) { + $this->mailname = $params['mailname']; + } else { + // try to find a valid mailname + if (function_exists('posix_uname')) { + $uname = posix_uname(); + $this->mailname = $uname['nodename']; + } + } + + // port number + if (isset($params['port'])) { + $this->_port = $params['port']; + } else { + $this->_port = getservbyname('smtp', 'tcp'); + } + + if (isset($params['timeout'])) $this->timeout = $params['timeout']; + if (isset($params['verp'])) $this->verp = $params['verp']; + if (isset($params['test'])) $this->test = $params['test']; + if (isset($params['peardebug'])) $this->test = $params['peardebug']; + if (isset($params['netdns'])) $this->withNetDns = $params['netdns']; + } + + /** + * Constructor wrapper for PHP4 + * + * @access public + * @param array Hash containing any parameters different from the defaults + * @see __construct() + */ + function Mail_smtpmx($params) + { + $this->__construct($params); + register_shutdown_function(array(&$this, '__destruct')); + } + + /** + * Destructor implementation to ensure that we disconnect from any + * potentially-alive persistent SMTP connections. + */ + function __destruct() + { + if (is_object($this->_smtp)) { + $this->_smtp->disconnect(); + $this->_smtp = null; + } + } + + /** + * Implements Mail::send() function using SMTP direct delivery + * + * @access public + * @param mixed $recipients in RFC822 style or array + * @param array $headers The array of headers to send with the mail. + * @param string $body The full text of the message body, + * @return mixed Returns true on success, or a PEAR_Error + */ + function send($recipients, $headers, $body) + { + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + // Prepare headers + $headerElements = $this->prepareHeaders($headers); + if (is_a($headerElements, 'PEAR_Error')) { + return $headerElements; + } + list($from, $textHeaders) = $headerElements; + + // use 'Return-Path' if possible + if (!empty($headers['Return-Path'])) { + $from = $headers['Return-Path']; + } + if (!isset($from)) { + return $this->_raiseError('no_from'); + } + + // Prepare recipients + $recipients = $this->parseRecipients($recipients); + if (is_a($recipients, 'PEAR_Error')) { + return $recipients; + } + + foreach ($recipients as $rcpt) { + list($user, $host) = explode('@', $rcpt); + + $mx = $this->_getMx($host); + if (is_a($mx, 'PEAR_Error')) { + return $mx; + } + + if (empty($mx)) { + $info = array('rcpt' => $rcpt); + return $this->_raiseError('no_mx', $info); + } + + $connected = false; + foreach ($mx as $mserver => $mpriority) { + $this->_smtp = new Net_SMTP($mserver, $this->port, $this->mailname); + + // configure the SMTP connection. + if ($this->debug) { + $this->_smtp->setDebug(true); + } + + // attempt to connect to the configured SMTP server. + $res = $this->_smtp->connect($this->timeout); + if (is_a($res, 'PEAR_Error')) { + $this->_smtp = null; + continue; + } + + // connection established + if ($res) { + $connected = true; + break; + } + } + + if (!$connected) { + $info = array( + 'host' => implode(', ', array_keys($mx)), + 'port' => $this->port, + 'rcpt' => $rcpt, + ); + return $this->_raiseError('not_connected', $info); + } + + // Verify recipient + if ($this->vrfy) { + $res = $this->_smtp->vrfy($rcpt); + if (is_a($res, 'PEAR_Error')) { + $info = array('rcpt' => $rcpt); + return $this->_raiseError('failed_vrfy_rcpt', $info); + } + } + + // mail from: + $args['verp'] = $this->verp; + $res = $this->_smtp->mailFrom($from, $args); + if (is_a($res, 'PEAR_Error')) { + $info = array('from' => $from); + return $this->_raiseError('failed_set_from', $info); + } + + // rcpt to: + $res = $this->_smtp->rcptTo($rcpt); + if (is_a($res, 'PEAR_Error')) { + $info = array('rcpt' => $rcpt); + return $this->_raiseError('failed_set_rcpt', $info); + } + + // Don't send anything in test mode + if ($this->test) { + $result = $this->_smtp->rset(); + $res = $this->_smtp->rset(); + if (is_a($res, 'PEAR_Error')) { + return $this->_raiseError('failed_rset'); + } + + $this->_smtp->disconnect(); + $this->_smtp = null; + return true; + } + + // Send data + $res = $this->_smtp->data("$textHeaders\r\n$body"); + if (is_a($res, 'PEAR_Error')) { + $info = array('rcpt' => $rcpt); + return $this->_raiseError('failed_send_data', $info); + } + + $this->_smtp->disconnect(); + $this->_smtp = null; + } + + return true; + } + + /** + * Recieve mx rexords for a spciefied host + * + * The MX records + * + * @access private + * @param string $host mail host + * @return mixed sorted + */ + function _getMx($host) + { + $mx = array(); + + if ($this->withNetDns) { + $res = $this->_loadNetDns(); + if (is_a($res, 'PEAR_Error')) { + return $res; + } + + $response = $this->resolver->query($host, 'MX'); + if (!$response) { + return false; + } + + foreach ($response->answer as $rr) { + if ($rr->type == 'MX') { + $mx[$rr->exchange] = $rr->preference; + } + } + } else { + $mxHost = array(); + $mxWeight = array(); + + if (!getmxrr($host, $mxHost, $mxWeight)) { + return false; + } + for ($i = 0; $i < count($mxHost); ++$i) { + $mx[$mxHost[$i]] = $mxWeight[$i]; + } + } + + asort($mx); + return $mx; + } + + /** + * initialize PEAR:Net_DNS_Resolver + * + * @access private + * @return boolean true on success + */ + function _loadNetDns() + { + if (is_object($this->resolver)) { + return true; + } + + if (!include_once 'Net/DNS.php') { + return $this->_raiseError('no_resolver'); + } + + $this->resolver = new Net_DNS_Resolver(); + if ($this->debug) { + $this->resolver->test = 1; + } + + return true; + } + + /** + * raise standardized error + * + * include additional information in error message + * + * @access private + * @param string $id maps error ids to codes and message + * @param array $info optional information in associative array + * @see _errorCode + */ + function _raiseError($id, $info = array()) + { + $code = $this->errorCode[$id]['code']; + $msg = $this->errorCode[$id]['msg']; + + // include info to messages + if (!empty($info)) { + $search = array(); + $replace = array(); + + foreach ($info as $key => $value) { + array_push($search, '{' . strtoupper($key) . '}'); + array_push($replace, $value); + } + + $msg = str_replace($search, $replace, $msg); + } + + return PEAR::raiseError($msg, $code); + } + +} diff --git a/extlib/Net/SMTP.php b/extlib/Net/SMTP.php new file mode 100644 index 000000000..d632258d6 --- /dev/null +++ b/extlib/Net/SMTP.php @@ -0,0 +1,1082 @@ +<?php +/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Authors: Chuck Hagenbuch <chuck@horde.org> | +// | Jon Parise <jon@php.net> | +// | Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> | +// +----------------------------------------------------------------------+ +// +// $Id: SMTP.php,v 1.63 2008/06/10 05:39:12 jon Exp $ + +require_once 'PEAR.php'; +require_once 'Net/Socket.php'; + +/** + * Provides an implementation of the SMTP protocol using PEAR's + * Net_Socket:: class. + * + * @package Net_SMTP + * @author Chuck Hagenbuch <chuck@horde.org> + * @author Jon Parise <jon@php.net> + * @author Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> + * + * @example basic.php A basic implementation of the Net_SMTP package. + */ +class Net_SMTP +{ + /** + * The server to connect to. + * @var string + * @access public + */ + var $host = 'localhost'; + + /** + * The port to connect to. + * @var int + * @access public + */ + var $port = 25; + + /** + * The value to give when sending EHLO or HELO. + * @var string + * @access public + */ + var $localhost = 'localhost'; + + /** + * List of supported authentication methods, in preferential order. + * @var array + * @access public + */ + var $auth_methods = array('DIGEST-MD5', 'CRAM-MD5', 'LOGIN', 'PLAIN'); + + /** + * Use SMTP command pipelining (specified in RFC 2920) if the SMTP + * server supports it. + * + * When pipeling is enabled, rcptTo(), mailFrom(), sendFrom(), + * somlFrom() and samlFrom() do not wait for a response from the + * SMTP server but return immediately. + * + * @var bool + * @access public + */ + var $pipelining = false; + + /** + * Number of pipelined commands. + * @var int + * @access private + */ + var $_pipelined_commands = 0; + + /** + * Should debugging output be enabled? + * @var boolean + * @access private + */ + var $_debug = false; + + /** + * The socket resource being used to connect to the SMTP server. + * @var resource + * @access private + */ + var $_socket = null; + + /** + * The most recent server response code. + * @var int + * @access private + */ + var $_code = -1; + + /** + * The most recent server response arguments. + * @var array + * @access private + */ + var $_arguments = array(); + + /** + * Stores detected features of the SMTP server. + * @var array + * @access private + */ + var $_esmtp = array(); + + /** + * Instantiates a new Net_SMTP object, overriding any defaults + * with parameters that are passed in. + * + * If you have SSL support in PHP, you can connect to a server + * over SSL using an 'ssl://' prefix: + * + * // 465 is a common smtps port. + * $smtp = new Net_SMTP('ssl://mail.host.com', 465); + * $smtp->connect(); + * + * @param string $host The server to connect to. + * @param integer $port The port to connect to. + * @param string $localhost The value to give when sending EHLO or HELO. + * @param boolean $pipeling Use SMTP command pipelining + * + * @access public + * @since 1.0 + */ + function Net_SMTP($host = null, $port = null, $localhost = null, $pipelining = false) + { + if (isset($host)) { + $this->host = $host; + } + if (isset($port)) { + $this->port = $port; + } + if (isset($localhost)) { + $this->localhost = $localhost; + } + $this->pipelining = $pipelining; + + $this->_socket = new Net_Socket(); + + /* Include the Auth_SASL package. If the package is not + * available, we disable the authentication methods that + * depend upon it. */ + if ((@include_once 'Auth/SASL.php') === false) { + $pos = array_search('DIGEST-MD5', $this->auth_methods); + unset($this->auth_methods[$pos]); + $pos = array_search('CRAM-MD5', $this->auth_methods); + unset($this->auth_methods[$pos]); + } + } + + /** + * Set the value of the debugging flag. + * + * @param boolean $debug New value for the debugging flag. + * + * @access public + * @since 1.1.0 + */ + function setDebug($debug) + { + $this->_debug = $debug; + } + + /** + * Send the given string of data to the server. + * + * @param string $data The string of data to send. + * + * @return mixed True on success or a PEAR_Error object on failure. + * + * @access private + * @since 1.1.0 + */ + function _send($data) + { + if ($this->_debug) { + echo "DEBUG: Send: $data\n"; + } + + if (PEAR::isError($error = $this->_socket->write($data))) { + return PEAR::raiseError('Failed to write to socket: ' . + $error->getMessage()); + } + + return true; + } + + /** + * Send a command to the server with an optional string of + * arguments. A carriage return / linefeed (CRLF) sequence will + * be appended to each command string before it is sent to the + * SMTP server - an error will be thrown if the command string + * already contains any newline characters. Use _send() for + * commands that must contain newlines. + * + * @param string $command The SMTP command to send to the server. + * @param string $args A string of optional arguments to append + * to the command. + * + * @return mixed The result of the _send() call. + * + * @access private + * @since 1.1.0 + */ + function _put($command, $args = '') + { + if (!empty($args)) { + $command .= ' ' . $args; + } + + if (strcspn($command, "\r\n") !== strlen($command)) { + return PEAR::raiseError('Commands cannot contain newlines'); + } + + return $this->_send($command . "\r\n"); + } + + /** + * Read a reply from the SMTP server. The reply consists of a response + * code and a response message. + * + * @param mixed $valid The set of valid response codes. These + * may be specified as an array of integer + * values or as a single integer value. + * @param bool $later Do not parse the response now, but wait + * until the last command in the pipelined + * command group + * + * @return mixed True if the server returned a valid response code or + * a PEAR_Error object is an error condition is reached. + * + * @access private + * @since 1.1.0 + * + * @see getResponse + */ + function _parseResponse($valid, $later = false) + { + $this->_code = -1; + $this->_arguments = array(); + + if ($later) { + $this->_pipelined_commands++; + return true; + } + + for ($i = 0; $i <= $this->_pipelined_commands; $i++) { + while ($line = $this->_socket->readLine()) { + if ($this->_debug) { + echo "DEBUG: Recv: $line\n"; + } + + /* If we receive an empty line, the connection has been closed. */ + if (empty($line)) { + $this->disconnect(); + return PEAR::raiseError('Connection was unexpectedly closed'); + } + + /* Read the code and store the rest in the arguments array. */ + $code = substr($line, 0, 3); + $this->_arguments[] = trim(substr($line, 4)); + + /* Check the syntax of the response code. */ + if (is_numeric($code)) { + $this->_code = (int)$code; + } else { + $this->_code = -1; + break; + } + + /* If this is not a multiline response, we're done. */ + if (substr($line, 3, 1) != '-') { + break; + } + } + } + + $this->_pipelined_commands = 0; + + /* Compare the server's response code with the valid code/codes. */ + if (is_int($valid) && ($this->_code === $valid)) { + return true; + } elseif (is_array($valid) && in_array($this->_code, $valid, true)) { + return true; + } + + return PEAR::raiseError('Invalid response code received from server', + $this->_code); + } + + /** + * Return a 2-tuple containing the last response from the SMTP server. + * + * @return array A two-element array: the first element contains the + * response code as an integer and the second element + * contains the response's arguments as a string. + * + * @access public + * @since 1.1.0 + */ + function getResponse() + { + return array($this->_code, join("\n", $this->_arguments)); + } + + /** + * Attempt to connect to the SMTP server. + * + * @param int $timeout The timeout value (in seconds) for the + * socket connection. + * @param bool $persistent Should a persistent socket connection + * be used? + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function connect($timeout = null, $persistent = false) + { + $result = $this->_socket->connect($this->host, $this->port, + $persistent, $timeout); + if (PEAR::isError($result)) { + return PEAR::raiseError('Failed to connect socket: ' . + $result->getMessage()); + } + + if (PEAR::isError($error = $this->_parseResponse(220))) { + return $error; + } + if (PEAR::isError($error = $this->_negotiate())) { + return $error; + } + + return true; + } + + /** + * Attempt to disconnect from the SMTP server. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function disconnect() + { + if (PEAR::isError($error = $this->_put('QUIT'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(221))) { + return $error; + } + if (PEAR::isError($error = $this->_socket->disconnect())) { + return PEAR::raiseError('Failed to disconnect socket: ' . + $error->getMessage()); + } + + return true; + } + + /** + * Attempt to send the EHLO command and obtain a list of ESMTP + * extensions available, and failing that just send HELO. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access private + * @since 1.1.0 + */ + function _negotiate() + { + if (PEAR::isError($error = $this->_put('EHLO', $this->localhost))) { + return $error; + } + + if (PEAR::isError($this->_parseResponse(250))) { + /* If we receive a 503 response, we're already authenticated. */ + if ($this->_code === 503) { + return true; + } + + /* If the EHLO failed, try the simpler HELO command. */ + if (PEAR::isError($error = $this->_put('HELO', $this->localhost))) { + return $error; + } + if (PEAR::isError($this->_parseResponse(250))) { + return PEAR::raiseError('HELO was not accepted: ', $this->_code); + } + + return true; + } + + foreach ($this->_arguments as $argument) { + $verb = strtok($argument, ' '); + $arguments = substr($argument, strlen($verb) + 1, + strlen($argument) - strlen($verb) - 1); + $this->_esmtp[$verb] = $arguments; + } + + if (!isset($this->_esmtp['PIPELINING'])) { + $this->pipelining = false; + } + + return true; + } + + /** + * Returns the name of the best authentication method that the server + * has advertised. + * + * @return mixed Returns a string containing the name of the best + * supported authentication method or a PEAR_Error object + * if a failure condition is encountered. + * @access private + * @since 1.1.0 + */ + function _getBestAuthMethod() + { + $available_methods = explode(' ', $this->_esmtp['AUTH']); + + foreach ($this->auth_methods as $method) { + if (in_array($method, $available_methods)) { + return $method; + } + } + + return PEAR::raiseError('No supported authentication methods'); + } + + /** + * Attempt to do SMTP authentication. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * @param string The requested authentication method. If none is + * specified, the best supported method will be used. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function auth($uid, $pwd , $method = '') + { + if (empty($this->_esmtp['AUTH'])) { + if (version_compare(PHP_VERSION, '5.1.0', '>=')) { + if (!isset($this->_esmtp['STARTTLS'])) { + return PEAR::raiseError('SMTP server does not support authentication'); + } + if (PEAR::isError($result = $this->_put('STARTTLS'))) { + return $result; + } + if (PEAR::isError($result = $this->_parseResponse(220))) { + return $result; + } + if (PEAR::isError($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT))) { + return $result; + } elseif ($result !== true) { + return PEAR::raiseError('STARTTLS failed'); + } + + /* Send EHLO again to recieve the AUTH string from the + * SMTP server. */ + $this->_negotiate(); + if (empty($this->_esmtp['AUTH'])) { + return PEAR::raiseError('SMTP server does not support authentication'); + } + } else { + return PEAR::raiseError('SMTP server does not support authentication'); + } + } + + /* If no method has been specified, get the name of the best + * supported method advertised by the SMTP server. */ + if (empty($method)) { + if (PEAR::isError($method = $this->_getBestAuthMethod())) { + /* Return the PEAR_Error object from _getBestAuthMethod(). */ + return $method; + } + } else { + $method = strtoupper($method); + if (!in_array($method, $this->auth_methods)) { + return PEAR::raiseError("$method is not a supported authentication method"); + } + } + + switch ($method) { + case 'DIGEST-MD5': + $result = $this->_authDigest_MD5($uid, $pwd); + break; + + case 'CRAM-MD5': + $result = $this->_authCRAM_MD5($uid, $pwd); + break; + + case 'LOGIN': + $result = $this->_authLogin($uid, $pwd); + break; + + case 'PLAIN': + $result = $this->_authPlain($uid, $pwd); + break; + + default: + $result = PEAR::raiseError("$method is not a supported authentication method"); + break; + } + + /* If an error was encountered, return the PEAR_Error object. */ + if (PEAR::isError($result)) { + return $result; + } + + return true; + } + + /** + * Authenticates the user using the DIGEST-MD5 method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authDigest_MD5($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'DIGEST-MD5'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $digest = &Auth_SASL::factory('digestmd5'); + $auth_str = base64_encode($digest->getResponse($uid, $pwd, $challenge, + $this->host, "smtp")); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + return $error; + } + + /* We don't use the protocol's third step because SMTP doesn't + * allow subsequent authentication, so we just silently ignore + * it. */ + if (PEAR::isError($error = $this->_put(''))) { + return $error; + } + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + } + + /** + * Authenticates the user using the CRAM-MD5 method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authCRAM_MD5($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'CRAM-MD5'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $cram = &Auth_SASL::factory('crammd5'); + $auth_str = base64_encode($cram->getResponse($uid, $pwd, $challenge)); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + } + + /** + * Authenticates the user using the LOGIN method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authLogin($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'LOGIN'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + if (PEAR::isError($error = $this->_put(base64_encode($uid)))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + return $error; + } + + if (PEAR::isError($error = $this->_put(base64_encode($pwd)))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + + return true; + } + + /** + * Authenticates the user using the PLAIN method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authPlain($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'PLAIN'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $auth_str = base64_encode(chr(0) . $uid . chr(0) . $pwd); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + + return true; + } + + /** + * Send the HELO command. + * + * @param string The domain name to say we are. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function helo($domain) + { + if (PEAR::isError($error = $this->_put('HELO', $domain))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Return the list of SMTP service extensions advertised by the server. + * + * @return array The list of SMTP service extensions. + * @access public + * @since 1.3 + */ + function getServiceExtensions() + { + return $this->_esmtp; + } + + /** + * Send the MAIL FROM: command. + * + * @param string $sender The sender (reverse path) to set. + * @param string $params String containing additional MAIL parameters, + * such as the NOTIFY flags defined by RFC 1891 + * or the VERP protocol. + * + * If $params is an array, only the 'verp' option + * is supported. If 'verp' is true, the XVERP + * parameter is appended to the MAIL command. If + * the 'verp' value is a string, the full + * XVERP=value parameter is appended. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function mailFrom($sender, $params = null) + { + $args = "FROM:<$sender>"; + + /* Support the deprecated array form of $params. */ + if (is_array($params) && isset($params['verp'])) { + /* XVERP */ + if ($params['verp'] === true) { + $args .= ' XVERP'; + + /* XVERP=something */ + } elseif (trim($params['verp'])) { + $args .= ' XVERP=' . $params['verp']; + } + } elseif (is_string($params)) { + $args .= ' ' . $params; + } + + if (PEAR::isError($error = $this->_put('MAIL', $args))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Send the RCPT TO: command. + * + * @param string $recipient The recipient (forward path) to add. + * @param string $params String containing additional RCPT parameters, + * such as the NOTIFY flags defined by RFC 1891. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + */ + function rcptTo($recipient, $params = null) + { + $args = "TO:<$recipient>"; + if (is_string($params)) { + $args .= ' ' . $params; + } + + if (PEAR::isError($error = $this->_put('RCPT', $args))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(array(250, 251), $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Quote the data so that it meets SMTP standards. + * + * This is provided as a separate public function to facilitate + * easier overloading for the cases where it is desirable to + * customize the quoting behavior. + * + * @param string $data The message text to quote. The string must be passed + * by reference, and the text will be modified in place. + * + * @access public + * @since 1.2 + */ + function quotedata(&$data) + { + /* Change Unix (\n) and Mac (\r) linefeeds into + * Internet-standard CRLF (\r\n) linefeeds. */ + $data = preg_replace(array('/(?<!\r)\n/','/\r(?!\n)/'), "\r\n", $data); + + /* Because a single leading period (.) signifies an end to the + * data, legitimate leading periods need to be "doubled" + * (e.g. '..'). */ + $data = str_replace("\n.", "\n..", $data); + } + + /** + * Send the DATA command. + * + * @param string $data The message body to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function data($data) + { + /* RFC 1870, section 3, subsection 3 states "a value of zero + * indicates that no fixed maximum message size is in force". + * Furthermore, it says that if "the parameter is omitted no + * information is conveyed about the server's fixed maximum + * message size". */ + if (isset($this->_esmtp['SIZE']) && ($this->_esmtp['SIZE'] > 0)) { + if (strlen($data) >= $this->_esmtp['SIZE']) { + $this->disconnect(); + return PEAR::raiseError('Message size excedes the server limit'); + } + } + + /* Quote the data based on the SMTP standards. */ + $this->quotedata($data); + + if (PEAR::isError($error = $this->_put('DATA'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(354))) { + return $error; + } + + if (PEAR::isError($result = $this->_send($data . "\r\n.\r\n"))) { + return $result; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Send the SEND FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function sendFrom($path) + { + if (PEAR::isError($error = $this->_put('SEND', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for sendFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function send_from($path) + { + return sendFrom($path); + } + + /** + * Send the SOML FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function somlFrom($path) + { + if (PEAR::isError($error = $this->_put('SOML', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for somlFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function soml_from($path) + { + return somlFrom($path); + } + + /** + * Send the SAML FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function samlFrom($path) + { + if (PEAR::isError($error = $this->_put('SAML', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for samlFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function saml_from($path) + { + return samlFrom($path); + } + + /** + * Send the RSET command. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function rset() + { + if (PEAR::isError($error = $this->_put('RSET'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Send the VRFY command. + * + * @param string The string to verify + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function vrfy($string) + { + /* Note: 251 is also a valid response code */ + if (PEAR::isError($error = $this->_put('VRFY', $string))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(array(250, 252)))) { + return $error; + } + + return true; + } + + /** + * Send the NOOP command. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function noop() + { + if (PEAR::isError($error = $this->_put('NOOP'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility method. identifySender()'s functionality is + * now handled internally. + * + * @return boolean This method always return true. + * + * @access public + * @since 1.0 + */ + function identifySender() + { + return true; + } + +} diff --git a/extlib/OAuth.php b/extlib/OAuth.php new file mode 100644 index 000000000..6dc6b3f35 --- /dev/null +++ b/extlib/OAuth.php @@ -0,0 +1,755 @@ +<?php +// vim: foldmethod=marker + +/* Generic exception class + */ +class OAuthException extends Exception {/*{{{*/ + // pass +}/*}}}*/ + +class OAuthConsumer {/*{{{*/ + public $key; + public $secret; + + function __construct($key, $secret, $callback_url=NULL) {/*{{{*/ + $this->key = $key; + $this->secret = $secret; + $this->callback_url = $callback_url; + }/*}}}*/ +}/*}}}*/ + +class OAuthToken {/*{{{*/ + // access tokens and request tokens + public $key; + public $secret; + + /** + * key = the token + * secret = the token secret + */ + function __construct($key, $secret) {/*{{{*/ + $this->key = $key; + $this->secret = $secret; + }/*}}}*/ + + /** + * generates the basic string serialization of a token that a server + * would respond to request_token and access_token calls with + */ + function to_string() {/*{{{*/ + return "oauth_token=" . OAuthUtil::urlencodeRFC3986($this->key) . + "&oauth_token_secret=" . OAuthUtil::urlencodeRFC3986($this->secret); + }/*}}}*/ + + function __toString() {/*{{{*/ + return $this->to_string(); + }/*}}}*/ +}/*}}}*/ + +class OAuthSignatureMethod {/*{{{*/ + public function check_signature(&$request, $consumer, $token, $signature) { + $built = $this->build_signature($request, $consumer, $token); + return $built == $signature; + } +}/*}}}*/ + +class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {/*{{{*/ + function get_name() {/*{{{*/ + return "HMAC-SHA1"; + }/*}}}*/ + + public function build_signature($request, $consumer, $token) {/*{{{*/ + $base_string = $request->get_signature_base_string(); + $request->base_string = $base_string; + + $key_parts = array( + $consumer->secret, + ($token) ? $token->secret : "" + ); + + $key_parts = array_map(array('OAuthUtil','urlencodeRFC3986'), $key_parts); + $key = implode('&', $key_parts); + + return base64_encode( hash_hmac('sha1', $base_string, $key, true)); + }/*}}}*/ +}/*}}}*/ + +class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {/*{{{*/ + public function get_name() {/*{{{*/ + return "PLAINTEXT"; + }/*}}}*/ + + public function build_signature($request, $consumer, $token) {/*{{{*/ + $sig = array( + OAuthUtil::urlencodeRFC3986($consumer->secret) + ); + + if ($token) { + array_push($sig, OAuthUtil::urlencodeRFC3986($token->secret)); + } else { + array_push($sig, ''); + } + + $raw = implode("&", $sig); + // for debug purposes + $request->base_string = $raw; + + return OAuthUtil::urlencodeRFC3986($raw); + }/*}}}*/ +}/*}}}*/ + +class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {/*{{{*/ + public function get_name() {/*{{{*/ + return "RSA-SHA1"; + }/*}}}*/ + + protected function fetch_public_cert(&$request) {/*{{{*/ + // not implemented yet, ideas are: + // (1) do a lookup in a table of trusted certs keyed off of consumer + // (2) fetch via http using a url provided by the requester + // (3) some sort of specific discovery code based on request + // + // either way should return a string representation of the certificate + throw Exception("fetch_public_cert not implemented"); + }/*}}}*/ + + protected function fetch_private_cert(&$request) {/*{{{*/ + // not implemented yet, ideas are: + // (1) do a lookup in a table of trusted certs keyed off of consumer + // + // either way should return a string representation of the certificate + throw Exception("fetch_private_cert not implemented"); + }/*}}}*/ + + public function build_signature(&$request, $consumer, $token) {/*{{{*/ + $base_string = $request->get_signature_base_string(); + $request->base_string = $base_string; + + // Fetch the private key cert based on the request + $cert = $this->fetch_private_cert($request); + + // Pull the private key ID from the certificate + $privatekeyid = openssl_get_privatekey($cert); + + // Sign using the key + $ok = openssl_sign($base_string, $signature, $privatekeyid); + + // Release the key resource + openssl_free_key($privatekeyid); + + return base64_encode($signature); + } /*}}}*/ + + public function check_signature(&$request, $consumer, $token, $signature) {/*{{{*/ + $decoded_sig = base64_decode($signature); + + $base_string = $request->get_signature_base_string(); + + // Fetch the public key cert based on the request + $cert = $this->fetch_public_cert($request); + + // Pull the public key ID from the certificate + $publickeyid = openssl_get_publickey($cert); + + // Check the computed signature against the one passed in the query + $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); + + // Release the key resource + openssl_free_key($publickeyid); + + return $ok == 1; + } /*}}}*/ +}/*}}}*/ + +class OAuthRequest {/*{{{*/ + private $parameters; + private $http_method; + private $http_url; + // for debug purposes + public $base_string; + public static $version = '1.0'; + + function __construct($http_method, $http_url, $parameters=NULL) {/*{{{*/ + @$parameters or $parameters = array(); + $this->parameters = $parameters; + $this->http_method = $http_method; + $this->http_url = $http_url; + }/*}}}*/ + + + /** + * attempt to build up a request from what was passed to the server + */ + public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {/*{{{*/ + $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https'; + @$http_url or $http_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + @$http_method or $http_method = $_SERVER['REQUEST_METHOD']; + + $request_headers = OAuthRequest::get_headers(); + + // let the library user override things however they'd like, if they know + // which parameters to use then go for it, for example XMLRPC might want to + // do this + if ($parameters) { + $req = new OAuthRequest($http_method, $http_url, $parameters); + } + // next check for the auth header, we need to do some extra stuff + // if that is the case, namely suck in the parameters from GET or POST + // so that we can include them in the signature + else if (@substr($request_headers['Authorization'], 0, 5) == "OAuth") { + $header_parameters = OAuthRequest::split_header($request_headers['Authorization']); + if ($http_method == "GET") { + $req_parameters = $_GET; + } + else if ($http_method == "POST") { + $req_parameters = $_POST; + } + $parameters = array_merge($header_parameters, $req_parameters); + $req = new OAuthRequest($http_method, $http_url, $parameters); + } + else if ($http_method == "GET") { + $req = new OAuthRequest($http_method, $http_url, $_GET); + } + else if ($http_method == "POST") { + $req = new OAuthRequest($http_method, $http_url, $_POST); + } + return $req; + }/*}}}*/ + + /** + * pretty much a helper function to set up the request + */ + public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {/*{{{*/ + @$parameters or $parameters = array(); + $defaults = array("oauth_version" => OAuthRequest::$version, + "oauth_nonce" => OAuthRequest::generate_nonce(), + "oauth_timestamp" => OAuthRequest::generate_timestamp(), + "oauth_consumer_key" => $consumer->key); + $parameters = array_merge($defaults, $parameters); + + if ($token) { + $parameters['oauth_token'] = $token->key; + } + return new OAuthRequest($http_method, $http_url, $parameters); + }/*}}}*/ + + public function set_parameter($name, $value) {/*{{{*/ + $this->parameters[$name] = $value; + }/*}}}*/ + + public function get_parameter($name) {/*{{{*/ + return $this->parameters[$name]; + }/*}}}*/ + + public function get_parameters() {/*{{{*/ + return $this->parameters; + }/*}}}*/ + + /** + * Returns the normalized parameters of the request + * + * This will be all (except oauth_signature) parameters, + * sorted first by key, and if duplicate keys, then by + * value. + * + * The returned string will be all the key=value pairs + * concated by &. + * + * @return string + */ + public function get_signable_parameters() {/*{{{*/ + // Grab all parameters + $params = $this->parameters; + + // Remove oauth_signature if present + if (isset($params['oauth_signature'])) { + unset($params['oauth_signature']); + } + + // Urlencode both keys and values + $keys = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_keys($params)); + $values = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_values($params)); + $params = array_combine($keys, $values); + + // Sort by keys (natsort) + uksort($params, 'strnatcmp'); + + // Generate key=value pairs + $pairs = array(); + foreach ($params as $key=>$value ) { + if (is_array($value)) { + // If the value is an array, it's because there are multiple + // with the same key, sort them, then add all the pairs + natsort($value); + foreach ($value as $v2) { + $pairs[] = $key . '=' . $v2; + } + } else { + $pairs[] = $key . '=' . $value; + } + } + + // Return the pairs, concated with & + return implode('&', $pairs); + }/*}}}*/ + + /** + * Returns the base string of this request + * + * The base string defined as the method, the url + * and the parameters (normalized), each urlencoded + * and the concated with &. + */ + public function get_signature_base_string() {/*{{{*/ + $parts = array( + $this->get_normalized_http_method(), + $this->get_normalized_http_url(), + $this->get_signable_parameters() + ); + + $parts = array_map(array('OAuthUtil', 'urlencodeRFC3986'), $parts); + + return implode('&', $parts); + }/*}}}*/ + + /** + * just uppercases the http method + */ + public function get_normalized_http_method() {/*{{{*/ + return strtoupper($this->http_method); + }/*}}}*/ + + /** + * parses the url and rebuilds it to be + * scheme://host/path + */ + public function get_normalized_http_url() {/*{{{*/ + $parts = parse_url($this->http_url); + + $port = @$parts['port']; + $scheme = $parts['scheme']; + $host = $parts['host']; + $path = @$parts['path']; + + $port or $port = ($scheme == 'https') ? '443' : '80'; + + if (($scheme == 'https' && $port != '443') + || ($scheme == 'http' && $port != '80')) { + $host = "$host:$port"; + } + return "$scheme://$host$path"; + }/*}}}*/ + + /** + * builds a url usable for a GET request + */ + public function to_url() {/*{{{*/ + $out = $this->get_normalized_http_url() . "?"; + $out .= $this->to_postdata(); + return $out; + }/*}}}*/ + + /** + * builds the data one would send in a POST request + */ + public function to_postdata() {/*{{{*/ + $total = array(); + foreach ($this->parameters as $k => $v) { + $total[] = OAuthUtil::urlencodeRFC3986($k) . "=" . OAuthUtil::urlencodeRFC3986($v); + } + $out = implode("&", $total); + return $out; + }/*}}}*/ + + /** + * builds the Authorization: header + */ + public function to_header($realm="") {/*{{{*/ + $out ='"Authorization: OAuth realm="' . $realm . '",'; + $total = array(); + foreach ($this->parameters as $k => $v) { + if (substr($k, 0, 5) != "oauth") continue; + $out .= ',' . OAuthUtil::urlencodeRFC3986($k) . '="' . OAuthUtil::urlencodeRFC3986($v) . '"'; + } + return $out; + }/*}}}*/ + + public function __toString() {/*{{{*/ + return $this->to_url(); + }/*}}}*/ + + + public function sign_request($signature_method, $consumer, $token) {/*{{{*/ + $this->set_parameter("oauth_signature_method", $signature_method->get_name()); + $signature = $this->build_signature($signature_method, $consumer, $token); + $this->set_parameter("oauth_signature", $signature); + }/*}}}*/ + + public function build_signature($signature_method, $consumer, $token) {/*{{{*/ + $signature = $signature_method->build_signature($this, $consumer, $token); + return $signature; + }/*}}}*/ + + /** + * util function: current timestamp + */ + private static function generate_timestamp() {/*{{{*/ + return time(); + }/*}}}*/ + + /** + * util function: current nonce + */ + private static function generate_nonce() {/*{{{*/ + $mt = microtime(); + $rand = mt_rand(); + + return md5($mt . $rand); // md5s look nicer than numbers + }/*}}}*/ + + /** + * util function for turning the Authorization: header into + * parameters, has to do some unescaping + */ + private static function split_header($header) {/*{{{*/ + // remove 'OAuth ' at the start of a header + $header = substr($header, 6); + + // error cases: commas in parameter values? + $parts = explode(",", $header); + $out = array(); + foreach ($parts as $param) { + $param = ltrim($param); + // skip the "realm" param, nobody ever uses it anyway + if (substr($param, 0, 5) != "oauth") continue; + + $param_parts = explode("=", $param); + + // rawurldecode() used because urldecode() will turn a "+" in the + // value into a space + $out[$param_parts[0]] = rawurldecode(substr($param_parts[1], 1, -1)); + } + return $out; + }/*}}}*/ + + /** + * helper to try to sort out headers for people who aren't running apache + */ + private static function get_headers() {/*{{{*/ + if (function_exists('apache_request_headers')) { + // we need this to get the actual Authorization: header + // because apache tends to tell us it doesn't exist + return apache_request_headers(); + } + // otherwise we don't have apache and are just going to have to hope + // that $_SERVER actually contains what we need + $out = array(); + foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) == "HTTP_") { + // this is chaos, basically it is just there to capitalize the first + // letter of every word that is not an initial HTTP and strip HTTP + // code from przemek + $key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5))))); + $out[$key] = $value; + } + } + return $out; + }/*}}}*/ +}/*}}}*/ + +class OAuthServer {/*{{{*/ + protected $timestamp_threshold = 300; // in seconds, five minutes + protected $version = 1.0; // hi blaine + protected $signature_methods = array(); + + protected $data_store; + + function __construct($data_store) {/*{{{*/ + $this->data_store = $data_store; + }/*}}}*/ + + public function add_signature_method($signature_method) {/*{{{*/ + $this->signature_methods[$signature_method->get_name()] = + $signature_method; + }/*}}}*/ + + // high level functions + + /** + * process a request_token request + * returns the request token on success + */ + public function fetch_request_token(&$request) {/*{{{*/ + $this->get_version($request); + + $consumer = $this->get_consumer($request); + + // no token required for the initial token request + $token = NULL; + + $this->check_signature($request, $consumer, $token); + + $new_token = $this->data_store->new_request_token($consumer); + + return $new_token; + }/*}}}*/ + + /** + * process an access_token request + * returns the access token on success + */ + public function fetch_access_token(&$request) {/*{{{*/ + $this->get_version($request); + + $consumer = $this->get_consumer($request); + + // requires authorized request token + $token = $this->get_token($request, $consumer, "request"); + + $this->check_signature($request, $consumer, $token); + + $new_token = $this->data_store->new_access_token($token, $consumer); + + return $new_token; + }/*}}}*/ + + /** + * verify an api call, checks all the parameters + */ + public function verify_request(&$request) {/*{{{*/ + $this->get_version($request); + $consumer = $this->get_consumer($request); + $token = $this->get_token($request, $consumer, "access"); + $this->check_signature($request, $consumer, $token); + return array($consumer, $token); + }/*}}}*/ + + // Internals from here + /** + * version 1 + */ + private function get_version(&$request) {/*{{{*/ + $version = $request->get_parameter("oauth_version"); + if (!$version) { + $version = 1.0; + } + if ($version && $version != $this->version) { + throw new OAuthException("OAuth version '$version' not supported"); + } + return $version; + }/*}}}*/ + + /** + * figure out the signature with some defaults + */ + private function get_signature_method(&$request) {/*{{{*/ + $signature_method = + @$request->get_parameter("oauth_signature_method"); + if (!$signature_method) { + $signature_method = "PLAINTEXT"; + } + if (!in_array($signature_method, + array_keys($this->signature_methods))) { + throw new OAuthException( + "Signature method '$signature_method' not supported try one of the following: " . implode(", ", array_keys($this->signature_methods)) + ); + } + return $this->signature_methods[$signature_method]; + }/*}}}*/ + + /** + * try to find the consumer for the provided request's consumer key + */ + private function get_consumer(&$request) {/*{{{*/ + $consumer_key = @$request->get_parameter("oauth_consumer_key"); + if (!$consumer_key) { + throw new OAuthException("Invalid consumer key"); + } + + $consumer = $this->data_store->lookup_consumer($consumer_key); + if (!$consumer) { + throw new OAuthException("Invalid consumer"); + } + + return $consumer; + }/*}}}*/ + + /** + * try to find the token for the provided request's token key + */ + private function get_token(&$request, $consumer, $token_type="access") {/*{{{*/ + $token_field = @$request->get_parameter('oauth_token'); + $token = $this->data_store->lookup_token( + $consumer, $token_type, $token_field + ); + if (!$token) { + throw new OAuthException("Invalid $token_type token: $token_field"); + } + return $token; + }/*}}}*/ + + /** + * all-in-one function to check the signature on a request + * should guess the signature method appropriately + */ + private function check_signature(&$request, $consumer, $token) {/*{{{*/ + // this should probably be in a different method + $timestamp = @$request->get_parameter('oauth_timestamp'); + $nonce = @$request->get_parameter('oauth_nonce'); + + $this->check_timestamp($timestamp); + $this->check_nonce($consumer, $token, $nonce, $timestamp); + + $signature_method = $this->get_signature_method($request); + + $signature = $request->get_parameter('oauth_signature'); + $valid_sig = $signature_method->check_signature( + $request, + $consumer, + $token, + $signature + ); + + if (!$valid_sig) { + throw new OAuthException("Invalid signature"); + } + }/*}}}*/ + + /** + * check that the timestamp is new enough + */ + private function check_timestamp($timestamp) {/*{{{*/ + // verify that timestamp is recentish + $now = time(); + if ($now - $timestamp > $this->timestamp_threshold) { + throw new OAuthException("Expired timestamp, yours $timestamp, ours $now"); + } + }/*}}}*/ + + /** + * check that the nonce is not repeated + */ + private function check_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/ + // verify that the nonce is uniqueish + $found = $this->data_store->lookup_nonce($consumer, $token, $nonce, $timestamp); + if ($found) { + throw new OAuthException("Nonce already used: $nonce"); + } + }/*}}}*/ + + + +}/*}}}*/ + +class OAuthDataStore {/*{{{*/ + function lookup_consumer($consumer_key) {/*{{{*/ + // implement me + }/*}}}*/ + + function lookup_token($consumer, $token_type, $token) {/*{{{*/ + // implement me + }/*}}}*/ + + function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/ + // implement me + }/*}}}*/ + + function fetch_request_token($consumer) {/*{{{*/ + // return a new token attached to this consumer + }/*}}}*/ + + function fetch_access_token($token, $consumer) {/*{{{*/ + // return a new access token attached to this consumer + // for the user associated with this token if the request token + // is authorized + // should also invalidate the request token + }/*}}}*/ + +}/*}}}*/ + + +/* A very naive dbm-based oauth storage + */ +class SimpleOAuthDataStore extends OAuthDataStore {/*{{{*/ + private $dbh; + + function __construct($path = "oauth.gdbm") {/*{{{*/ + $this->dbh = dba_popen($path, 'c', 'gdbm'); + }/*}}}*/ + + function __destruct() {/*{{{*/ + dba_close($this->dbh); + }/*}}}*/ + + function lookup_consumer($consumer_key) {/*{{{*/ + $rv = dba_fetch("consumer_$consumer_key", $this->dbh); + if ($rv === FALSE) { + return NULL; + } + $obj = unserialize($rv); + if (!($obj instanceof OAuthConsumer)) { + return NULL; + } + return $obj; + }/*}}}*/ + + function lookup_token($consumer, $token_type, $token) {/*{{{*/ + $rv = dba_fetch("${token_type}_${token}", $this->dbh); + if ($rv === FALSE) { + return NULL; + } + $obj = unserialize($rv); + if (!($obj instanceof OAuthToken)) { + return NULL; + } + return $obj; + }/*}}}*/ + + function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/ + if (dba_exists("nonce_$nonce", $this->dbh)) { + return TRUE; + } else { + dba_insert("nonce_$nonce", "1", $this->dbh); + return FALSE; + } + }/*}}}*/ + + function new_token($consumer, $type="request") {/*{{{*/ + $key = md5(time()); + $secret = time() + time(); + $token = new OAuthToken($key, md5(md5($secret))); + if (!dba_insert("${type}_$key", serialize($token), $this->dbh)) { + throw new OAuthException("doooom!"); + } + return $token; + }/*}}}*/ + + function new_request_token($consumer) {/*{{{*/ + return $this->new_token($consumer, "request"); + }/*}}}*/ + + function new_access_token($token, $consumer) {/*{{{*/ + + $token = $this->new_token($consumer, 'access'); + dba_delete("request_" . $token->key, $this->dbh); + return $token; + }/*}}}*/ +}/*}}}*/ + +class OAuthUtil {/*{{{*/ + public static function urlencodeRFC3986($string) {/*{{{*/ + return str_replace('+', ' ', + str_replace('%7E', '~', rawurlencode($string))); + + }/*}}}*/ + + + // This decode function isn't taking into consideration the above + // modifications to the encoding process. However, this method doesn't + // seem to be used anywhere so leaving it as is. + public static function urldecodeRFC3986($string) {/*{{{*/ + return rawurldecode($string); + }/*}}}*/ +}/*}}}*/ + +?> diff --git a/extlib/PEAR.php b/extlib/PEAR.php new file mode 100644 index 000000000..4c24c6006 --- /dev/null +++ b/extlib/PEAR.php @@ -0,0 +1,1118 @@ +<?php +/** + * PEAR, the PHP Extension and Application Repository + * + * PEAR class and PEAR_Error class + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category pear + * @package PEAR + * @author Sterling Hughes <sterling@php.net> + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V.Cox <cox@idecnet.com> + * @author Greg Beaver <cellog@php.net> + * @copyright 1997-2008 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: PEAR.php,v 1.104 2008/01/03 20:26:34 cellog Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/**#@+ + * ERROR constants + */ +define('PEAR_ERROR_RETURN', 1); +define('PEAR_ERROR_PRINT', 2); +define('PEAR_ERROR_TRIGGER', 4); +define('PEAR_ERROR_DIE', 8); +define('PEAR_ERROR_CALLBACK', 16); +/** + * WARNING: obsolete + * @deprecated + */ +define('PEAR_ERROR_EXCEPTION', 32); +/**#@-*/ +define('PEAR_ZE2', (function_exists('version_compare') && + version_compare(zend_version(), "2-dev", "ge"))); + +if (substr(PHP_OS, 0, 3) == 'WIN') { + define('OS_WINDOWS', true); + define('OS_UNIX', false); + define('PEAR_OS', 'Windows'); +} else { + define('OS_WINDOWS', false); + define('OS_UNIX', true); + define('PEAR_OS', 'Unix'); // blatant assumption +} + +// instant backwards compatibility +if (!defined('PATH_SEPARATOR')) { + if (OS_WINDOWS) { + define('PATH_SEPARATOR', ';'); + } else { + define('PATH_SEPARATOR', ':'); + } +} + +$GLOBALS['_PEAR_default_error_mode'] = PEAR_ERROR_RETURN; +$GLOBALS['_PEAR_default_error_options'] = E_USER_NOTICE; +$GLOBALS['_PEAR_destructor_object_list'] = array(); +$GLOBALS['_PEAR_shutdown_funcs'] = array(); +$GLOBALS['_PEAR_error_handler_stack'] = array(); + +@ini_set('track_errors', true); + +/** + * Base class for other PEAR classes. Provides rudimentary + * emulation of destructors. + * + * If you want a destructor in your class, inherit PEAR and make a + * destructor method called _yourclassname (same name as the + * constructor, but with a "_" prefix). Also, in your constructor you + * have to call the PEAR constructor: $this->PEAR();. + * The destructor method will be called without parameters. Note that + * at in some SAPI implementations (such as Apache), any output during + * the request shutdown (in which destructors are called) seems to be + * discarded. If you need to get any debug information from your + * destructor, use error_log(), syslog() or something similar. + * + * IMPORTANT! To use the emulated destructors you need to create the + * objects by reference: $obj =& new PEAR_child; + * + * @category pear + * @package PEAR + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Greg Beaver <cellog@php.net> + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.2 + * @link http://pear.php.net/package/PEAR + * @see PEAR_Error + * @since Class available since PHP 4.0.2 + * @link http://pear.php.net/manual/en/core.pear.php#core.pear.pear + */ +class PEAR +{ + // {{{ properties + + /** + * Whether to enable internal debug messages. + * + * @var bool + * @access private + */ + var $_debug = false; + + /** + * Default error mode for this object. + * + * @var int + * @access private + */ + var $_default_error_mode = null; + + /** + * Default error options used for this object when error mode + * is PEAR_ERROR_TRIGGER. + * + * @var int + * @access private + */ + var $_default_error_options = null; + + /** + * Default error handler (callback) for this object, if error mode is + * PEAR_ERROR_CALLBACK. + * + * @var string + * @access private + */ + var $_default_error_handler = ''; + + /** + * Which class to use for error objects. + * + * @var string + * @access private + */ + var $_error_class = 'PEAR_Error'; + + /** + * An array of expected errors. + * + * @var array + * @access private + */ + var $_expected_errors = array(); + + // }}} + + // {{{ constructor + + /** + * Constructor. Registers this object in + * $_PEAR_destructor_object_list for destructor emulation if a + * destructor object exists. + * + * @param string $error_class (optional) which class to use for + * error objects, defaults to PEAR_Error. + * @access public + * @return void + */ + function PEAR($error_class = null) + { + $classname = strtolower(get_class($this)); + if ($this->_debug) { + print "PEAR constructor called, class=$classname\n"; + } + if ($error_class !== null) { + $this->_error_class = $error_class; + } + while ($classname && strcasecmp($classname, "pear")) { + $destructor = "_$classname"; + if (method_exists($this, $destructor)) { + global $_PEAR_destructor_object_list; + $_PEAR_destructor_object_list[] = &$this; + if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) { + register_shutdown_function("_PEAR_call_destructors"); + $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true; + } + break; + } else { + $classname = get_parent_class($classname); + } + } + } + + // }}} + // {{{ destructor + + /** + * Destructor (the emulated type of...). Does nothing right now, + * but is included for forward compatibility, so subclass + * destructors should always call it. + * + * See the note in the class desciption about output from + * destructors. + * + * @access public + * @return void + */ + function _PEAR() { + if ($this->_debug) { + printf("PEAR destructor called, class=%s\n", strtolower(get_class($this))); + } + } + + // }}} + // {{{ getStaticProperty() + + /** + * If you have a class that's mostly/entirely static, and you need static + * properties, you can use this method to simulate them. Eg. in your method(s) + * do this: $myVar = &PEAR::getStaticProperty('myclass', 'myVar'); + * You MUST use a reference, or they will not persist! + * + * @access public + * @param string $class The calling classname, to prevent clashes + * @param string $var The variable to retrieve. + * @return mixed A reference to the variable. If not set it will be + * auto initialised to NULL. + */ + function &getStaticProperty($class, $var) + { + static $properties; + if (!isset($properties[$class])) { + $properties[$class] = array(); + } + if (!array_key_exists($var, $properties[$class])) { + $properties[$class][$var] = null; + } + return $properties[$class][$var]; + } + + // }}} + // {{{ registerShutdownFunc() + + /** + * Use this function to register a shutdown method for static + * classes. + * + * @access public + * @param mixed $func The function name (or array of class/method) to call + * @param mixed $args The arguments to pass to the function + * @return void + */ + function registerShutdownFunc($func, $args = array()) + { + // if we are called statically, there is a potential + // that no shutdown func is registered. Bug #6445 + if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) { + register_shutdown_function("_PEAR_call_destructors"); + $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true; + } + $GLOBALS['_PEAR_shutdown_funcs'][] = array($func, $args); + } + + // }}} + // {{{ isError() + + /** + * Tell whether a value is a PEAR error. + * + * @param mixed $data the value to test + * @param int $code if $data is an error object, return true + * only if $code is a string and + * $obj->getMessage() == $code or + * $code is an integer and $obj->getCode() == $code + * @access public + * @return bool true if parameter is an error + */ + function isError($data, $code = null) + { + if (is_a($data, 'PEAR_Error')) { + if (is_null($code)) { + return true; + } elseif (is_string($code)) { + return $data->getMessage() == $code; + } else { + return $data->getCode() == $code; + } + } + return false; + } + + // }}} + // {{{ setErrorHandling() + + /** + * Sets how errors generated by this object should be handled. + * Can be invoked both in objects and statically. If called + * statically, setErrorHandling sets the default behaviour for all + * PEAR objects. If called in an object, setErrorHandling sets + * the default behaviour for that object. + * + * @param int $mode + * One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, + * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE, + * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION. + * + * @param mixed $options + * When $mode is PEAR_ERROR_TRIGGER, this is the error level (one + * of E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR). + * + * When $mode is PEAR_ERROR_CALLBACK, this parameter is expected + * to be the callback function or method. A callback + * function is a string with the name of the function, a + * callback method is an array of two elements: the element + * at index 0 is the object, and the element at index 1 is + * the name of the method to call in the object. + * + * When $mode is PEAR_ERROR_PRINT or PEAR_ERROR_DIE, this is + * a printf format string used when printing the error + * message. + * + * @access public + * @return void + * @see PEAR_ERROR_RETURN + * @see PEAR_ERROR_PRINT + * @see PEAR_ERROR_TRIGGER + * @see PEAR_ERROR_DIE + * @see PEAR_ERROR_CALLBACK + * @see PEAR_ERROR_EXCEPTION + * + * @since PHP 4.0.5 + */ + + function setErrorHandling($mode = null, $options = null) + { + if (isset($this) && is_a($this, 'PEAR')) { + $setmode = &$this->_default_error_mode; + $setoptions = &$this->_default_error_options; + } else { + $setmode = &$GLOBALS['_PEAR_default_error_mode']; + $setoptions = &$GLOBALS['_PEAR_default_error_options']; + } + + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $setmode = $mode; + $setoptions = $options; + break; + + case PEAR_ERROR_CALLBACK: + $setmode = $mode; + // class/object method callback + if (is_callable($options)) { + $setoptions = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + } + + // }}} + // {{{ expectError() + + /** + * This method is used to tell which errors you expect to get. + * Expected errors are always returned with error mode + * PEAR_ERROR_RETURN. Expected error codes are stored in a stack, + * and this method pushes a new element onto it. The list of + * expected errors are in effect until they are popped off the + * stack with the popExpect() method. + * + * Note that this method can not be called statically + * + * @param mixed $code a single error code or an array of error codes to expect + * + * @return int the new depth of the "expected errors" stack + * @access public + */ + function expectError($code = '*') + { + if (is_array($code)) { + array_push($this->_expected_errors, $code); + } else { + array_push($this->_expected_errors, array($code)); + } + return sizeof($this->_expected_errors); + } + + // }}} + // {{{ popExpect() + + /** + * This method pops one element off the expected error codes + * stack. + * + * @return array the list of error codes that were popped + */ + function popExpect() + { + return array_pop($this->_expected_errors); + } + + // }}} + // {{{ _checkDelExpect() + + /** + * This method checks unsets an error code if available + * + * @param mixed error code + * @return bool true if the error code was unset, false otherwise + * @access private + * @since PHP 4.3.0 + */ + function _checkDelExpect($error_code) + { + $deleted = false; + + foreach ($this->_expected_errors AS $key => $error_array) { + if (in_array($error_code, $error_array)) { + unset($this->_expected_errors[$key][array_search($error_code, $error_array)]); + $deleted = true; + } + + // clean up empty arrays + if (0 == count($this->_expected_errors[$key])) { + unset($this->_expected_errors[$key]); + } + } + return $deleted; + } + + // }}} + // {{{ delExpect() + + /** + * This method deletes all occurences of the specified element from + * the expected error codes stack. + * + * @param mixed $error_code error code that should be deleted + * @return mixed list of error codes that were deleted or error + * @access public + * @since PHP 4.3.0 + */ + function delExpect($error_code) + { + $deleted = false; + + if ((is_array($error_code) && (0 != count($error_code)))) { + // $error_code is a non-empty array here; + // we walk through it trying to unset all + // values + foreach($error_code as $key => $error) { + if ($this->_checkDelExpect($error)) { + $deleted = true; + } else { + $deleted = false; + } + } + return $deleted ? true : PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } elseif (!empty($error_code)) { + // $error_code comes alone, trying to unset it + if ($this->_checkDelExpect($error_code)) { + return true; + } else { + return PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } + } else { + // $error_code is empty + return PEAR::raiseError("The expected error you submitted is empty"); // IMPROVE ME + } + } + + // }}} + // {{{ raiseError() + + /** + * This method is a wrapper that returns an instance of the + * configured error class with this object's default error + * handling applied. If the $mode and $options parameters are not + * specified, the object's defaults are used. + * + * @param mixed $message a text error message or a PEAR error object + * + * @param int $code a numeric error code (it is up to your class + * to define these if you want to use codes) + * + * @param int $mode One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, + * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE, + * PEAR_ERROR_CALLBACK, PEAR_ERROR_EXCEPTION. + * + * @param mixed $options If $mode is PEAR_ERROR_TRIGGER, this parameter + * specifies the PHP-internal error level (one of + * E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR). + * If $mode is PEAR_ERROR_CALLBACK, this + * parameter specifies the callback function or + * method. In other error modes this parameter + * is ignored. + * + * @param string $userinfo If you need to pass along for example debug + * information, this parameter is meant for that. + * + * @param string $error_class The returned error object will be + * instantiated from this class, if specified. + * + * @param bool $skipmsg If true, raiseError will only pass error codes, + * the error message parameter will be dropped. + * + * @access public + * @return object a PEAR error object + * @see PEAR::setErrorHandling + * @since PHP 4.0.5 + */ + function &raiseError($message = null, + $code = null, + $mode = null, + $options = null, + $userinfo = null, + $error_class = null, + $skipmsg = false) + { + // The error is yet a PEAR error object + if (is_object($message)) { + $code = $message->getCode(); + $userinfo = $message->getUserInfo(); + $error_class = $message->getType(); + $message->error_message_prefix = ''; + $message = $message->getMessage(); + } + + if (isset($this) && isset($this->_expected_errors) && sizeof($this->_expected_errors) > 0 && sizeof($exp = end($this->_expected_errors))) { + if ($exp[0] == "*" || + (is_int(reset($exp)) && in_array($code, $exp)) || + (is_string(reset($exp)) && in_array($message, $exp))) { + $mode = PEAR_ERROR_RETURN; + } + } + // No mode given, try global ones + if ($mode === null) { + // Class error handler + if (isset($this) && isset($this->_default_error_mode)) { + $mode = $this->_default_error_mode; + $options = $this->_default_error_options; + // Global error handler + } elseif (isset($GLOBALS['_PEAR_default_error_mode'])) { + $mode = $GLOBALS['_PEAR_default_error_mode']; + $options = $GLOBALS['_PEAR_default_error_options']; + } + } + + if ($error_class !== null) { + $ec = $error_class; + } elseif (isset($this) && isset($this->_error_class)) { + $ec = $this->_error_class; + } else { + $ec = 'PEAR_Error'; + } + if (intval(PHP_VERSION) < 5) { + // little non-eval hack to fix bug #12147 + include 'PEAR/FixPHP5PEARWarnings.php'; + return $a; + } + if ($skipmsg) { + $a = new $ec($code, $mode, $options, $userinfo); + } else { + $a = new $ec($message, $code, $mode, $options, $userinfo); + } + return $a; + } + + // }}} + // {{{ throwError() + + /** + * Simpler form of raiseError with fewer options. In most cases + * message, code and userinfo are enough. + * + * @param string $message + * + */ + function &throwError($message = null, + $code = null, + $userinfo = null) + { + if (isset($this) && is_a($this, 'PEAR')) { + $a = &$this->raiseError($message, $code, null, null, $userinfo); + return $a; + } else { + $a = &PEAR::raiseError($message, $code, null, null, $userinfo); + return $a; + } + } + + // }}} + function staticPushErrorHandling($mode, $options = null) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $def_mode = &$GLOBALS['_PEAR_default_error_mode']; + $def_options = &$GLOBALS['_PEAR_default_error_options']; + $stack[] = array($def_mode, $def_options); + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $def_mode = $mode; + $def_options = $options; + break; + + case PEAR_ERROR_CALLBACK: + $def_mode = $mode; + // class/object method callback + if (is_callable($options)) { + $def_options = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + $stack[] = array($mode, $options); + return true; + } + + function staticPopErrorHandling() + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $setmode = &$GLOBALS['_PEAR_default_error_mode']; + $setoptions = &$GLOBALS['_PEAR_default_error_options']; + array_pop($stack); + list($mode, $options) = $stack[sizeof($stack) - 1]; + array_pop($stack); + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $setmode = $mode; + $setoptions = $options; + break; + + case PEAR_ERROR_CALLBACK: + $setmode = $mode; + // class/object method callback + if (is_callable($options)) { + $setoptions = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + return true; + } + + // {{{ pushErrorHandling() + + /** + * Push a new error handler on top of the error handler options stack. With this + * you can easily override the actual error handler for some code and restore + * it later with popErrorHandling. + * + * @param mixed $mode (same as setErrorHandling) + * @param mixed $options (same as setErrorHandling) + * + * @return bool Always true + * + * @see PEAR::setErrorHandling + */ + function pushErrorHandling($mode, $options = null) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + if (isset($this) && is_a($this, 'PEAR')) { + $def_mode = &$this->_default_error_mode; + $def_options = &$this->_default_error_options; + } else { + $def_mode = &$GLOBALS['_PEAR_default_error_mode']; + $def_options = &$GLOBALS['_PEAR_default_error_options']; + } + $stack[] = array($def_mode, $def_options); + + if (isset($this) && is_a($this, 'PEAR')) { + $this->setErrorHandling($mode, $options); + } else { + PEAR::setErrorHandling($mode, $options); + } + $stack[] = array($mode, $options); + return true; + } + + // }}} + // {{{ popErrorHandling() + + /** + * Pop the last error handler used + * + * @return bool Always true + * + * @see PEAR::pushErrorHandling + */ + function popErrorHandling() + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + array_pop($stack); + list($mode, $options) = $stack[sizeof($stack) - 1]; + array_pop($stack); + if (isset($this) && is_a($this, 'PEAR')) { + $this->setErrorHandling($mode, $options); + } else { + PEAR::setErrorHandling($mode, $options); + } + return true; + } + + // }}} + // {{{ loadExtension() + + /** + * OS independant PHP extension load. Remember to take care + * on the correct extension name for case sensitive OSes. + * + * @param string $ext The extension name + * @return bool Success or not on the dl() call + */ + function loadExtension($ext) + { + if (!extension_loaded($ext)) { + // if either returns true dl() will produce a FATAL error, stop that + if ((ini_get('enable_dl') != 1) || (ini_get('safe_mode') == 1)) { + return false; + } + if (OS_WINDOWS) { + $suffix = '.dll'; + } elseif (PHP_OS == 'HP-UX') { + $suffix = '.sl'; + } elseif (PHP_OS == 'AIX') { + $suffix = '.a'; + } elseif (PHP_OS == 'OSX') { + $suffix = '.bundle'; + } else { + $suffix = '.so'; + } + return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); + } + return true; + } + + // }}} +} + +// {{{ _PEAR_call_destructors() + +function _PEAR_call_destructors() +{ + global $_PEAR_destructor_object_list; + if (is_array($_PEAR_destructor_object_list) && + sizeof($_PEAR_destructor_object_list)) + { + reset($_PEAR_destructor_object_list); + if (PEAR::getStaticProperty('PEAR', 'destructlifo')) { + $_PEAR_destructor_object_list = array_reverse($_PEAR_destructor_object_list); + } + while (list($k, $objref) = each($_PEAR_destructor_object_list)) { + $classname = get_class($objref); + while ($classname) { + $destructor = "_$classname"; + if (method_exists($objref, $destructor)) { + $objref->$destructor(); + break; + } else { + $classname = get_parent_class($classname); + } + } + } + // Empty the object list to ensure that destructors are + // not called more than once. + $_PEAR_destructor_object_list = array(); + } + + // Now call the shutdown functions + if (is_array($GLOBALS['_PEAR_shutdown_funcs']) AND !empty($GLOBALS['_PEAR_shutdown_funcs'])) { + foreach ($GLOBALS['_PEAR_shutdown_funcs'] as $value) { + call_user_func_array($value[0], $value[1]); + } + } +} + +// }}} +/** + * Standard PEAR error class for PHP 4 + * + * This class is supserseded by {@link PEAR_Exception} in PHP 5 + * + * @category pear + * @package PEAR + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Gregory Beaver <cellog@php.net> + * @copyright 1997-2006 The PHP Group + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: 1.7.2 + * @link http://pear.php.net/manual/en/core.pear.pear-error.php + * @see PEAR::raiseError(), PEAR::throwError() + * @since Class available since PHP 4.0.2 + */ +class PEAR_Error +{ + // {{{ properties + + var $error_message_prefix = ''; + var $mode = PEAR_ERROR_RETURN; + var $level = E_USER_NOTICE; + var $code = -1; + var $message = ''; + var $userinfo = ''; + var $backtrace = null; + + // }}} + // {{{ constructor + + /** + * PEAR_Error constructor + * + * @param string $message message + * + * @param int $code (optional) error code + * + * @param int $mode (optional) error mode, one of: PEAR_ERROR_RETURN, + * PEAR_ERROR_PRINT, PEAR_ERROR_DIE, PEAR_ERROR_TRIGGER, + * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION + * + * @param mixed $options (optional) error level, _OR_ in the case of + * PEAR_ERROR_CALLBACK, the callback function or object/method + * tuple. + * + * @param string $userinfo (optional) additional user/debug info + * + * @access public + * + */ + function PEAR_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + if ($mode === null) { + $mode = PEAR_ERROR_RETURN; + } + $this->message = $message; + $this->code = $code; + $this->mode = $mode; + $this->userinfo = $userinfo; + if (!PEAR::getStaticProperty('PEAR_Error', 'skiptrace')) { + $this->backtrace = debug_backtrace(); + if (isset($this->backtrace[0]) && isset($this->backtrace[0]['object'])) { + unset($this->backtrace[0]['object']); + } + } + if ($mode & PEAR_ERROR_CALLBACK) { + $this->level = E_USER_NOTICE; + $this->callback = $options; + } else { + if ($options === null) { + $options = E_USER_NOTICE; + } + $this->level = $options; + $this->callback = null; + } + if ($this->mode & PEAR_ERROR_PRINT) { + if (is_null($options) || is_int($options)) { + $format = "%s"; + } else { + $format = $options; + } + printf($format, $this->getMessage()); + } + if ($this->mode & PEAR_ERROR_TRIGGER) { + trigger_error($this->getMessage(), $this->level); + } + if ($this->mode & PEAR_ERROR_DIE) { + $msg = $this->getMessage(); + if (is_null($options) || is_int($options)) { + $format = "%s"; + if (substr($msg, -1) != "\n") { + $msg .= "\n"; + } + } else { + $format = $options; + } + die(sprintf($format, $msg)); + } + if ($this->mode & PEAR_ERROR_CALLBACK) { + if (is_callable($this->callback)) { + call_user_func($this->callback, $this); + } + } + if ($this->mode & PEAR_ERROR_EXCEPTION) { + trigger_error("PEAR_ERROR_EXCEPTION is obsolete, use class PEAR_Exception for exceptions", E_USER_WARNING); + eval('$e = new Exception($this->message, $this->code);throw($e);'); + } + } + + // }}} + // {{{ getMode() + + /** + * Get the error mode from an error object. + * + * @return int error mode + * @access public + */ + function getMode() { + return $this->mode; + } + + // }}} + // {{{ getCallback() + + /** + * Get the callback function/method from an error object. + * + * @return mixed callback function or object/method array + * @access public + */ + function getCallback() { + return $this->callback; + } + + // }}} + // {{{ getMessage() + + + /** + * Get the error message from an error object. + * + * @return string full error message + * @access public + */ + function getMessage() + { + return ($this->error_message_prefix . $this->message); + } + + + // }}} + // {{{ getCode() + + /** + * Get error code from an error object + * + * @return int error code + * @access public + */ + function getCode() + { + return $this->code; + } + + // }}} + // {{{ getType() + + /** + * Get the name of this error/exception. + * + * @return string error/exception name (type) + * @access public + */ + function getType() + { + return get_class($this); + } + + // }}} + // {{{ getUserInfo() + + /** + * Get additional user-supplied information. + * + * @return string user-supplied information + * @access public + */ + function getUserInfo() + { + return $this->userinfo; + } + + // }}} + // {{{ getDebugInfo() + + /** + * Get additional debug information supplied by the application. + * + * @return string debug information + * @access public + */ + function getDebugInfo() + { + return $this->getUserInfo(); + } + + // }}} + // {{{ getBacktrace() + + /** + * Get the call backtrace from where the error was generated. + * Supported with PHP 4.3.0 or newer. + * + * @param int $frame (optional) what frame to fetch + * @return array Backtrace, or NULL if not available. + * @access public + */ + function getBacktrace($frame = null) + { + if (defined('PEAR_IGNORE_BACKTRACE')) { + return null; + } + if ($frame === null) { + return $this->backtrace; + } + return $this->backtrace[$frame]; + } + + // }}} + // {{{ addUserInfo() + + function addUserInfo($info) + { + if (empty($this->userinfo)) { + $this->userinfo = $info; + } else { + $this->userinfo .= " ** $info"; + } + } + + // }}} + // {{{ toString() + function __toString() + { + return $this->getMessage(); + } + // }}} + // {{{ toString() + + /** + * Make a string representation of this object. + * + * @return string a string with an object summary + * @access public + */ + function toString() { + $modes = array(); + $levels = array(E_USER_NOTICE => 'notice', + E_USER_WARNING => 'warning', + E_USER_ERROR => 'error'); + if ($this->mode & PEAR_ERROR_CALLBACK) { + if (is_array($this->callback)) { + $callback = (is_object($this->callback[0]) ? + strtolower(get_class($this->callback[0])) : + $this->callback[0]) . '::' . + $this->callback[1]; + } else { + $callback = $this->callback; + } + return sprintf('[%s: message="%s" code=%d mode=callback '. + 'callback=%s prefix="%s" info="%s"]', + strtolower(get_class($this)), $this->message, $this->code, + $callback, $this->error_message_prefix, + $this->userinfo); + } + if ($this->mode & PEAR_ERROR_PRINT) { + $modes[] = 'print'; + } + if ($this->mode & PEAR_ERROR_TRIGGER) { + $modes[] = 'trigger'; + } + if ($this->mode & PEAR_ERROR_DIE) { + $modes[] = 'die'; + } + if ($this->mode & PEAR_ERROR_RETURN) { + $modes[] = 'return'; + } + return sprintf('[%s: message="%s" code=%d mode=%s level=%s '. + 'prefix="%s" info="%s"]', + strtolower(get_class($this)), $this->message, $this->code, + implode("|", $modes), $levels[$this->level], + $this->error_message_prefix, + $this->userinfo); + } + + // }}} +} + +/* + * Local Variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ +?> diff --git a/extlib/XMPPHP/Exception.php b/extlib/XMPPHP/Exception.php new file mode 100644 index 000000000..32b2e0924 --- /dev/null +++ b/extlib/XMPPHP/Exception.php @@ -0,0 +1,39 @@ +<?php +/** + * XMPPHP: The PHP XMPP Library + * Copyright (C) 2008 Nathanael C. Fritz + * This file is part of SleekXMPP. + * + * XMPPHP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * XMPPHP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with XMPPHP; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category xmpphp + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + */ + +/** + * XMPPHP Exception + * + * @category xmpphp + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + * @version $Id$ + */ +class XMPPHP_Exception extends Exception { +} diff --git a/extlib/XMPPHP/Log.php b/extlib/XMPPHP/Log.php new file mode 100644 index 000000000..635e68da4 --- /dev/null +++ b/extlib/XMPPHP/Log.php @@ -0,0 +1,116 @@ +<?php +/** + * XMPPHP: The PHP XMPP Library + * Copyright (C) 2008 Nathanael C. Fritz + * This file is part of SleekXMPP. + * + * XMPPHP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * XMPPHP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with XMPPHP; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category xmpphp + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + */ + +/** + * XMPPHP Log + * + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + * @version $Id$ + */ +class XMPPHP_Log { + + const LEVEL_ERROR = 0; + const LEVEL_WARNING = 1; + const LEVEL_INFO = 2; + const LEVEL_DEBUG = 3; + const LEVEL_VERBOSE = 4; + + /** + * @var array + */ + protected $data = array(); + + /** + * @var array + */ + protected $names = array('ERROR', 'WARNING', 'INFO', 'DEBUG', 'VERBOSE'); + + /** + * @var integer + */ + protected $runlevel; + + /** + * @var boolean + */ + protected $printout; + + /** + * Constructor + * + * @param boolean $printout + * @param string $runlevel + */ + public function __construct($printout = false, $runlevel = self::LEVEL_INFO) { + $this->printout = (boolean)$printout; + $this->runlevel = (int)$runlevel; + } + + /** + * Add a message to the log data array + * If printout in this instance is set to true, directly output the message + * + * @param string $msg + * @param integer $runlevel + */ + public function log($msg, $runlevel = self::LEVEL_INFO) { + $time = time(); + $this->data[] = array($this->runlevel, $msg, $time); + if($this->printout and $runlevel <= $this->runlevel) { + $this->writeLine($msg, $runlevel, $time); + } + } + + /** + * Output the complete log. + * Log will be cleared if $clear = true + * + * @param boolean $clear + * @param integer $runlevel + */ + public function printout($clear = true, $runlevel = null) { + if($runlevel === null) { + $runlevel = $this->runlevel; + } + foreach($this->data as $data) { + if($runlevel <= $data[0]) { + $this->writeLine($data[1], $runlevel, $data[2]); + } + } + if($clear) { + $this->data = array(); + } + } + + protected function writeLine($msg, $runlevel, $time) { + //echo date('Y-m-d H:i:s', $time)." [".$this->names[$runlevel]."]: ".$msg."\n"; + echo $time." [".$this->names[$runlevel]."]: ".$msg."\n"; + } +} diff --git a/extlib/XMPPHP/XMLObj.php b/extlib/XMPPHP/XMLObj.php new file mode 100644 index 000000000..79fef9b24 --- /dev/null +++ b/extlib/XMPPHP/XMLObj.php @@ -0,0 +1,155 @@ +<?php +/** + * XMPPHP: The PHP XMPP Library + * Copyright (C) 2008 Nathanael C. Fritz + * This file is part of SleekXMPP. + * + * XMPPHP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * XMPPHP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with XMPPHP; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category xmpphp + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + */ + +/** + * XMPPHP XML Object + * + * @category xmpphp + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + * @version $Id$ + */ +class XMPPHP_XMLObj { + /** + * Tag name + * + * @var string + */ + public $name; + + /** + * Namespace + * + * @var string + */ + public $ns; + + /** + * Attributes + * + * @var array + */ + public $attrs = array(); + + /** + * Subs? + * + * @var array + */ + public $subs = array(); + + /** + * Node data + * + * @var string + */ + public $data = ''; + + /** + * Constructor + * + * @param string $name + * @param string $ns + * @param array $attrs + * @param string $data + */ + public function __construct($name, $ns = '', $attrs = array(), $data = '') { + $this->name = strtolower($name); + $this->ns = $ns; + if(is_array($attrs) && count($attrs)) { + foreach($attrs as $key => $value) { + $this->attrs[strtolower($key)] = $value; + } + } + $this->data = $data; + } + + /** + * Dump this XML Object to output. + * + * @param integer $depth + */ + public function printObj($depth = 0) { + print str_repeat("\t", $depth) . $this->name . " " . $this->ns . ' ' . $this->data; + print "\n"; + foreach($this->subs as $sub) { + $sub->printObj($depth + 1); + } + } + + /** + * Return this XML Object in xml notation + * + * @param string $str + */ + public function toString($str = '') { + $str .= "<{$this->name} xmlns='{$this->ns}' "; + foreach($this->attrs as $key => $value) { + if($key != 'xmlns') { + $value = htmlspecialchars($value); + $str .= "$key='$value' "; + } + } + $str .= ">"; + foreach($this->subs as $sub) { + $str .= $sub->toString(); + } + $body = htmlspecialchars($this->data); + $str .= "$body</{$this->name}>"; + return $str; + } + + /** + * Has this XML Object the given sub? + * + * @param string $name + * @return boolean + */ + public function hasSub($name) { + foreach($this->subs as $sub) { + if($sub->name == $name) return true; + } + return false; + } + + /** + * Return a sub + * + * @param string $name + * @param string $attrs + * @param string $ns + */ + public function sub($name, $attrs = null, $ns = null) { + foreach($this->subs as $sub) { + if($sub->name == $name) { + return $sub; + } + } + } +} diff --git a/extlib/XMPPHP/XMLStream.php b/extlib/XMPPHP/XMLStream.php new file mode 100644 index 000000000..6f4ca6743 --- /dev/null +++ b/extlib/XMPPHP/XMLStream.php @@ -0,0 +1,619 @@ +<?php +/** + * XMPPHP: The PHP XMPP Library + * Copyright (C) 2008 Nathanael C. Fritz + * This file is part of SleekXMPP. + * + * XMPPHP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * XMPPHP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with XMPPHP; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category xmpphp + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + */ + +/** XMPPHP_Exception */ +require_once 'Exception.php'; + +/** XMPPHP_XMLObj */ +require_once 'XMLObj.php'; + +/** XMPPHP_Log */ +require_once 'Log.php'; + +/** + * XMPPHP XML Stream + * + * @category xmpphp + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + * @version $Id$ + */ +class XMPPHP_XMLStream { + /** + * @var resource + */ + protected $socket; + /** + * @var resource + */ + protected $parser; + /** + * @var string + */ + protected $buffer; + /** + * @var integer + */ + protected $xml_depth = 0; + /** + * @var string + */ + protected $host; + /** + * @var integer + */ + protected $port; + /** + * @var string + */ + protected $stream_start = '<stream>'; + /** + * @var string + */ + protected $stream_end = '</stream>'; + /** + * @var boolean + */ + protected $disconnected = false; + /** + * @var boolean + */ + protected $sent_disconnect = false; + /** + * @var array + */ + protected $ns_map = array(); + /** + * @var array + */ + protected $current_ns = array(); + /** + * @var array + */ + protected $xmlobj = null; + /** + * @var array + */ + protected $nshandlers = array(); + /** + * @var array + */ + protected $idhandlers = array(); + /** + * @var array + */ + protected $eventhandlers = array(); + /** + * @var integer + */ + protected $lastid = 0; + /** + * @var string + */ + protected $default_ns; + /** + * @var string + */ + protected $until = ''; + /** + * @var array + */ + protected $until_happened = false; + /** + * @var array + */ + protected $until_payload = array(); + /** + * @var XMPPHP_Log + */ + protected $log; + /** + * @var boolean + */ + protected $reconnect = true; + /** + * @var boolean + */ + protected $been_reset = false; + /** + * @var boolean + */ + protected $is_server; + /** + * @var float + */ + protected $last_send = 0; + /** + * @var boolean + */ + protected $use_ssl = false; + + /** + * Constructor + * + * @param string $host + * @param string $port + * @param boolean $printlog + * @param string $loglevel + * @param boolean $is_server + */ + public function __construct($host = null, $port = null, $printlog = false, $loglevel = null, $is_server = false) { + $this->reconnect = !$is_server; + $this->is_server = $is_server; + $this->host = $host; + $this->port = $port; + $this->setupParser(); + $this->log = new XMPPHP_Log($printlog, $loglevel); + } + + /** + * Destructor + * Cleanup connection + */ + public function __destruct() { + if(!$this->disconnected && $this->socket) { + $this->disconnect(); + } + } + + /** + * Return the log instance + * + * @return XMPPHP_Log + */ + public function getLog() { + return $this->log; + } + + /** + * Get next ID + * + * @return integer + */ + public function getId() { + $this->lastid++; + return $this->lastid; + } + + /** + * Set SSL + * + * @return integer + */ + public function useSSL($use=true) { + $this->use_ssl = $use; + } + + /** + * Add ID Handler + * + * @param integer $id + * @param string $pointer + * @param string $obj + */ + public function addIdHandler($id, $pointer, $obj = null) { + $this->idhandlers[$id] = array($pointer, $obj); + } + + /** + * Add Handler + * + * @param integer $id + * @param string $ns + * @param string $pointer + * @param string $obj + * @param integer $depth + */ + public function addHandler($name, $ns, $pointer, $obj = null, $depth = 1) { + $this->nshandlers[] = array($name,$ns,$pointer,$obj, $depth); + } + + /** + * Add Evemt Handler + * + * @param integer $id + * @param string $pointer + * @param string $obj + */ + public function addEventHandler($name, $pointer, $obj) { + $this->eventhanders[] = array($name, $pointer, $obj); + } + + /** + * Connect to XMPP Host + * + * @param integer $timeout + * @param boolean $persistent + * @param boolean $sendinit + */ + public function connect($timeout = 30, $persistent = false, $sendinit = true) { + $this->disconnected = false; + $this->sent_disconnect = false; + if($persistent) { + $conflag = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT; + } else { + $conflag = STREAM_CLIENT_CONNECT; + } + $conntype = 'tcp'; + if($this->use_ssl) $conntype = 'ssl'; + $this->log->log("Connecting to $conntype://{$this->host}:{$this->port}"); + try { + $this->socket = @stream_socket_client("$conntype://{$this->host}:{$this->port}", $errno, $errstr, $timeout, $conflag); + } catch (Exception $e) { + throw new XMPPHP_Exception($e->getMessage()); + } + if(!$this->socket) { + $this->log->log("Could not connect.", XMPPHP_Log::LEVEL_ERROR); + $this->disconnected = true; + + throw new XMPPHP_Exception('Could not connect.'); + } + stream_set_blocking($this->socket, 1); + if($sendinit) $this->send($this->stream_start); + } + + /** + * Reconnect XMPP Host + */ + public function doReconnect() { + if(!$this->is_server) { + $this->log->log("Reconnecting...", XMPPHP_Log::LEVEL_WARNING); + $this->connect(30, false, false); + $this->reset(); + } + } + + /** + * Disconnect from XMPP Host + */ + public function disconnect() { + $this->log->log("Disconnecting...", XMPPHP_Log::LEVEL_VERBOSE); + $this->reconnect = false; + $this->send($this->stream_end); + $this->sent_disconnect = true; + $this->processUntil('end_stream', 5); + $this->disconnected = true; + } + + /** + * Are we are disconnected? + * + * @return boolean + */ + public function isDisconnected() { + return $this->disconnected; + } + + private function __process() { + $read = array($this->socket); + $write = null; + $except = null; + $updated = @stream_select($read, $write, $except, 1); + if ($updated > 0) { + $buff = @fread($this->socket, 1024); + if(!$buff) { + if($this->reconnect) { + $this->doReconnect(); + } else { + fclose($this->socket); + return false; + } + } + $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE); + xml_parse($this->parser, $buff, false); + } + } + + /** + * Process + * + * @return string + */ + public function process() { + $updated = ''; + while(!$this->disconnect) { + $this->__process(); + } + } + + /** + * Process until a timeout occurs + * + * @param integer $timeout + * @return string + */ + public function processTime($timeout = -1) { + $start = time(); + $updated = ''; + while(!$this->disconnected and ($timeout == -1 or time() - $start < $timeout)) { + $this->__process(); + } + } + + /** + * Process until a specified event or a timeout occurs + * + * @param string|array $event + * @param integer $timeout + * @return string + */ + public function processUntil($event, $timeout=-1) { + $start = time(); + if(!is_array($event)) $event = array($event); + $this->until[] = $event; + end($this->until); + $event_key = key($this->until); + reset($this->until); + $updated = ''; + while(!$this->disconnected and $this->until[$event_key] and (time() - $start < $timeout or $timeout == -1)) { + $this->__process(); + } + if(array_key_exists($event_key, $this->until_payload)) { + $payload = $this->until_payload[$event_key]; + } else { + $payload = array(); + } + unset($this->until_payload[$event_key]); + return $payload; + } + + /** + * Obsolete? + */ + public function Xapply_socket($socket) { + $this->socket = $socket; + } + + /** + * XML start callback + * + * @see xml_set_element_handler + * + * @param resource $parser + * @param string $name + */ + public function startXML($parser, $name, $attr) { + if($this->been_reset) { + $this->been_reset = false; + $this->xml_depth = 0; + } + $this->xml_depth++; + if(array_key_exists('XMLNS', $attr)) { + $this->current_ns[$this->xml_depth] = $attr['XMLNS']; + } else { + $this->current_ns[$this->xml_depth] = $this->current_ns[$this->xml_depth - 1]; + if(!$this->current_ns[$this->xml_depth]) $this->current_ns[$this->xml_depth] = $this->default_ns; + } + $ns = $this->current_ns[$this->xml_depth]; + foreach($attr as $key => $value) { + if(strstr($key, ":")) { + $key = explode(':', $key); + $key = $key[1]; + $this->ns_map[$key] = $value; + } + } + if(!strstr($name, ":") === false) + { + $name = explode(':', $name); + $ns = $this->ns_map[$name[0]]; + $name = $name[1]; + } + $obj = new XMPPHP_XMLObj($name, $ns, $attr); + if($this->xml_depth > 1) { + $this->xmlobj[$this->xml_depth - 1]->subs[] = $obj; + } + $this->xmlobj[$this->xml_depth] = $obj; + } + + /** + * XML end callback + * + * @see xml_set_element_handler + * + * @param resource $parser + * @param string $name + */ + public function endXML($parser, $name) { + #$this->log->log("Ending $name", XMPPHP_Log::LEVEL_DEBUG); + #print "$name\n"; + if($this->been_reset) { + $this->been_reset = false; + $this->xml_depth = 0; + } + $this->xml_depth--; + if($this->xml_depth == 1) { + #clean-up old objects + $found = false; + foreach($this->nshandlers as $handler) { + if($handler[4] != 1 and $this->xmlobj[2]->hasSub($handler[0])) { + $searchxml = $this->xmlobj[2]->sub($handler[0]); + } elseif(is_array($this->xmlobj) and array_key_exists(2, $this->xmlobj)) { + $searchxml = $this->xmlobj[2]; + } + if($searchxml !== null and $searchxml->name == $handler[0] and ($searchxml->ns == $handler[1] or (!$handler[1] and $searchxml->ns == $this->default_ns))) { + if($handler[3] === null) $handler[3] = $this; + $this->log->log("Calling {$handler[2]}", XMPPHP_Log::LEVEL_DEBUG); + $handler[3]->$handler[2]($this->xmlobj[2]); + } + } + foreach($this->idhandlers as $id => $handler) { + if(array_key_exists('id', $this->xmlobj[2]->attrs) and $this->xmlobj[2]->attrs['id'] == $id) { + if($handler[1] === null) $handler[1] = $this; + $handler[1]->$handler[0]($this->xmlobj[2]); + #id handlers are only used once + unset($this->idhandlers[$id]); + break; + } + } + if(is_array($this->xmlobj)) { + $this->xmlobj = array_slice($this->xmlobj, 0, 1); + if(isset($this->xmlobj[0]) && $this->xmlobj[0] instanceof XMPPHP_XMLObj) { + $this->xmlobj[0]->subs = null; + } + } + unset($this->xmlobj[2]); + } + if($this->xml_depth == 0 and !$this->been_reset) { + if(!$this->disconnected) { + if(!$this->sent_disconnect) { + $this->send($this->stream_end); + } + $this->disconnected = true; + $this->sent_disconnect = true; + fclose($this->socket); + if($this->reconnect) { + $this->doReconnect(); + } + } + $this->event('end_stream'); + } + } + + /** + * XML character callback + * @see xml_set_character_data_handler + * + * @param resource $parser + * @param string $data + */ + public function charXML($parser, $data) { + if(array_key_exists($this->xml_depth, $this->xmlobj)) { + $this->xmlobj[$this->xml_depth]->data .= $data; + } + } + + /** + * Event? + * + * @param string $name + * @param string $payload + */ + public function event($name, $payload = null) { + $this->log->log("EVENT: $name", XMPPHP_Log::LEVEL_DEBUG); + foreach($this->eventhandlers as $handler) { + if($name == $handler[0]) { + if($handler[2] === null) { + $handler[2] = $this; + } + $handler[2]->$handler[1]($payload); + } + } + foreach($this->until as $key => $until) { + if(is_array($until)) { + if(in_array($name, $until)) { + $this->until_payload[$key][] = array($name, $payload); + $this->until[$key] = false; + } + } + } + } + + /** + * Read from socket + */ + public function read() { + $buff = @fread($this->socket, 1024); + if(!$buff) { + if($this->reconnect) { + $this->doReconnect(); + } else { + fclose($this->socket); + return false; + } + } + $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE); + xml_parse($this->parser, $buff, false); + } + + /** + * Send to socket + * + * @param string $msg + */ + public function send($msg, $rec=false) { + if($this->time() - $this->last_send < .1) { + usleep(100000); + } + $wait = true; + while($wait) { + $read = null; + $write = array($this->socket); + $except = null; + $select = @stream_select($read, $write, $except, 0, 0); + if($select === False) { + $this->doReconnect(); + return false; + } elseif ($select > 0) { + $wait = false; + } else { + usleep(100000); + //$this->processTime(.25); + } + } + $sentbytes = @fwrite($this->socket, $msg, 1024); + $this->last_send = $this->time(); + $this->log->log("SENT: " . mb_substr($msg, 0, $sentbytes, '8bit'), XMPPHP_Log::LEVEL_VERBOSE); + if($sentbytes === FALSE) { + $this->doReconnect(); + } elseif ($sentbytes != mb_strlen($msg, '8bit')) { + $this->send(mb_substr($msg, $sentbytes, mb_strlen($msg, '8bit'), '8bit'), true); + } + } + + public function time() { + list($usec, $sec) = explode(" ", microtime()); + return (float)$sec + (float)$usec; + } + + /** + * Reset connection + */ + public function reset() { + $this->xml_depth = 0; + unset($this->xmlobj); + $this->xmlobj = array(); + $this->setupParser(); + if(!$this->is_server) { + $this->send($this->stream_start); + } + $this->been_reset = true; + } + + /** + * Setup the XML parser + */ + public function setupParser() { + $this->parser = xml_parser_create('UTF-8'); + xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 1); + xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, 'UTF-8'); + xml_set_object($this->parser, $this); + xml_set_element_handler($this->parser, 'startXML', 'endXML'); + xml_set_character_data_handler($this->parser, 'charXML'); + } +} diff --git a/extlib/XMPPHP/XMPP.php b/extlib/XMPPHP/XMPP.php new file mode 100644 index 000000000..a69a647b0 --- /dev/null +++ b/extlib/XMPPHP/XMPP.php @@ -0,0 +1,321 @@ +<?php +/** + * XMPPHP: The PHP XMPP Library + * Copyright (C) 2008 Nathanael C. Fritz + * This file is part of SleekXMPP. + * + * XMPPHP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * XMPPHP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with XMPPHP; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category xmpphp + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + */ + +/** XMPPHP_XMLStream */ +require_once "XMLStream.php"; + +/** + * XMPPHP Main Class + * + * @category xmpphp + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + * @version $Id$ + */ +class XMPPHP_XMPP extends XMPPHP_XMLStream { + /** + * @var string + */ + protected $server; + + /** + * @var string + */ + protected $user; + + /** + * @var string + */ + protected $password; + + /** + * @var string + */ + protected $resource; + + /** + * @var string + */ + protected $fulljid; + + /** + * @var string + */ + protected $basejid; + + /** + * @var boolean + */ + protected $authed = false; + + /** + * @var boolean + */ + protected $auto_subscribe = false; + + /** + * @var boolean + */ + protected $use_encryption = true; + + /** + * Constructor + * + * @param string $host + * @param integer $port + * @param string $user + * @param string $password + * @param string $resource + * @param string $server + * @param boolean $printlog + * @param string $loglevel + */ + public function __construct($host, $port, $user, $password, $resource, $server = null, $printlog = false, $loglevel = null) { + parent::__construct($host, $port, $printlog, $loglevel); + + $this->user = $user; + $this->password = $password; + $this->resource = $resource; + if(!$server) $server = $host; + $this->basejid = $this->user . '@' . $this->host; + + $this->stream_start = '<stream:stream to="' . $server . '" xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0">'; + $this->stream_end = '</stream:stream>'; + $this->default_ns = 'jabber:client'; + + $this->addHandler('features', 'http://etherx.jabber.org/streams', 'features_handler'); + $this->addHandler('success', 'urn:ietf:params:xml:ns:xmpp-sasl', 'sasl_success_handler'); + $this->addHandler('failure', 'urn:ietf:params:xml:ns:xmpp-sasl', 'sasl_failure_handler'); + $this->addHandler('proceed', 'urn:ietf:params:xml:ns:xmpp-tls', 'tls_proceed_handler'); + $this->addHandler('message', 'jabber:client', 'message_handler'); + $this->addHandler('presence', 'jabber:client', 'presence_handler'); + } + + /** + * Turn encryption on/ff + * + * @param boolean $useEncryption + */ + public function useEncryption($useEncryption = true) { + $this->use_encryption = $useEncryption; + } + + /** + * Turn on auto-authorization of subscription requests. + * + * @param boolean $autoSubscribe + */ + public function autoSubscribe($autoSubscribe = true) { + $this->auto_subscribe = $autoSubscribe; + } + + /** + * Send XMPP Message + * + * @param string $to + * @param string $body + * @param string $type + * @param string $subject + */ + public function message($to, $body, $type = 'chat', $subject = null, $payload = null) { + $to = htmlspecialchars($to); + $body = htmlspecialchars($body); + $subject = htmlspecialchars($subject); + + $out = "<message from='{$this->fulljid}' to='$to' type='$type'>"; + if($subject) $out .= "<subject>$subject</subject>"; + $out .= "<body>$body</body>"; + if($payload) $out .= $payload; + $out .= "</message>"; + + $this->send($out); + } + + /** + * Set Presence + * + * @param string $status + * @param string $show + * @param string $to + */ + public function presence($status = null, $show = 'available', $to = null, $type='available') { + if($type == 'available') $type = ''; + $to = htmlspecialchars($to); + $status = htmlspecialchars($status); + if($show == 'unavailable') $type = 'unavailable'; + + $out = "<presence"; + if($to) $out .= " to='$to'"; + if($type) $out .= " type='$type'"; + if($show == 'available' and !$status) { + $out .= "/>"; + } else { + $out .= ">"; + if($show != 'available') $out .= "<show>$show</show>"; + if($status) $out .= "<status>$status</status>"; + $out .= "</presence>"; + } + + $this->send($out); + } + + /** + * Message handler + * + * @param string $xml + */ + public function message_handler($xml) { + if(isset($xml->attrs['type'])) { + $payload['type'] = $xml->attrs['type']; + } else { + $payload['type'] = 'chat'; + } + $payload['from'] = $xml->attrs['from']; + $payload['body'] = $xml->sub('body')->data; + $this->log->log("Message: {$xml->sub('body')->data}", XMPPHP_Log::LEVEL_DEBUG); + $this->event('message', $payload); + } + + /** + * Presence handler + * + * @param string $xml + */ + public function presence_handler($xml) { + $payload['type'] = (isset($xml->attrs['type'])) ? $xml->attrs['type'] : 'available'; + $payload['show'] = (isset($xml->sub('show')->data)) ? $xml->sub('show')->data : $payload['type']; + $payload['from'] = $xml->attrs['from']; + $payload['status'] = (isset($xml->sub('status')->data)) ? $xml->sub('status')->data : ''; + $this->log->log("Presence: {$payload['from']} [{$payload['show']}] {$payload['status']}", XMPPHP_Log::LEVEL_DEBUG); + if(array_key_exists('type', $xml->attrs) and $xml->attrs['type'] == 'subscribe') { + if($this->auto_subscribe) $this->send("<presence type='subscribed' to='{$xml->attrs['from']}' from='{$this->fulljid}' /><presence type='subscribe' to='{$xml->attrs['from']}' from='{$this->fulljid}' />"); + $this->event('subscription_requested', $payload); + } elseif(array_key_exists('type', $xml->attrs) and $xml->attrs['type'] == 'subscribed') { + $this->event('subscription_accepted', $payload); + } else { + $this->event('presence', $payload); + } + } + + /** + * Features handler + * + * @param string $xml + */ + protected function features_handler($xml) { + if($xml->hasSub('starttls') and $this->use_encryption) { + $this->send("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required /></starttls>"); + } elseif($xml->hasSub('bind')) { + $id = $this->getId(); + $this->addIdHandler($id, 'resource_bind_handler'); + $this->send("<iq xmlns=\"jabber:client\" type=\"set\" id=\"$id\"><bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"><resource>{$this->resource}</resource></bind></iq>"); + } else { + $this->log->log("Attempting Auth..."); + $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>" . base64_encode("\x00" . $this->user . "\x00" . $this->password) . "</auth>"); + } + } + + /** + * SASL success handler + * + * @param string $xml + */ + protected function sasl_success_handler($xml) { + $this->log->log("Auth success!"); + $this->authed = true; + $this->reset(); + } + + /** + * SASL feature handler + * + * @param string $xml + */ + protected function sasl_failure_handler($xml) { + $this->log->log("Auth failed!", XMPPHP_Log::LEVEL_ERROR); + $this->disconnect(); + + throw new XMPPHP_Exception('Auth failed!'); + } + + /** + * Resource bind handler + * + * @param string $xml + */ + protected function resource_bind_handler($xml) { + if($xml->attrs['type'] == 'result') { + $this->log->log("Bound to " . $xml->sub('bind')->sub('jid')->data); + $this->fulljid = $xml->sub('bind')->sub('jid')->data; + } + $id = $this->getId(); + $this->addIdHandler($id, 'session_start_handler'); + $this->send("<iq xmlns='jabber:client' type='set' id='$id'><session xmlns='urn:ietf:params:xml:ns:xmpp-session' /></iq>"); + } + + /** + * Retrieves the roster + * + */ + public function getRoster() { + $id = $this->getID(); + $this->addIdHandler($id, 'roster_get_handler'); + $this->send("<iq xmlns='jabber:client' type='get' id='$id'><query xmlns='jabber:iq:roster' /></iq>"); + } + + /** + * Roster retrieval handler + * + * @param string $xml + */ + protected function roster_get_handler($xml) { + // TODO: make this work + } + + /** + * Session start handler + * + * @param string $xml + */ + protected function session_start_handler($xml) { + $this->log->log("Session started"); + $this->event('session_start'); + } + + /** + * TLS proceed handler + * + * @param string $xml + */ + protected function tls_proceed_handler($xml) { + $this->log->log("Starting TLS encryption"); + stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_SSLv23_CLIENT); + $this->reset(); + } +} diff --git a/extlib/XMPPHP/XMPP_Old.php b/extlib/XMPPHP/XMPP_Old.php new file mode 100644 index 000000000..e5649effe --- /dev/null +++ b/extlib/XMPPHP/XMPP_Old.php @@ -0,0 +1,113 @@ +<?php +/** + * XMPPHP: The PHP XMPP Library + * Copyright (C) 2008 Nathanael C. Fritz + * This file is part of SleekXMPP. + * + * XMPPHP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * XMPPHP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with XMPPHP; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category xmpphp + * @package XMPPHP + * @author Nathanael C. Fritz <JID: fritzy@netflint.net> + * @author Stephan Wentz <JID: stephan@jabber.wentz.it> + * @copyright 2008 Nathanael C. Fritz + */ + +/** XMPPHP_XMPP + * + * This file is unnecessary unless you need to connect to older, non-XMPP-compliant servers like Dreamhost's. + * In this case, use instead of XMPPHP_XMPP, otherwise feel free to delete it. + * The old Jabber protocol wasn't standardized, so use at your own risk. + * + */ +require_once "XMPP.php"; + + class XMPPHP_XMPPOld extends XMPPHP_XMPP { + /** + * + * @var string + */ + protected $session_id; + + public function __construct($host, $port, $user, $password, $resource, $server = null, $printlog = false, $loglevel = null) { + parent::__construct($host, $port, $user, $password, $resource, $server, $printlog, $loglevel); + if(!$server) $server = $host; + $this->stream_start = '<stream:stream to="' . $server . '" xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">'; + $this->fulljid = "{$user}@{$server}/{$resource}"; + } + + /** + * Override XMLStream's startXML + * + * @param parser $parser + * @param string $name + * @param array $attr + */ + public function startXML($parser, $name, $attr) { + if($this->xml_depth == 0) { + $this->session_id = $attr['ID']; + $this->authenticate(); + } + parent::startXML($parser, $name, $attr); + } + + /** + * Send Authenticate Info Request + * + */ + public function authenticate() { + $id = $this->getId(); + $this->addidhandler($id, 'authfieldshandler'); + $this->send("<iq type='get' id='$id'><query xmlns='jabber:iq:auth'><username>{$this->user}</username></query></iq>"); + } + + /** + * Retrieve auth fields and send auth attempt + * + * @param XMLObj $xml + */ + public function authFieldsHandler($xml) { + $id = $this->getId(); + $this->addidhandler($id, 'oldAuthResultHandler'); + if($xml->sub('query')->hasSub('digest')) { + $hash = sha1($this->session_id . $this->password); + print "{$this->session_id} {$this->password}\n"; + $out = "<iq type='set' id='$id'><query xmlns='jabber:iq:auth'><username>{$this->user}</username><digest>{$hash}</digest><resource>{$this->resource}</resource></query></iq>"; + } else { + $out = "<iq type='set' id='$id'><query xmlns='jabber:iq:auth'><username>{$this->user}</username><password>{$this->password}</password><resource>{$this->resource}</resource></query></iq>"; + } + $this->send($out); + + } + + /** + * Determine authenticated or failure + * + * @param XMLObj $xml + */ + public function oldAuthResultHandler($xml) { + if($xml->attrs['type'] != 'result') { + $this->log->log("Auth failed!", XMPPHP_Log::LEVEL_ERROR); + $this->disconnect(); + throw new XMPPHP_Exception('Auth failed!'); + } else { + $this->log->log("Session started"); + $this->event('session_start'); + } + } + } + + +?> diff --git a/extlib/markdown.php b/extlib/markdown.php new file mode 100644 index 000000000..8179b568b --- /dev/null +++ b/extlib/markdown.php @@ -0,0 +1,1710 @@ +<?php +# +# Markdown - A text-to-HTML conversion tool for web writers +# +# PHP Markdown +# Copyright (c) 2004-2008 Michel Fortin +# <http://www.michelf.com/projects/php-markdown/> +# +# Original Markdown +# Copyright (c) 2004-2006 John Gruber +# <http://daringfireball.net/projects/markdown/> +# + + +define( 'MARKDOWN_VERSION', "1.0.1m" ); # Sat 21 Jun 2008 + + +# +# Global default settings: +# + +# Change to ">" for HTML output +@define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />"); + +# Define the width of a tab for code blocks. +@define( 'MARKDOWN_TAB_WIDTH', 4 ); + + +# +# WordPress settings: +# + +# Change to false to remove Markdown from posts and/or comments. +@define( 'MARKDOWN_WP_POSTS', true ); +@define( 'MARKDOWN_WP_COMMENTS', true ); + + + +### Standard Function Interface ### + +@define( 'MARKDOWN_PARSER_CLASS', 'Markdown_Parser' ); + +function Markdown($text) { +# +# Initialize the parser and return the result of its transform method. +# + # Setup static parser variable. + static $parser; + if (!isset($parser)) { + $parser_class = MARKDOWN_PARSER_CLASS; + $parser = new $parser_class; + } + + # Transform text using parser. + return $parser->transform($text); +} + + +### WordPress Plugin Interface ### + +/* +Plugin Name: Markdown +Plugin URI: http://www.michelf.com/projects/php-markdown/ +Description: <a href="http://daringfireball.net/projects/markdown/syntax">Markdown syntax</a> allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by <a href="http://daringfireball.net/">John Gruber</a>. <a href="http://www.michelf.com/projects/php-markdown/">More...</a> +Version: 1.0.1m +Author: Michel Fortin +Author URI: http://www.michelf.com/ +*/ + +if (isset($wp_version)) { + # More details about how it works here: + # <http://www.michelf.com/weblog/2005/wordpress-text-flow-vs-markdown/> + + # Post content and excerpts + # - Remove WordPress paragraph generator. + # - Run Markdown on excerpt, then remove all tags. + # - Add paragraph tag around the excerpt, but remove it for the excerpt rss. + if (MARKDOWN_WP_POSTS) { + remove_filter('the_content', 'wpautop'); + remove_filter('the_content_rss', 'wpautop'); + remove_filter('the_excerpt', 'wpautop'); + add_filter('the_content', 'Markdown', 6); + add_filter('the_content_rss', 'Markdown', 6); + add_filter('get_the_excerpt', 'Markdown', 6); + add_filter('get_the_excerpt', 'trim', 7); + add_filter('the_excerpt', 'mdwp_add_p'); + add_filter('the_excerpt_rss', 'mdwp_strip_p'); + + remove_filter('content_save_pre', 'balanceTags', 50); + remove_filter('excerpt_save_pre', 'balanceTags', 50); + add_filter('the_content', 'balanceTags', 50); + add_filter('get_the_excerpt', 'balanceTags', 9); + } + + # Comments + # - Remove WordPress paragraph generator. + # - Remove WordPress auto-link generator. + # - Scramble important tags before passing them to the kses filter. + # - Run Markdown on excerpt then remove paragraph tags. + if (MARKDOWN_WP_COMMENTS) { + remove_filter('comment_text', 'wpautop', 30); + remove_filter('comment_text', 'make_clickable'); + add_filter('pre_comment_content', 'Markdown', 6); + add_filter('pre_comment_content', 'mdwp_hide_tags', 8); + add_filter('pre_comment_content', 'mdwp_show_tags', 12); + add_filter('get_comment_text', 'Markdown', 6); + add_filter('get_comment_excerpt', 'Markdown', 6); + add_filter('get_comment_excerpt', 'mdwp_strip_p', 7); + + global $mdwp_hidden_tags, $mdwp_placeholders; + $mdwp_hidden_tags = explode(' ', + '<p> </p> <pre> </pre> <ol> </ol> <ul> </ul> <li> </li>'); + $mdwp_placeholders = explode(' ', str_rot13( + 'pEj07ZbbBZ U1kqgh4w4p pre2zmeN6K QTi31t9pre ol0MP1jzJR '. + 'ML5IjmbRol ulANi1NsGY J7zRLJqPul liA8ctl16T K9nhooUHli')); + } + + function mdwp_add_p($text) { + if (!preg_match('{^$|^<(p|ul|ol|dl|pre|blockquote)>}i', $text)) { + $text = '<p>'.$text.'</p>'; + $text = preg_replace('{\n{2,}}', "</p>\n\n<p>", $text); + } + return $text; + } + + function mdwp_strip_p($t) { return preg_replace('{</?p>}i', '', $t); } + + function mdwp_hide_tags($text) { + global $mdwp_hidden_tags, $mdwp_placeholders; + return str_replace($mdwp_hidden_tags, $mdwp_placeholders, $text); + } + function mdwp_show_tags($text) { + global $mdwp_hidden_tags, $mdwp_placeholders; + return str_replace($mdwp_placeholders, $mdwp_hidden_tags, $text); + } +} + + +### bBlog Plugin Info ### + +function identify_modifier_markdown() { + return array( + 'name' => 'markdown', + 'type' => 'modifier', + 'nicename' => 'Markdown', + 'description' => 'A text-to-HTML conversion tool for web writers', + 'authors' => 'Michel Fortin and John Gruber', + 'licence' => 'BSD-like', + 'version' => MARKDOWN_VERSION, + 'help' => '<a href="http://daringfireball.net/projects/markdown/syntax">Markdown syntax</a> allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by <a href="http://daringfireball.net/">John Gruber</a>. <a href="http://www.michelf.com/projects/php-markdown/">More...</a>' + ); +} + + +### Smarty Modifier Interface ### + +function smarty_modifier_markdown($text) { + return Markdown($text); +} + + +### Textile Compatibility Mode ### + +# Rename this file to "classTextile.php" and it can replace Textile everywhere. + +if (strcasecmp(substr(__FILE__, -16), "classTextile.php") == 0) { + # Try to include PHP SmartyPants. Should be in the same directory. + @include_once 'smartypants.php'; + # Fake Textile class. It calls Markdown instead. + class Textile { + function TextileThis($text, $lite='', $encode='') { + if ($lite == '' && $encode == '') $text = Markdown($text); + if (function_exists('SmartyPants')) $text = SmartyPants($text); + return $text; + } + # Fake restricted version: restrictions are not supported for now. + function TextileRestricted($text, $lite='', $noimage='') { + return $this->TextileThis($text, $lite); + } + # Workaround to ensure compatibility with TextPattern 4.0.3. + function blockLite($text) { return $text; } + } +} + + + +# +# Markdown Parser Class +# + +class Markdown_Parser { + + # Regex to match balanced [brackets]. + # Needed to insert a maximum bracked depth while converting to PHP. + var $nested_brackets_depth = 6; + var $nested_brackets_re; + + var $nested_url_parenthesis_depth = 4; + var $nested_url_parenthesis_re; + + # Table of hash values for escaped characters: + var $escape_chars = '\`*_{}[]()>#+-.!'; + var $escape_chars_re; + + # Change to ">" for HTML output. + var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; + var $tab_width = MARKDOWN_TAB_WIDTH; + + # Change to `true` to disallow markup or entities. + var $no_markup = false; + var $no_entities = false; + + # Predefined urls and titles for reference links and images. + var $predef_urls = array(); + var $predef_titles = array(); + + + function Markdown_Parser() { + # + # Constructor function. Initialize appropriate member variables. + # + $this->_initDetab(); + $this->prepareItalicsAndBold(); + + $this->nested_brackets_re = + str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). + str_repeat('\])*', $this->nested_brackets_depth); + + $this->nested_url_parenthesis_re = + str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). + str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + + $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + + # Sort document, block, and span gamut in ascendent priority order. + asort($this->document_gamut); + asort($this->block_gamut); + asort($this->span_gamut); + } + + + # Internal hashes used during transformation. + var $urls = array(); + var $titles = array(); + var $html_hashes = array(); + + # Status flag to avoid invalid nesting. + var $in_anchor = false; + + + function setup() { + # + # Called before the transformation process starts to setup parser + # states. + # + # Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); + + $in_anchor = false; + } + + function teardown() { + # + # Called after the transformation process to clear any variable + # which may be taking up memory unnecessarly. + # + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } + + + function transform($text) { + # + # Main function. Performs some preprocessing on the input text + # and pass it through the document gamut. + # + $this->setup(); + + # Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + + # Standardize line endings: + # DOS to Unix and Mac to Unix + $text = preg_replace('{\r\n?}', "\n", $text); + + # Make sure $text ends with a couple of newlines: + $text .= "\n\n"; + + # Convert all tabs to spaces. + $text = $this->detab($text); + + # Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks($text); + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ ]*\n+/ . + $text = preg_replace('/^[ ]+$/m', '', $text); + + # Run document gamut methods. + foreach ($this->document_gamut as $method => $priority) { + $text = $this->$method($text); + } + + $this->teardown(); + + return $text . "\n"; + } + + var $document_gamut = array( + # Strip link definitions, store in hashes. + "stripLinkDefinitions" => 20, + + "runBasicBlockGamut" => 30, + ); + + + function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + <?(\S+?)>? # url = $2 + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $3 + [")] + [ ]* + )? # title is optional + (?:\n+|\Z) + }xm', + array(&$this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $this->urls[$link_id] = $matches[2]; + $this->titles[$link_id] =& $matches[3]; + return ''; # String that will replace the block + } + + + function hashHTMLBlocks($text) { + if ($this->no_markup) return $text; + + $less_than_tab = $this->tab_width - 1; + + # Hashify HTML blocks: + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap <p>s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded: + # + # * List "a" is made of tags which can be both inline or block-level. + # These will be treated block-level when the start tag is alone on + # its line, otherwise they're not matched here and will be taken as + # inline later. + # * List "b" is made of tags which are always block-level; + # + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. + 'script|noscript|form|fieldset|iframe|math'; + + # Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' + (?> # optional tag attributes + \s # starts with whitespace + (?> + [^>"/]+ # text outside quotes + | + /+(?!>) # slash not followed by ">" + | + "[^"]*" # text inside double quotes (tolerate ">") + | + \'[^\']*\' # text inside single quotes (tolerate ">") + )* + )? + '; + $content = + str_repeat(' + (?> + [^<]+ # content without tag + | + <\2 # nested opening tag + '.$attr.' # attributes + (?> + /> + | + >', $nested_tags_level). # end of opening tag + '.*?'. # last level nested tag content + str_repeat(' + </\2\s*> # closing nested tag + ) + | + <(?!/\2\s*> # other tags with a different name + ) + )*', + $nested_tags_level); + $content2 = str_replace('\2', '\3', $content); + + # First, look for nested blocks, e.g.: + # <div> + # <div> + # tags for inner block must be indented. + # </div> + # </div> + # + # The outermost tags must start at the left margin for this to match, and + # the inner nested divs must be indented. + # We need to do this before the next, more liberal match, because the next + # match will start at the first `<div>` and stop at the first `</div>`. + $text = preg_replace_callback('{(?> + (?> + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + + # Match from `\n<tag>` to `</tag>\n`, handling nested tags + # in between. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_b_re.')# start tag = $2 + '.$attr.'> # attributes followed by > and \n + '.$content.' # content, support nesting + </\2> # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special version for tags of group a. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_a_re.')# start tag = $3 + '.$attr.'>[ ]*\n # attributes followed by > + '.$content2.' # content, support nesting + </\3> # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for <hr />. It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,'.$less_than_tab.'} + <(hr) # start tag = $2 + '.$attr.' # attributes + /?> # the matching end tag + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # Special case for standalone HTML comments: + + [ ]{0,'.$less_than_tab.'} + (?s: + <!-- .*? --> + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # PHP and ASP-style processor instructions (<? and <%) + + [ ]{0,'.$less_than_tab.'} + (?s: + <([?%]) # $2 + .*? + \2> + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + ) + )}Sxmi', + array(&$this, '_hashHTMLBlocks_callback'), + $text); + + return $text; + } + function _hashHTMLBlocks_callback($matches) { + $text = $matches[1]; + $key = $this->hashBlock($text); + return "\n\n$key\n\n"; + } + + + function hashPart($text, $boundary = 'X') { + # + # Called whenever a tag must be hashed when a function insert an atomic + # element in the text stream. Passing $text to through this function gives + # a unique text-token which will be reverted back when calling unhash. + # + # The $boundary argument specify what character should be used to surround + # the token. By convension, "B" is used for block elements that needs not + # to be wrapped into paragraph tags at the end, ":" is used for elements + # that are word separators and "X" is used in the general case. + # + # Swap back any tag hash found in $text so we do not have to `unhash` + # multiple times at the end. + $text = $this->unhash($text); + + # Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++$i . $boundary; + $this->html_hashes[$key] = $text; + return $key; # String that will replace the tag. + } + + + function hashBlock($text) { + # + # Shortcut function for hashPart with block-level boundaries. + # + return $this->hashPart($text, 'B'); + } + + + var $block_gamut = array( + # + # These are all the transformations that form block-level + # tags like paragraphs, headers, and list items. + # + "doHeaders" => 10, + "doHorizontalRules" => 20, + + "doLists" => 40, + "doCodeBlocks" => 50, + "doBlockQuotes" => 60, + ); + + function runBlockGamut($text) { + # + # Run block gamut tranformations. + # + # We need to escape raw HTML in Markdown source before doing anything + # else. This need to be done for each block, and not only at the + # begining in the Markdown function since hashed blocks can be part of + # list items and could have been indented. Indented blocks would have + # been seen as a code block in a previous pass of hashHTMLBlocks. + $text = $this->hashHTMLBlocks($text); + + return $this->runBasicBlockGamut($text); + } + + function runBasicBlockGamut($text) { + # + # Run block gamut tranformations, without hashing HTML blocks. This is + # useful when HTML blocks are known to be already hashed, like in the first + # whole-document pass. + # + foreach ($this->block_gamut as $method => $priority) { + $text = $this->$method($text); + } + + # Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs($text); + + return $text; + } + + + function doHorizontalRules($text) { + # Do Horizontal Rules: + return preg_replace( + '{ + ^[ ]{0,3} # Leading space + ([-*_]) # $1: First marker + (?> # Repeated marker group + [ ]{0,2} # Zero, one, or two spaces. + \1 # Marker character + ){2,} # Group repeated at least twice + [ ]* # Tailing spaces + $ # End of line. + }mx', + "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n", + $text); + } + + + var $span_gamut = array( + # + # These are all the transformations that occur *within* block-level + # tags like paragraphs, headers, and list items. + # + # Process character escapes, code spans, and inline HTML + # in one shot. + "parseSpan" => -30, + + # Process anchor and image tags. Images must come first, + # because ![foo][f] looks like an anchor. + "doImages" => 10, + "doAnchors" => 20, + + # Make links out of things like `<http://example.com/>` + # Must come after doAnchors, because you can use < and > + # delimiters in inline links like [this](<url>). + "doAutoLinks" => 30, + "encodeAmpsAndAngles" => 40, + + "doItalicsAndBold" => 50, + "doHardBreaks" => 60, + ); + + function runSpanGamut($text) { + # + # Run span gamut tranformations. + # + foreach ($this->span_gamut as $method => $priority) { + $text = $this->$method($text); + } + + return $text; + } + + + function doHardBreaks($text) { + # Do hard breaks: + return preg_replace_callback('/ {2,}\n/', + array(&$this, '_doHardBreaks_callback'), $text); + } + function _doHardBreaks_callback($matches) { + return $this->hashPart("<br$this->empty_element_suffix\n"); + } + + + function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML <a> tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ ]* + (?: + <(\S*)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ ]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ ]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + ) + }xs', + array(&$this, '_DoAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link test][1] + # or [link test](/foo) + # +// $text = preg_replace_callback('{ +// ( # wrap whole match in $1 +// \[ +// ([^\[\]]+) # link text = $2; can\'t contain [ or ] +// \] +// ) +// }xs', +// array(&$this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeAttribute($url); + + $result = "<a href=\"$url\""; + if ( isset( $this->titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $url = $this->encodeAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + + function doImages($text) { + # + # Turn Markdown image shortcuts into <img> tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array(&$this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ ]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ ]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ ]* + )? # title is optional + \) + ) + }xs', + array(&$this, '_doImages_inline_callback'), $text); + + return $text; + } + function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeAttribute($url); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + + function doHeaders($text) { + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + # + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 + # ## Header 2 + # ## Header 2 with closing hashes ## + # ... + # ###### Header 6 + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + function _doHeaders_callback_setext($matches) { + # Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) + return $matches[0]; + + $level = $matches[2]{0} == '=' ? 1 : 2; + $block = "<h$level>".$this->runSpanGamut($matches[1])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $block = "<h$level>".$this->runSpanGamut($matches[2])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + function doLists($text) { + # + # Form HTML ordered (numbered) and unordered (bulleted) lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $markers_relist = array($marker_ul_re, $marker_ol_re); + + foreach ($markers_relist as $marker_re) { + # Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + [ ]{0,'.$less_than_tab.'} + ('.$marker_re.') # $3 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + ) + ) + '; // mx + + # We use a different prefix before nested lists than top-level lists. + # See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + } + + return $text; + } + function _doLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[3]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>"); + return "\n". $result ."\n\n"; + } + + var $list_level = 0; + + function processListItems($list_str, $marker_any_re) { + # + # Process the contents of a single ordered or unordered list, splitting it + # into individual list items. + # + # The $this->list_level global keeps track of when we're inside a list. + # Each time we enter a list, we increment it; when we leave a list, + # we decrement. If it's zero, we're not in a list anymore. + # + # We do this because when we're not inside a list, we want to treat + # something like this: + # + # I recommend upgrading to version + # 8. Oops, now this line is treated + # as a sub-list. + # + # As a single paragraph, despite the fact that the second line starts + # with a digit-period-space sequence. + # + # Whereas when we're inside a list (or sub-list), that line will be + # treated as the start of a sub-list. What a kludge, huh? This is + # an aspect of Markdown's syntax that's hard to parse perfectly + # without resorting to mind-reading. Perhaps the solution is to + # change the syntax rules such that sub-lists must start with a + # starting cardinal number; e.g. "1." or "a.". + + $this->list_level++; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array(&$this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + # Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } + else { + # Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = preg_replace('/\n+$/', '', $item); + $item = $this->runSpanGamut($item); + } + + return "<li>" . $item . "</li>\n"; + } + + + function doCodeBlocks($text) { + # + # Process Markdown `<pre><code>` blocks. + # + $text = preg_replace_callback('{ + (?:\n\n|\A\n?) + ( # $1 = the code block -- one or more lines, starting with a space/tab + (?> + [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc + }xm', + array(&$this, '_doCodeBlocks_callback'), $text); + + return $text; + } + function _doCodeBlocks_callback($matches) { + $codeblock = $matches[1]; + + $codeblock = $this->outdent($codeblock); + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + + # trim leading newlines and trailing newlines + $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock); + + $codeblock = "<pre><code>$codeblock\n</code></pre>"; + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + + + function makeCodeSpan($code) { + # + # Create a code span markup for $code. Called from handleSpanToken. + # + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + return $this->hashPart("<code>$code</code>"); + } + + + var $em_relist = array( + '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?=\S)(?![.,:;]\s)', + '*' => '(?<=\S)(?<!\*)\*(?!\*)', + '_' => '(?<=\S)(?<!_)_(?!_)', + ); + var $strong_relist = array( + '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?=\S)(?![.,:;]\s)', + '**' => '(?<=\S)(?<!\*)\*\*(?!\*)', + '__' => '(?<=\S)(?<!_)__(?!_)', + ); + var $em_strong_relist = array( + '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?=\S)(?![.,:;]\s)', + '***' => '(?<=\S)(?<!\*)\*\*\*(?!\*)', + '___' => '(?<=\S)(?<!_)___(?!_)', + ); + var $em_strong_prepared_relist; + + function prepareItalicsAndBold() { + # + # Prepare regular expressions for seraching emphasis tokens in any + # context. + # + foreach ($this->em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + # Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + # Construct master expression from list. + $token_re = '{('. implode('|', $token_relist) .')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + function doItalicsAndBold($text) { + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + # + # Get prepared regular expression for seraching emphasis tokens + # in current context. + # + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + # + # Each loop iteration seach for the next emphasis token. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + # Reached end of text span: empty stack without emitting. + # any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + # Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + # Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong><em>$span</em></strong>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + # Other closing marker: close one em or strong and + # change current token state to match the other + $token_stack[0] = str_repeat($token{0}, 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + # Reached closing marker for both em and strong. + # Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + } else { + # Reached opening three-char emphasis marker. Push on token + # stack; will be handled by the special condition above. + $em = $token{0}; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + # Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + # Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong>$span</strong>"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + # Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + # Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<em>$span</em>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + return $text_stack[0]; + } + + + function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array(&$this, '_doBlockQuotes_callback'), $text); + + return $text; + } + function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + # trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); # recurse + + $bq = preg_replace('/^/m', " ", $bq); + # These leading spaces cause problem with <pre> content, + # so we need to fix that: + $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx', + array(&$this, '_DoBlockQuotes_callback2'), $bq); + + return "\n". $this->hashBlock("<blockquote>\n$bq\n</blockquote>")."\n\n"; + } + function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + + function formParagraphs($text) { + # + # Params: + # $text - string to process with html <p> tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap <p> tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + # Is a paragraph. + $value = $this->runSpanGamut($value); + $value = preg_replace('/^([ ]*)/', "<p>", $value); + $value .= "</p>"; + $grafs[$key] = $this->unhash($value); + } + else { + # Is a block. + # Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 = <div> tag +// <div \s+ +// [^>]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (</div>) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// # We can't call Markdown(), because that resets the hash; +// # that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// # Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + + function encodeAttribute($text) { + # + # Encode text for a double-quoted HTML attribute. This function + # is *not* suitable for attributes enclosed in single quotes. + # + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '"', $text); + return $text; + } + + + function encodeAmpsAndAngles($text) { + # + # Smart processing for ampersands and angle brackets that need to + # be encoded. Valid character entities are left alone unless the + # no-entities mode is set. + # + if ($this->no_entities) { + $text = str_replace('&', '&', $text); + } else { + # Ampersand-encoding based entirely on Nat Irons's Amputator + # MT plugin: <http://bumppo.net/projects/amputator/> + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&', $text);; + } + # Encode remaining <'s + $text = str_replace('<', '<', $text); + + return $text; + } + + + function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i', + array(&$this, '_doAutoLinks_url_callback'), $text); + + # Email addresses: <address@domain.foo> + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + [-.\w\x80-\xFF]+ + \@ + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + ) + > + }xi', + array(&$this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + function _doAutoLinks_url_callback($matches) { + $url = $this->encodeAttribute($matches[1]); + $link = "<a href=\"$url\">$url</a>"; + return $this->hashPart($link); + } + function _doAutoLinks_email_callback($matches) { + $address = $matches[1]; + $link = $this->encodeEmailAddress($address); + return $this->hashPart($link); + } + + + function encodeEmailAddress($addr) { + # + # Input: an email address, e.g. "foo@example.com" + # + # Output: the email address as a mailto link, with each character + # of the address encoded as either a decimal or hex entity, in + # the hopes of foiling most address harvesting spam bots. E.g.: + # + # <p><a href="mailto:foo + # @example.co + # m">foo@exampl + # e.com</a></p> + # + # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + # With some optimizations by Milian Wolff. + # + $addr = "mailto:" . $addr; + $chars = preg_split('/(?<!^)(?!$)/', $addr); + $seed = (int)abs(crc32($addr) / strlen($addr)); # Deterministic seed. + + foreach ($chars as $key => $char) { + $ord = ord($char); + # Ignore non-ascii chars. + if ($ord < 128) { + $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. + # roughly 10% raw, 45% hex, 45% dec + # '@' *must* be encoded. I insist. + if ($r > 90 && $char != '@') /* do nothing */; + else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; + else $chars[$key] = '&#'.$ord.';'; + } + } + + $addr = implode('', $chars); + $text = implode('', array_slice($chars, 7)); # text without `mailto:` + $addr = "<a href=\"$addr\">$text</a>"; + + return $addr; + } + + + function parseSpan($str) { + # + # Take the string $str and parse it into tokens, hashing embeded HTML, + # escaped characters and handling code spans. + # + $output = ''; + + $span_re = '{ + ( + \\\\'.$this->escape_chars_re.' + | + (?<![`\\\\]) + `+ # code span marker + '.( $this->no_markup ? '' : ' + | + <!-- .*? --> # comment + | + <\?.*?\?> | <%.*?%> # processing instruction + | + <[/!$]?[-a-zA-Z0-9:]+ # regular tags + (?> + \s + (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* + )? + > + ').' + ) + }xs'; + + while (1) { + # + # Each loop iteration seach for either the next tag, the next + # openning code span marker, or the next escaped character. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); + + # Create token from text preceding tag. + if ($parts[0] != "") { + $output .= $parts[0]; + } + + # Check if we reach the end. + if (isset($parts[1])) { + $output .= $this->handleSpanToken($parts[1], $parts[2]); + $str = $parts[2]; + } + else { + break; + } + } + + return $output; + } + + + function handleSpanToken($token, &$str) { + # + # Handle $token provided by parseSpan by determining its nature and + # returning the corresponding value that should replace it. + # + switch ($token{0}) { + case "\\": + return $this->hashPart("&#". ord($token{1}). ";"); + case "`": + # Search for end marker in remaining text. + if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', + $str, $matches)) + { + $str = $matches[2]; + $codespan = $this->makeCodeSpan($matches[1]); + return $this->hashPart($codespan); + } + return $token; // return as text since no ending marker found. + default: + return $this->hashPart($token); + } + } + + + function outdent($text) { + # + # Remove one level of line-leading tabs or spaces + # + return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); + } + + + # String length function for detab. `_initDetab` will create a function to + # hanlde UTF-8 if the default function does not exist. + var $utf8_strlen = 'mb_strlen'; + + function detab($text) { + # + # Replace tabs with the appropriate amount of space. + # + # For each line we separate the line in blocks delemited by + # tab characters. Then we reconstruct every line by adding the + # appropriate number of space between each blocks. + + $text = preg_replace_callback('/^.*\t.*$/m', + array(&$this, '_detab_callback'), $text); + + return $text; + } + function _detab_callback($matches) { + $line = $matches[0]; + $strlen = $this->utf8_strlen; # strlen function for UTF-8. + + # Split in blocks. + $blocks = explode("\t", $line); + # Add each blocks to the line. + $line = $blocks[0]; + unset($blocks[0]); # Do not add first block twice. + foreach ($blocks as $block) { + # Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - + $strlen($line, 'UTF-8') % $this->tab_width; + $line .= str_repeat(" ", $amount) . $block; + } + return $line; + } + function _initDetab() { + # + # Check for the availability of the function in the `utf8_strlen` property + # (initially `mb_strlen`). If the function is not available, create a + # function that will loosely count the number of UTF-8 characters with a + # regular expression. + # + if (function_exists($this->utf8_strlen)) return; + $this->utf8_strlen = create_function('$text', 'return preg_match_all( + "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", + $text, $m);'); + } + + + function unhash($text) { + # + # Swap back in all the tags hashed by _HashHTMLBlocks. + # + return preg_replace_callback('/(.)\x1A[0-9]+\1/', + array(&$this, '_unhash_callback'), $text); + } + function _unhash_callback($matches) { + return $this->html_hashes[$matches[0]]; + } + +} + +/* + +PHP Markdown +============ + +Description +----------- + +This is a PHP translation of the original Markdown formatter written in +Perl by John Gruber. + +Markdown is a text-to-HTML filter; it translates an easy-to-read / +easy-to-write structured text format into HTML. Markdown's text format +is most similar to that of plain text email, and supports features such +as headers, *emphasis*, code blocks, blockquotes, and links. + +Markdown's syntax is designed not as a generic markup language, but +specifically to serve as a front-end to (X)HTML. You can use span-level +HTML tags anywhere in a Markdown document, and you can use block level +HTML tags (like <div> and <table> as well). + +For more information about Markdown's syntax, see: + +<http://daringfireball.net/projects/markdown/> + + +Bugs +---- + +To file bug reports please send email to: + +<michel.fortin@michelf.com> + +Please include with your report: (1) the example input; (2) the output you +expected; (3) the output Markdown actually produced. + + +Version History +--------------- + +See the readme file for detailed release notes for this version. + + +Copyright and License +--------------------- + +PHP Markdown +Copyright (c) 2004-2008 Michel Fortin +<http://www.michelf.com/> +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2006 John Gruber +<http://daringfireball.net/> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. + +*/ +?>
\ No newline at end of file |