Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
SaveHandler
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
11 / 11
18
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 prepareConfig
n/a
0 / 0
n/a
0 / 0
1
 getConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 log
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFingerprint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeFingerprint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSameFingerprint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxlifetime
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIP
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUA
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getKeySuffix
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 validateId
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 open
n/a
0 / 0
n/a
0 / 0
0
 read
n/a
0 / 0
n/a
0 / 0
0
 write
n/a
0 / 0
n/a
0 / 0
0
 updateTimestamp
n/a
0 / 0
n/a
0 / 0
0
 close
n/a
0 / 0
n/a
0 / 0
0
 destroy
n/a
0 / 0
n/a
0 / 0
0
 gc
n/a
0 / 0
n/a
0 / 0
0
 lock
n/a
0 / 0
n/a
0 / 0
0
 unlock
n/a
0 / 0
n/a
0 / 0
0
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Session Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\Session;
11
12use Framework\Log\Logger;
13use Framework\Log\LogLevel;
14use SensitiveParameter;
15
16/**
17 * Class SaveHandler.
18 *
19 * @see https://www.php.net/manual/en/class.sessionhandler.php
20 * @see https://gist.github.com/mindplay-dk/623bdd50c1b4c0553cd3
21 * @see https://www.cloudways.com/blog/setup-redis-as-session-handler-php/#sessionlifecycle
22 *
23 * @package session
24 */
25abstract class SaveHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
26{
27    /**
28     * The configurations used by the save handler.
29     *
30     * @var array<string,mixed>
31     */
32    protected array $config;
33    /**
34     * The current data fingerprint.
35     *
36     * @var string
37     */
38    protected string $fingerprint;
39    /**
40     * The lock id or false if is not locked.
41     *
42     * @var false|string
43     */
44    protected string | false $lockId = false;
45    /**
46     * Tells if the session exists (if was read).
47     *
48     * @var bool
49     */
50    protected bool $sessionExists = false;
51    /**
52     * The current session ID.
53     *
54     * @var string|null
55     */
56    protected ?string $sessionId;
57    /**
58     * The Logger instance or null if it was not set.
59     *
60     * @var Logger|null
61     */
62    protected ?Logger $logger;
63
64    /**
65     * SessionSaveHandler constructor.
66     *
67     * @param array<string,mixed> $config
68     * @param Logger|null $logger
69     */
70    public function __construct(
71        #[SensitiveParameter] array $config = [],
72        Logger $logger = null
73    ) {
74        $this->prepareConfig($config);
75        $this->logger = $logger;
76    }
77
78    /**
79     * Prepare configurations to be used by the save handler.
80     *
81     * @param array<string,mixed> $config Custom configs
82     *
83     * @codeCoverageIgnore
84     */
85    protected function prepareConfig(#[SensitiveParameter] array $config) : void
86    {
87        $this->config = $config;
88    }
89
90    /**
91     * @return array<string,mixed>
92     */
93    public function getConfig() : array
94    {
95        return $this->config;
96    }
97
98    /**
99     * Log a message if the Logger is set.
100     *
101     * @param string $message The message to log
102     * @param LogLevel $level The log level
103     */
104    protected function log(string $message, LogLevel $level = LogLevel::ERROR) : void
105    {
106        $this->logger?->log($level, $message);
107    }
108
109    /**
110     * Set the data fingerprint.
111     *
112     * @param string $data The data to set the new fingerprint
113     */
114    protected function setFingerprint(string $data) : void
115    {
116        $this->fingerprint = $this->makeFingerprint($data);
117    }
118
119    /**
120     * Make the fingerprint value.
121     *
122     * @param string $data The data to get the fingerprint
123     *
124     * @return string The fingerprint hash
125     */
126    private function makeFingerprint(string $data) : string
127    {
128        return \hash('xxh3', $data);
129    }
130
131    /**
132     * Tells if the data has the same current fingerprint.
133     *
134     * @param string $data The data to compare
135     *
136     * @return bool True if the fingerprints are the same, otherwise false
137     */
138    protected function hasSameFingerprint(string $data) : bool
139    {
140        return $this->fingerprint === $this->makeFingerprint($data);
141    }
142
143    /**
144     * Get the maxlifetime (TTL) used by cache handlers or locking.
145     *
146     * NOTE: It will use the `maxlifetime` config or the ini value of
147     * `session.gc_maxlifetime` as fallback.
148     *
149     * @return int The maximum lifetime of a session in seconds
150     */
151    protected function getMaxlifetime() : int
152    {
153        return (int) ($this->config['maxlifetime'] ?? \ini_get('session.gc_maxlifetime'));
154    }
155
156    /**
157     * Get the remote IP address.
158     *
159     * @return string
160     */
161    protected function getIP() : string
162    {
163        return $_SERVER['REMOTE_ADDR'] ?? '';
164    }
165
166    /**
167     * Get the HTTP User-Agent.
168     *
169     * @return string
170     */
171    protected function getUA() : string
172    {
173        return $_SERVER['HTTP_USER_AGENT'] ?? '';
174    }
175
176    protected function getKeySuffix() : string
177    {
178        $suffix = '';
179        if ($this->config['match_ip']) {
180            $suffix .= ':' . $this->getIP();
181        }
182        if ($this->config['match_ua']) {
183            $suffix .= ':' . $this->getUA();
184        }
185        if ($suffix) {
186            $suffix = \hash('xxh3', $suffix);
187        }
188        return $suffix;
189    }
190
191    /**
192     * Validate session id.
193     *
194     * @param string $id The session id
195     *
196     * @see https://www.php.net/manual/en/sessionupdatetimestamphandlerinterface.validateid.php
197     *
198     * @return bool Returns TRUE if the id is valid, otherwise FALSE
199     */
200    public function validateId($id) : bool
201    {
202        $bits = \ini_get('session.sid_bits_per_character') ?: 5;
203        $length = \ini_get('session.sid_length') ?: 40;
204        $bitsRegex = [
205            4 => '[0-9a-f]',
206            5 => '[0-9a-v]',
207            6 => '[0-9a-zA-Z,-]',
208        ];
209        return isset($bitsRegex[$bits])
210            && \preg_match('#\A' . $bitsRegex[$bits] . '{' . $length . '}\z#', $id);
211    }
212
213    /**
214     * Initialize the session.
215     *
216     * @param string $path The path where to store/retrieve the session
217     * @param string $name The session name
218     *
219     * @see https://www.php.net/manual/en/sessionhandlerinterface.open.php
220     *
221     * @return bool Returns TRUE on success, FALSE on failure
222     */
223    abstract public function open($path, $name) : bool;
224
225    /**
226     * Read session data.
227     *
228     * @param string $id The session id to read data for
229     *
230     * @see https://www.php.net/manual/en/sessionhandlerinterface.read.php
231     *
232     * @return string Returns an encoded string of the read data.
233     * If nothing was read, it returns an empty string
234     */
235    abstract public function read($id) : string;
236
237    /**
238     * Write session data.
239     *
240     * @param string $id The session id
241     * @param string $data The encoded session data. This data is the result
242     * of the PHP internally encoding the $_SESSION superglobal to a serialized
243     * string and passing it as this parameter.
244     *
245     * NOTE: Sessions can use an alternative serialization method
246     *
247     * @see https://www.php.net/manual/en/sessionhandlerinterface.write.php
248     *
249     * @return bool Returns TRUE on success, FALSE on failure
250     */
251    abstract public function write($id, $data) : bool;
252
253    /**
254     * Update the timestamp of a session.
255     *
256     * @param string $id The session id
257     * @param string $data The encoded session data. This data is the result
258     * of the PHP internally encoding the $_SESSION superglobal to a serialized
259     * string and passing it as this parameter.
260     *
261     * NOTE: Sessions can use an alternative serialization method
262     *
263     * @see https://www.php.net/manual/en/sessionupdatetimestamphandlerinterface.updatetimestamp.php
264     *
265     * @return bool Returns TRUE on success, FALSE on failure
266     */
267    abstract public function updateTimestamp($id, $data) : bool;
268
269    /**
270     * Close the session.
271     *
272     * @see https://www.php.net/manual/en/sessionhandlerinterface.close.php
273     *
274     * @return bool Returns TRUE on success, FALSE on failure
275     */
276    abstract public function close() : bool;
277
278    /**
279     * Destroy a session.
280     *
281     * @param string $id The session ID being destroyed
282     *
283     * @see https://www.php.net/manual/en/sessionhandlerinterface.destroy.php
284     *
285     * @return bool Returns TRUE on success, FALSE on failure
286     */
287    abstract public function destroy($id) : bool;
288
289    /**
290     * Cleanup old sessions.
291     *
292     * @param int $max_lifetime Sessions that have not updated for
293     * the last $maxLifetime seconds will be removed
294     *
295     * @see https://www.php.net/manual/en/sessionhandlerinterface.gc.php
296     *
297     * @return false|int Returns the number of deleted session data for success,
298     * false for failure
299     */
300    abstract public function gc($max_lifetime) : int | false;
301
302    /**
303     * Acquire a lock for a session id.
304     *
305     * @param string $id The session id
306     *
307     * @return bool Returns TRUE on success, FALSE on failure
308     */
309    abstract protected function lock(string $id) : bool;
310
311    /**
312     * Unlock the current session lock id.
313     *
314     * @return bool Returns TRUE on success, FALSE on failure
315     */
316    abstract protected function unlock() : bool;
317}