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 __construct($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 = [ 148 "server_url" => [ 149 "type" => "text", 150 "length" => 2047, 151 "notnull" => true 152 ], 153 "timestamp" => [ 154 "type" => "integer", 155 "notnull" => true 156 ], 157 "salt" => [ 158 "type" => "text", 159 "length" => 40, 160 "fixed" => true, 161 "notnull" => true 162 ] 163 ]; 164 $constraint = [ 165 "unique" => 1, 166 "fields" => [ 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 = [ 221 "server_url" => [ 222 "type" => "text", 223 "length" => 2047, 224 "notnull" => true 225 ], 226 "handle" => [ 227 "type" => "text", 228 "length" => 255, 229 "notnull" => true 230 ], 231 "secret" => [ 232 "type" => "blob", 233 "length" => "255", 234 "notnull" => true 235 ], 236 "issued" => [ 237 "type" => "integer", 238 "notnull" => true 239 ], 240 "lifetime" => [ 241 "type" => "integer", 242 "notnull" => true 243 ], 244 "assoc_type" => [ 245 "type" => "text", 246 "length" => 64, 247 "notnull" => true 248 ] 249 ]; 250 $options = [ 251 "primary" => [ 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 = [ 273 "server_url" => [ 274 "value" => $server_url, 275 "key" => true 276 ], 277 "handle" => [ 278 "value" => $association->handle, 279 "key" => true 280 ], 281 "secret" => [ 282 "value" => $association->secret, 283 "type" => "blob" 284 ], 285 "issued" => [ 286 "value" => $association->issued 287 ], 288 "lifetime" => [ 289 "value" => $association->lifetime 290 ], 291 "assoc_type" => [ 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 = [ 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 = [$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 = [$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 [$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 = [ 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