From 8521832df41118536ed48cd630217ef7f3dd0e47 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Sun, 16 Sep 2007 01:58:39 +0000 Subject: [PATCH] mlanthaler: Workaround: This will fix a conflict of the built-in DB class with PEAR's DB class. These changes can be reverted as soon as the developers of OpenID applied my patch to their library. (merged from branches/gsoc) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@42029 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- security/OpenID/OpenIDStorage.php | 565 +++++++++++++++++++++++++++++- 1 file changed, 559 insertions(+), 6 deletions(-) diff --git a/security/OpenID/OpenIDStorage.php b/security/OpenID/OpenIDStorage.php index 68d926861..f8360d744 100644 --- a/security/OpenID/OpenIDStorage.php +++ b/security/OpenID/OpenIDStorage.php @@ -8,9 +8,15 @@ /** - * Require the {@link Auth_OpenID_MySQLStore MySQL storage class} + * Require the {@link Auth_OpenID OpenID utility function class} */ -require_once "Auth/OpenID/MySQLStore.php"; +require_once 'Auth/OpenID.php'; + + +/** + * Require the {@link Auth_OpenID_OpenIDStore storage class} + */ +require_once 'Auth/OpenID/Interface.php'; /** @@ -30,7 +36,7 @@ require_once "Auth/OpenID/DatabaseConnection.php"; * * @author Markus Lanthaler */ -class OpenIDStorage extends Auth_OpenID_MySQLStore { +class OpenIDStorage extends Auth_OpenID_OpenIDStore { /** * This static variable is used to decrease the number of table existence @@ -75,7 +81,76 @@ class OpenIDStorage extends Auth_OpenID_MySQLStore { $connection = new OpenIDDatabaseConnection(); - parent::__construct($connection, $associations_table, $nonces_table); + //-------------------------------------------------------------------// + // This part normally resided in the Auth_OpenID_SQLStore class, but + // due to a name conflict of the DB class we can't simple inherit from + // it! + + $this->associations_table_name = "oid_associations"; + $this->nonces_table_name = "oid_nonces"; + + // Check the connection object type to be sure it's a PEAR compatible + // 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 compatible " . + "connection object (got ".get_class($connection).")", + E_USER_ERROR); + return; + } + + $this->connection = $connection; + + + 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(); + //-------------------------------------------------------------------------------- if(self::$S_checkedTableExistence == false) { @@ -106,8 +181,6 @@ class OpenIDStorage extends Auth_OpenID_MySQLStore { * @access private */ function setSQL() { - parent::setSQL(); - $this->sql['nonce_table'] = "CREATE TABLE %s (\n". " server_url VARCHAR(2047),\n". @@ -126,6 +199,26 @@ class OpenIDStorage extends Auth_OpenID_MySQLStore { " assoc_type VARCHAR(64),\n". " PRIMARY KEY (server_url(255), handle)\n". ")"; + + $this->sql['set_assoc'] = + "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['get_expired'] = + "SELECT server_url FROM %s WHERE issued + lifetime < ?"; } @@ -161,6 +254,466 @@ class OpenIDStorage extends Auth_OpenID_MySQLStore { return $this->resultToBool( $this->connection->query($this->sql['assoc_table'])); } + + + /** + * Check if a table exists + * + * @param string $table_name Table to check + * @return bool Returns TRUE if the table exists, otherwise FALSE. + */ + function tableExists($table_name) + { + return !$this->isError($this->connection->query(sprintf( + "SELECT * FROM %s LIMIT 0", $table_name))); + } + + + /** + * Converts a query result to a boolean + * + * @param object $obj Query result + * @return bool If the result is a database error according to + * {@link isError()}, this returns FALSE; otherwise, this + * returns TRUE. + */ + function resultToBool($obj) + { + if($this->isError($obj)) { + return false; + } else { + return true; + } + } + + + /** + * 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)); + } + + + /** + * Check if all the required SQL statements are set + * + * @return array Returns an array of in the form of + * array($missing, $empty) containing the missing and + * empty SQL statements. + */ + private function _verifySQL() + { + $missing = array(); + $empty = array(); + + $required_sql_keys = array('nonce_table', + 'assoc_table', + 'set_assoc', + 'get_assoc', + 'get_assocs', + 'remove_assoc', + 'get_expired', + ); + + 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); + } + + + /** + * Fix SQL statements + * + * This function replaces the place holders in the set SQL statements + * with the right table names. + */ + private function _fixSQL() + { + $replacements = array(array('value' => $this->nonces_table_name, + 'keys' => array('nonce_table', + 'add_nonce') + ), + array('value' => $this->associations_table_name, + 'keys' => array('assoc_table', + 'set_assoc', + 'get_assoc', + 'get_assocs', + 'remove_assoc', + 'get_expired') + ) + ); + + 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); + } + } + } + } + + + /** + * Decode a BLOB field + * + * @param mixed $blob The BLOB field's value + * @return mixed The decoded BLOB value + */ + function blobDecode($blob) + { + return $blob; + } + + + /** + * Encode a value for a BLOB field + * + * @param mixed $blob The value for the BLOB field + * @return mixed The encoded value + */ + function blobEncode($blob) + { + return "0x" . bin2hex($blob); + } + + + /** + * Create the needed tables + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + 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; + } + } + + + /** + * Set an association + * + * Helper function for {@link storeAssociation()}. + * + * @param string $server_url The server URL + * @param string $handle The handle for the association + * @param string $secret The secret + * @param string $issued When was the association issued? + * @param string $lifetime The lifetime of the association + * @param string $assoc_type The association type + * @return + */ + 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)); + } + + + /** + * Store an association + * + * @param string $server_url The URL of the server + * @param Auth_OpenID_Association The association object to store + */ + function storeAssociation($server_url, Auth_OpenID_Association $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(); + } + } + + + /** + * Get an association + * + * This is a helper function for {@link getAssociation()} + * + * @param string $server_url The server URL + * @param string $server_url The handle + * @return array Returns the association row or NULL on error. + */ + 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; + } + } + + + /** + * Get all associations for a specific server URL + * + * This is a helper function for {@link getAssociation()} + * + * @param string $server_url The server URL + * @return array Returns the association rows or an empty array on error. + */ + 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; + } + } + + + /** + * Get an association + * + * @param string $server_url The URL of the server for which the + * associations should be retrieved + * @param string $handle Optional: The handle if one specific association + * should be returned. If set to NULL the most + * recently issued one will be returned. + * @return Auth_OpenID_Association The association or NULL if not found. + */ + 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; + } + } + } + + + /** + * Remove an association + * + * @param string $server_url The server URL + * @param string $server_url The handle + * @return array Returns the association row or NULL on error. + */ + 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; + } + + + /** + * Get the expired associations + * + * @return array Returns an array of expired server URLs + */ + function getExpired() + { + $sql = $this->sql['get_expired']; + $result = $this->connection->getAll($sql, array(time())); + + $expired = array(); + + foreach($result as $row) { + $expired[] = $row['server_url']; + } + + return $expired; + } + + + /** + * Store a nonce + * + * This is a helper function for {@link useNonce()}. + * + * @param string $server_url The URL of the server for which the nonce is + * used + * @param string $timestamp The timestamp of the creation of the nonce + * @param string $salt The value of the nonce + * @return bool Returns TRUE on success, FALSE on failure. + */ + 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); + } + + + /** + * Store a nonce + * + * @param string $server_url The URL of the server for which the nonce is + * used + * @param string $timestamp The timestamp of the creation of the nonce + * @param string $salt The value of the nonce + * @return bool Returns TRUE on success, FALSE on failure. + */ + function useNonce($server_url, $timestamp, $salt) + { + 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. + * + * @param string $str The binary string to octify + * @return string The octified string + */ + 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. + * + * @param string $str The octified string + * @return string The unoctified (binary) string + */ + 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; + } }