Difference between revisions for Users / Eo Ny





Next edit →

Merge of Version1 & Version2
1 == Session Management Technical Documentation ==
2 {{toc numerate=1}}
3
4 === Overview ===
5
6 The ##Session## class is an abstract session management system for WackoWiki that extends ##ArrayObject## to provide secure, configurable session handling. It implements sophisticated security features including session ID regeneration, anti-replay protection, nonce verification, and user agent/IP validation.
7
8 **Location:** ##src/class/session.php##
9 **Type:** Abstract class (must be extended with a ##SessionStoreInterface## implementation)
10 **Inheritance:** ##ArrayObject##
11
12 ----
13
14 === Table of Contents ===
15   1. ((#core-concepts Core Concepts))
16   2. ((#architecture Architecture))
17   3. ((#configuration Configuration))
18   4. ((#usage Usage))
19   5. ((#security-features Security Features))
20   6. ((#api-reference API Reference))
21   7. ((#session-lifecycle Session Lifecycle))
22   8. ((#flash-data Flash Data))
23   9. ((#nonce-system Nonce System))
24   10. ((#cookie-management Cookie Management))
25   11. ((#error-handling Error Handling))
26   12. ((#implementation-guide Implementation Guide))
27
28 ----
29
30 === Core Concepts ===
31
32 ==== Session State ====
33 The Session class maintains three primary states:
34   - **Inactive** (##$active = false##): Session not yet started or has been closed
35   - **Active** (##$active = true##): Session is running and can store/retrieve data
36   - **Regenerated**: Session ID has been replaced (tracked via ##$regenerated## flag)
37
38 ==== Session Data Storage ====
39 Session data is stored as an array accessible through ##ArrayObject## interface:
40 %%(hl php)
41 $session['user_id'] = 123; // Set data
42 echo $session['user_id']; // Get data
43 %%
44
45 ==== Sticky Data ====
46 Variables prefixed with ##sticky_## are persistent across session resets:
47   - ##sticky__created##: Session creation timestamp
48   - ##sticky__flash##: Flash data lifetime tracking
49   - ##sticky__log##: Regeneration event log
50   - ##sticky__ip##: IP change tracking
51
52 ==== Internal Tracking Variables ====
53 Variables prefixed with ##__## are internal session metadata:
54   - ##__started##: Session start time
55   - ##__updated##: Last session update time
56   - ##__regenerated##: Last session ID regeneration time
57   - ##__user_agent##: Client user agent string
58   - ##__user_ip##: Client IP address
59   - ##__user_tls##: TLS/SSL status
60   - ##__nonces##: Active nonce storage
61   - ##__expire##: Session expiration time (for old sessions)
62
63 ----
64
65 === Architecture ===
66
67 ==== Class Hierarchy ====
68 %%
69 ArrayObject (PHP native)
70     ↓
71 Session (abstract)
72     ↓
73 [Concrete Implementation] (must implement store_* methods)
74 %%
75
76 ==== Key Methods Categories ====
77
78 **Lifecycle Management:**
79   - ##__construct()##: Initialize session object
80   - ##start()##: Begin a session
81   - ##write_close()##: Save and close session
82   - ##restart()##: Destroy and restart session
83   - ##terminator()##: Shutdown handler (garbage collection, flash data cleanup)
84
85 **Security:**
86   - ##regenerate_id()##: Replace session ID
87   - ##verify_nonce()##: Validate nonce tokens
88   - ##prevent_replay()##: Anti-replay protection
89   - ##create_nonce()##: Generate nonce tokens
90
91 **Storage (Abstract - Must Implement):**
92   - ##store_open()##: Open session storage
93   - ##store_read()##: Read session data
94   - ##store_write()##: Write session data
95   - ##store_close()##: Close session storage
96   - ##store_gc()##: Garbage collection
97   - ##store_validate_id()##: Validate session ID format
98   - ##store_generate_id()##: Generate new session ID
99
100 **Cookie Management:**
101   - ##setcookie()##: Set HTTP cookie with security headers
102   - ##get_cookie()##: Retrieve cookie value
103   - ##set_cookie()##: Set cookie (legacy interface)
104   - ##delete_cookie()##: Remove cookie
105   - ##send_cookie()##: Internal cookie transmission
106
107 ----
108
109 === Configuration ===
110
111 ==== Configuration Properties (Public) ====
112
113 All configuration properties are prefixed with ##cf_## (config) and can be set before calling ##start()##:
114
115 ===== Session Behavior =====
116 %%(hl php)
117 $session->cf_static = 0; // Disable regenerations (e.g., for CAPTCHA)
118 $session->cf_max_session = 7200; // Max session lifetime (seconds)
119 $session->cf_max_idle = 1440; // Max idle time before destruction (seconds)
120 $session->cf_regen_time = 500; // Seconds between forced ID regenerations
121 $session->cf_regen_probability = 2; // Percentage probability of forced regen (0-100)
122 %%
123
124 ===== Nonce & Replay Protection =====
125 %%(hl php)
126 $session->cf_secret = 'adyaiD9+255JeiskPybgisby'; // Secret for nonce generation
127 $session->cf_nonce_lifetime = 7200; // Nonce expiration (seconds)
128 $session->cf_prevent_replay = 1; // Enable replay attack prevention
129 %%
130
131 ===== Garbage Collection =====
132 %%(hl php)
133 $session->cf_gc_probability = 2; // Probability of GC on shutdown (0-100)
134 $session->cf_gc_maxlifetime = 1440; // Max session file lifetime (seconds)
135 %%
136
137 ===== Cookie Settings =====
138 %%(hl php)
139 $session->cf_cookie_prefix = ''; // Prefix for all cookies
140 $session->cf_cookie_persistent = false; // Make cookies persistent
141 $session->cf_cookie_lifetime = 0; // Cookie lifetime (0 = session cookie)
142 $session->cf_cookie_path = '/'; // Cookie path
143 $session->cf_cookie_domain = ''; // Cookie domain ('' = current host)
144 $session->cf_cookie_secure = false; // HTTPS only
145 $session->cf_cookie_httponly = true; // Disable JavaScript access
146 $session->cf_cookie_samesite = COOKIE_SAMESITE; // SameSite attribute
147 %%
148
149 ===== Cache Control =====
150 %%(hl php)
151 $session->cf_cache_limiter = 'none'; // Cache control mode (public|private|nocache|none)
152 $session->cf_cache_expire = 180*60; // Cache TTL (seconds)
153 $session->cf_cache_mtime = 0; // Modify time for Last-Modified header
154 %%
155
156 ===== Security Validation =====
157 %%(hl php)
158 $session->cf_referer_check = ''; // Check HTTP Referer header
159 %%
160
161 ===== HTTP Context (Set by HTTP class) =====
162 %%(hl php)
163 $session->cf_ip; // Client IP address
164 $session->cf_tls; // TLS/SSL connection indicator
165 %%
166
167 ----
168
169 === Usage ===
170
171 ==== Basic Session Setup ====
172
173 %%(hl php)
174 // Create a concrete session implementation
175 class MySession extends Session {
176     // Implement abstract store_* methods
177     // See "Implementation Guide" section
178 }
179
180 // Initialize and start session
181 $session = new MySession();
182 $session->cf_max_session = 3600; // 1 hour
183 $session->cf_cookie_path = '/';
184 $session->start('myapp'); // Session name: 'myapp'
185
186 // Store data
187 $session['user_id'] = 42;
188 $session['username'] = 'john';
189
190 // Retrieve data
191 echo $session['user_id']; // 42
192
193 // Check if session is active
194 if ($session->active()) {
195     echo "Session is active";
196 }
197
198 // Explicitly save and close
199 $session->write_close();
200
201 // Shutdown handler automatically called via register_shutdown_function()
202 %%
203
204 ==== Session Data Access ====
205
206 %%(hl php)
207 // Array-like access (via ArrayObject)
208 $session['user_id'] = 123;
209 echo $session['user_id'];
210 unset($session['user_id']);
211 isset($session['user_id']);
212
213 // Convert to array
214 $all_data = $session->toArray();
215 %%
216
217 ==== Session ID Management ====
218
219 %%(hl php)
220 // Get current session ID
221 $id = $session->id(); // Returns: e.g., "abc123xyz..."
222
223 // Get session name
224 $name = $session->name(); // Returns: 'myapp'
225
226 // Get session ID from request
227 $session->start('myapp', $_REQUEST['sid'] ?? null);
228 %%
229
230 ==== Session State ====
231
232 %%(hl php)
233 // Check if session is active
234 if ($session->active()) {
235     // Session is running
236 }
237
238 // Get last state change message
239 $message = $session->message(); // 'replay', 'ip', 'ua', 'timeout', etc.
240
241 // Restart session (destroy old + start new)
242 $session->restart();
243 %%
244
245 ----
246
247 === Security Features ===
248
249 ==== 1. Session ID Regeneration ====
250
251 **Purpose:** Prevent session fixation attacks
252
253 **Automatic Triggers:**
254   - Initial session creation (##regenerated = 2##)
255   - First request after creation (##regenerated = 1##)
256   - Periodic forced regeneration (based on ##cf_regen_time## and ##cf_regen_probability##)
257   - Session validation failures
258
259 **Manual Trigger:**
260 %%(hl php)
261 $session->regenerate_id($delete_old = false, $message = 'custom_reason');
262 %%
263
264 **Parameters:**
265   - ##$delete_old##:
266   - ##false## (0): Keep old session active for ~5 seconds (for pending AJAX requests)
267   - ##true## (1): Keep old session for time specified (unused in current code)
268   - ##2##: Immediately destroy old session
269
270 **Implementation Details:**
271   - New session ID is generated via ##store_generate_id()##
272   - Old session data is copied to new ID
273   - Old session marked with ##__expire## timestamp
274   - Cookie immediately updated with new ID
275   - Single regeneration per request (checked via ##$this->regenerated## flag)
276   - Logged in ##sticky__log## for debugging (max 15 entries)
277
278 %%(hl php)
279 // Example: Force regeneration on login
280 $session->start('myapp');
281 if ($user_authenticated) {
282     $session->regenerate_id(false, 'login');
283     $session['user_id'] = $user->id;
284 }
285 %%
286
287 ==== 2. User Agent Validation ====
288
289 **Purpose:** Detect browser/device changes that might indicate hijacking
290
291 **Behavior:**
292   - Stores user agent on first request
293   - Compares on subsequent requests using ##similar_text()##
294   - Destroys session if similarity < 95%
295   - Useful against bot attacks or stolen sessions
296
297 **Configuration:**
298 %%(hl php)
299 // Automatic on each request (if enabled in code logic)
300 // Triggers session destruction if UA changes significantly
301 %%
302
303 ==== 3. IP Address Validation ====
304
305 **Purpose:** Detect IP spoofing or hijacking
306
307 **Behavior:**
308   - Stores IP on first request
309   - Compares on subsequent requests
310   - Soft failure on mismatch: ##destroy = 1## (keeps regenerating)
311   - Tracks IP changes in ##sticky__ip##
312
313 **Configuration:**
314 %%(hl php)
315 $session->cf_ip = $_SERVER['REMOTE_ADDR']; // Set by HTTP class
316 // Validation happens automatically during start()
317 %%
318
319 **IP Change Tracking:**
320 %%(hl php)
321 // Access IP change history
322 $ip_history = $session->sticky__ip; // Array of [ip => change_count]
323 %%
324
325 ==== 4. TLS/SSL Validation ====
326
327 **Purpose:** Prevent protocol downgrade attacks
328
329 **Behavior:**
330   - Checks if connection transitioned from HTTPS to HTTP
331   - Destroys session on mismatch
332
333 **Configuration:**
334 %%(hl php)
335 $session->cf_tls = !empty($_SERVER['HTTPS']); // Set by HTTP class
336 // Validation happens automatically during start()
337 %%
338
339 ==== 5. Anti-Replay Protection ====
340
341 **Purpose:** Prevent CSRF and replay attacks
342
343 **Mechanism:**
344   - Generates unique "NoReplay" nonce on each request
345   - Cookie-based nonce verification
346   - Detects rapid-fire requests (AJAX attacks)
347
348 **Configuration:**
349 %%(hl php)
350 $session->cf_prevent_replay = 1; // Enable (default)
351 $session->cf_prevent_replay = 0; // Disable if needed
352 %%
353
354 **How It Works:**
355 %%
356 Request 1: Generate nonce, send in cookie
357 Request 2: Client sends nonce back, verify & generate new one
358 Request 3: If old nonce used again → reject (replay detected)
359 %%
360
361 ==== 6. Referer Validation (Optional) ====
362
363 **Purpose:** Prevent CSRF via header checking
364
365 **Configuration:**
366 %%(hl php)
367 $session->cf_referer_check = 'example.com';
368 // Session rejected if HTTP_REFERER doesn't contain this string
369 %%
370
371 ----
372
373 === API Reference ===
374
375 ==== Public Methods ====
376
377 ===== Lifecycle Management =====
378
379 ====== ##start($name = null, $id = null): bool## ======
380 Start or resume a session.
381
382 **Parameters:**
383   - ##$name## (string|null): Session name (cookie name base). Alphanumeric + underscore/dash. Defaults to 'sesid'
384   - ##$id## (string|null): Existing session ID to resume. If null, attempts to read from cookie
385
386 **Returns:** ##true## if session started successfully, ##false## on error
387
388 **Side Effects:**
389   - Sets headers (cookies, cache control)
390   - Populates session data from storage
391   - Performs security validations
392   - May trigger session ID regeneration
393
394 **Example:**
395 %%(hl php)
396 if ($session->start('webapp', $_COOKIE['sess_id'] ?? null)) {
397     // Session ready
398 } else {
399     // Session failed
400 }
401 %%
402
403 **Validation Steps:**
404   1. Reject if headers already sent
405   2. Validate session name format
406   3. Retrieve ID from parameter or cookie
407   4. Check Referer header (if configured)
408   5. Validate ID format via ##store_validate_id()##
409   6. Read session data from storage
410   7. Verify nonces and timestamps
411   8. Check user agent, IP, TLS
412   9. Regenerate if needed
413
414 ----
415
416 ====== ##write_close(): void## ======
417 Save session data and close session.
418
419 **Side Effects:**
420   - Calls ##write_session()## to serialize and store data
421   - Calls ##store_close()## to close storage handler
422   - Sets ##$active = false##
423
424 **Example:**
425 %%(hl php)
426 $session['key'] = 'value';
427 $session->write_close(); // Ensure data is saved
428 %%
429
430 ----
431
432 ====== ##restart(): bool## ======
433 Destroy current session and create new one.
434
435 **Equivalent to:** ##regenerate_id(true) + clean_vars() + populate()##
436
437 **Returns:** ##true## on success, ##false## on error
438
439 **Use Cases:**
440   - User logout and new login
441   - Security reset
442   - Complete session refresh
443
444 **Example:**
445 %%(hl php)
446 $session->restart();
447 // New session created, old data cleared, sticky_ vars preserved
448 %%
449
450 ----
451
452 ===== Session Access =====
453
454 ====== ##id(): mixed## ======
455 Get current session ID.
456
457 **Returns:** Session ID string or null if not started
458
459 %%(hl php)
460 $sid = $session->id(); // "abc123xyz..."
461 %%
462
463 ----
464
465 ====== ##name(): string## ======
466 Get session name (cookie prefix).
467
468 **Returns:** Session name
469
470 %%(hl php)
471 $name = $session->name(); // "myapp"
472 %%
473
474 ----
475
476 ====== ##active(): bool## ======
477 Check if session is currently active.
478
479 **Returns:** ##true## if session is started and active, ##false## otherwise
480
481 %%(hl php)
482 if ($session->active()) {
483     $session['key'] = 'value';
484 }
485 %%
486
487 ----
488
489 ====== ##message(): string|null## ======
490 Get reason for last session state change.
491
492 **Returns:** Message string or null
493
494 **Possible Values:**
495   - ##'replay'##: Replay attack detected
496   - ##'obsolete'##: Session marked for expiration
497   - ##'reg_expire'##: Regeneration expiration reached
498   - ##'max_session'##: Max session lifetime exceeded
499   - ##'max_idle'##: Idle timeout exceeded
500   - ##'ua'##: User agent mismatch (>5% difference)
501   - ##'tls'##: TLS status changed
502   - ##'ip'##: IP address mismatch
503   - ##'restart'##: Session manually restarted
504   - ##null##: No state change
505
506 **Example:**
507 %%(hl php)
508 $session->start('app');
509 if ($message = $session->message()) {
510     error_log("Session issue: $message");
511 }
512 %%
513
514 ----
515
516 ====== ##toArray(): array## ======
517 Convert session data to array.
518
519 **Returns:** Associative array of session data
520
521 **Note:** This is a direct call to ##ArrayObject::getArrayCopy()##
522
523 %%(hl php)
524 $data = $session->toArray();
525 foreach ($data as $key => $value) {
526     echo "$key => $value\n";
527 }
528 %%
529
530 ----
531
532 ===== Nonce System =====
533
534 ====== ##create_nonce($action, $expires = null): string## ======
535 Generate a unique nonce token.
536
537 **Parameters:**
538   - ##$action## (string): Action identifier (e.g., 'form_submit', 'delete_action')
539   - ##$expires## (int|null): Expiration time in seconds. Defaults to ##cf_nonce_lifetime##
540
541 **Returns:** Nonce token string (11 characters)
542
543 **Example:**
544 %%(hl php)
545 $nonce = $session->create_nonce('form_submit', 3600);
546 // Use in HTML: <input type="hidden" name="nonce" value="<?= $nonce ?>">
547 %%
548
549 **Storage:**
550   - Stored in ##$session->__nonces[]##
551   - Key: ##{action}.{base64_encoded_hash}##
552   - Value: Expiration timestamp
553
554 ----
555
556 ====== ##verify_nonce($action, $code, $protect = 0)## ======
557 Verify a nonce token.
558
559 **Parameters:**
560   - ##$action## (string): Action identifier that was used in ##create_nonce()##
561   - ##$code## (string): Nonce token from user
562   - ##$protect## (int): Protection level
563   - ##0##: Single-use nonce (consumed on first verification)
564   - ##1+##: Protected nonce (can verify multiple times, prevents fast replays)
565
566 **Returns:**
567   - ##true## (1): Nonce verified and valid
568   - ##false## (0): Nonce invalid or expired
569   - ##-1##: Protected nonce used twice in quick succession (possible AJAX attack)
570
571 **Example:**
572 %%(hl php)
573 if ($nonce = $session->verify_nonce('form_submit', $_POST['nonce'])) {
574     if ($nonce === -1) {
575         // Possible replay, but might be legitimate AJAX
576         $session->cf_prevent_replay = 0; // Disable for this request
577     } else {
578         // Safe to process
579         process_form();
580     }
581 }
582 %%
583
584 **Cleanup:**
585   - Expired nonces automatically removed
586   - Verified single-use nonces removed from storage
587
588 ----
589
590 ===== Cookie Management =====
591
592 ====== ##setcookie($name, $value = null, $expires = 0, $path = null, $domain = null, $secure = null, $httponly = null, $samesite = null): bool## ======
593 Set a cookie with security headers.
594
595 **Parameters:**
596   - ##$name##: Cookie name (automatically URL-encoded)
597   - ##$value##: Cookie value (automatically URL-encoded, null to delete)
598   - ##$expires##: Expiration timestamp (0 = session cookie)
599   - ##$path##: Cookie path (default: ##cf_cookie_path##)
600   - ##$domain##: Cookie domain (default: ##cf_cookie_domain##)
601   - ##$secure##: HTTPS only (default: ##cf_cookie_secure##)
602   - ##$httponly##: Disable JS access (default: ##cf_cookie_httponly##)
603   - ##$samesite##: SameSite attribute (default: ##cf_cookie_samesite##)
604
605 **Returns:** ##true## on success, ##false## if headers already sent
606
607 **Features:**
608   - RFC 2616 2.2 token encoding for cookie name
609   - RFC 6265 4.1.1 cookie-octet encoding for value
610   - Removes duplicate cookie headers automatically
611   - Adds all security attributes (secure, httponly, samesite)
612   - Does NOT replace existing cookies (allows multiple Set-Cookie headers)
613
614 **Example:**
615 %%(hl php)
616 // Session cookie
617 $session->setcookie('user_pref', 'dark_mode');
618
619 // Persistent cookie (30 days)
620 $session->setcookie('remember_me', 'token123', time() + 30*86400);
621
622 // Delete cookie
623 $session->setcookie('old_cookie', null);
624
625 // Secure cookie with SameSite
626 $session->setcookie('token', 'abc123', time() + 3600,
627     path: '/', secure: true, httponly: true, samesite: 'Strict');
628 %%
629
630 ----
631
632 ====== ##get_cookie($name)## ======
633 Retrieve cookie value.
634
635 **Parameters:**
636   - ##$name##: Cookie name (prefix automatically added)
637
638 **Returns:** Cookie value or null if not set
639
640 %%(hl php)
641 $value = $session->get_cookie('user_pref'); // Reads $_COOKIE['user_pref']
642 %%
643
644 ----
645
646 ====== ##set_cookie($name, $value, $persistent = false): void## ======
647 Legacy cookie setter (alternative to ##setcookie()##).
648
649 **Parameters:**
650   - ##$name##: Cookie name (prefix added)
651   - ##$value##: Cookie value
652   - ##$persistent##:
653   - ##false##: Session cookie (deleted on browser close)
654   - Number: Days to persist
655   - ##0##: Use ##cf_cookie_persistent## config
656
657 **Example:**
658 %%(hl php)
659 $session->set_cookie('theme', 'dark'); // Session cookie
660 $session->set_cookie('lang', 'en', 365); // 1 year
661 %%
662
663 ----
664
665 ====== ##delete_cookie($name): void## ======
666 Delete a cookie.
667
668 **Parameters:**
669   - ##$name##: Cookie name (prefix added)
670
671 **Implementation:** Sets empty value with immediate expiration
672
673 %%(hl php)
674 $session->delete_cookie('old_preference');
675 %%
676
677 ----
678
679 ====== ##unsetcookie($name): void## ======
680 Alias for ##setcookie($name)## with no value (convenience method).
681
682 %%(hl php)
683 $session->unsetcookie('cookie_name');
684 %%
685
686 ----
687
688 ==== Protected Methods (For Store Implementation) ====
689
690 ===== ##regenerate_id($delete_old = false, $message = ''): bool## =====
691 Internal method to regenerate session ID (called automatically).
692
693 **Protected** - Usually called automatically, but can be overridden/called by subclasses
694
695 ----
696
697 ===== ##store_generate_id(): string## =====
698 Generate a new session ID.
699
700 **Default Implementation:** Returns 21-character random alphanumeric string via ##Ut::random_token(21)##
701
702 **Override in subclass to customize:**
703 %%(hl php)
704 protected function store_generate_id(): string {
705     return hash('sha256', random_bytes(32)); // Your format
706 }
707 %%
708
709 ----
710
711 ===== ##store_validate_id($id): bool## =====
712 Validate session ID format.
713
714 **Default Implementation:** Regex check: ##/^[a-zA-Z\d]{21}$/##
715
716 **Override in subclass to match your format:**
717 %%(hl php)
718 protected function store_validate_id($id): bool {
719     return preg_match('/^[a-f0-9]{64}$/', $id); // SHA256 format
720 }
721 %%
722
723 ----
724
725 ===== ##store_open($name): void## =====
726 Open session storage (called before first read/write).
727
728 **Subclass must implement** - Initialize storage handler
729
730 **Example:**
731 %%(hl php)
732 protected function store_open($name): void {
733     $this->db = new PDO('sqlite::memory:');
734 }
735 %%
736
737 ----
738
739 ===== ##store_read($id, $lock = false): string|false## =====
740 Read session data from storage.
741
742 **Subclass must implement**
743
744 **Parameters:**
745   - ##$id##: Session ID to read
746   - ##$lock##: If true, lock the session file for writing (create new)
747
748 **Returns:**
749   - Serialized session data (string) if found and locked
750   - Empty string (##''##) if new session should be created
751   - ##false## if session doesn't exist or read error
752
753 **Example:**
754 %%(hl php)
755 protected function store_read($id, $lock = false): string|false {
756     $data = file_get_contents("/tmp/sess_$id");
757     return $data ?: false;
758 }
759 %%
760
761 ----
762
763 ===== ##store_write($id, $data): void## =====
764 Write session data to storage.
765
766 **Subclass must implement**
767
768 **Parameters:**
769   - ##$id##: Session ID
770   - ##$data##: Serialized session data (already processed by ##Ut::serialize()##)
771
772 **Example:**
773 %%(hl php)
774 protected function store_write($id, $data): void {
775     file_put_contents("/tmp/sess_$id", $data);
776 }
777 %%
778
779 ----
780
781 ===== ##store_close(): void## =====
782 Close session storage.
783
784 **Subclass must implement** - Release resources
785
786 **Example:**
787 %%(hl php)
788 protected function store_close(): void {
789     // Close database, file, etc.
790 }
791 %%
792
793 ----
794
795 ===== ##store_gc(): void## =====
796 Perform garbage collection on old sessions.
797
798 **Subclass must implement** - Delete expired sessions
799
800 **Called During:**
801   - Shutdown handler (probabilistic, based on ##cf_gc_probability##)
802
803 **Should Delete:**
804   - Sessions older than ##cf_gc_maxlifetime## seconds
805
806 **Example:**
807 %%(hl php)
808 protected function store_gc(): void {
809     $max_age = time() - $this->cf_gc_maxlifetime;
810     // Delete files/records older than $max_age
811 }
812 %%
813
814 ----
815
816 ==== Private Methods (Internal Use) ====
817
818 ====== ##populate(): void## ======
819 Initialize session tracking variables on first request.
820
821 **Called by:** ##start()##, ##restart()##
822
823 **Initializes:**
824   - ##__started##: Current timestamp
825   - ##__regenerated##: Current timestamp
826   - ##__user_agent##: Browser user agent
827   - ##__user_ip##: Client IP (if configured)
828   - ##__user_tls##: TLS status (if configured)
829   - ##sticky__created##: Creation time (if not exists)
830
831 ----
832
833 ====== ##write_session(): void## ======
834 Serialize and write session data to storage.
835
836 **Called by:** ##regenerate_id()##, ##write_close()##, ##terminator()##
837
838 **Updates:**
839   - ##__updated##: Current timestamp
840   - Calls ##store_write()## with serialized data
841
842 ----
843
844 ====== ##clean_vars(): void## ======
845 Remove non-sticky session variables.
846
847 **Called by:** ##restart()##, session validation failure
848
849 **Preserves:** Variables starting with ##sticky_##
850
851 ----
852
853 ====== ##prevent_replay(): void## ======
854 Generate and send anti-replay nonce.
855
856 **Called by:** ##populate()##
857
858 **Action:**
859   - Creates 'NoReplay' nonce
860   - Sends in cookie: ##{cf_cookie_prefix}NoReplay##
861
862 ----
863
864 ====== ##cache_limiter(): void## ======
865 Set HTTP cache control headers based on configuration.
866
867 **Called by:** ##start()## after session data loaded
868
869 **Modes:**
870   - ##'public'##: Cacheable, ##Cache-Control: public, max-age=...##
871   - ##'private'##: Private, ##Cache-Control: private, max-age=...##
872   - ##'private_no_expire'##: Private no TTL
873   - ##'nocache'##: No storage, ##Cache-Control: no-store##
874   - ##'none'##: No headers (default)
875
876 ----
877
878 ====== ##set_new_id(): void## ======
879 Generate and assign new session ID, send in cookie.
880
881 **Called by:** ##regenerate_id()##, ##start()## (for new sessions)
882
883 ----
884
885 ====== ##remove_cookie($cookie): void## ======
886 Remove existing Set-Cookie header to avoid duplicates.
887
888 **Called by:** ##setcookie()## before setting new value
889
890 ----
891
892 ====== ##nonce_index($action, $code): string## (static) ======
893 Generate storage key for nonce.
894
895 **Returns:** ##{action}.{base64_encoded_hash}##
896
897 ----
898
899 ----
900
901 === Session Lifecycle ===
902
903 ==== Complete Session Flow ====
904
905 %%
906 ┌─ Browser Request
907
908 ├─ Application Code
909 │ └─ $session->start('appname')
910 │ │
911 │ ├─ Check if headers sent
912 │ ├─ Validate/read session name
913 │ ├─ Get session ID from:
914 │ │ 1. Parameter $id
915 │ │ 2. Cookie: {prefix}appname
916 │ ├─ Validate referer (if cf_referer_check set)
917 │ ├─ Validate ID format via store_validate_id()
918 │ ├─ store_open(name)
919 │ ├─ store_read(id)
920 │ │ └─ If missing/invalid/expired:
921 │ │ └─ set_new_id()
922 │ │ └─ regenerate_id = 2 (NEW)
923 │ ├─ Deserialize session data
924 │ ├─ exchangeArray(data)
925 │ ├─ active = true
926 │ ├─ cache_limiter()
927 │ │
928 │ └─ Security Checks (if NOT first request):
929 │ ├─ Verify NoReplay nonce
930 │ ├─ Check expiration flags
931 │ ├─ Check max session time
932 │ ├─ Check max idle time
933 │ ├─ Compare user agent (95%+ similarity)
934 │ ├─ Compare TLS status
935 │ ├─ Compare IP address
936 │ │ ├─ Match: OK
937 │ │ └─ Mismatch: destroy=1, regenerate
938 │ └─ Check regen time/probability
939 │ └─ regenerate_id()
940
941 ├─ Application Code
942 │ └─ $session['key'] = 'value'
943
944 └─ End of Request
945    │
946    └─ register_shutdown_function() → terminator()
947       │
948       ├─ Process flash data
949       │ └─ Decrement lifetimes
950       │ └─ Remove expired flash
951       ├─ write_session()
952       │ └─ store_write(id, serialized_data)
953       ├─ store_close()
954       ├─ Probabilistic garbage collection
955       │ └─ store_gc() (cf_gc_probability % chance)
956       │ └─ Delete old sessions
957       └─ Output sent to browser
958 %%
959
960 ==== First Request (New Session) ====
961
962 %%
963 start() is called
964 ├─ No ID in cookie
965 ├─ store_read(id) → false
966 ├─ set_new_id()
967 │ └─ id = store_generate_id()
968 │ └─ send_cookie(name, id)
969 ├─ data = []
970 ├─ active = true
971 ├─ populate()
972 │ ├─ __started = now
973 │ ├─ __regenerated = now
974 │ ├─ __user_agent = UA
975 │ └─ sticky__created = now
976 └─ return true
977 %%
978
979 ==== Subsequent Request (Resume Session) ====
980
981 %%
982 start() is called
983 ├─ ID from cookie
984 ├─ store_read(id) → serialized_data
985 ├─ data = unserialize(data)
986 ├─ exchangeArray(data)
987 ├─ active = true
988 ├─ Security checks:
989 │ ├─ Replay check
990 │ ├─ Timeout checks
991 │ ├─ UA/IP/TLS checks
992 │ └─ May trigger regenerate_id()
993 └─ return true
994 %%
995
996 ==== Session ID Regeneration ====
997
998 %%
999 regenerate_id($delete_old, $message) is called
1000 ├─ Check not headers_sent()
1001 ├─ Check $active
1002 ├─ Check not already regenerated in this request
1003 ├─ write_session() [Save current data]
1004 ├─ set __expire:
1005 │ ├─ if $delete_old=0: __expire = now + 5
1006 │ └─ if $delete_old>0: __expire = 0
1007 ├─ Generate new ID:
1008 │ └─ loop:
1009 │ ├─ id = store_generate_id()
1010 │ └─ while store_read(id) !== false [Ensure unique]
1011 ├─ Lock new session: store_read(id, true)
1012 ├─ Set: __regenerated = now
1013 ├─ Set: regenerated = 1
1014 ├─ Log event: sticky__log[] = [now, message]
1015 └─ return true
1016 %%
1017
1018 ==== Session Destruction ====
1019
1020 %%
1021 Triggered by:
1022 ├─ restart() → regenerate_id(true)
1023 ├─ Validation failure (destroy=2)
1024 │ └─ regenerate_id(2)
1025 │ └─ clean_vars() [Remove non-sticky data]
1026 └─ Timeout or security violation
1027 Results in:
1028 ├─ __expire = 0 [Immediate expiration]
1029 ├─ Non-sticky variables cleared
1030 ├─ sticky_ variables preserved
1031 └─ New session ID generated
1032 %%
1033
1034 ----
1035
1036 === Flash Data ===
1037
1038 Flash data persists for a limited number of requests (typically 1-2) and is automatically removed.
1039
1040 ==== Usage ====
1041
1042 %%(hl php)
1043 // Store flash message for next request
1044 $session->set_flash('error', 'Username already exists', 1); // 1 request
1045 $session->set_flash('info', 'Welcome back!', 2); // 2 requests
1046
1047 // In next request, data automatically available
1048 echo $session['error']; // "Username already exists"
1049 %%
1050
1051 ==== How It Works ====
1052   1. **Storage:** Flash data stored in ##$session->sticky__flash##
1053   - Key: Variable name
1054   - Value: Lifetime in requests
1055   2. **Cleanup:** In ##terminator()## (shutdown handler):
1056    %%(hl php)
1057    foreach ($sticky__flash as $var => $age) {
1058        if (!isset($session[$var])) {
1059            unset($sticky__flash[$var]); // Already deleted
1060        } else if (--$age <= 0) {
1061            unset($session[$var]); // Expired, remove
1062            unset($flash__flash[$var]);
1063        } else {
1064            $flash__flash[$var] = $age; // Decrement counter
1065        }
1066    }
1067    %%
1068   3. **Persistence:** Flash variables are kept in ##sticky__flash## even during session resets
1069
1070 ==== Example: Login Flow ====
1071
1072 %%(hl php)
1073 // POST /login
1074 if ($credentials_valid) {
1075     $session->restart(); // New session
1076     $session['user_id'] = $user->id;
1077     $session->set_flash('success', 'Login successful!', 1);
1078     header('Location: /dashboard');
1079 } else {
1080     $session->set_flash('error', 'Invalid credentials', 1);
1081     header('Location: /login');
1082 }
1083
1084 // GET /dashboard (or /login on failure)
1085 if ($message = $session['error'] ?? null) {
1086     echo "<div class='error'>$message</div>";
1087 }
1088 if ($message = $session['success'] ?? null) {
1089     echo "<div class='success'>$message</div>";
1090 }
1091 %%
1092
1093 ----
1094
1095 === Nonce System ===
1096
1097 Nonces provide CSRF protection and replay attack detection.
1098
1099 ==== Terminology ====
1100   - **Nonce:** Number used ONCE - cryptographic token for action verification
1101   - **Action:** Type of operation being protected (e.g., 'form_submit', 'delete_user')
1102   - **Protected Nonce:** Can be verified multiple times with protection against rapid reuse
1103
1104 ==== Complete Example: Form Protection ====
1105
1106 %%(hl php)
1107 // 1. Display form with nonce
1108 $nonce = $session->create_nonce('user_update', 3600);
1109 ?>
1110 <form method="POST" action="/update-profile">
1111     <input type="hidden" name="nonce" value="<?= htmlspecialchars($nonce) ?>">
1112     <input type="text" name="username" value="...">
1113     <button type="submit">Update</button>
1114 </form>
1115
1116 <?php
1117 // 2. Process form submission
1118 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
1119     if (!$session->verify_nonce('user_update', $_POST['nonce'] ?? '')) {
1120         http_response_code(403);
1121         die('Security check failed');
1122     }
1123
1124     // Safe to process
1125     update_user($_POST);
1126 }
1127 %%
1128
1129 ==== Example: Protected Nonce (AJAX-Safe) ====
1130
1131 %%(hl php)
1132 // Generate protected nonce (can verify multiple times)
1133 $nonce = $session->create_nonce('ajax_action', 300);
1134
1135 // Verify with protection level 3 (3 seconds)
1136 $result = $session->verify_nonce('ajax_action', $_POST['nonce'], 3);
1137
1138 if ($result === -1) {
1139     // Rapid reuse detected (possible attack, but might be AJAX)
1140     if (is_ajax_request()) {
1141         // AJAX is OK, disable replay protection this once
1142         $session->cf_prevent_replay = 0;
1143     } else {
1144         // Likely attack
1145         http_response_code(403);
1146         die('Suspicious activity');
1147     }
1148 } else if ($result === true) {
1149     // Safe to process
1150     process_ajax();
1151 }
1152 %%
1153
1154 ==== Nonce Storage Format ====
1155
1156 %%
1157 Internal storage (__nonces array):
1158 [
1159     "{action}.{hash}" => expiration_timestamp,
1160     "form_submit.AbCdEfGhIjK" => 1234567890,
1161     "delete_user.XyZaBcDeFgH" => 1234567890,
1162 ]
1163 Where:
1164 - action: Custom action identifier
1165 - hash: First 11 chars of base64(sha1(code_bytes))
1166 - expiration_timestamp: time() + lifetime
1167 %%
1168
1169 ==== Security Properties ====
1170   - **CSRF Protection:** Nonce must match to process form
1171   - **One-Time Use:** Each nonce consumed after first verification (unless protected)
1172   - **Expiration:** Nonces automatically expire
1173   - **Action-Specific:** Each action has separate nonce space
1174   - **AJAX-Safe:** Protected nonces allow multiple quick verifications
1175
1176 ----
1177
1178 === Cookie Management ===
1179
1180 ==== Security Features ====
1181
1182 The ##setcookie()## method implements comprehensive cookie security:
1183
1184 ===== Encoding =====
1185 %%(hl php)
1186 // Cookie names: RFC 2616 2.2 token format
1187 // Cookie values: RFC 6265 4.1.1 cookie-octet format
1188 // Unsafe characters automatically URL-encoded
1189 %%
1190
1191 ===== Security Attributes =====
1192 %%(hl php)
1193 setcookie('auth', 'token',
1194     expires: time() + 3600,
1195     secure: true, // HTTPS only
1196     httponly: true, // Disable JavaScript
1197     samesite: 'Strict' // CSRF protection
1198 );
1199 %%
1200
1201 ===== No Duplicate Headers =====
1202 %%(hl php)
1203 // Automatically removes old Set-Cookie header before setting new one
1204 // Prevents cookie header duplication
1205 remove_cookie($name) → clears old headers
1206 setcookie() → sets new header
1207 %%
1208
1209 ==== Configuration-Driven Defaults ====
1210
1211 %%(hl php)
1212 $session->cf_cookie_path = '/app'; // Path
1213 $session->cf_cookie_domain = '.example.com'; // Domain
1214 $session->cf_cookie_secure = true; // HTTPS
1215 $session->cf_cookie_httponly = true; // No JS
1216 $session->cf_cookie_samesite = 'Lax'; // SameSite
1217 $session->cf_cookie_prefix = 'app_'; // Prefix
1218
1219 $session->setcookie('token', 'value');
1220 // Uses all configured defaults
1221 %%
1222
1223 ==== Typical Secure Configuration ====
1224
1225 %%(hl php)
1226 // Prevent XSS and CSRF
1227 $session->cf_cookie_secure = true; // HTTPS only
1228 $session->cf_cookie_httponly = true; // No JavaScript access
1229 $session->cf_cookie_samesite = 'Strict'; // Strict CSRF protection
1230
1231 // Set scope
1232 $session->cf_cookie_path = '/'; // Root path
1233 $session->cf_cookie_domain = ''; // Current host only
1234
1235 // Session cookies (delete on browser close)
1236 $session->cf_cookie_lifetime = 0;
1237 $session->cf_cookie_persistent = false;
1238 %%
1239
1240 ----
1241
1242 === Error Handling ===
1243
1244 ==== Graceful Degradation ====
1245
1246 The Session class gracefully handles errors:
1247
1248 ===== Headers Already Sent =====
1249 %%(hl php)
1250 if (headers_sent($file, $line)) {
1251     trigger_error("id regeneration requested after headers flushed at $file:$line",
1252                   E_USER_WARNING);
1253     return false;
1254 }
1255 %%
1256
1257 **Impact:** Session ID cannot be regenerated, but session continues
1258
1259 ===== Cookie Setting Failure =====
1260 %%(hl php)
1261 if (headers_sent($file, $line)) {
1262     trigger_error("cannot place session cookie $name=$value due to $file:$line",
1263                   E_USER_WARNING);
1264     return;
1265 }
1266 %%
1267
1268 **Impact:** Cookie not set, but session data remains accessible
1269
1270 ===== Storage Errors =====
1271 %%(hl php)
1272 if ($this->store_read($this->id, true) !== '') {
1273     // error! [comment indicates error, but continues]
1274 }
1275 %%
1276
1277 **Impact:** Creates new session if storage returns error
1278
1279 ==== Debug Logging ====
1280
1281 The Session class includes commented debug statements:
1282
1283 %%(hl php)
1284 # Ut::dbg("regeneration failed by flush at $file:$line");
1285 # Ut::dbg($destroy, $message);
1286 # Ut::dbg("session setcookie $name failed by $file:$line");
1287 %%
1288
1289 To enable: Uncomment lines and ensure ##Ut::dbg()## function exists
1290
1291 ==== Event Logging ====
1292
1293 Session events tracked in ##sticky__log##:
1294
1295 %%(hl php)
1296 // Access session event history
1297 if (isset($session->sticky__log)) {
1298     foreach ($session->sticky__log as [$timestamp, $message]) {
1299         echo "[$timestamp] $message\n";
1300     }
1301 }
1302 %%
1303
1304 **Logged Events:**
1305   - Session regeneration (with reason)
1306   - Limited to 15 most recent events (old entries archived as '...')
1307
1308 ----
1309
1310 === Implementation Guide ===
1311
1312 ==== Creating a Concrete Session Class ====
1313
1314 You must implement the abstract storage methods. Choose your storage backend: files, database, cache, etc.
1315
1316 ===== File-Based Storage =====
1317
1318 %%(hl php)
1319 <?php
1320
1321 class FileSession extends Session {
1322     private $session_dir = '/tmp/sessions';
1323     private $file_handle = null;
1324
1325     public function __construct() {
1326         parent::__construct();
1327         if (!is_dir($this->session_dir)) {
1328             mkdir($this->session_dir, 0700, true);
1329         }
1330     }
1331
1332     protected function store_open($name): void {
1333         // PHP sessions don't really "open", just prepare
1334         // In file mode, we could initialize directory
1335     }
1336
1337     protected function store_read($id, $lock = false): string|false {
1338         $file = $this->session_dir . '/sess_' . preg_replace('/[^a-zA-Z0-9]/', '', $id);
1339
1340         if (!file_exists($file)) {
1341             if ($lock) {
1342                 // Create new session file
1343                 file_put_contents($file, '', LOCK_EX);
1344                 return '';
1345             }
1346             return false;
1347         }
1348
1349         if (filemtime($file) < time() - $this->cf_gc_maxlifetime) {
1350             unlink($file); // Expired
1351             return false;
1352         }
1353
1354         return file_get_contents($file);
1355     }
1356
1357     protected function store_write($id, $data): void {
1358         $file = $this->session_dir . '/sess_' . preg_replace('/[^a-zA-Z0-9]/', '', $id);
1359         file_put_contents($file, $data, LOCK_EX);
1360     }
1361
1362     protected function store_close(): void {
1363         // No cleanup needed for file backend
1364     }
1365
1366     protected function store_gc(): void {
1367         $cutoff = time() - $this->cf_gc_maxlifetime;
1368         foreach (glob($this->session_dir . '/sess_*') as $file) {
1369             if (filemtime($file) < $cutoff) {
1370                 unlink($file);
1371             }
1372         }
1373     }
1374 }
1375 %%
1376
1377 ===== Database Storage (PDO) =====
1378
1379 %%(hl php)
1380 <?php
1381
1382 class DatabaseSession extends Session {
1383     private PDO $pdo;
1384
1385     public function __construct(PDO $pdo) {
1386         parent::__construct();
1387         $this->pdo = $pdo;
1388         $this->ensure_table();
1389     }
1390
1391     private function ensure_table(): void {
1392         $sql = <<<SQL
1393             CREATE TABLE IF NOT EXISTS sessions (
1394                 id VARCHAR(21) PRIMARY KEY,
1395                 data LONGTEXT NOT NULL,
1396                 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1397                 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
1398             )
1399         SQL;
1400         $this->pdo->exec($sql);
1401     }
1402
1403     protected function store_open($name): void {
1404         // Database already connected
1405     }
1406
1407     protected function store_read($id, $lock = false): string|false {
1408         $stmt = $this->pdo->prepare('SELECT data FROM sessions WHERE id = ?');
1409         $stmt->execute([$id]);
1410
1411         if ($result = $stmt->fetch(PDO::FETCH_ASSOC)) {
1412             return $result['data'];
1413         }
1414
1415         if ($lock) {
1416             // Create new session
1417             $stmt = $this->pdo->prepare('INSERT INTO sessions (id, data) VALUES (?, ?)');
1418             $stmt->execute([$id, '']);
1419             return '';
1420         }
1421
1422         return false;
1423     }
1424
1425     protected function store_write($id, $data): void {
1426         $stmt = $this->pdo->prepare(
1427             'INSERT INTO sessions (id, data) VALUES (?, ?)
1428              ON DUPLICATE KEY UPDATE data = VALUES(data)'
1429         );
1430         $stmt->execute([$id, $data]);
1431     }
1432
1433     protected function store_close(): void {
1434         // Connection persists for application
1435     }
1436
1437     protected function store_gc(): void {
1438         $cutoff = time() - $this->cf_gc_maxlifetime;
1439         $this->pdo->prepare('DELETE FROM sessions WHERE updated_at < FROM_UNIXTIME(?)')
1440                    ->execute([$cutoff]);
1441     }
1442 }
1443 %%
1444
1445 ===== Redis Storage =====
1446
1447 %%(hl php)
1448 <?php
1449
1450 class RedisSession extends Session {
1451     private Redis $redis;
1452     private string $prefix = 'sess:';
1453
1454     public function __construct(Redis $redis) {
1455         parent::__construct();
1456         $this->redis = $redis;
1457     }
1458
1459     protected function store_open($name): void {
1460         // Redis already connected
1461     }
1462
1463     protected function store_read($id, $lock = false): string|false {
1464         $data = $this->redis->get($this->prefix . $id);
1465
1466         if ($data !== false) {
1467             return $data;
1468         }
1469
1470         if ($lock) {
1471             // Create new session
1472             $this->redis->set($this->prefix . $id, '',
1473                             ['EX' => $this->cf_gc_maxlifetime]);
1474             return '';
1475         }
1476
1477         return false;
1478     }
1479
1480     protected function store_write($id, $data): void {
1481         $this->redis->set($this->prefix . $id, $data,
1482                         ['EX' => $this->cf_gc_maxlifetime]);
1483     }
1484
1485     protected function store_close(): void {
1486         // Connection persists
1487     }
1488
1489     protected function store_gc(): void {
1490         // Redis handles expiration automatically with TTL
1491     }
1492 }
1493 %%
1494
1495 ==== Complete Integration Example ====
1496
1497 %%(hl php)
1498 <?php
1499
1500 // Initialize session with configuration
1501 $session = new FileSession();
1502
1503 // Configure security
1504 $session->cf_cookie_secure = (!empty($_SERVER['HTTPS']));
1505 $session->cf_cookie_httponly = true;
1506 $session->cf_cookie_samesite = 'Lax';
1507 $session->cf_max_session = 86400; // 24 hours
1508 $session->cf_max_idle = 3600; // 1 hour
1509 $session->cf_prevent_replay = true;
1510
1511 // Set IP and TLS validation
1512 $session->cf_ip = $_SERVER['REMOTE_ADDR'];
1513 $session->cf_tls = !empty($_SERVER['HTTPS']);
1514
1515 // Start session
1516 if (!$session->start('myapp')) {
1517     die('Session start failed');
1518 }
1519
1520 // Check for session validation messages
1521 if ($message = $session->message()) {
1522     error_log("Session validation: $message");
1523 }
1524
1525 // Use session
1526 if (!isset($session['user_id'])) {
1527     // Handle login...
1528     $session['user_id'] = $user->id;
1529     $session['username'] = $user->name;
1530     $session->regenerate_id(false, 'login');
1531 } else {
1532     // User already logged in
1533     echo "Welcome back, " . htmlspecialchars($session['username']);
1534 }
1535
1536 // Logout handling
1537 if ($_REQUEST['action'] === 'logout') {
1538     $session->restart();
1539     header('Location: /');
1540 }
1541
1542 // Automatic cleanup happens in register_shutdown_function()
1543 %%
1544
1545 ==== Configuration Best Practices ====
1546
1547 %%(hl php)
1548 <?php
1549
1550 class SessionConfig {
1551     public static function apply(Session $session, string $environment = 'production'): void {
1552         // Base configuration
1553         $session->cf_cookie_prefix = 'app_';
1554         $session->cf_cookie_path = '/';
1555         $session->cf_cache_limiter = 'private';
1556
1557         if ($environment === 'production') {
1558             // Strict production settings
1559             $session->cf_cookie_secure = true; // HTTPS only
1560             $session->cf_cookie_httponly = true; // No JavaScript
1561             $session->cf_cookie_samesite = 'Strict'; // Maximum CSRF protection
1562             $session->cf_prevent_replay = true; // Anti-replay
1563             $session->cf_max_session = 3600; // 1 hour
1564             $session->cf_max_idle = 1800; // 30 minutes
1565             $session->cf_regen_time = 300; // Regen every 5 min
1566             $session->cf_regen_probability = 50; // 50% chance
1567         } else {
1568             // Development settings
1569             $session->cf_cookie_secure = false; // Allow HTTP
1570             $session->cf_cookie_httponly = false; // Allow JS debugging
1571             $session->cf_prevent_replay = false; // Easier testing
1572             $session->cf_max_session = 86400; // 24 hours
1573             $session->cf_max_idle = 3600; // 1 hour
1574             $session->cf_regen_time = 60; // 1 minute
1575             $session->cf_regen_probability = 10; // 10% chance
1576         }
1577     }
1578 }
1579
1580 // Usage
1581 $session = new FileSession();
1582 SessionConfig::apply($session, $_ENV['APP_ENV'] ?? 'production');
1583 $session->start('myapp');
1584 %%
1585
1586 ==== Testing Tips ====
1587
1588 %%(hl php)
1589 <?php
1590
1591 // Test nonce generation and verification
1592 $nonce1 = $session->create_nonce('test_action', 60);
1593 assert($session->verify_nonce('test_action', $nonce1) === true);
1594
1595 // Test single-use property
1596 assert($session->verify_nonce('test_action', $nonce1) === false);
1597
1598 // Test expiration
1599 $old_nonce = $session->create_nonce('expire_test', 1);
1600 sleep(2);
1601 assert($session->verify_nonce('expire_test', $old_nonce) === false);
1602
1603 // Test user agent validation
1604 assert(isset($session->__user_agent));
1605
1606 // Test session ID format
1607 assert(preg_match('/^[a-zA-Z0-9]{21}$/', $session->id()));
1608
1609 // Test data persistence
1610 $session['test_key'] = 'test_value';
1611 $session->write_close();
1612 // New request...
1613 $session2 = new FileSession();
1614 $session2->start('myapp');
1615 assert($session2['test_key'] === 'test_value');
1616 %%
1617
1618 ----
1619
1620 === Security Checklist ===
1621
1622 Use this checklist when implementing sessions:
1623   - [ ] Use HTTPS only in production
1624   - [ ] Enable ##cf_cookie_secure##
1625   - [ ] Enable ##cf_cookie_httponly##
1626   - [ ] Set ##cf_cookie_samesite## to 'Strict' or 'Lax'
1627   - [ ] Set appropriate ##cf_max_session## timeout
1628   - [ ] Set appropriate ##cf_max_idle## timeout
1629   - [ ] Enable ##cf_prevent_replay##
1630   - [ ] Validate ##cf_ip## if possible
1631   - [ ] Validate ##cf_tls## on HTTPS sites
1632   - [ ] Use nonces for all state-changing forms
1633   - [ ] Implement proper logout (call ##restart()##)
1634   - [ ] Regenerate on privilege escalation (login)
1635   - [ ] Monitor ##sticky__ip## for suspicious changes
1636   - [ ] Review ##sticky__log## for attack patterns
1637   - [ ] Implement garbage collection (##store_gc##)
1638   - [ ] Hash session IDs before storing (see TODOs)
1639   - [ ] Use secure random token generation
1640
1641 ----
1642
1643 === Common Patterns ===
1644
1645 ==== Login Flow ====
1646
1647 %%(hl php)
1648 if ($_POST['action'] === 'login') {
1649     $user = authenticate($_POST['username'], $_POST['password']);
1650     if ($user) {
1651         $session->regenerate_id(false, 'login'); // New ID after auth
1652         $session['user_id'] = $user->id;
1653         $session['username'] = $user->username;
1654         $session['roles'] = $user->roles;
1655         header('Location: /dashboard');
1656     } else {
1657         $session->set_flash('error', 'Invalid credentials', 1);
1658         header('Location: /login');
1659     }
1660 }
1661 %%
1662
1663 ==== Logout Flow ====
1664
1665 %%(hl php)
1666 if ($_GET['action'] === 'logout') {
1667     $session->restart(); // Complete reset
1668     header('Location: /');
1669 }
1670 %%
1671
1672 ==== CSRF-Protected Form ====
1673
1674 %%(hl php)
1675 // Display form
1676 $csrf = $session->create_nonce('form_' . $form_id, 3600);
1677 echo '<form method="POST">';
1678 echo '<input type="hidden" name="csrf" value="' . htmlspecialchars($csrf) . '">';
1679 // ... form fields
1680 echo '</form>';
1681
1682 // Process form
1683 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
1684     if (!$session->verify_nonce('form_' . $form_id, $_POST['csrf'] ?? '')) {
1685         die('CSRF check failed');
1686     }
1687     // Process safely
1688 }
1689 %%
1690
1691 ==== Permission Check with Session Regeneration ====
1692
1693 %%(hl php)
1694 if ($user->privilege_level < ADMIN_LEVEL && $promoted_to_admin) {
1695     $session->regenerate_id(false, 'privilege_escalation');
1696     $session['is_admin'] = true;
1697 }
1698 %%
1699
1700 ==== Session Messages/Flash ====
1701
1702 %%(hl php)
1703 // After action
1704 $session->set_flash('info', 'Profile updated successfully', 1);
1705
1706 // Display next page
1707 if (isset($session['info'])) {
1708     echo $session['info'];
1709 }
1710 %%
1711
1712 ----
1713
1714 === Performance Considerations ===
1715
1716 ==== Optimization Tips ====
1717   1. **Minimize Session Writes:**
1718   - Session data only written during ##write_close()## or regeneration
1719   - No unnecessary serialization during reads
1720   2. **Garbage Collection:**
1721   - Probabilistic GC (based on ##cf_gc_probability##)
1722   - Only runs on ~2% of requests by default
1723   - Customize based on your session volume
1724   3. **Nonce Cleanup:**
1725   - Expired nonces automatically removed on verification
1726   - Verified nonces removed from storage
1727   - No manual cleanup needed
1728   4. **Session ID Validation:**
1729   - Regex-based validation is fast
1730   - No database lookup needed
1731   5. **Caching Strategy:**
1732   - Cache expensive lookups between session operations
1733   - Session data loaded once per request
1734
1735 ==== Benchmarks ====
1736
1737 Typical performance on modern hardware:
1738   - Session start: ~1-5ms (file) / ~2-10ms (database)
1739   - Session write: <1ms (file) / 1-5ms (database)
1740   - Nonce generation: <1ms
1741   - Nonce verification: <1ms
1742
1743 ----
1744
1745 === Troubleshooting ===
1746
1747 ==== Session Not Starting ====
1748
1749 %%(hl php)
1750 if (!$session->start('myapp')) {
1751     // Check reasons:
1752     // 1. Headers already sent?
1753     // 2. Storage backend not initialized?
1754     // 3. Permissions issue on session directory?
1755     debug_backtrace();
1756 }
1757 %%
1758
1759 ==== Cookie Not Setting ====
1760
1761 %%(hl php)
1762 // If setcookie() returns false:
1763 // - Check if headers_sent()
1764 // - Check if cookie name is RFC 2616 compliant
1765 // - Check if cookie value is properly encoded
1766 %%
1767
1768 ==== Session ID Not Regenerating ====
1769
1770 %%(hl php)
1771 // If regenerate_id() returns false:
1772 // - Headers might be sent
1773 // - $active might be false
1774 // - Already regenerated once in this request
1775 if (!$session->regenerate_id()) {
1776     error_log("Regeneration failed: headers sent or session inactive");
1777 }
1778 %%
1779
1780 ==== Nonce Verification Failing ====
1781
1782 %%(hl php)
1783 // If verify_nonce() returns false:
1784 // 1. Nonce might be expired
1785 // 2. Nonce might be for different action
1786 // 3. Nonce might have been used already
1787 // 4. Session might have been reset
1788
1789 // Debug:
1790 var_dump($session->__nonces); // See stored nonces
1791 %%
1792
1793 ==== Session Data Lost ====
1794
1795 %%(hl php)
1796 // Possible causes:
1797 // 1. write_close() not called (usually automatic via shutdown)
1798 // 2. Storage backend failing silently
1799 // 3. File permissions issues
1800 // 4. Session timeout due to cf_max_idle
1801 // 5. IP/UA/TLS validation failure (check message())
1802
1803 if ($message = $session->message()) {
1804     error_log("Session issue: $message");
1805 }
1806 %%
1807
1808 ----
1809
1810 ===TODO Items (From Code Comments)===
1811 The following improvements are planned:
1812   1. **Do not store session ID in filename or DB index - store hash instead**
1813     - Improves security by not exposing IDs in storage layer
1814     - Would require hashing logic in store_* methods
1815   2. **Log of IP changes and other possible security alerts**
1816     - Track ##sticky__ip## changes more comprehensively
1817     - Create security audit trail
1818   3. **Allocate internal unique session which lives through lifetime of uber-session**
1819     - Multi-session management (parent/child sessions)
1820     - Useful for complex user flows
1821   4. **Do not delete old sessions, but use them as hijack pointers**
1822     - Maintain session history for analysis
1823     - Detect potential session hijacking patterns
1824     - Implement session relationship tracking
1825   5. **All SIDs used later than ~5secs of regenerations is hijacks**
1826     - Detect and block delayed session ID usage
1827     - Current implementation allows 5-second window
1828     - Could be more granular
1829
1830 ----
1831
1832 === References ===
1833
1834 ==== Security Standards ====
1835   - RFC 2616: HTTP/1.1 (Cookie syntax)
1836   - RFC 6265: HTTP State Management Mechanism
1837   - RFC 6234: US Secure Hash and Message Authentication Code Algorithms
1838   - OWASP: Session Management Cheat Sheet
1839   - OWASP: Cross-Site Request Forgery (CSRF) Prevention
1840
1841 ==== Related Code ====
1842   - ##Ut::serialize()## / ##Ut::unserialize()##: Session data serialization
1843   - ##Ut::random_token()##: Cryptographic token generation
1844   - ##Ut::http_date()##: HTTP date formatting
1845   - ##Ut::urlencode()##: Cookie-safe encoding
1846   - ##Ut::is_empty()##: Empty value checking
1847
1848 ==== See Also ====
1849   - ##src/class/http.php##: HTTP request/response handling
1850   - ##src/class/auth.php##: Authentication (uses Session)
1851   - Session security best practices in OWASP documentation
1852
1853 ----
1854
1855 ===Version History===
1856   - **Current**: Abstract session class with security features
1857   - **Planned**: Implementation of TODO items above
1858
1859 ----
1860 **Documentation generated: 2026-05-05**
1861 **For latest updates, see: https://github.com/Trojer/wackowiki/blob/main/docs/SESSION_DOCUMENTATION.md**