[ Index ] |
PHP Cross Reference of phpBB-3.2.11-deutsch |
[Summary view] [Print] [Text view]
1 <?php 2 3 /* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12 namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 14 /** 15 * Session handler using a PDO connection to read and write data. 16 * 17 * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements 18 * different locking strategies to handle concurrent access to the same session. 19 * Locking is necessary to prevent loss of data due to race conditions and to keep 20 * the session data consistent between read() and write(). With locking, requests 21 * for the same session will wait until the other one finished writing. For this 22 * reason it's best practice to close a session as early as possible to improve 23 * concurrency. PHPs internal files session handler also implements locking. 24 * 25 * Attention: Since SQLite does not support row level locks but locks the whole database, 26 * it means only one session can be accessed at a time. Even different sessions would wait 27 * for another to finish. So saving session in SQLite should only be considered for 28 * development or prototypes. 29 * 30 * Session data is a binary string that can contain non-printable characters like the null byte. 31 * For this reason it must be saved in a binary column in the database like BLOB in MySQL. 32 * Saving it in a character column could corrupt the data. You can use createTable() 33 * to initialize a correctly defined table. 34 * 35 * @see http://php.net/sessionhandlerinterface 36 * 37 * @author Fabien Potencier <fabien@symfony.com> 38 * @author Michael Williams <michael.williams@funsational.com> 39 * @author Tobias Schultze <http://tobion.de> 40 */ 41 class PdoSessionHandler implements \SessionHandlerInterface 42 { 43 /** 44 * No locking is done. This means sessions are prone to loss of data due to 45 * race conditions of concurrent requests to the same session. The last session 46 * write will win in this case. It might be useful when you implement your own 47 * logic to deal with this like an optimistic approach. 48 */ 49 const LOCK_NONE = 0; 50 51 /** 52 * Creates an application-level lock on a session. The disadvantage is that the 53 * lock is not enforced by the database and thus other, unaware parts of the 54 * application could still concurrently modify the session. The advantage is it 55 * does not require a transaction. 56 * This mode is not available for SQLite and not yet implemented for oci and sqlsrv. 57 */ 58 const LOCK_ADVISORY = 1; 59 60 /** 61 * Issues a real row lock. Since it uses a transaction between opening and 62 * closing a session, you have to be careful when you use same database connection 63 * that you also use for your application logic. This mode is the default because 64 * it's the only reliable solution across DBMSs. 65 */ 66 const LOCK_TRANSACTIONAL = 2; 67 68 /** 69 * @var \PDO|null PDO instance or null when not connected yet 70 */ 71 private $pdo; 72 73 /** 74 * @var string|false|null DSN string or null for session.save_path or false when lazy connection disabled 75 */ 76 private $dsn = false; 77 78 /** 79 * @var string Database driver 80 */ 81 private $driver; 82 83 /** 84 * @var string Table name 85 */ 86 private $table = 'sessions'; 87 88 /** 89 * @var string Column for session id 90 */ 91 private $idCol = 'sess_id'; 92 93 /** 94 * @var string Column for session data 95 */ 96 private $dataCol = 'sess_data'; 97 98 /** 99 * @var string Column for lifetime 100 */ 101 private $lifetimeCol = 'sess_lifetime'; 102 103 /** 104 * @var string Column for timestamp 105 */ 106 private $timeCol = 'sess_time'; 107 108 /** 109 * @var string Username when lazy-connect 110 */ 111 private $username = ''; 112 113 /** 114 * @var string Password when lazy-connect 115 */ 116 private $password = ''; 117 118 /** 119 * @var array Connection options when lazy-connect 120 */ 121 private $connectionOptions = array(); 122 123 /** 124 * @var int The strategy for locking, see constants 125 */ 126 private $lockMode = self::LOCK_TRANSACTIONAL; 127 128 /** 129 * It's an array to support multiple reads before closing which is manual, non-standard usage. 130 * 131 * @var \PDOStatement[] An array of statements to release advisory locks 132 */ 133 private $unlockStatements = array(); 134 135 /** 136 * @var bool True when the current session exists but expired according to session.gc_maxlifetime 137 */ 138 private $sessionExpired = false; 139 140 /** 141 * @var bool Whether a transaction is active 142 */ 143 private $inTransaction = false; 144 145 /** 146 * @var bool Whether gc() has been called 147 */ 148 private $gcCalled = false; 149 150 /** 151 * You can either pass an existing database connection as PDO instance or 152 * pass a DSN string that will be used to lazy-connect to the database 153 * when the session is actually used. Furthermore it's possible to pass null 154 * which will then use the session.save_path ini setting as PDO DSN parameter. 155 * 156 * List of available options: 157 * * db_table: The name of the table [default: sessions] 158 * * db_id_col: The column where to store the session id [default: sess_id] 159 * * db_data_col: The column where to store the session data [default: sess_data] 160 * * db_lifetime_col: The column where to store the lifetime [default: sess_lifetime] 161 * * db_time_col: The column where to store the timestamp [default: sess_time] 162 * * db_username: The username when lazy-connect [default: ''] 163 * * db_password: The password when lazy-connect [default: ''] 164 * * db_connection_options: An array of driver-specific connection options [default: array()] 165 * * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL] 166 * 167 * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null 168 * @param array $options An associative array of options 169 * 170 * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION 171 */ 172 public function __construct($pdoOrDsn = null, array $options = array()) 173 { 174 if ($pdoOrDsn instanceof \PDO) { 175 if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { 176 throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__)); 177 } 178 179 $this->pdo = $pdoOrDsn; 180 $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); 181 } else { 182 $this->dsn = $pdoOrDsn; 183 } 184 185 $this->table = isset($options['db_table']) ? $options['db_table'] : $this->table; 186 $this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol; 187 $this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol; 188 $this->lifetimeCol = isset($options['db_lifetime_col']) ? $options['db_lifetime_col'] : $this->lifetimeCol; 189 $this->timeCol = isset($options['db_time_col']) ? $options['db_time_col'] : $this->timeCol; 190 $this->username = isset($options['db_username']) ? $options['db_username'] : $this->username; 191 $this->password = isset($options['db_password']) ? $options['db_password'] : $this->password; 192 $this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions; 193 $this->lockMode = isset($options['lock_mode']) ? $options['lock_mode'] : $this->lockMode; 194 } 195 196 /** 197 * Creates the table to store sessions which can be called once for setup. 198 * 199 * Session ID is saved in a column of maximum length 128 because that is enough even 200 * for a 512 bit configured session.hash_function like Whirlpool. Session data is 201 * saved in a BLOB. One could also use a shorter inlined varbinary column 202 * if one was sure the data fits into it. 203 * 204 * @throws \PDOException When the table already exists 205 * @throws \DomainException When an unsupported PDO driver is used 206 */ 207 public function createTable() 208 { 209 // connect if we are not yet 210 $this->getConnection(); 211 212 switch ($this->driver) { 213 case 'mysql': 214 // We use varbinary for the ID column because it prevents unwanted conversions: 215 // - character set conversions between server and client 216 // - trailing space removal 217 // - case-insensitivity 218 // - language processing like é == e 219 $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol MEDIUMINT NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB"; 220 break; 221 case 'sqlite': 222 $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; 223 break; 224 case 'pgsql': 225 $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; 226 break; 227 case 'oci': 228 $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; 229 break; 230 case 'sqlsrv': 231 $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; 232 break; 233 default: 234 throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); 235 } 236 237 try { 238 $this->pdo->exec($sql); 239 } catch (\PDOException $e) { 240 $this->rollback(); 241 242 throw $e; 243 } 244 } 245 246 /** 247 * Returns true when the current session exists but expired according to session.gc_maxlifetime. 248 * 249 * Can be used to distinguish between a new session and one that expired due to inactivity. 250 * 251 * @return bool Whether current session expired 252 */ 253 public function isSessionExpired() 254 { 255 return $this->sessionExpired; 256 } 257 258 /** 259 * {@inheritdoc} 260 */ 261 public function open($savePath, $sessionName) 262 { 263 if (null === $this->pdo) { 264 $this->connect($this->dsn ?: $savePath); 265 } 266 267 return true; 268 } 269 270 /** 271 * {@inheritdoc} 272 */ 273 public function read($sessionId) 274 { 275 try { 276 return $this->doRead($sessionId); 277 } catch (\PDOException $e) { 278 $this->rollback(); 279 280 throw $e; 281 } 282 } 283 284 /** 285 * {@inheritdoc} 286 */ 287 public function gc($maxlifetime) 288 { 289 // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process. 290 // This way, pruning expired sessions does not block them from being started while the current session is used. 291 $this->gcCalled = true; 292 293 return true; 294 } 295 296 /** 297 * {@inheritdoc} 298 */ 299 public function destroy($sessionId) 300 { 301 // delete the record associated with this id 302 $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; 303 304 try { 305 $stmt = $this->pdo->prepare($sql); 306 $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); 307 $stmt->execute(); 308 } catch (\PDOException $e) { 309 $this->rollback(); 310 311 throw $e; 312 } 313 314 return true; 315 } 316 317 /** 318 * {@inheritdoc} 319 */ 320 public function write($sessionId, $data) 321 { 322 $maxlifetime = (int) ini_get('session.gc_maxlifetime'); 323 324 try { 325 // We use a single MERGE SQL query when supported by the database. 326 $mergeStmt = $this->getMergeStatement($sessionId, $data, $maxlifetime); 327 if (null !== $mergeStmt) { 328 $mergeStmt->execute(); 329 330 return true; 331 } 332 333 $updateStmt = $this->getUpdateStatement($sessionId, $data, $maxlifetime); 334 $updateStmt->execute(); 335 336 // When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in 337 // duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior). 338 // We can just catch such an error and re-execute the update. This is similar to a serializable 339 // transaction with retry logic on serialization failures but without the overhead and without possible 340 // false positives due to longer gap locking. 341 if (!$updateStmt->rowCount()) { 342 try { 343 $insertStmt = $this->getInsertStatement($sessionId, $data, $maxlifetime); 344 $insertStmt->execute(); 345 } catch (\PDOException $e) { 346 // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys 347 if (0 === strpos($e->getCode(), '23')) { 348 $updateStmt->execute(); 349 } else { 350 throw $e; 351 } 352 } 353 } 354 } catch (\PDOException $e) { 355 $this->rollback(); 356 357 throw $e; 358 } 359 360 return true; 361 } 362 363 /** 364 * {@inheritdoc} 365 */ 366 public function close() 367 { 368 $this->commit(); 369 370 while ($unlockStmt = array_shift($this->unlockStatements)) { 371 $unlockStmt->execute(); 372 } 373 374 if ($this->gcCalled) { 375 $this->gcCalled = false; 376 377 // delete the session records that have expired 378 if ('mysql' === $this->driver) { 379 $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time"; 380 } else { 381 $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time - $this->timeCol"; 382 } 383 384 $stmt = $this->pdo->prepare($sql); 385 $stmt->bindValue(':time', time(), \PDO::PARAM_INT); 386 $stmt->execute(); 387 } 388 389 if (false !== $this->dsn) { 390 $this->pdo = null; // only close lazy-connection 391 } 392 393 return true; 394 } 395 396 /** 397 * Lazy-connects to the database. 398 * 399 * @param string $dsn DSN string 400 */ 401 private function connect($dsn) 402 { 403 $this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions); 404 $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 405 $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); 406 } 407 408 /** 409 * Helper method to begin a transaction. 410 * 411 * Since SQLite does not support row level locks, we have to acquire a reserved lock 412 * on the database immediately. Because of https://bugs.php.net/42766 we have to create 413 * such a transaction manually which also means we cannot use PDO::commit or 414 * PDO::rollback or PDO::inTransaction for SQLite. 415 * 416 * Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions 417 * due to http://www.mysqlperformanceblog.com/2013/12/12/one-more-innodb-gap-lock-to-avoid/ . 418 * So we change it to READ COMMITTED. 419 */ 420 private function beginTransaction() 421 { 422 if (!$this->inTransaction) { 423 if ('sqlite' === $this->driver) { 424 $this->pdo->exec('BEGIN IMMEDIATE TRANSACTION'); 425 } else { 426 if ('mysql' === $this->driver) { 427 $this->pdo->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); 428 } 429 $this->pdo->beginTransaction(); 430 } 431 $this->inTransaction = true; 432 } 433 } 434 435 /** 436 * Helper method to commit a transaction. 437 */ 438 private function commit() 439 { 440 if ($this->inTransaction) { 441 try { 442 // commit read-write transaction which also releases the lock 443 if ('sqlite' === $this->driver) { 444 $this->pdo->exec('COMMIT'); 445 } else { 446 $this->pdo->commit(); 447 } 448 $this->inTransaction = false; 449 } catch (\PDOException $e) { 450 $this->rollback(); 451 452 throw $e; 453 } 454 } 455 } 456 457 /** 458 * Helper method to rollback a transaction. 459 */ 460 private function rollback() 461 { 462 // We only need to rollback if we are in a transaction. Otherwise the resulting 463 // error would hide the real problem why rollback was called. We might not be 464 // in a transaction when not using the transactional locking behavior or when 465 // two callbacks (e.g. destroy and write) are invoked that both fail. 466 if ($this->inTransaction) { 467 if ('sqlite' === $this->driver) { 468 $this->pdo->exec('ROLLBACK'); 469 } else { 470 $this->pdo->rollBack(); 471 } 472 $this->inTransaction = false; 473 } 474 } 475 476 /** 477 * Reads the session data in respect to the different locking strategies. 478 * 479 * We need to make sure we do not return session data that is already considered garbage according 480 * to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. 481 * 482 * @param string $sessionId Session ID 483 * 484 * @return string The session data 485 */ 486 private function doRead($sessionId) 487 { 488 $this->sessionExpired = false; 489 490 if (self::LOCK_ADVISORY === $this->lockMode) { 491 $this->unlockStatements[] = $this->doAdvisoryLock($sessionId); 492 } 493 494 $selectSql = $this->getSelectSql(); 495 $selectStmt = $this->pdo->prepare($selectSql); 496 $selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); 497 $insertStmt = null; 498 499 do { 500 $selectStmt->execute(); 501 $sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM); 502 503 if ($sessionRows) { 504 if ($sessionRows[0][1] + $sessionRows[0][2] < time()) { 505 $this->sessionExpired = true; 506 507 return ''; 508 } 509 510 return \is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; 511 } 512 513 if (null !== $insertStmt) { 514 $this->rollback(); 515 throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.'); 516 } 517 518 if (self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { 519 // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block 520 // until other connections to the session are committed. 521 try { 522 $insertStmt = $this->getInsertStatement($sessionId, '', 0); 523 $insertStmt->execute(); 524 } catch (\PDOException $e) { 525 // Catch duplicate key error because other connection created the session already. 526 // It would only not be the case when the other connection destroyed the session. 527 if (0 === strpos($e->getCode(), '23')) { 528 // Retrieve finished session data written by concurrent connection by restarting the loop. 529 // We have to start a new transaction as a failed query will mark the current transaction as 530 // aborted in PostgreSQL and disallow further queries within it. 531 $this->rollback(); 532 $this->beginTransaction(); 533 continue; 534 } 535 536 throw $e; 537 } 538 } 539 540 return ''; 541 } while (true); 542 } 543 544 /** 545 * Executes an application-level lock on the database. 546 * 547 * @param string $sessionId Session ID 548 * 549 * @return \PDOStatement The statement that needs to be executed later to release the lock 550 * 551 * @throws \DomainException When an unsupported PDO driver is used 552 * 553 * @todo implement missing advisory locks 554 * - for oci using DBMS_LOCK.REQUEST 555 * - for sqlsrv using sp_getapplock with LockOwner = Session 556 */ 557 private function doAdvisoryLock($sessionId) 558 { 559 switch ($this->driver) { 560 case 'mysql': 561 // MySQL 5.7.5 and later enforces a maximum length on lock names of 64 characters. Previously, no limit was enforced. 562 $lockId = \substr($sessionId, 0, 64); 563 // should we handle the return value? 0 on timeout, null on error 564 // we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout 565 $stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)'); 566 $stmt->bindValue(':key', $lockId, \PDO::PARAM_STR); 567 $stmt->execute(); 568 569 $releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)'); 570 $releaseStmt->bindValue(':key', $lockId, \PDO::PARAM_STR); 571 572 return $releaseStmt; 573 case 'pgsql': 574 // Obtaining an exclusive session level advisory lock requires an integer key. 575 // When session.sid_bits_per_character > 4, the session id can contain non-hex-characters. 576 // So we cannot just use hexdec(). 577 if (4 === \PHP_INT_SIZE) { 578 $sessionInt1 = $this->convertStringToInt($sessionId); 579 $sessionInt2 = $this->convertStringToInt(substr($sessionId, 4, 4)); 580 581 $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key1, :key2)'); 582 $stmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); 583 $stmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); 584 $stmt->execute(); 585 586 $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key1, :key2)'); 587 $releaseStmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); 588 $releaseStmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); 589 } else { 590 $sessionBigInt = $this->convertStringToInt($sessionId); 591 592 $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key)'); 593 $stmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); 594 $stmt->execute(); 595 596 $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key)'); 597 $releaseStmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); 598 } 599 600 return $releaseStmt; 601 case 'sqlite': 602 throw new \DomainException('SQLite does not support advisory locks.'); 603 default: 604 throw new \DomainException(sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver)); 605 } 606 } 607 608 /** 609 * Encodes the first 4 (when PHP_INT_SIZE == 4) or 8 characters of the string as an integer. 610 * 611 * Keep in mind, PHP integers are signed. 612 * 613 * @param string $string 614 * 615 * @return int 616 */ 617 private function convertStringToInt($string) 618 { 619 if (4 === \PHP_INT_SIZE) { 620 return (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); 621 } 622 623 $int1 = (\ord($string[7]) << 24) + (\ord($string[6]) << 16) + (\ord($string[5]) << 8) + \ord($string[4]); 624 $int2 = (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); 625 626 return $int2 + ($int1 << 32); 627 } 628 629 /** 630 * Return a locking or nonlocking SQL query to read session information. 631 * 632 * @return string The SQL string 633 * 634 * @throws \DomainException When an unsupported PDO driver is used 635 */ 636 private function getSelectSql() 637 { 638 if (self::LOCK_TRANSACTIONAL === $this->lockMode) { 639 $this->beginTransaction(); 640 641 switch ($this->driver) { 642 case 'mysql': 643 case 'oci': 644 case 'pgsql': 645 return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE"; 646 case 'sqlsrv': 647 return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id"; 648 case 'sqlite': 649 // we already locked when starting transaction 650 break; 651 default: 652 throw new \DomainException(sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver)); 653 } 654 } 655 656 return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id"; 657 } 658 659 /** 660 * Returns an insert statement supported by the database for writing session data. 661 * 662 * @param string $sessionId Session ID 663 * @param string $sessionData Encoded session data 664 * @param int $maxlifetime session.gc_maxlifetime 665 * 666 * @return \PDOStatement The insert statement 667 */ 668 private function getInsertStatement($sessionId, $sessionData, $maxlifetime) 669 { 670 switch ($this->driver) { 671 case 'oci': 672 $data = fopen('php://memory', 'r+'); 673 fwrite($data, $sessionData); 674 rewind($data); 675 $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :lifetime, :time) RETURNING $this->dataCol into :data"; 676 break; 677 default: 678 $data = $sessionData; 679 $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"; 680 break; 681 } 682 683 $stmt = $this->pdo->prepare($sql); 684 $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); 685 $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); 686 $stmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); 687 $stmt->bindValue(':time', time(), \PDO::PARAM_INT); 688 689 return $stmt; 690 } 691 692 /** 693 * Returns an update statement supported by the database for writing session data. 694 * 695 * @param string $sessionId Session ID 696 * @param string $sessionData Encoded session data 697 * @param int $maxlifetime session.gc_maxlifetime 698 * 699 * @return \PDOStatement The update statement 700 */ 701 private function getUpdateStatement($sessionId, $sessionData, $maxlifetime) 702 { 703 switch ($this->driver) { 704 case 'oci': 705 $data = fopen('php://memory', 'r+'); 706 fwrite($data, $sessionData); 707 rewind($data); 708 $sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data"; 709 break; 710 default: 711 $data = $sessionData; 712 $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"; 713 break; 714 } 715 716 $stmt = $this->pdo->prepare($sql); 717 $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); 718 $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); 719 $stmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); 720 $stmt->bindValue(':time', time(), \PDO::PARAM_INT); 721 722 return $stmt; 723 } 724 725 /** 726 * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data. 727 * 728 * @param string $sessionId Session ID 729 * @param string $data Encoded session data 730 * @param int $maxlifetime session.gc_maxlifetime 731 * 732 * @return \PDOStatement|null The merge statement or null when not supported 733 */ 734 private function getMergeStatement($sessionId, $data, $maxlifetime) 735 { 736 switch (true) { 737 case 'mysql' === $this->driver: 738 $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". 739 "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; 740 break; 741 case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='): 742 // MERGE is only available since SQL Server 2008 and must be terminated by semicolon 743 // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx 744 $mergeSql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". 745 "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". 746 "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; 747 break; 748 case 'sqlite' === $this->driver: 749 $mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"; 750 break; 751 case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='): 752 $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". 753 "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; 754 break; 755 default: 756 // MERGE is not supported with LOBs: http://www.oracle.com/technetwork/articles/fuecks-lobs-095315.html 757 return null; 758 } 759 760 $mergeStmt = $this->pdo->prepare($mergeSql); 761 762 if ('sqlsrv' === $this->driver) { 763 $mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR); 764 $mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR); 765 $mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB); 766 $mergeStmt->bindParam(4, $maxlifetime, \PDO::PARAM_INT); 767 $mergeStmt->bindValue(5, time(), \PDO::PARAM_INT); 768 $mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB); 769 $mergeStmt->bindParam(7, $maxlifetime, \PDO::PARAM_INT); 770 $mergeStmt->bindValue(8, time(), \PDO::PARAM_INT); 771 } else { 772 $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); 773 $mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB); 774 $mergeStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); 775 $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT); 776 } 777 778 return $mergeStmt; 779 } 780 781 /** 782 * Return a PDO instance. 783 * 784 * @return \PDO 785 */ 786 protected function getConnection() 787 { 788 if (null === $this->pdo) { 789 $this->connect($this->dsn ?: ini_get('session.save_path')); 790 } 791 792 return $this->pdo; 793 } 794 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Nov 11 20:33:01 2020 | Cross-referenced by PHPXref 0.7.1 |