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