13
13
class Session implements SessionHandlerInterface
14
14
{
15
15
private string $ savePath ;
16
+ private string $ prefix ;
16
17
private array $ data = [];
17
18
private bool $ changed = false ;
18
19
private ?string $ sessionId = null ;
19
20
private ?string $ encryptionKey = null ;
20
21
private bool $ autoCommit = true ;
21
22
private bool $ testMode = false ;
23
+ private bool $ inRegenerate = false ;
22
24
23
25
/**
24
26
* Constructor to initialize the session handler.
@@ -34,11 +36,12 @@ class Session implements SessionHandlerInterface
34
36
public function __construct (array $ config = [])
35
37
{
36
38
$ this ->savePath = $ config ['save_path ' ] ?? sys_get_temp_dir () . '/flight_sessions ' ;
39
+ $ this ->prefix = $ config ['prefix ' ] ?? 'sess_ ' ;
37
40
$ this ->encryptionKey = $ config ['encryption_key ' ] ?? null ;
38
41
$ this ->autoCommit = $ config ['auto_commit ' ] ?? true ;
39
42
$ startSession = $ config ['start_session ' ] ?? true ;
40
43
$ this ->testMode = $ config ['test_mode ' ] ?? false ;
41
-
44
+
42
45
// Set test session ID if provided
43
46
if ($ this ->testMode === true && isset ($ config ['test_session_id ' ])) {
44
47
$ this ->sessionId = $ config ['test_session_id ' ];
@@ -52,7 +55,7 @@ public function __construct(array $config = [])
52
55
// Initialize session handler
53
56
$ this ->initializeSession ($ startSession );
54
57
}
55
-
58
+
56
59
/**
57
60
* Initialize the session handler and optionally start the session.
58
61
*
@@ -69,21 +72,19 @@ private function initializeSession(bool $startSession): void
69
72
$ this ->read ($ this ->sessionId ); // Load session data for the test session ID
70
73
return ; // Skip actual session operations in test mode
71
74
}
72
-
75
+
73
76
// @codeCoverageIgnoreStart
74
77
// Register the session handler only if no session is active yet
75
78
if ($ startSession === true && session_status () === PHP_SESSION_NONE ) {
76
79
// Make sure to register our handler before calling session_start
77
80
session_set_save_handler ($ this , true );
78
-
81
+
79
82
// Start the session with proper options
80
83
session_start ([
81
84
'use_strict_mode ' => true ,
82
85
'use_cookies ' => 1 ,
83
86
'use_only_cookies ' => 1 ,
84
- 'cookie_httponly ' => 1 ,
85
- 'sid_length ' => 48 ,
86
- 'sid_bits_per_character ' => 6
87
+ 'cookie_httponly ' => 1
87
88
]);
88
89
$ this ->sessionId = session_id ();
89
90
} elseif (session_status () === PHP_SESSION_ACTIVE ) {
@@ -98,7 +99,7 @@ private function initializeSession(bool $startSession): void
98
99
// @codeCoverageIgnoreEnd
99
100
}
100
101
101
-
102
+
102
103
/**
103
104
* Open a session.
104
105
*
@@ -131,6 +132,7 @@ public function close(): bool
131
132
* @param string $id The session ID.
132
133
* @return string The session data.
133
134
*/
135
+ #[\ReturnTypeWillChange]
134
136
public function read ($ id ): string
135
137
{
136
138
$ this ->sessionId = $ id ;
@@ -155,39 +157,27 @@ public function read($id): string
155
157
156
158
// Handle plain data (no encryption)
157
159
if ($ prefix === 'P ' && $ this ->encryptionKey === null ) {
158
- try {
159
- $ unserialized = unserialize ($ dataStr );
160
- if ($ unserialized !== false ) {
161
- $ this ->data = $ unserialized ;
162
- return '' ; // Return empty string to let PHP handle serialization
163
- }
164
- } catch (\Exception $ e ) {
165
- // Silently handle unserialization errors
160
+ $ unserialized = unserialize ($ dataStr );
161
+ if ($ unserialized !== false ) {
162
+ $ this ->data = $ unserialized ;
163
+ return '' ; // Return empty string to let PHP handle serialization
166
164
}
167
-
168
- $ this ->data = [];
169
- return '' ;
170
165
}
171
166
172
167
// Handle encrypted data
173
168
if ($ prefix === 'E ' && $ this ->encryptionKey !== null ) {
174
- try {
175
169
$ iv = substr ($ dataStr , 0 , 16 );
176
170
$ encrypted = substr ($ dataStr , 16 );
177
171
$ decrypted = openssl_decrypt ($ encrypted , 'AES-256-CBC ' , $ this ->encryptionKey , 0 , $ iv );
178
172
179
- if ($ decrypted !== false ) {
180
- $ unserialized = unserialize ($ decrypted );
181
- if ($ unserialized !== false ) {
182
- $ this ->data = $ unserialized ;
183
- return '' ;
184
- }
173
+ if ($ decrypted !== false ) {
174
+ $ unserialized = unserialize ($ decrypted );
175
+ if ($ unserialized !== false ) {
176
+ $ this ->data = $ unserialized ;
177
+ return '' ;
185
178
}
186
- } catch (\Exception $ e ) {
187
- // Silently handle decryption or unserialization errors
188
179
}
189
180
}
190
-
191
181
// Fail fast: mismatch between prefix and encryption state or corruption
192
182
$ this ->data = [];
193
183
return '' ;
@@ -204,11 +194,11 @@ protected function encryptData(string $data)
204
194
{
205
195
$ iv = openssl_random_pseudo_bytes (16 );
206
196
$ encrypted = openssl_encrypt ($ data , 'AES-256-CBC ' , $ this ->encryptionKey , 0 , $ iv );
207
-
197
+
208
198
if ($ encrypted === false ) {
209
199
return false ; // @codeCoverageIgnore
210
200
}
211
-
201
+
212
202
return 'E ' . $ iv . $ encrypted ;
213
203
}
214
204
@@ -220,7 +210,7 @@ public function write($id, $data): bool
220
210
// When PHP calls this method, it passes serialized data
221
211
// We ignore this parameter because we maintain our data internally
222
212
// and handle serialization ourselves
223
-
213
+
224
214
// Fail fast: no changes to write
225
215
if ($ this ->changed === false && empty ($ this ->data ) === false ) {
226
216
return true ;
@@ -232,7 +222,7 @@ public function write($id, $data): bool
232
222
// Handle encryption if key is provided
233
223
if ($ this ->encryptionKey !== null ) {
234
224
$ content = $ this ->encryptData ($ serialized );
235
-
225
+
236
226
// Fail fast: encryption failed
237
227
if ($ content === false ) {
238
228
return false ;
@@ -253,12 +243,27 @@ public function write($id, $data): bool
253
243
*/
254
244
public function destroy ($ id ): bool
255
245
{
246
+ // If we're destroying the current session, clear the data
247
+ if ($ id === $ this ->sessionId ) {
248
+ $ this ->data = [];
249
+ $ this ->changed = true ;
250
+ $ this ->autoCommit = false ; // Disable auto-commit to prevent writing empty data
251
+ $ this ->commit ();
252
+ if ($ this ->testMode === false && $ this ->inRegenerate === false && session_status () === PHP_SESSION_ACTIVE ) {
253
+ // Ensure session is closed
254
+ session_write_close (); // @codeCoverageIgnore
255
+ }
256
+ $ this ->sessionId = null ; // Clear session ID
257
+ }
258
+
256
259
$ file = $ this ->getSessionFile ($ id );
257
- if (file_exists ($ file )) {
258
- unlink ($ file );
260
+ if (file_exists ($ file ) === true ) {
261
+ $ result = unlink ($ file );
262
+ if ($ result === false ) {
263
+ return false ; // @codeCoverageIgnore
264
+ }
259
265
}
260
- $ this ->data = [];
261
- $ this ->changed = true ;
266
+
262
267
return true ;
263
268
}
264
269
@@ -276,7 +281,7 @@ public function gc($maxLifetime)
276
281
{
277
282
$ count = 0 ;
278
283
$ time = time ();
279
- $ pattern = $ this ->savePath . '/sess_ * ' ;
284
+ $ pattern = $ this ->savePath . '/ ' . $ this -> prefix . ' * ' ;
280
285
281
286
// Get session files; return 0 if glob fails or no files exist
282
287
$ files = glob ($ pattern );
@@ -382,29 +387,34 @@ public function id(): ?string
382
387
/**
383
388
* Regenerates the session ID.
384
389
*
385
- * @param bool $deleteOld Whether to delete the old session data or not.
390
+ * @param bool $deleteOldFile Whether to delete the old session data or not.
386
391
* @return self Returns the current instance for method chaining.
387
392
*/
388
- public function regenerate (bool $ deleteOld = false ): self
393
+ public function regenerate (bool $ deleteOldFile = false ): self
389
394
{
390
395
if ($ this ->sessionId ) {
396
+ $ oldId = $ this ->sessionId ;
397
+ $ oldData = $ this ->data ;
398
+ $ this ->inRegenerate = true ;
399
+
391
400
if ($ this ->testMode ) {
392
- // In test mode, simply generate a new ID without affecting PHP sessions
393
- $ oldId = $ this ->sessionId ;
401
+ // In test mode, generate a new ID without affecting PHP sessions
394
402
$ this ->sessionId = bin2hex (random_bytes (16 ));
395
- if ($ deleteOld ) {
396
- $ this ->destroy ($ oldId );
397
- }
398
403
} else {
399
404
// @codeCoverageIgnoreStart
400
- session_regenerate_id ($ deleteOld );
401
- $ newId = session_id ();
402
- if ($ deleteOld ) {
403
- $ this ->destroy ($ this ->sessionId );
404
- }
405
- $ this ->sessionId = $ newId ;
405
+ session_regenerate_id ($ deleteOldFile );
406
+ $ this ->sessionId = session_id ();
406
407
// @codeCoverageIgnoreEnd
407
408
}
409
+ $ this ->inRegenerate = false ;
410
+
411
+ // Save the current data with the new session ID first
412
+ if (empty ($ oldData ) === false ) {
413
+ $ this ->changed = true ;
414
+ $ this ->data = $ oldData ;
415
+ $ this ->commit ();
416
+ }
417
+
408
418
$ this ->changed = true ;
409
419
}
410
420
return $ this ;
@@ -418,6 +428,6 @@ public function regenerate(bool $deleteOld = false): self
418
428
*/
419
429
private function getSessionFile (string $ id ): string
420
430
{
421
- return $ this ->savePath . '/sess_ ' . $ id ;
431
+ return $ this ->savePath . '/ ' . $ this -> prefix . $ id ;
422
432
}
423
433
}
0 commit comments