1<?php
2
3/**
4 * SQL-backed OpenID stores for use with PEAR::MDB2.
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
16require_once 'MDB2.php';
17
18/**
19 * @access private
20 */
21require_once 'Auth/OpenID/Interface.php';
22
23/**
24 * @access private
25 */
26require_once 'Auth/OpenID.php';
27
28/**
29 * @access private
30 */
31require_once 'Auth/OpenID/Nonce.php';
32
33/**
34 * This store uses a PEAR::MDB2 connection to store persistence
35 * information.
36 *
37 * The table names used are determined by the class variables
38 * associations_table_name and nonces_table_name.  To change the name
39 * of the tables used, pass new table names into the constructor.
40 *
41 * To create the tables with the proper schema, see the createTables
42 * method.
43 *
44 * @package OpenID
45 */
46class Auth_OpenID_MDB2Store extends Auth_OpenID_OpenIDStore {
47    /**
48     * This creates a new MDB2Store instance.  It requires an
49     * established database connection be given to it, and it allows
50     * overriding the default table names.
51     *
52     * @param connection $connection This must be an established
53     * connection to a database of the correct type for the SQLStore
54     * subclass you're using.  This must be a PEAR::MDB2 connection
55     * handle.
56     *
57     * @param associations_table: This is an optional parameter to
58     * specify the name of the table used for storing associations.
59     * The default value is 'oid_associations'.
60     *
61     * @param nonces_table: This is an optional parameter to specify
62     * the name of the table used for storing nonces.  The default
63     * value is 'oid_nonces'.
64     */
65    function Auth_OpenID_MDB2Store($connection,
66                                  $associations_table = null,
67                                  $nonces_table = null)
68    {
69        $this->associations_table_name = "oid_associations";
70        $this->nonces_table_name = "oid_nonces";
71
72        // Check the connection object type to be sure it's a PEAR
73        // database connection.
74        if (!is_object($connection) ||
75            !is_subclass_of($connection, 'mdb2_driver_common')) {
76            trigger_error("Auth_OpenID_MDB2Store expected PEAR connection " .
77                          "object (got ".get_class($connection).")",
78                          E_USER_ERROR);
79            return;
80        }
81
82        $this->connection = $connection;
83
84        // Be sure to set the fetch mode so the results are keyed on
85        // column name instead of column index.
86        $this->connection->setFetchMode(MDB2_FETCHMODE_ASSOC);
87
88        if (PEAR::isError($this->connection->loadModule('Extended'))) {
89            trigger_error("Unable to load MDB2_Extended module", E_USER_ERROR);
90            return;
91        }
92
93        if ($associations_table) {
94            $this->associations_table_name = $associations_table;
95        }
96
97        if ($nonces_table) {
98            $this->nonces_table_name = $nonces_table;
99        }
100
101        $this->max_nonce_age = 6 * 60 * 60;
102    }
103
104    function tableExists($table_name)
105    {
106        return !PEAR::isError($this->connection->query(
107                                  sprintf("SELECT * FROM %s LIMIT 0",
108                                          $table_name)));
109    }
110
111    function createTables()
112    {
113        $n = $this->create_nonce_table();
114        $a = $this->create_assoc_table();
115
116        if (!$n || !$a) {
117            return false;
118        }
119        return true;
120    }
121
122    function create_nonce_table()
123    {
124        if (!$this->tableExists($this->nonces_table_name)) {
125            switch ($this->connection->phptype) {
126                case "mysql":
127                case "mysqli":
128                    // Custom SQL for MySQL to use InnoDB and variable-
129                    // length keys
130                    $r = $this->connection->exec(
131                        sprintf("CREATE TABLE %s (\n".
132                                "  server_url VARCHAR(2047) NOT NULL DEFAULT '',\n".
133                                "  timestamp INTEGER NOT NULL,\n".
134                                "  salt CHAR(40) NOT NULL,\n".
135                                "  UNIQUE (server_url(255), timestamp, salt)\n".
136                                ") TYPE=InnoDB",
137                                $this->nonces_table_name));
138                    if (PEAR::isError($r)) {
139                        return false;
140                    }
141                    break;
142                default:
143                    if (PEAR::isError(
144                        $this->connection->loadModule('Manager'))) {
145                        return false;
146                    }
147                    $fields = array(
148                        "server_url" => array(
149                            "type" => "text",
150                            "length" => 2047,
151                            "notnull" => true
152                        ),
153                        "timestamp" => array(
154                            "type" => "integer",
155                            "notnull" => true
156                        ),
157                        "salt" => array(
158                            "type" => "text",
159                            "length" => 40,
160                            "fixed" => true,
161                            "notnull" => true
162                        )
163                    );
164                    $constraint = array(
165                        "unique" => 1,
166                        "fields" => array(
167                            "server_url" => true,
168                            "timestamp" => true,
169                            "salt" => true
170                        )
171                    );
172
173                    $r = $this->connection->createTable($this->nonces_table_name,
174                                                        $fields);
175                    if (PEAR::isError($r)) {
176                        return false;
177                    }
178
179                    $r = $this->connection->createConstraint(
180                        $this->nonces_table_name,
181                        $this->nonces_table_name . "_constraint",
182                        $constraint);
183                    if (PEAR::isError($r)) {
184                        return false;
185                    }
186                    break;
187            }
188        }
189        return true;
190    }
191
192    function create_assoc_table()
193    {
194        if (!$this->tableExists($this->associations_table_name)) {
195            switch ($this->connection->phptype) {
196                case "mysql":
197                case "mysqli":
198                    // Custom SQL for MySQL to use InnoDB and variable-
199                    // length keys
200                    $r = $this->connection->exec(
201                        sprintf("CREATE TABLE %s(\n".
202                                "  server_url VARCHAR(2047) NOT NULL DEFAULT '',\n".
203                                "  handle VARCHAR(255) NOT NULL,\n".
204                                "  secret BLOB NOT NULL,\n".
205                                "  issued INTEGER NOT NULL,\n".
206                                "  lifetime INTEGER NOT NULL,\n".
207                                "  assoc_type VARCHAR(64) NOT NULL,\n".
208                                "  PRIMARY KEY (server_url(255), handle)\n".
209                                ") TYPE=InnoDB",
210                            $this->associations_table_name));
211                    if (PEAR::isError($r)) {
212                        return false;
213                    }
214                    break;
215                default:
216                    if (PEAR::isError(
217                        $this->connection->loadModule('Manager'))) {
218                        return false;
219                    }
220                    $fields = array(
221                        "server_url" => array(
222                            "type" => "text",
223                            "length" => 2047,
224                            "notnull" => true
225                        ),
226                        "handle" => array(
227                            "type" => "text",
228                            "length" => 255,
229                            "notnull" => true
230                        ),
231                        "secret" => array(
232                            "type" => "blob",
233                            "length" => "255",
234                            "notnull" => true
235                        ),
236                        "issued" => array(
237                            "type" => "integer",
238                            "notnull" => true
239                        ),
240                        "lifetime" => array(
241                            "type" => "integer",
242                            "notnull" => true
243                        ),
244                        "assoc_type" => array(
245                            "type" => "text",
246                            "length" => 64,
247                            "notnull" => true
248                        )
249                    );
250                    $options = array(
251                        "primary" => array(
252                            "server_url" => true,
253                            "handle" => true
254                        )
255                    );
256
257                    $r = $this->connection->createTable(
258                        $this->associations_table_name,
259                        $fields,
260                        $options);
261                    if (PEAR::isError($r)) {
262                        return false;
263                    }
264                    break;
265            }
266        }
267        return true;
268    }
269
270    function storeAssociation($server_url, $association)
271    {
272        $fields = array(
273            "server_url" => array(
274                "value" => $server_url,
275                "key" => true
276            ),
277            "handle" => array(
278                "value" => $association->handle,
279                "key" => true
280            ),
281            "secret" => array(
282                "value" => $association->secret,
283                "type" => "blob"
284            ),
285            "issued" => array(
286                "value" => $association->issued
287            ),
288            "lifetime" => array(
289                "value" => $association->lifetime
290            ),
291            "assoc_type" => array(
292                "value" => $association->assoc_type
293            )
294        );
295
296        return !PEAR::isError($this->connection->replace(
297                                  $this->associations_table_name,
298                                  $fields));
299    }
300
301    function cleanupNonces()
302    {
303        global $Auth_OpenID_SKEW;
304        $v = time() - $Auth_OpenID_SKEW;
305
306        return $this->connection->exec(
307            sprintf("DELETE FROM %s WHERE timestamp < %d",
308                    $this->nonces_table_name, $v));
309    }
310
311    function cleanupAssociations()
312    {
313        return $this->connection->exec(
314            sprintf("DELETE FROM %s WHERE issued + lifetime < %d",
315                    $this->associations_table_name, time()));
316    }
317
318    function getAssociation($server_url, $handle = null)
319    {
320        $sql = "";
321        $params = null;
322        $types = array(
323                       "text",
324                       "blob",
325                       "integer",
326                       "integer",
327                       "text"
328                       );
329        if ($handle !== null) {
330            $sql = sprintf("SELECT handle, secret, issued, lifetime, assoc_type " .
331                           "FROM %s WHERE server_url = ? AND handle = ?",
332                           $this->associations_table_name);
333            $params = array($server_url, $handle);
334        } else {
335            $sql = sprintf("SELECT handle, secret, issued, lifetime, assoc_type " .
336                           "FROM %s WHERE server_url = ? ORDER BY issued DESC",
337                           $this->associations_table_name);
338            $params = array($server_url);
339        }
340
341        $assoc = $this->connection->getRow($sql, $types, $params);
342
343        if (!$assoc || PEAR::isError($assoc)) {
344            return null;
345        } else {
346            $association = new Auth_OpenID_Association($assoc['handle'],
347                                                       stream_get_contents(
348                                                           $assoc['secret']),
349                                                       $assoc['issued'],
350                                                       $assoc['lifetime'],
351                                                       $assoc['assoc_type']);
352            fclose($assoc['secret']);
353            return $association;
354        }
355    }
356
357    function removeAssociation($server_url, $handle)
358    {
359        $r = $this->connection->execParam(
360            sprintf("DELETE FROM %s WHERE server_url = ? AND handle = ?",
361                    $this->associations_table_name),
362            array($server_url, $handle));
363
364        if (PEAR::isError($r) || $r == 0) {
365            return false;
366        }
367        return true;
368    }
369
370    function useNonce($server_url, $timestamp, $salt)
371    {
372        global $Auth_OpenID_SKEW;
373
374        if (abs($timestamp - time()) > $Auth_OpenID_SKEW ) {
375            return false;
376        }
377
378        $fields = array(
379                        "timestamp" => $timestamp,
380                        "salt" => $salt
381                        );
382
383        if (!empty($server_url)) {
384            $fields["server_url"] = $server_url;
385        }
386
387        $r = $this->connection->autoExecute(
388            $this->nonces_table_name,
389            $fields,
390            MDB2_AUTOQUERY_INSERT);
391
392        if (PEAR::isError($r)) {
393            return false;
394        }
395        return true;
396    }
397
398    /**
399     * Resets the store by removing all records from the store's
400     * tables.
401     */
402    function reset()
403    {
404        $this->connection->query(sprintf("DELETE FROM %s",
405                                         $this->associations_table_name));
406
407        $this->connection->query(sprintf("DELETE FROM %s",
408                                         $this->nonces_table_name));
409    }
410
411}
412
413?>
414