diff options
Diffstat (limited to 'models/Auth/OpenID/SQLStore.php')
| -rw-r--r-- | models/Auth/OpenID/SQLStore.php | 557 | 
1 files changed, 557 insertions, 0 deletions
diff --git a/models/Auth/OpenID/SQLStore.php b/models/Auth/OpenID/SQLStore.php new file mode 100644 index 000000000..c04059732 --- /dev/null +++ b/models/Auth/OpenID/SQLStore.php @@ -0,0 +1,557 @@ +<?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; +    } +} + +  | 
