Version 1
[yaffs-website] / vendor / symfony / http-foundation / Session / Storage / Handler / LegacyPdoSessionHandler.php
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 @trigger_error('The '.__NAMESPACE__.'\LegacyPdoSessionHandler class is deprecated since version 2.6 and will be removed in 3.0. Use the Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler class instead.', E_USER_DEPRECATED);
15
16 /**
17  * Session handler using a PDO connection to read and write data.
18  *
19  * Session data is a binary string that can contain non-printable characters like the null byte.
20  * For this reason this handler base64 encodes the data to be able to save it in a character column.
21  *
22  * This version of the PdoSessionHandler does NOT implement locking. So concurrent requests to the
23  * same session can result in data loss due to race conditions.
24  *
25  * @author Fabien Potencier <fabien@symfony.com>
26  * @author Michael Williams <michael.williams@funsational.com>
27  * @author Tobias Schultze <http://tobion.de>
28  *
29  * @deprecated since version 2.6, to be removed in 3.0. Use
30  *             {@link PdoSessionHandler} instead.
31  */
32 class LegacyPdoSessionHandler implements \SessionHandlerInterface
33 {
34     /**
35      * @var \PDO PDO instance
36      */
37     private $pdo;
38
39     /**
40      * @var string Table name
41      */
42     private $table;
43
44     /**
45      * @var string Column for session id
46      */
47     private $idCol;
48
49     /**
50      * @var string Column for session data
51      */
52     private $dataCol;
53
54     /**
55      * @var string Column for timestamp
56      */
57     private $timeCol;
58
59     /**
60      * Constructor.
61      *
62      * List of available options:
63      *  * db_table: The name of the table [required]
64      *  * db_id_col: The column where to store the session id [default: sess_id]
65      *  * db_data_col: The column where to store the session data [default: sess_data]
66      *  * db_time_col: The column where to store the timestamp [default: sess_time]
67      *
68      * @param \PDO  $pdo       A \PDO instance
69      * @param array $dbOptions An associative array of DB options
70      *
71      * @throws \InvalidArgumentException When "db_table" option is not provided
72      */
73     public function __construct(\PDO $pdo, array $dbOptions = array())
74     {
75         if (!array_key_exists('db_table', $dbOptions)) {
76             throw new \InvalidArgumentException('You must provide the "db_table" option for a PdoSessionStorage.');
77         }
78         if (\PDO::ERRMODE_EXCEPTION !== $pdo->getAttribute(\PDO::ATTR_ERRMODE)) {
79             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__));
80         }
81
82         $this->pdo = $pdo;
83         $dbOptions = array_merge(array(
84             'db_id_col' => 'sess_id',
85             'db_data_col' => 'sess_data',
86             'db_time_col' => 'sess_time',
87         ), $dbOptions);
88
89         $this->table = $dbOptions['db_table'];
90         $this->idCol = $dbOptions['db_id_col'];
91         $this->dataCol = $dbOptions['db_data_col'];
92         $this->timeCol = $dbOptions['db_time_col'];
93     }
94
95     /**
96      * {@inheritdoc}
97      */
98     public function open($savePath, $sessionName)
99     {
100         return true;
101     }
102
103     /**
104      * {@inheritdoc}
105      */
106     public function close()
107     {
108         return true;
109     }
110
111     /**
112      * {@inheritdoc}
113      */
114     public function destroy($sessionId)
115     {
116         // delete the record associated with this id
117         $sql = "DELETE FROM $this->table WHERE $this->idCol = :id";
118
119         try {
120             $stmt = $this->pdo->prepare($sql);
121             $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
122             $stmt->execute();
123         } catch (\PDOException $e) {
124             throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete a session: %s', $e->getMessage()), 0, $e);
125         }
126
127         return true;
128     }
129
130     /**
131      * {@inheritdoc}
132      */
133     public function gc($maxlifetime)
134     {
135         // delete the session records that have expired
136         $sql = "DELETE FROM $this->table WHERE $this->timeCol < :time";
137
138         try {
139             $stmt = $this->pdo->prepare($sql);
140             $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT);
141             $stmt->execute();
142         } catch (\PDOException $e) {
143             throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete expired sessions: %s', $e->getMessage()), 0, $e);
144         }
145
146         return true;
147     }
148
149     /**
150      * {@inheritdoc}
151      */
152     public function read($sessionId)
153     {
154         $sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id";
155
156         try {
157             $stmt = $this->pdo->prepare($sql);
158             $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
159             $stmt->execute();
160
161             // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed
162             $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM);
163
164             if ($sessionRows) {
165                 return base64_decode($sessionRows[0][0]);
166             }
167
168             return '';
169         } catch (\PDOException $e) {
170             throw new \RuntimeException(sprintf('PDOException was thrown when trying to read the session data: %s', $e->getMessage()), 0, $e);
171         }
172     }
173
174     /**
175      * {@inheritdoc}
176      */
177     public function write($sessionId, $data)
178     {
179         $encoded = base64_encode($data);
180
181         try {
182             // We use a single MERGE SQL query when supported by the database.
183             $mergeSql = $this->getMergeSql();
184
185             if (null !== $mergeSql) {
186                 $mergeStmt = $this->pdo->prepare($mergeSql);
187                 $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
188                 $mergeStmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
189                 $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT);
190                 $mergeStmt->execute();
191
192                 return true;
193             }
194
195             $updateStmt = $this->pdo->prepare(
196                 "UPDATE $this->table SET $this->dataCol = :data, $this->timeCol = :time WHERE $this->idCol = :id"
197             );
198             $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
199             $updateStmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
200             $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
201             $updateStmt->execute();
202
203             // When MERGE is not supported, like in Postgres, we have to use this approach that can result in
204             // duplicate key errors when the same session is written simultaneously. We can just catch such an
205             // error and re-execute the update. This is similar to a serializable transaction with retry logic
206             // on serialization failures but without the overhead and without possible false positives due to
207             // longer gap locking.
208             if (!$updateStmt->rowCount()) {
209                 try {
210                     $insertStmt = $this->pdo->prepare(
211                         "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"
212                     );
213                     $insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
214                     $insertStmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
215                     $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT);
216                     $insertStmt->execute();
217                 } catch (\PDOException $e) {
218                     // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
219                     if (0 === strpos($e->getCode(), '23')) {
220                         $updateStmt->execute();
221                     } else {
222                         throw $e;
223                     }
224                 }
225             }
226         } catch (\PDOException $e) {
227             throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e);
228         }
229
230         return true;
231     }
232
233     /**
234      * Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database.
235      *
236      * @return string|null The SQL string or null when not supported
237      */
238     private function getMergeSql()
239     {
240         $driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
241
242         switch ($driver) {
243             case 'mysql':
244                 return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
245                 "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)";
246             case 'oci':
247                 // DUAL is Oracle specific dummy table
248                 return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ".
249                 "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
250                 "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time";
251             case 'sqlsrv' === $driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='):
252                 // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
253                 // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
254                 return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ".
255                 "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
256                 "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;";
257             case 'sqlite':
258                 return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)";
259         }
260     }
261
262     /**
263      * Return a PDO instance.
264      *
265      * @return \PDO
266      */
267     protected function getConnection()
268     {
269         return $this->pdo;
270     }
271 }