Difference between revisions for Users / Eo Ny





Next edit →

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