Issues (902)

framework/web/DbSession.php (2 issues)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\web;
9
10
use Yii;
11
use yii\base\InvalidConfigException;
12
use yii\db\Connection;
13
use yii\db\PdoValue;
14
use yii\db\Query;
15
use yii\di\Instance;
16
17
/**
18
 * DbSession extends [[Session]] by using database as session data storage.
19
 *
20
 * By default, DbSession stores session data in a DB table named 'session'. This table
21
 * must be pre-created. The table name can be changed by setting [[sessionTable]].
22
 *
23
 * The following example shows how you can configure the application to use DbSession:
24
 * Add the following to your application config under `components`:
25
 *
26
 * ```php
27
 * 'session' => [
28
 *     'class' => 'yii\web\DbSession',
29
 *     // 'db' => 'mydb',
30
 *     // 'sessionTable' => 'my_session',
31
 * ]
32
 * ```
33
 *
34
 * DbSession extends [[MultiFieldSession]], thus it allows saving extra fields into the [[sessionTable]].
35
 * Refer to [[MultiFieldSession]] for more details.
36
 *
37
 * @author Qiang Xue <[email protected]>
38
 * @since 2.0
39
 */
40
class DbSession extends MultiFieldSession
41
{
42
    /**
43
     * @var Connection|array|string the DB connection object or the application component ID of the DB connection.
44
     * After the DbSession object is created, if you want to change this property, you should only assign it
45
     * with a DB connection object.
46
     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
47
     */
48
    public $db = 'db';
49
    /**
50
     * @var string the name of the DB table that stores the session data.
51
     * The table should be pre-created as follows:
52
     *
53
     * ```sql
54
     * CREATE TABLE session
55
     * (
56
     *     id CHAR(40) NOT NULL PRIMARY KEY,
57
     *     expire INTEGER,
58
     *     data BLOB
59
     * )
60
     * ```
61
     *
62
     * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type
63
     * that can be used for some popular DBMS:
64
     *
65
     * - MySQL: LONGBLOB
66
     * - PostgreSQL: BYTEA
67
     * - MSSQL: BLOB
68
     *
69
     * When using DbSession in a production server, we recommend you create a DB index for the 'expire'
70
     * column in the session table to improve the performance.
71
     *
72
     * Note that according to the php.ini setting of `session.hash_function`, you may need to adjust
73
     * the length of the `id` column. For example, if `session.hash_function=sha256`, you should use
74
     * length 64 instead of 40.
75
     */
76
    public $sessionTable = '{{%session}}';
77
78
    /**
79
     * @var array Session fields to be written into session table columns
80
     * @since 2.0.17
81
     */
82
    protected $fields = [];
83
84
85
    /**
86
     * Initializes the DbSession component.
87
     * This method will initialize the [[db]] property to make sure it refers to a valid DB connection.
88
     * @throws InvalidConfigException if [[db]] is invalid.
89
     */
90 27
    public function init()
91
    {
92 27
        parent::init();
93 27
        $this->db = Instance::ensure($this->db, Connection::className());
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

93
        $this->db = Instance::ensure($this->db, /** @scrutinizer ignore-deprecated */ Connection::className());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
94
    }
95
96
    /**
97
     * Session open handler.
98
     * @internal Do not call this method directly.
99
     * @param string $savePath session save path
100
     * @param string $sessionName session name
101
     * @return bool whether session is opened successfully
102
     */
103 6
    public function openSession($savePath, $sessionName)
104
    {
105 6
        if ($this->getUseStrictMode()) {
106 6
            $id = $this->getId();
107 6
            if (!$this->getReadQuery($id)->exists($this->db)) {
108
                //This session id does not exist, mark it for forced regeneration
109 6
                $this->_forceRegenerateId = $id;
110
            }
111
        }
112
113 6
        return parent::openSession($savePath, $sessionName);
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 6
    public function regenerateID($deleteOldSession = false)
120
    {
121 6
        $oldID = session_id();
122
123
        // if no session is started, there is nothing to regenerate
124 6
        if (empty($oldID)) {
125
            return;
126
        }
127
128 6
        parent::regenerateID(false);
129 6
        $newID = session_id();
130
        // if session id regeneration failed, no need to create/update it.
131 6
        if (empty($newID)) {
132
            Yii::warning('Failed to generate new session ID', __METHOD__);
133
            return;
134
        }
135
136 6
        $row = $this->db->useMaster(function () use ($oldID) {
137 6
            return (new Query())->from($this->sessionTable)
138 6
               ->where(['id' => $oldID])
139 6
               ->createCommand($this->db)
140 6
               ->queryOne();
141 6
        });
142
143 6
        if ($row !== false && $this->getIsActive()) {
144
            if ($deleteOldSession) {
145
                $this->db->createCommand()
146
                    ->update($this->sessionTable, ['id' => $newID], ['id' => $oldID])
147
                    ->execute();
148
            } else {
149
                $row['id'] = $newID;
150
                $this->db->createCommand()
151
                    ->insert($this->sessionTable, $row)
152
                    ->execute();
153
            }
154
        }
155
    }
156
157
    /**
158
     * Ends the current session and store session data.
159
     * @since 2.0.17
160
     */
161 9
    public function close()
162
    {
163 9
        if ($this->getIsActive()) {
164
            // prepare writeCallback fields before session closes
165 6
            $this->fields = $this->composeFields();
166 6
            YII_DEBUG ? session_write_close() : @session_write_close();
167
        }
168
    }
169
170
    /**
171
     * Session read handler.
172
     * @internal Do not call this method directly.
173
     * @param string $id session ID
174
     * @return string the session data
175
     */
176 21
    public function readSession($id)
177
    {
178 21
        $query = $this->getReadQuery($id);
179
180 21
        if ($this->readCallback !== null) {
181
            $fields = $query->one($this->db);
182
            return $fields === false ? '' : $this->extractData($fields);
183
        }
184
185 21
        $data = $query->select(['data'])->scalar($this->db);
186 21
        return $data === false ? '' : $data;
187
    }
188
189
    /**
190
     * Session write handler.
191
     * @internal Do not call this method directly.
192
     * @param string $id session ID
193
     * @param string $data session data
194
     * @return bool whether session write is successful
195
     */
196 21
    public function writeSession($id, $data)
197
    {
198 21
        if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) {
199
            //Ignore write when forceRegenerate is active for this id
200 6
            return true;
201
        }
202
203
        // exception must be caught in session write handler
204
        // https://www.php.net/manual/en/function.session-set-save-handler.php#refsect1-function.session-set-save-handler-notes
205
        try {
206
            // ensure backwards compatability (fixed #9438)
207 21
            if ($this->writeCallback && !$this->fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
208 3
                $this->fields = $this->composeFields();
209
            }
210
            // ensure data consistency
211 21
            if (!isset($this->fields['data'])) {
212 18
                $this->fields['data'] = $data;
213
            } else {
214 3
                $_SESSION = $this->fields['data'];
215
            }
216
            // ensure 'id' and 'expire' are never affected by [[writeCallback]]
217 21
            $this->fields = array_merge($this->fields, [
218 21
                'id' => $id,
219 21
                'expire' => time() + $this->getTimeout(),
220 21
            ]);
221 21
            $this->fields = $this->typecastFields($this->fields);
222 21
            $this->db->createCommand()->upsert($this->sessionTable, $this->fields)->execute();
223 21
            $this->fields = [];
224
        } catch (\Exception $e) {
225
            Yii::$app->errorHandler->handleException($e);
226
            return false;
227
        }
228 21
        return true;
229
    }
230
231
    /**
232
     * Session destroy handler.
233
     * @internal Do not call this method directly.
234
     * @param string $id session ID
235
     * @return bool whether session is destroyed successfully
236
     */
237 9
    public function destroySession($id)
238
    {
239 9
        $this->db->createCommand()
240 9
            ->delete($this->sessionTable, ['id' => $id])
241 9
            ->execute();
242
243 9
        return true;
244
    }
245
246
    /**
247
     * Session GC (garbage collection) handler.
248
     * @internal Do not call this method directly.
249
     * @param int $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up.
250
     * @return bool whether session is GCed successfully
251
     */
252 3
    public function gcSession($maxLifetime)
253
    {
254 3
        $this->db->createCommand()
255 3
            ->delete($this->sessionTable, '[[expire]]<:expire', [':expire' => time()])
256 3
            ->execute();
257
258 3
        return true;
259
    }
260
261
    /**
262
     * Generates a query to get the session from db
263
     * @param string $id The id of the session
264
     * @return Query
265
     */
266 21
    protected function getReadQuery($id)
267
    {
268 21
        return (new Query())
269 21
            ->from($this->sessionTable)
270 21
            ->where('[[expire]]>:expire AND [[id]]=:id', [':expire' => time(), ':id' => $id]);
271
    }
272
273
    /**
274
     * Method typecasts $fields before passing them to PDO.
275
     * Default implementation casts field `data` to `\PDO::PARAM_LOB`.
276
     * You can override this method in case you need special type casting.
277
     *
278
     * @param array $fields Fields, that will be passed to PDO. Key - name, Value - value
279
     * @return array
280
     * @since 2.0.13
281
     */
282 21
    protected function typecastFields($fields)
283
    {
284 21
        if (isset($fields['data']) && !is_array($fields['data']) && !is_object($fields['data'])) {
285 21
            $fields['data'] = new PdoValue($fields['data'], \PDO::PARAM_LOB);
286
        }
287
288 21
        return $fields;
289
    }
290
}
291