PageRenderTime 119ms CodeModel.GetById 73ms app.highlight 19ms RepoModel.GetById 21ms app.codeStats 1ms

/inc/lib/Auth/OpenID/SQLStore.php

https://bitbucket.org/yoander/mtrack
PHP | 569 lines | 357 code | 69 blank | 143 comment | 57 complexity | 29d29b55d83598a79dad4bed1b3ebb4e MD5 | raw file
  1<?php
  2
  3/**
  4 * SQL-backed OpenID stores.
  5 *
  6 * PHP versions 4 and 5
  7 *
  8 * LICENSE: See the COPYING file included in this distribution.
  9 *
 10 * @package OpenID
 11 * @author JanRain, Inc. <openid@janrain.com>
 12 * @copyright 2005-2008 Janrain, Inc.
 13 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
 14 */
 15
 16/**
 17 * Require the PEAR DB module because we'll need it for the SQL-based
 18 * stores implemented here.  We silence any errors from the inclusion
 19 * because it might not be present, and a user of the SQL stores may
 20 * supply an Auth_OpenID_DatabaseConnection instance that implements
 21 * its own storage.
 22 */
 23global $__Auth_OpenID_PEAR_AVAILABLE;
 24$__Auth_OpenID_PEAR_AVAILABLE = @include_once 'DB.php';
 25
 26/**
 27 * @access private
 28 */
 29require_once 'Auth/OpenID/Interface.php';
 30require_once 'Auth/OpenID/Nonce.php';
 31
 32/**
 33 * @access private
 34 */
 35require_once 'Auth/OpenID.php';
 36
 37/**
 38 * @access private
 39 */
 40require_once 'Auth/OpenID/Nonce.php';
 41
 42/**
 43 * This is the parent class for the SQL stores, which contains the
 44 * logic common to all of the SQL stores.
 45 *
 46 * The table names used are determined by the class variables
 47 * associations_table_name and nonces_table_name.  To change the name
 48 * of the tables used, pass new table names into the constructor.
 49 *
 50 * To create the tables with the proper schema, see the createTables
 51 * method.
 52 *
 53 * This class shouldn't be used directly.  Use one of its subclasses
 54 * instead, as those contain the code necessary to use a specific
 55 * database.  If you're an OpenID integrator and you'd like to create
 56 * an SQL-driven store that wraps an application's database
 57 * abstraction, be sure to create a subclass of
 58 * {@link Auth_OpenID_DatabaseConnection} that calls the application's
 59 * database abstraction calls.  Then, pass an instance of your new
 60 * database connection class to your SQLStore subclass constructor.
 61 *
 62 * All methods other than the constructor and createTables should be
 63 * considered implementation details.
 64 *
 65 * @package OpenID
 66 */
 67class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore {
 68
 69    /**
 70     * This creates a new SQLStore instance.  It requires an
 71     * established database connection be given to it, and it allows
 72     * overriding the default table names.
 73     *
 74     * @param connection $connection This must be an established
 75     * connection to a database of the correct type for the SQLStore
 76     * subclass you're using.  This must either be an PEAR DB
 77     * connection handle or an instance of a subclass of
 78     * Auth_OpenID_DatabaseConnection.
 79     *
 80     * @param associations_table: This is an optional parameter to
 81     * specify the name of the table used for storing associations.
 82     * The default value is 'oid_associations'.
 83     *
 84     * @param nonces_table: This is an optional parameter to specify
 85     * the name of the table used for storing nonces.  The default
 86     * value is 'oid_nonces'.
 87     */
 88    function Auth_OpenID_SQLStore($connection,
 89                                  $associations_table = null,
 90                                  $nonces_table = null)
 91    {
 92        global $__Auth_OpenID_PEAR_AVAILABLE;
 93
 94        $this->associations_table_name = "oid_associations";
 95        $this->nonces_table_name = "oid_nonces";
 96
 97        // Check the connection object type to be sure it's a PEAR
 98        // database connection.
 99        if (!(is_object($connection) &&
100              (is_subclass_of($connection, 'db_common') ||
101               is_subclass_of($connection,
102                              'auth_openid_databaseconnection')))) {
103            trigger_error("Auth_OpenID_SQLStore expected PEAR connection " .
104                          "object (got ".get_class($connection).")",
105                          E_USER_ERROR);
106            return;
107        }
108
109        $this->connection = $connection;
110
111        // Be sure to set the fetch mode so the results are keyed on
112        // column name instead of column index.  This is a PEAR
113        // constant, so only try to use it if PEAR is present.  Note
114        // that Auth_Openid_Databaseconnection instances need not
115        // implement ::setFetchMode for this reason.
116        if ($__Auth_OpenID_PEAR_AVAILABLE) {
117            $this->connection->setFetchMode(DB_FETCHMODE_ASSOC);
118        }
119
120        if ($associations_table) {
121            $this->associations_table_name = $associations_table;
122        }
123
124        if ($nonces_table) {
125            $this->nonces_table_name = $nonces_table;
126        }
127
128        $this->max_nonce_age = 6 * 60 * 60;
129
130        // Be sure to run the database queries with auto-commit mode
131        // turned OFF, because we want every function to run in a
132        // transaction, implicitly.  As a rule, methods named with a
133        // leading underscore will NOT control transaction behavior.
134        // Callers of these methods will worry about transactions.
135        $this->connection->autoCommit(false);
136
137        // Create an empty SQL strings array.
138        $this->sql = array();
139
140        // Call this method (which should be overridden by subclasses)
141        // to populate the $this->sql array with SQL strings.
142        $this->setSQL();
143
144        // Verify that all required SQL statements have been set, and
145        // raise an error if any expected SQL strings were either
146        // absent or empty.
147        list($missing, $empty) = $this->_verifySQL();
148
149        if ($missing) {
150            trigger_error("Expected keys in SQL query list: " .
151                          implode(", ", $missing),
152                          E_USER_ERROR);
153            return;
154        }
155
156        if ($empty) {
157            trigger_error("SQL list keys have no SQL strings: " .
158                          implode(", ", $empty),
159                          E_USER_ERROR);
160            return;
161        }
162
163        // Add table names to queries.
164        $this->_fixSQL();
165    }
166
167    function tableExists($table_name)
168    {
169        return !$this->isError(
170                      $this->connection->query(
171                          sprintf("SELECT * FROM %s LIMIT 0",
172                                  $table_name)));
173    }
174
175    /**
176     * Returns true if $value constitutes a database error; returns
177     * false otherwise.
178     */
179    function isError($value)
180    {
181        return PEAR::isError($value);
182    }
183
184    /**
185     * Converts a query result to a boolean.  If the result is a
186     * database error according to $this->isError(), this returns
187     * false; otherwise, this returns true.
188     */
189    function resultToBool($obj)
190    {
191        if ($this->isError($obj)) {
192            return false;
193        } else {
194            return true;
195        }
196    }
197
198    /**
199     * This method should be overridden by subclasses.  This method is
200     * called by the constructor to set values in $this->sql, which is
201     * an array keyed on sql name.
202     */
203    function setSQL()
204    {
205    }
206
207    /**
208     * Resets the store by removing all records from the store's
209     * tables.
210     */
211    function reset()
212    {
213        $this->connection->query(sprintf("DELETE FROM %s",
214                                         $this->associations_table_name));
215
216        $this->connection->query(sprintf("DELETE FROM %s",
217                                         $this->nonces_table_name));
218    }
219
220    /**
221     * @access private
222     */
223    function _verifySQL()
224    {
225        $missing = array();
226        $empty = array();
227
228        $required_sql_keys = array(
229                                   'nonce_table',
230                                   'assoc_table',
231                                   'set_assoc',
232                                   'get_assoc',
233                                   'get_assocs',
234                                   'remove_assoc'
235                                   );
236
237        foreach ($required_sql_keys as $key) {
238            if (!array_key_exists($key, $this->sql)) {
239                $missing[] = $key;
240            } else if (!$this->sql[$key]) {
241                $empty[] = $key;
242            }
243        }
244
245        return array($missing, $empty);
246    }
247
248    /**
249     * @access private
250     */
251    function _fixSQL()
252    {
253        $replacements = array(
254                              array(
255                                    'value' => $this->nonces_table_name,
256                                    'keys' => array('nonce_table',
257                                                    'add_nonce',
258                                                    'clean_nonce')
259                                    ),
260                              array(
261                                    'value' => $this->associations_table_name,
262                                    'keys' => array('assoc_table',
263                                                    'set_assoc',
264                                                    'get_assoc',
265                                                    'get_assocs',
266                                                    'remove_assoc',
267                                                    'clean_assoc')
268                                    )
269                              );
270
271        foreach ($replacements as $item) {
272            $value = $item['value'];
273            $keys = $item['keys'];
274
275            foreach ($keys as $k) {
276                if (is_array($this->sql[$k])) {
277                    foreach ($this->sql[$k] as $part_key => $part_value) {
278                        $this->sql[$k][$part_key] = sprintf($part_value,
279                                                            $value);
280                    }
281                } else {
282                    $this->sql[$k] = sprintf($this->sql[$k], $value);
283                }
284            }
285        }
286    }
287
288    function blobDecode($blob)
289    {
290        return $blob;
291    }
292
293    function blobEncode($str)
294    {
295        return $str;
296    }
297
298    function createTables()
299    {
300        $this->connection->autoCommit(true);
301        $n = $this->create_nonce_table();
302        $a = $this->create_assoc_table();
303        $this->connection->autoCommit(false);
304
305        if ($n && $a) {
306            return true;
307        } else {
308            return false;
309        }
310    }
311
312    function create_nonce_table()
313    {
314        if (!$this->tableExists($this->nonces_table_name)) {
315            $r = $this->connection->query($this->sql['nonce_table']);
316            return $this->resultToBool($r);
317        }
318        return true;
319    }
320
321    function create_assoc_table()
322    {
323        if (!$this->tableExists($this->associations_table_name)) {
324            $r = $this->connection->query($this->sql['assoc_table']);
325            return $this->resultToBool($r);
326        }
327        return true;
328    }
329
330    /**
331     * @access private
332     */
333    function _set_assoc($server_url, $handle, $secret, $issued,
334                        $lifetime, $assoc_type)
335    {
336        return $this->connection->query($this->sql['set_assoc'],
337                                        array(
338                                              $server_url,
339                                              $handle,
340                                              $secret,
341                                              $issued,
342                                              $lifetime,
343                                              $assoc_type));
344    }
345
346    function storeAssociation($server_url, $association)
347    {
348        if ($this->resultToBool($this->_set_assoc(
349                                            $server_url,
350                                            $association->handle,
351                                            $this->blobEncode(
352                                                  $association->secret),
353                                            $association->issued,
354                                            $association->lifetime,
355                                            $association->assoc_type
356                                            ))) {
357            $this->connection->commit();
358        } else {
359            $this->connection->rollback();
360        }
361    }
362
363    /**
364     * @access private
365     */
366    function _get_assoc($server_url, $handle)
367    {
368        $result = $this->connection->getRow($this->sql['get_assoc'],
369                                            array($server_url, $handle));
370        if ($this->isError($result)) {
371            return null;
372        } else {
373            return $result;
374        }
375    }
376
377    /**
378     * @access private
379     */
380    function _get_assocs($server_url)
381    {
382        $result = $this->connection->getAll($this->sql['get_assocs'],
383                                            array($server_url));
384
385        if ($this->isError($result)) {
386            return array();
387        } else {
388            return $result;
389        }
390    }
391
392    function removeAssociation($server_url, $handle)
393    {
394        if ($this->_get_assoc($server_url, $handle) == null) {
395            return false;
396        }
397
398        if ($this->resultToBool($this->connection->query(
399                              $this->sql['remove_assoc'],
400                              array($server_url, $handle)))) {
401            $this->connection->commit();
402        } else {
403            $this->connection->rollback();
404        }
405
406        return true;
407    }
408
409    function getAssociation($server_url, $handle = null)
410    {
411        if ($handle !== null) {
412            $assoc = $this->_get_assoc($server_url, $handle);
413
414            $assocs = array();
415            if ($assoc) {
416                $assocs[] = $assoc;
417            }
418        } else {
419            $assocs = $this->_get_assocs($server_url);
420        }
421
422        if (!$assocs || (count($assocs) == 0)) {
423            return null;
424        } else {
425            $associations = array();
426
427            foreach ($assocs as $assoc_row) {
428                $assoc = new Auth_OpenID_Association($assoc_row['handle'],
429                                                     $assoc_row['secret'],
430                                                     $assoc_row['issued'],
431                                                     $assoc_row['lifetime'],
432                                                     $assoc_row['assoc_type']);
433
434                $assoc->secret = $this->blobDecode($assoc->secret);
435
436                if ($assoc->getExpiresIn() == 0) {
437                    $this->removeAssociation($server_url, $assoc->handle);
438                } else {
439                    $associations[] = array($assoc->issued, $assoc);
440                }
441            }
442
443            if ($associations) {
444                $issued = array();
445                $assocs = array();
446                foreach ($associations as $key => $assoc) {
447                    $issued[$key] = $assoc[0];
448                    $assocs[$key] = $assoc[1];
449                }
450
451                array_multisort($issued, SORT_DESC, $assocs, SORT_DESC,
452                                $associations);
453
454                // return the most recently issued one.
455                list($issued, $assoc) = $associations[0];
456                return $assoc;
457            } else {
458                return null;
459            }
460        }
461    }
462
463    /**
464     * @access private
465     */
466    function _add_nonce($server_url, $timestamp, $salt)
467    {
468        $sql = $this->sql['add_nonce'];
469        $result = $this->connection->query($sql, array($server_url,
470                                                       $timestamp,
471                                                       $salt));
472        if ($this->isError($result)) {
473            $this->connection->rollback();
474        } else {
475            $this->connection->commit();
476        }
477        return $this->resultToBool($result);
478    }
479
480    function useNonce($server_url, $timestamp, $salt)
481    {
482        global $Auth_OpenID_SKEW;
483
484        if ( abs($timestamp - time()) > $Auth_OpenID_SKEW ) {
485            return False;
486        }
487
488        return $this->_add_nonce($server_url, $timestamp, $salt);
489    }
490
491    /**
492     * "Octifies" a binary string by returning a string with escaped
493     * octal bytes.  This is used for preparing binary data for
494     * PostgreSQL BYTEA fields.
495     *
496     * @access private
497     */
498    function _octify($str)
499    {
500        $result = "";
501        for ($i = 0; $i < Auth_OpenID::bytes($str); $i++) {
502            $ch = substr($str, $i, 1);
503            if ($ch == "\\") {
504                $result .= "\\\\\\\\";
505            } else if (ord($ch) == 0) {
506                $result .= "\\\\000";
507            } else {
508                $result .= "\\" . strval(decoct(ord($ch)));
509            }
510        }
511        return $result;
512    }
513
514    /**
515     * "Unoctifies" octal-escaped data from PostgreSQL and returns the
516     * resulting ASCII (possibly binary) string.
517     *
518     * @access private
519     */
520    function _unoctify($str)
521    {
522        $result = "";
523        $i = 0;
524        while ($i < strlen($str)) {
525            $char = $str[$i];
526            if ($char == "\\") {
527                // Look to see if the next char is a backslash and
528                // append it.
529                if ($str[$i + 1] != "\\") {
530                    $octal_digits = substr($str, $i + 1, 3);
531                    $dec = octdec($octal_digits);
532                    $char = chr($dec);
533                    $i += 4;
534                } else {
535                    $char = "\\";
536                    $i += 2;
537                }
538            } else {
539                $i += 1;
540            }
541
542            $result .= $char;
543        }
544
545        return $result;
546    }
547
548    function cleanupNonces()
549    {
550        global $Auth_OpenID_SKEW;
551        $v = time() - $Auth_OpenID_SKEW;
552
553        $this->connection->query($this->sql['clean_nonce'], array($v));
554        $num = $this->connection->affectedRows();
555        $this->connection->commit();
556        return $num;
557    }
558
559    function cleanupAssociations()
560    {
561        $this->connection->query($this->sql['clean_assoc'],
562                                 array(time()));
563        $num = $this->connection->affectedRows();
564        $this->connection->commit();
565        return $num;
566    }
567}
568
569?>