• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 3755643596

pending completion
3755643596

push

github

GitHub
Merge pull request #7004 from kenjis/fix-db-session-tests

16207 of 18979 relevant lines covered (85.39%)

174.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.38
/system/Session/Handlers/RedisHandler.php
1
<?php
2

3
/**
4
 * This file is part of CodeIgniter 4 framework.
5
 *
6
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11

12
namespace CodeIgniter\Session\Handlers;
13

14
use CodeIgniter\I18n\Time;
15
use CodeIgniter\Session\Exceptions\SessionException;
16
use Config\App as AppConfig;
17
use Redis;
18
use RedisException;
19
use ReturnTypeWillChange;
20

21
/**
22
 * Session handler using Redis for persistence
23
 */
24
class RedisHandler extends BaseHandler
25
{
26
    private const DEFAULT_PORT = 6379;
27

28
    /**
29
     * phpRedis instance
30
     *
31
     * @var Redis|null
32
     */
33
    protected $redis;
34

35
    /**
36
     * Key prefix
37
     *
38
     * @var string
39
     */
40
    protected $keyPrefix = 'ci_session:';
41

42
    /**
43
     * Lock key
44
     *
45
     * @var string|null
46
     */
47
    protected $lockKey;
48

49
    /**
50
     * Key exists flag
51
     *
52
     * @var bool
53
     */
54
    protected $keyExists = false;
55

56
    /**
57
     * Number of seconds until the session ends.
58
     *
59
     * @var int
60
     */
61
    protected $sessionExpiration = 7200;
62

63
    /**
64
     * @param string $ipAddress User's IP address
65
     *
66
     * @throws SessionException
67
     */
68
    public function __construct(AppConfig $config, string $ipAddress)
69
    {
70
        parent::__construct($config, $ipAddress);
7✔
71

72
        $this->setSavePath();
7✔
73

74
        // Add sessionCookieName for multiple session cookies.
75
        $this->keyPrefix .= $config->sessionCookieName . ':';
7✔
76

77
        if ($this->matchIP === true) {
7✔
78
            $this->keyPrefix .= $this->ipAddress . ':';
×
79
        }
80

81
        $this->sessionExpiration = empty($config->sessionExpiration)
7✔
82
            ? (int) ini_get('session.gc_maxlifetime')
×
83
            : (int) $config->sessionExpiration;
7✔
84
    }
85

86
    protected function setSavePath(): void
87
    {
88
        if (empty($this->savePath)) {
7✔
89
            throw SessionException::forEmptySavepath();
×
90
        }
91

92
        if (preg_match('#(?:tcp://)?([^:?]+)(?:\:(\d+))?(\?.+)?#', $this->savePath, $matches)) {
7✔
93
            if (! isset($matches[3])) {
7✔
94
                $matches[3] = ''; // Just to avoid undefined index notices below
5✔
95
            }
96

97
            $this->savePath = [
7✔
98
                'host'     => $matches[1],
7✔
99
                'port'     => empty($matches[2]) ? self::DEFAULT_PORT : $matches[2],
7✔
100
                'password' => preg_match('#auth=([^\s&]+)#', $matches[3], $match) ? $match[1] : null,
7✔
101
                'database' => preg_match('#database=(\d+)#', $matches[3], $match) ? (int) $match[1] : 0,
7✔
102
                'timeout'  => preg_match('#timeout=(\d+\.\d+|\d+)#', $matches[3], $match) ? (float) $match[1] : 0.0,
7✔
103
            ];
7✔
104

105
            preg_match('#prefix=([^\s&]+)#', $matches[3], $match) && $this->keyPrefix = $match[1];
7✔
106
        } else {
107
            throw SessionException::forInvalidSavePathFormat($this->savePath);
×
108
        }
109
    }
110

111
    /**
112
     * Re-initialize existing session, or creates a new one.
113
     *
114
     * @param string $path The path where to store/retrieve the session
115
     * @param string $name The session name
116
     */
117
    public function open($path, $name): bool
118
    {
119
        if (empty($this->savePath)) {
4✔
120
            return false;
×
121
        }
122

123
        $redis = new Redis();
4✔
124

125
        if (! $redis->connect($this->savePath['host'], ($this->savePath['host'][0] === '/' ? 0 : $this->savePath['port']), $this->savePath['timeout'])) {
4✔
126
            $this->logger->error('Session: Unable to connect to Redis with the configured settings.');
×
127
        } elseif (isset($this->savePath['password']) && ! $redis->auth($this->savePath['password'])) {
4✔
128
            $this->logger->error('Session: Unable to authenticate to Redis instance.');
×
129
        } elseif (isset($this->savePath['database']) && ! $redis->select($this->savePath['database'])) {
4✔
130
            $this->logger->error('Session: Unable to select Redis database with index ' . $this->savePath['database']);
×
131
        } else {
132
            $this->redis = $redis;
4✔
133

134
            return true;
4✔
135
        }
136

137
        return false;
×
138
    }
139

140
    /**
141
     * Reads the session data from the session storage, and returns the results.
142
     *
143
     * @param string $id The session ID
144
     *
145
     * @return false|string Returns an encoded string of the read data.
146
     *                      If nothing was read, it must return false.
147
     */
148
    #[ReturnTypeWillChange]
149
    public function read($id)
150
    {
151
        if (isset($this->redis) && $this->lockSession($id)) {
3✔
152
            if (! isset($this->sessionID)) {
3✔
153
                $this->sessionID = $id;
3✔
154
            }
155

156
            $data = $this->redis->get($this->keyPrefix . $id);
3✔
157

158
            if (is_string($data)) {
3✔
159
                $this->keyExists = true;
1✔
160
            } else {
161
                $data = '';
2✔
162
            }
163

164
            $this->fingerprint = md5($data);
3✔
165

166
            return $data;
3✔
167
        }
168

169
        return '';
×
170
    }
171

172
    /**
173
     * Writes the session data to the session storage.
174
     *
175
     * @param string $id   The session ID
176
     * @param string $data The encoded session data
177
     */
178
    public function write($id, $data): bool
179
    {
180
        if (! isset($this->redis)) {
1✔
181
            return false;
×
182
        }
183

184
        if ($this->sessionID !== $id) {
1✔
185
            if (! $this->releaseLock() || ! $this->lockSession($id)) {
×
186
                return false;
×
187
            }
188

189
            $this->keyExists = false;
×
190
            $this->sessionID = $id;
×
191
        }
192

193
        if (isset($this->lockKey)) {
1✔
194
            $this->redis->expire($this->lockKey, 300);
1✔
195

196
            if ($this->fingerprint !== ($fingerprint = md5($data)) || $this->keyExists === false) {
1✔
197
                if ($this->redis->set($this->keyPrefix . $id, $data, $this->sessionExpiration)) {
1✔
198
                    $this->fingerprint = $fingerprint;
1✔
199
                    $this->keyExists   = true;
1✔
200

201
                    return true;
1✔
202
                }
203

204
                return false;
×
205
            }
206

207
            return $this->redis->expire($this->keyPrefix . $id, $this->sessionExpiration);
×
208
        }
209

210
        return false;
×
211
    }
212

213
    /**
214
     * Closes the current session.
215
     */
216
    public function close(): bool
217
    {
218
        if (isset($this->redis)) {
3✔
219
            try {
220
                $pingReply = $this->redis->ping();
3✔
221

222
                if (($pingReply === true) || ($pingReply === '+PONG')) {
3✔
223
                    if (isset($this->lockKey)) {
3✔
224
                        $this->redis->del($this->lockKey);
3✔
225
                    }
226

227
                    if (! $this->redis->close()) {
3✔
228
                        return false;
3✔
229
                    }
230
                }
231
            } catch (RedisException $e) {
×
232
                $this->logger->error('Session: Got RedisException on close(): ' . $e->getMessage());
×
233
            }
234

235
            $this->redis = null;
3✔
236

237
            return true;
3✔
238
        }
239

240
        return true;
×
241
    }
242

243
    /**
244
     * Destroys a session
245
     *
246
     * @param string $id The session ID being destroyed
247
     */
248
    public function destroy($id): bool
249
    {
250
        if (isset($this->redis, $this->lockKey)) {
×
251
            if (($result = $this->redis->del($this->keyPrefix . $id)) !== 1) {
×
252
                $this->logger->debug('Session: Redis::del() expected to return 1, got ' . var_export($result, true) . ' instead.');
×
253
            }
254

255
            return $this->destroyCookie();
×
256
        }
257

258
        return false;
×
259
    }
260

261
    /**
262
     * Cleans up expired sessions.
263
     *
264
     * @param int $max_lifetime Sessions that have not updated
265
     *                          for the last max_lifetime seconds will be removed.
266
     *
267
     * @return false|int Returns the number of deleted sessions on success, or false on failure.
268
     */
269
    #[ReturnTypeWillChange]
270
    public function gc($max_lifetime)
271
    {
272
        return 1;
1✔
273
    }
274

275
    /**
276
     * Acquires an emulated lock.
277
     *
278
     * @param string $sessionID Session ID
279
     */
280
    protected function lockSession(string $sessionID): bool
281
    {
282
        $lockKey = $this->keyPrefix . $sessionID . ':lock';
3✔
283

284
        // PHP 7 reuses the SessionHandler object on regeneration,
285
        // so we need to check here if the lock key is for the
286
        // correct session ID.
287
        if ($this->lockKey === $lockKey) {
3✔
288
            return $this->redis->expire($this->lockKey, 300);
×
289
        }
290

291
        $attempt = 0;
3✔
292

293
        do {
294
            if (($ttl = $this->redis->ttl($lockKey)) > 0) {
3✔
295
                sleep(1);
×
296

297
                continue;
×
298
            }
299

300
            if (! $this->redis->setex($lockKey, 300, (string) Time::now()->getTimestamp())) {
3✔
301
                $this->logger->error('Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID);
×
302

303
                return false;
×
304
            }
305

306
            $this->lockKey = $lockKey;
3✔
307
            break;
3✔
308
        } while (++$attempt < 30);
×
309

310
        if ($attempt === 30) {
3✔
311
            log_message('error', 'Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID . ' after 30 attempts, aborting.');
×
312

313
            return false;
×
314
        }
315

316
        if ($ttl === -1) {
3✔
317
            log_message('debug', 'Session: Lock for ' . $this->keyPrefix . $sessionID . ' had no TTL, overriding.');
×
318
        }
319

320
        $this->lock = true;
3✔
321

322
        return true;
3✔
323
    }
324

325
    /**
326
     * Releases a previously acquired lock
327
     */
328
    protected function releaseLock(): bool
329
    {
330
        if (isset($this->redis, $this->lockKey) && $this->lock) {
×
331
            if (! $this->redis->del($this->lockKey)) {
×
332
                $this->logger->error('Session: Error while trying to free lock for ' . $this->lockKey);
×
333

334
                return false;
×
335
            }
336

337
            $this->lockKey = null;
×
338
            $this->lock    = false;
×
339
        }
340

341
        return true;
×
342
    }
343
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc