forked from GNUsocial/gnu-social
		
	
		
			
				
	
	
		
			558 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			558 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?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
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * @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)
 | |
|     {
 | |
|         $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 (is_subclass_of($this->connection, 'db_common')) {
 | |
|             $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;
 | |
|     }
 | |
| }
 | |
| 
 | |
| 
 |