Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.85% |
109 / 130 |
|
46.15% |
6 / 13 |
CRAP | |
0.00% |
0 / 1 |
FilesHandler | |
83.85% |
109 / 130 |
|
46.15% |
6 / 13 |
63.40 | |
0.00% |
0 / 1 |
prepareConfig | |
86.96% |
20 / 23 |
|
0.00% |
0 / 1 |
7.11 | |||
getFilename | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
open | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
read | |
78.95% |
15 / 19 |
|
0.00% |
0 / 1 |
8.60 | |||
readData | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
write | |
81.25% |
13 / 16 |
|
0.00% |
0 / 1 |
8.42 | |||
updateTimestamp | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
close | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
destroy | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
gc | |
73.68% |
14 / 19 |
|
0.00% |
0 / 1 |
6.66 | |||
gcSubdir | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
7.01 | |||
lock | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
3.79 | |||
unlock | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 |
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 | */ |
10 | namespace Framework\Session\SaveHandlers; |
11 | |
12 | use Framework\Log\LogLevel; |
13 | use Framework\Session\SaveHandler; |
14 | use LogicException; |
15 | use RuntimeException; |
16 | use SensitiveParameter; |
17 | |
18 | /** |
19 | * Class FilesHandler. |
20 | * |
21 | * @package session |
22 | */ |
23 | class FilesHandler extends SaveHandler |
24 | { |
25 | /** |
26 | * @var resource|null |
27 | */ |
28 | protected $stream; |
29 | |
30 | /** |
31 | * Prepare configurations to be used by the FilesHandler. |
32 | * |
33 | * @param array<string,mixed> $config Custom configs |
34 | * |
35 | * The custom configs are: |
36 | * |
37 | * ```php |
38 | * $configs = [ |
39 | * // The directory path where the session files will be saved |
40 | * 'directory' => '', |
41 | * // A custom directory name inside the `directory` path |
42 | * 'prefix' => '', |
43 | * // Match IP? |
44 | * 'match_ip' => false, |
45 | * // Match User-Agent? |
46 | * 'match_ua' => false, |
47 | * ]; |
48 | * ``` |
49 | */ |
50 | protected function prepareConfig(#[SensitiveParameter] array $config) : void |
51 | { |
52 | $this->config = \array_replace([ |
53 | 'prefix' => '', |
54 | 'directory' => '', |
55 | 'match_ip' => false, |
56 | 'match_ua' => false, |
57 | ], $config); |
58 | if (empty($this->config['directory'])) { |
59 | throw new LogicException('Session config has not a directory'); |
60 | } |
61 | $this->config['directory'] = \rtrim( |
62 | $this->config['directory'], |
63 | \DIRECTORY_SEPARATOR |
64 | ) . \DIRECTORY_SEPARATOR; |
65 | if ( ! \is_dir($this->config['directory'])) { |
66 | throw new LogicException( |
67 | 'Session config directory does not exist: ' . $this->config['directory'] |
68 | ); |
69 | } |
70 | if ($this->config['prefix']) { |
71 | $dirname = $this->config['directory'] . $this->config['prefix'] . \DIRECTORY_SEPARATOR; |
72 | if ( ! \is_dir($dirname) && ! \mkdir($dirname, 0700) && ! \is_dir($dirname)) { |
73 | throw new RuntimeException( |
74 | "Session prefix directory '{$dirname}' was not created", |
75 | ); |
76 | } |
77 | $this->config['directory'] = $dirname; |
78 | } |
79 | } |
80 | |
81 | /** |
82 | * Get the filename, using the optional |
83 | * match IP and match User-Agent configs. |
84 | * |
85 | * @param string $id The session id |
86 | * |
87 | * @return string The final filename |
88 | */ |
89 | protected function getFilename(string $id) : string |
90 | { |
91 | $filename = $this->config['directory'] . $id[0] . $id[1] . \DIRECTORY_SEPARATOR . $id; |
92 | return $filename . $this->getKeySuffix(); |
93 | } |
94 | |
95 | public function open($path, $name) : bool |
96 | { |
97 | return true; |
98 | } |
99 | |
100 | public function read($id) : string |
101 | { |
102 | if ($this->stream !== null) { |
103 | \rewind($this->stream); |
104 | return $this->readData(); |
105 | } |
106 | $filename = $this->getFilename($id); |
107 | $dirname = \dirname($filename); |
108 | if ( ! \is_dir($dirname) && ! \mkdir($dirname, 0700) && ! \is_dir($dirname)) { |
109 | throw new RuntimeException( |
110 | "Session subdirectory '{$dirname}' was not created", |
111 | ); |
112 | } |
113 | $this->sessionExists = \is_file($filename); |
114 | if ( ! $this->lock($filename)) { |
115 | return ''; |
116 | } |
117 | if ( ! isset($this->sessionId)) { |
118 | $this->sessionId = $id; |
119 | } |
120 | if ( ! $this->sessionExists) { |
121 | \chmod($filename, 0600); |
122 | $this->setFingerprint(''); |
123 | return ''; |
124 | } |
125 | return $this->readData(); |
126 | } |
127 | |
128 | protected function readData() : string |
129 | { |
130 | $data = ''; |
131 | while ( ! \feof($this->stream)) { |
132 | $data .= \fread($this->stream, 1024); |
133 | } |
134 | $this->setFingerprint($data); |
135 | return $data; |
136 | } |
137 | |
138 | public function write($id, $data) : bool |
139 | { |
140 | if ( ! isset($this->stream)) { |
141 | return false; |
142 | } |
143 | if ($id !== $this->sessionId) { |
144 | $this->sessionId = $id; |
145 | } |
146 | if ($this->hasSameFingerprint($data)) { |
147 | return ! $this->sessionExists || \touch($this->getFilename($id)); |
148 | } |
149 | if ($this->sessionExists) { |
150 | \ftruncate($this->stream, 0); |
151 | \rewind($this->stream); |
152 | } |
153 | if ($data !== '') { |
154 | $written = \fwrite($this->stream, $data); |
155 | if ($written === false) { |
156 | $this->log('Session (files): Unable to write data'); |
157 | return false; |
158 | } |
159 | } |
160 | $this->setFingerprint($data); |
161 | return true; |
162 | } |
163 | |
164 | public function updateTimestamp($id, $data) : bool |
165 | { |
166 | $filename = $this->getFilename($id); |
167 | return \is_file($filename) |
168 | ? \touch($filename) |
169 | : \fwrite($this->stream, $data) !== false; |
170 | } |
171 | |
172 | public function close() : bool |
173 | { |
174 | if ( ! \is_resource($this->stream)) { |
175 | return true; |
176 | } |
177 | $this->unlock(); |
178 | $this->sessionExists = false; |
179 | return true; |
180 | } |
181 | |
182 | public function destroy($id) : bool |
183 | { |
184 | $this->close(); |
185 | \clearstatcache(); |
186 | $filename = $this->getFilename($id); |
187 | return ! \is_file($filename) || \unlink($filename); |
188 | } |
189 | |
190 | public function gc($max_lifetime) : int | false |
191 | { |
192 | $dirHandle = \opendir($this->config['directory']); |
193 | if ($dirHandle === false) { |
194 | $this->log( |
195 | "Session (files): Garbage Collector could not open directory '{$this->config['directory']}'", |
196 | LogLevel::DEBUG |
197 | ); |
198 | return false; |
199 | } |
200 | $gcCount = 0; |
201 | $max_lifetime = \time() - $max_lifetime; |
202 | while (($filename = \readdir($dirHandle)) !== false) { |
203 | if ($filename !== '.' |
204 | && $filename !== '..' |
205 | && \is_dir($this->config['directory'] . $filename) |
206 | ) { |
207 | $gcCount += $this->gcSubdir( |
208 | $this->config['directory'] . $filename, |
209 | $max_lifetime |
210 | ); |
211 | } |
212 | } |
213 | \closedir($dirHandle); |
214 | return $gcCount; |
215 | } |
216 | |
217 | protected function gcSubdir(string $directory, int $maxMtime) : int |
218 | { |
219 | $gcCount = 0; |
220 | $dirHandle = \opendir($directory); |
221 | if ($dirHandle === false) { |
222 | return $gcCount; |
223 | } |
224 | while (($filename = \readdir($dirHandle)) !== false) { |
225 | $filename = $directory . \DIRECTORY_SEPARATOR . $filename; |
226 | if (\is_dir($filename)) { |
227 | continue; |
228 | } |
229 | $mtime = \filemtime($filename); |
230 | if (($mtime < $maxMtime) && \unlink($filename)) { |
231 | $gcCount++; |
232 | } |
233 | } |
234 | \closedir($dirHandle); |
235 | if (\count((array) \scandir($directory)) === 2) { |
236 | \rmdir($directory); |
237 | } |
238 | return $gcCount; |
239 | } |
240 | |
241 | protected function lock(string $id) : bool |
242 | { |
243 | $stream = \fopen($id, 'c+b'); |
244 | if ($stream === false) { |
245 | return false; |
246 | } |
247 | if (\flock($stream, \LOCK_EX) === false) { |
248 | $this->log("Session (files): Error while trying to lock '{$id}'"); |
249 | \fclose($stream); |
250 | return false; |
251 | } |
252 | $this->stream = $stream; |
253 | return true; |
254 | } |
255 | |
256 | protected function unlock() : bool |
257 | { |
258 | if ($this->stream === null) { |
259 | return true; |
260 | } |
261 | $unlocked = \flock($this->stream, \LOCK_UN); |
262 | if ($unlocked === false) { |
263 | $this->log('Session (files): Error while trying to unlock ' . $this->getFilename($this->sessionId)); |
264 | } |
265 | \fclose($this->stream); |
266 | $this->stream = null; |
267 | return true; |
268 | } |
269 | } |