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
  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**