Subversion Repositories Applications.annuaire

Rev

Rev 42 | Details | Compare with Previous | Last modification | View Log | RSS feed

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