1313class Session implements SessionHandlerInterface
1414{
1515 private string $ savePath ;
16+ private string $ prefix ;
1617 private array $ data = [];
1718 private bool $ changed = false ;
1819 private ?string $ sessionId = null ;
1920 private ?string $ encryptionKey = null ;
2021 private bool $ autoCommit = true ;
2122 private bool $ testMode = false ;
23+ private bool $ inRegenerate = false ;
2224
2325 /**
2426 * Constructor to initialize the session handler.
@@ -34,11 +36,12 @@ class Session implements SessionHandlerInterface
3436 public function __construct (array $ config = [])
3537 {
3638 $ this ->savePath = $ config ['save_path ' ] ?? sys_get_temp_dir () . '/flight_sessions ' ;
39+ $ this ->prefix = $ config ['prefix ' ] ?? 'sess_ ' ;
3740 $ this ->encryptionKey = $ config ['encryption_key ' ] ?? null ;
3841 $ this ->autoCommit = $ config ['auto_commit ' ] ?? true ;
3942 $ startSession = $ config ['start_session ' ] ?? true ;
4043 $ this ->testMode = $ config ['test_mode ' ] ?? false ;
41-
44+
4245 // Set test session ID if provided
4346 if ($ this ->testMode === true && isset ($ config ['test_session_id ' ])) {
4447 $ this ->sessionId = $ config ['test_session_id ' ];
@@ -52,7 +55,7 @@ public function __construct(array $config = [])
5255 // Initialize session handler
5356 $ this ->initializeSession ($ startSession );
5457 }
55-
58+
5659 /**
5760 * Initialize the session handler and optionally start the session.
5861 *
@@ -69,21 +72,19 @@ private function initializeSession(bool $startSession): void
6972 $ this ->read ($ this ->sessionId ); // Load session data for the test session ID
7073 return ; // Skip actual session operations in test mode
7174 }
72-
75+
7376 // @codeCoverageIgnoreStart
7477 // Register the session handler only if no session is active yet
7578 if ($ startSession === true && session_status () === PHP_SESSION_NONE ) {
7679 // Make sure to register our handler before calling session_start
7780 session_set_save_handler ($ this , true );
78-
81+
7982 // Start the session with proper options
8083 session_start ([
8184 'use_strict_mode ' => true ,
8285 'use_cookies ' => 1 ,
8386 'use_only_cookies ' => 1 ,
84- 'cookie_httponly ' => 1 ,
85- 'sid_length ' => 48 ,
86- 'sid_bits_per_character ' => 6
87+ 'cookie_httponly ' => 1
8788 ]);
8889 $ this ->sessionId = session_id ();
8990 } elseif (session_status () === PHP_SESSION_ACTIVE ) {
@@ -98,7 +99,7 @@ private function initializeSession(bool $startSession): void
9899 // @codeCoverageIgnoreEnd
99100 }
100101
101-
102+
102103 /**
103104 * Open a session.
104105 *
@@ -131,6 +132,7 @@ public function close(): bool
131132 * @param string $id The session ID.
132133 * @return string The session data.
133134 */
135+ #[\ReturnTypeWillChange]
134136 public function read ($ id ): string
135137 {
136138 $ this ->sessionId = $ id ;
@@ -155,39 +157,27 @@ public function read($id): string
155157
156158 // Handle plain data (no encryption)
157159 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
166164 }
167-
168- $ this ->data = [];
169- return '' ;
170165 }
171166
172167 // Handle encrypted data
173168 if ($ prefix === 'E ' && $ this ->encryptionKey !== null ) {
174- try {
175169 $ iv = substr ($ dataStr , 0 , 16 );
176170 $ encrypted = substr ($ dataStr , 16 );
177171 $ decrypted = openssl_decrypt ($ encrypted , 'AES-256-CBC ' , $ this ->encryptionKey , 0 , $ iv );
178172
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 '' ;
185178 }
186- } catch (\Exception $ e ) {
187- // Silently handle decryption or unserialization errors
188179 }
189180 }
190-
191181 // Fail fast: mismatch between prefix and encryption state or corruption
192182 $ this ->data = [];
193183 return '' ;
@@ -204,11 +194,11 @@ protected function encryptData(string $data)
204194 {
205195 $ iv = openssl_random_pseudo_bytes (16 );
206196 $ encrypted = openssl_encrypt ($ data , 'AES-256-CBC ' , $ this ->encryptionKey , 0 , $ iv );
207-
197+
208198 if ($ encrypted === false ) {
209199 return false ; // @codeCoverageIgnore
210200 }
211-
201+
212202 return 'E ' . $ iv . $ encrypted ;
213203 }
214204
@@ -220,7 +210,7 @@ public function write($id, $data): bool
220210 // When PHP calls this method, it passes serialized data
221211 // We ignore this parameter because we maintain our data internally
222212 // and handle serialization ourselves
223-
213+
224214 // Fail fast: no changes to write
225215 if ($ this ->changed === false && empty ($ this ->data ) === false ) {
226216 return true ;
@@ -232,7 +222,7 @@ public function write($id, $data): bool
232222 // Handle encryption if key is provided
233223 if ($ this ->encryptionKey !== null ) {
234224 $ content = $ this ->encryptData ($ serialized );
235-
225+
236226 // Fail fast: encryption failed
237227 if ($ content === false ) {
238228 return false ;
@@ -253,12 +243,27 @@ public function write($id, $data): bool
253243 */
254244 public function destroy ($ id ): bool
255245 {
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+
256259 $ 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+ }
259265 }
260- $ this ->data = [];
261- $ this ->changed = true ;
266+
262267 return true ;
263268 }
264269
@@ -276,7 +281,7 @@ public function gc($maxLifetime)
276281 {
277282 $ count = 0 ;
278283 $ time = time ();
279- $ pattern = $ this ->savePath . '/sess_ * ' ;
284+ $ pattern = $ this ->savePath . '/ ' . $ this -> prefix . ' * ' ;
280285
281286 // Get session files; return 0 if glob fails or no files exist
282287 $ files = glob ($ pattern );
@@ -382,29 +387,34 @@ public function id(): ?string
382387 /**
383388 * Regenerates the session ID.
384389 *
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.
386391 * @return self Returns the current instance for method chaining.
387392 */
388- public function regenerate (bool $ deleteOld = false ): self
393+ public function regenerate (bool $ deleteOldFile = false ): self
389394 {
390395 if ($ this ->sessionId ) {
396+ $ oldId = $ this ->sessionId ;
397+ $ oldData = $ this ->data ;
398+ $ this ->inRegenerate = true ;
399+
391400 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
394402 $ this ->sessionId = bin2hex (random_bytes (16 ));
395- if ($ deleteOld ) {
396- $ this ->destroy ($ oldId );
397- }
398403 } else {
399404 // @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 ();
406407 // @codeCoverageIgnoreEnd
407408 }
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+
408418 $ this ->changed = true ;
409419 }
410420 return $ this ;
@@ -418,6 +428,6 @@ public function regenerate(bool $deleteOld = false): self
418428 */
419429 private function getSessionFile (string $ id ): string
420430 {
421- return $ this ->savePath . '/sess_ ' . $ id ;
431+ return $ this ->savePath . '/ ' . $ this -> prefix . $ id ;
422432 }
423433}
0 commit comments