Difference between revisions for Users / Eo Ny





Next edit →

Version1 Version2 Differences
1   **((user:EoNy EoNy))** (05.05.2026 16:15)
  1 == Session Management Technical Documentation ==
  2 {{toc numerate=1}}
  3 === Overview ===
  4
  5 The ##Session## class is an abstract session management system for WackoWiki that extends ##ArrayObject## to provide secure, configurable session handling. It implements sophisticated security features including session ID regeneration, anti-replay protection, nonce verification, and user agent/IP validation.
  6
  7 **Location:** ##src/class/session.php##
  8 **Type:** Abstract class (must be extended with a ##SessionStoreInterface## implementation)
  9 **Inheritance:** ##ArrayObject##
  10
  11 ----
  12
  13 === Table of Contents ===
  14   1. ((#core-concepts Core Concepts))
  15   2. ((#architecture Architecture))
  16   3. ((#configuration Configuration))
  17   4. ((#usage Usage))
  18   5. ((#security-features Security Features))
  19   6. ((#api-reference API Reference))
  20   7. ((#session-lifecycle Session Lifecycle))
  21   8. ((#flash-data Flash Data))
  22   9. ((#nonce-system Nonce System))
  23   10. ((#cookie-management Cookie Management))
  24   11. ((#error-handling Error Handling))
  25   12. ((#implementation-guide Implementation Guide))
  26
  27 ----
  28
  29 === Core Concepts ===
  30
  31 ==== Session State ====
  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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl php)
  157 $session->cf_referer_check = ''; // Check HTTP Referer header
  158 %%
  159
  160 ===== HTTP Context (Set by HTTP class) =====
  161 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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    %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl php)
  1665 if ($_GET['action'] === 'logout') {
  1666     $session->restart(); // Complete reset
  1667     header('Location: /');
  1668 }
  1669 %%
  1670
  1671 ==== CSRF-Protected Form ====
  1672
  1673 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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 %%(hl 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*