Difference between revisions for Users / Eo Ny




← Previous edit
Next edit →

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