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;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
 |