Passed
Pull Request — master (#19131)
by Sam
39:02
created

DbSession::composeFields()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 2
Bugs 1 Features 1
Metric Value
cc 3
eloc 5
nc 2
nop 2
dl 0
loc 9
ccs 3
cts 3
cp 1
crap 3
rs 10
c 2
b 1
f 1
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://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 45
    public function init()
91
    {
92 45
        parent::init();
93 45
        $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 45
    }
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 10
    public function openSession($savePath, $sessionName)
104
    {
105 10
        if ($this->getUseStrictMode()) {
106 8
            $id = $this->getId();
107 8
            if (!$this->getReadQuery($id)->exists($this->db)) {
108
                //This session id does not exist, mark it for forced regeneration
109 8
                $this->_forceRegenerateId = $id;
110
            }
111
        }
112
113 10
        return parent::openSession($savePath, $sessionName);
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 8
    public function regenerateID($deleteOldSession = false)
120
    {
121 8
        $oldID = session_id();
122
123
        // if no session is started, there is nothing to regenerate
124 8
        if (empty($oldID)) {
125
            return;
126
        }
127
128 8
        // Regenerate causes a write with the current session id, which means we need to prepare fields.
129 8
        $this->fields = $this->composeFields($oldID);
130
        parent::regenerateID(false);
131 8
        $newID = session_id();
132
        // if session id regeneration failed, no need to create/update it.
133
        if (empty($newID)) {
134
            Yii::warning('Failed to generate new session ID', __METHOD__);
135
            return;
136 8
        }
137 8
138 8
        $row = $this->db->useMaster(function() use ($oldID) {
139 8
            return (new Query())->from($this->sessionTable)
140 8
                ->where(['id' => $oldID])
141 8
                ->createCommand($this->db)
142
                ->queryOne();
143 8
        });
144
145
        if ($row !== false) {
146
            if ($deleteOldSession) {
147
                $this->db->createCommand()
148
                    ->update($this->sessionTable, ['id' => $newID], ['id' => $oldID])
149
                    ->execute();
150
            } else {
151
                $row['id'] = $newID;
152
                $this->db->createCommand()
153
                    ->insert($this->sessionTable, $row)
154
                    ->execute();
155
            }
156 8
        } else {
157 8
            // shouldn't reach here normally
158 8
            $this->db->createCommand()
159
                ->insert($this->sessionTable, $this->composeFields($newID, ''))
160 8
                ->execute();
161
        }
162
    }
163
164
    /**
165
     * Ends the current session and store session data.
166 15
     * @since 2.0.17
167
     */
168 15
    public function close()
169
    {
170 10
        if ($this->getIsActive()) {
171 10
172
            // prepare writeCallback fields before session closes
173 15
            $this->fields = $this->composeFields($this->id);
174
            YII_DEBUG ? session_write_close() : @session_write_close();
175
        }
176
    }
177
178
    /**
179
     * Session read handler.
180
     * @internal Do not call this method directly.
181 35
     * @param string $id session ID
182
     * @return string the session data
183 35
     */
184
    public function readSession($id)
185 35
    {
186
        $query = $this->getReadQuery($id);
187
188
        if ($this->readCallback !== null) {
189
            $fields = $query->one($this->db);
190 35
            return $fields === false ? '' : $this->extractData($fields);
191 35
        }
192
193
        $data = $query->select(['data'])->scalar($this->db);
194
        return $data === false ? '' : $data;
195
    }
196
197
    protected function composeFields($id = null, $data = null)
198
    {
199
        // We don't pass data up to the parent, since DbSession uses the opposite logic from the parent class :-|
200
        $fields = parent::composeFields($id, null);
201 35
        if (!isset($fields['data']) && isset($data)) {
202
            $fields['data'] = $data;
203 35
        }
204
        $fields['expire'] = time() + $this->getTimeout();
205 8
        return $fields;
206
    }
207
208
    /**
209
     * Session write handler.
210
     * @internal Do not call this method directly.
211
     * @param string $id session ID
212 35
     * @param string $data session data
213 5
     * @return bool whether session write is successful
214
     */
215
    public function writeSession($id, $data)
216 35
    {
217 30
        if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) {
218
            //Ignore write when forceRegenerate is active for this id
219 5
            return true;
220
        }
221
        // exception must be caught in session write handler
222 35
        // https://www.php.net/manual/en/function.session-set-save-handler.php#refsect1-function.session-set-save-handler-notes
223 35
        try {
224 35
            $fields = $this->fields;
225
            $this->fields = [];
226 35
            if (empty($fields)) {
227 35
                // This is a fallback for direct session management via PHP functions.
228 35
                // This will fail if the `writeCallback` uses the data from the session.
229
                $fields = $this->composeFields($id, $data);
230
            } else {
231
                // In case we went through the proper cycle where a session is closed or regenerated via this class,
232
                // we set the data manually since it wasn't available at the call to `composeFields()` earlier.
233 35
                $fields['data'] = $data;
234
            }
235
236
            $fields = $this->typecastFields($fields);
237
            $this->db->createCommand()->upsert($this->sessionTable, $fields)->execute();
238
        } catch (\Exception $e) {
239
            Yii::$app->errorHandler->handleException($e);
240
            return false;
241
        }
242 15
        return true;
243
    }
244 15
245 15
    /**
246 15
     * Session destroy handler.
247
     * @internal Do not call this method directly.
248 15
     * @param string $id session ID
249
     * @return bool whether session is destroyed successfully
250
     */
251
    public function destroySession($id)
252
    {
253
        $this->db->createCommand()
254
            ->delete($this->sessionTable, ['id' => $id])
255
            ->execute();
256
257 5
        return true;
258
    }
259 5
260 5
    /**
261 5
     * Session GC (garbage collection) handler.
262
     * @internal Do not call this method directly.
263 5
     * @param int $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up.
264
     * @return bool whether session is GCed successfully
265
     */
266
    public function gcSession($maxLifetime)
267
    {
268
        $this->db->createCommand()
269
            ->delete($this->sessionTable, '[[expire]]<:expire', [':expire' => time()])
270
            ->execute();
271 35
272
        return true;
273 35
    }
274 35
275 35
    /**
276
     * Generates a query to get the session from db
277
     * @param string $id The id of the session
278
     * @return Query
279
     */
280
    protected function getReadQuery($id)
281
    {
282
        return (new Query())
283
            ->from($this->sessionTable)
284
            ->where('[[expire]]>:expire AND [[id]]=:id', [':expire' => time(), ':id' => $id]);
285
    }
286
287 35
    /**
288
     * Method typecasts $fields before passing them to PDO.
289 35
     * Default implementation casts field `data` to `\PDO::PARAM_LOB`.
290 35
     * You can override this method in case you need special type casting.
291
     *
292
     * @param array $fields Fields, that will be passed to PDO. Key - name, Value - value
293 35
     * @return array
294
     * @since 2.0.13
295
     */
296
    protected function typecastFields($fields)
297
    {
298
        if (isset($fields['data']) && !is_array($fields['data']) && !is_object($fields['data'])) {
299
            $fields['data'] = new PdoValue($fields['data'], \PDO::PARAM_LOB);
300
        }
301
302
        return $fields;
303
    }
304
}
305