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