Difference between revisions for Users / Eo Ny




← Previous edit
Next edit →

#== Session Management Technical Documentation
## ==

===
Overview ===
The `Session` Session class is an abstract session management system for WackoWiki that extends `ArrayObject` 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.
Location: src/class/session.php
Type: Abstract class (must be extended with a `SessionStoreInterface` SessionStoreInterface implementation) Inheritance: ArrayObject


Table of Contents <!markup:2:begin>

<!markup:2:end>
  1. Core Concepts
  2. [Architecture](#architecture) Architecture
  3. Configuration
  4. Usage
  5. [Security Features](#security-features) Security Features
  6. [API Reference](#api-reference) API Reference
  7. Session Lifecycle
  8. [Flash Data](#flash-data) Flash Data
  9. Nonce System
  10. Cookie Management
  11. Error Handling
  12. [Implementation Guide](#implementation-guide)


## Implementation Guide


===
Core Concepts ===

Session State

The Session class maintains three primary states:
  • Inactive (`$active** (##$active = false`): false##): Session not yet started or has been closed
  • Active (`$active** (##$active = true`): true##): Session is running and can store/retrieve data
  • Regenerated: Session ID has been replaced (tracked via `$regenerated` $regenerated flag)

###
==== Session Data Storage ==== Session data is stored as an array accessible through `ArrayObject` ArrayObject interface:
`php<!--markup:1:end-->
<!--markup:2:begin-->%%(hl php)<!--markup:2:end-->
$session['user_id'] = 123;  // Set data
echo $session['user_id'];   // Get data
<!--markup:1:begin-->		
`
###

====<!--markup:2:end--> Sticky Data ====
Variables prefixed with ##sticky_## are persistent across session resets:
  - <!--markup:1:begin-->`sticky__created`:<!--markup:1:end--> <!--markup:2:begin-->##sticky__created##:<!--markup:2:end--> Session creation timestamp
  - <!--markup:1:begin-->`sticky__flash`:<!--markup:1:end--> <!--markup:2:begin-->##sticky__flash##:<!--markup:2:end--> Flash data lifetime tracking
  - ##sticky__log##: Regeneration event log
  - ##sticky__ip##: IP change tracking

==== Internal Tracking Variables <!--markup:2:begin-->====<!--markup:2:end-->
Variables prefixed with <!--markup:1:begin-->`__`<!--markup:1:end--> <!--markup:2:begin-->##__##<!--markup:2:end--> are internal session metadata:
  - <!--markup:1:begin-->`__started`:<!--markup:1:end--> <!--markup:2:begin-->##__started##:<!--markup:2:end--> Session start time
  - ##__updated##: Last session update time
  - <!--markup:1:begin-->`__regenerated`:<!--markup:1:end--> <!--markup:2:begin-->##__regenerated##:<!--markup:2:end--> Last session ID regeneration time
  - ##__user_agent##: Client user agent string
  - <!--markup:1:begin-->`__user_ip`:<!--markup:1:end--> <!--markup:2:begin-->##__user_ip##:<!--markup:2:end--> Client IP address
  - <!--markup:1:begin-->`__user_tls`:<!--markup:1:end--> <!--markup:2:begin-->##__user_tls##:<!--markup:2:end--> TLS/SSL status
  - <!--markup:1:begin-->`__nonces`:<!--markup:1:end--> <!--markup:2:begin-->##__nonces##:<!--markup:2:end--> Active nonce storage
  - ##__expire##: Session expiration time (for old sessions)

----

=== Architecture ===

==== Class Hierarchy
<!--markup:1:begin-->```<!--markup:1:end--> <!--markup:2:begin-->====		
ArrayObject (PHP native)
Session (abstract)
[Concrete Implementation] (must implement store_* methods)
`

###<!--markup:1:end-->
<!--markup:2:begin-->%%

====<!--markup:2:end--> Key Methods Categories <!--markup:2:begin-->====<!--markup:2:end-->

**Lifecycle Management:**
  - <!--markup:1:begin-->`__construct()`:<!--markup:1:end--> <!--markup:2:begin-->##__construct()##:<!--markup:2:end--> Initialize session object
  - ##start()##: Begin a session
  - ##write_close()##: Save and close session
  - <!--markup:1:begin-->`restart()`:<!--markup:1:end--> <!--markup:2:begin-->##restart()##:<!--markup:2:end--> Destroy and restart session
  - <!--markup:1:begin-->`terminator()`:<!--markup:1:end--> <!--markup:2:begin-->##terminator()##:<!--markup:2:end--> Shutdown handler (garbage collection, flash data cleanup)

**Security:**
  - <!--markup:1:begin-->`regenerate_id()`:<!--markup:1:end--> <!--markup:2:begin-->##regenerate_id()##:<!--markup:2:end--> Replace session ID
  - <!--markup:1:begin-->`verify_nonce()`:<!--markup:1:end--> <!--markup:2:begin-->##verify_nonce()##:<!--markup:2:end--> Validate nonce tokens
  - <!--markup:1:begin-->`prevent_replay()`:<!--markup:1:end--> <!--markup:2:begin-->##prevent_replay()##:<!--markup:2:end--> Anti-replay protection
  - <!--markup:1:begin-->`create_nonce()`:<!--markup:1:end--> <!--markup:2:begin-->##create_nonce()##:<!--markup:2:end--> Generate nonce tokens

**Storage (Abstract - Must Implement):**
  - <!--markup:1:begin-->`store_open()`:<!--markup:1:end--> <!--markup:2:begin-->##store_open()##:<!--markup:2:end--> Open session storage
  - <!--markup:1:begin-->`store_read()`:<!--markup:1:end--> <!--markup:2:begin-->##store_read()##:<!--markup:2:end--> Read session data
  - <!--markup:1:begin-->`store_write()`:<!--markup:1:end--> <!--markup:2:begin-->##store_write()##:<!--markup:2:end--> Write session data
  - <!--markup:1:begin-->`store_close()`:<!--markup:1:end--> <!--markup:2:begin-->##store_close()##:<!--markup:2:end--> Close session storage
  - <!--markup:1:begin-->`store_gc()`:<!--markup:1:end--> <!--markup:2:begin-->##store_gc()##:<!--markup:2:end--> Garbage collection
  - ##store_validate_id()##: Validate session ID format
  - ##store_generate_id()##: Generate new session ID

**Cookie Management:**
  - <!--markup:1:begin-->`setcookie()`:<!--markup:1:end--> <!--markup:2:begin-->##setcookie()##:<!--markup:2:end--> Set HTTP cookie with security headers
  - <!--markup:1:begin-->`get_cookie()`:<!--markup:1:end--> <!--markup:2:begin-->##get_cookie()##:<!--markup:2:end--> Retrieve cookie value
  - ##set_cookie()##: Set cookie (legacy interface)
  - ##delete_cookie()##: Remove cookie
  - <!--markup:1:begin-->`send_cookie()`:<!--markup:1:end--> <!--markup:2:begin-->##send_cookie()##:<!--markup:2:end--> Internal cookie transmission

<!--markup:1:begin-->---

##<!--markup:1:end-->

<!--markup:2:begin-->----

===<!--markup:2:end--> Configuration

<!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->===

====<!--markup:2:end--> Configuration Properties (Public) ====

All configuration properties are prefixed with <!--markup:1:begin-->`cf_`<!--markup:1:end--> <!--markup:2:begin-->##cf_##<!--markup:2:end--> (config) and can be set before calling <!--markup:1:begin-->`start()`:

####<!--markup:1:end--> <!--markup:2:begin-->##start()##:

=====<!--markup:2:end--> Session Behavior
<!--markup:1:begin-->		
`php
=====
PHP
<!--markup:2:end-->
$session->cf_static = ;                    // Disable regenerations (e.g., for CAPTCHA)
$session->cf_max_session = 72;            // Max session lifetime (seconds)
$session->cf_max_idle = 144;               // Max idle time before destruction (seconds)
$session->cf_regen_time = 5;              // Seconds between forced ID regenerations
$session->cf_regen_probability = 2;         // Percentage probability of forced regen (-1)
<!--markup:1:begin-->```

####<!--markup:1:end-->
<!--markup:2:begin-->

Nonce & Replay Protection
`php<!--markup:1:end--> <!--markup:2:begin-->=====
%%(hl php)<!--markup:2:end-->
$session->cf_secret = 'adyaiD9+255JeiskPybgisby';  // Secret for nonce generation
$session->cf_nonce_lifetime = 72;                 // Nonce expiration (seconds)
$session->cf_prevent_replay = 1;                    // Enable replay attack prevention
%%

===== Garbage Collection
<!--markup:1:begin-->		
`php
=====
PHP
<!--markup:2:end-->
$session->cf_gc_probability = 2;           // Probability of GC on shutdown (-1)
$session->cf_gc_maxlifetime = 144;        // Max session file lifetime (seconds)
<!--markup:1:begin-->```

####<!--markup:1:end-->
<!--markup:2:begin-->

Cookie Settings =====
PHP
$session->cf_cookie_prefix = '';                   // Prefix for all cookies
$session->cf_cookie_persistent = false;            // Make cookies persistent
$session->cf_cookie_lifetime = ;                  // Cookie lifetime ( = session cookie)
$session->cf_cookie_path = '/';                    // Cookie path
$session->cf_cookie_domain = '';                   // Cookie domain ('' = current host)
$session->cf_cookie_secure = false;                // HTTPS only
$session->cf_cookie_httponly = true;               // Disable JavaScript access
$session->cf_cookie_samesite = COOKIE_SAMESITE;   // SameSite attribute

===== Cache Control <!--markup:1:begin-->
`php<!--markup:1:end--> <!--markup:2:begin-->=====
%%(hl php)<!--markup:2:end-->
$session->cf_cache_limiter = 'none';       // Cache control mode (public|private|nocache|none)
$session->cf_cache_expire = 18*6;        // Cache TTL (seconds)
$session->cf_cache_mtime = ;              // Modify time for Last-Modified header
<!--markup:1:begin-->		
` ####<!--markup:1:end--> <!--markup:2:begin-->
=====<!--markup:2:end--> Security Validation
<!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->=====		
(hl php)<!--markup:2:end--> $session->cf_referer_check = ''; // Check HTTP Referer header
===== HTTP Context (Set by HTTP class) =====		
(hl php) $session->cf_ip; // Client IP address $session->cf_tls; // TLS/SSL connection indicator <!--markup:1:begin-->
`

---

##<!--markup:1:end-->
<!--markup:2:begin-->%%

----

===<!--markup:2:end--> Usage

<!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->===

====<!--markup:2:end--> Basic Session Setup ====

%%(hl php)
// Create a concrete session implementation
class MySession extends Session {
    // Implement abstract store_* methods
    // See "Implementation Guide" section
}

// Initialize and start session
$session = new MySession();
$session->cf_max_session = 36;  // 1 hour
$session->cf_cookie_path = '/';
$session->start('myapp');          // Session name: 'myapp'

// Store data
$session['user_id'] = 42;
$session['username'] = 'john';

// Retrieve data
echo $session['user_id'];  // 42

// Check if session is active
if ($session->active()) {
    echo "Session is active";
}

// Explicitly save and close
$session->write_close();

// Shutdown handler automatically called via register_shutdown_function()
<!--markup:1:begin-->		
` ###<!--markup:1:end--> <!--markup:2:begin-->
====<!--markup:2:end--> Session Data Access

<!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====		
(hl php)<!--markup:2:end--> // Array-like access (via ArrayObject) $session['user_id'] = 123; echo $session['user_id']; unset($session['user_id']); isset($session['user_id']); // Convert to array $all_data = $session->toArray(); <!--markup:1:begin-->
`

###<!--markup:1:end-->
<!--markup:2:begin-->%%

====<!--markup:2:end--> Session ID Management

<!--markup:1:begin-->		
`php<!--markup:1:end--> <!--markup:2:begin-->====

PHP
<!--markup:2:end-->
// Get current session ID
$id = $session->id();          // Returns: e.g., "abc123xyz..."

// Get session name
$name = $session->name();       // Returns: 'myapp'

// Get session ID from request
$session->start('myapp', $_REQUEST['sid'] ?? null);

active(): bool ====== Check if session is currently active.

Returns: true if session is started and active, `false` false otherwise
`php<!--markup:1:end--> <!--markup:2:begin-->====

%%(hl php)<!--markup:2:end-->
// Check if session is active
if ($session->active()) {
    // Session is running
}

// Get last state change message
$message = $session->message();  // 'replay', 'ip', 'ua', 'timeout', etc.

// Restart session (destroy old + start new)
$session->restart();
<!--markup:1:begin-->		



message(): string|null
<!markup:2:end> Get reason for last session state change.

Returns: Message string or null

Possible Values:
  • 'replay': Replay attack detected
  • `'obsolete'`: 'obsolete': Session marked for expiration
  • `'reg_expire'`: 'reg_expire': Regeneration expiration reached
  • `'max_session'`: 'max_session': Max session lifetime exceeded
  • `'max_idle'`: 'max_idle': Idle timeout exceeded
  • `'ua'`: 'ua': User agent mismatch (>5% difference)
  • 'tls': TLS status changed
  • 'ip': IP address mismatch
  • `'restart'`: 'restart': Session manually restarted
  • `null`: null: No state change

Example:
----

===<!--markup:2:end--> Security Features

<!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->===

====<!--markup:2:end--> 1. Session ID Regeneration <!--markup:2:begin-->====<!--markup:2:end-->

**Purpose:** Prevent session fixation attacks

**Automatic Triggers:**
  - Initial session creation <!--markup:1:begin-->(`regenerated<!--markup:1:end--> <!--markup:2:begin-->(##regenerated<!--markup:2:end--> = <!--markup:1:begin-->2`)<!--markup:1:end--> <!--markup:2:begin-->2##)<!--markup:2:end-->
  - First request after creation (##regenerated = 1##)
  - Periodic forced regeneration (based on ##cf_regen_time## and <!--markup:1:begin-->`cf_regen_probability`)<!--markup:1:end--> <!--markup:2:begin-->##cf_regen_probability##)<!--markup:2:end-->
  - Session validation failures

**Manual Trigger:**
<!--markup:1:begin-->```php<!--markup:1:end-->**
<!--markup:2:begin-->		



toArray(): array
<!markup:2:end> Convert session data to array.

Returns: Associative array of session data

Note: This is a direct call to ArrayObject::getArrayCopy()

**Parameters:**
  - <!--markup:1:begin-->`$delete_old`:<!--markup:1:end--> <!--markup:2:begin-->##$delete_old##:<!--markup:2:end--> 
  - <!--markup:1:begin-->`false`<!--markup:1:end--> <!--markup:2:begin-->##false##<!--markup:2:end--> (): Keep old session active for ~5 seconds (for pending AJAX requests)
  - <!--markup:1:begin-->`true`<!--markup:1:end--> <!--markup:2:begin-->##true##<!--markup:2:end--> (1): Keep old session for time specified (unused in current code)
  - ##2##: Immediately destroy old session

**Implementation Details:**
  - New session ID is generated via ##store_generate_id()##
  - Old session data is copied to new ID
  - Old session marked with ##__expire## timestamp
  - Cookie immediately updated with new ID
  - Single regeneration per request (checked via ##$this->regenerated## flag)
  - Logged in ##sticky__log## for debugging (max 15 entries)		



===== Nonce System <!--markup:1:begin-->##### `create_nonce($action,<!--markup:1:end--> <!--markup:2:begin-->=====
create_nonce($action,<!markup:2:end> $expires = null): string
Generate a unique nonce token.

Parameters:
  • $action (string): Action identifier (e.g., 'form_submit', 'delete_action')
  • `$expires` $expires (int|null): Expiration time in seconds. Defaults to cf_nonce_lifetime

Returns: Nonce token string (11 characters)

Example:
==== 2. User Agent Validation ====

**Purpose:** Detect browser/device changes that might indicate hijacking

**Behavior:**
  - Stores user agent on first request
  - Compares on subsequent requests using ##similar_text()##
  - Destroys session if similarity < 95%
  - Useful against bot attacks or stolen sessions

**Configuration:**		
<!markup:2:end>

Storage:
  • Stored in `$session->__nonces[]` $session->__nonces[]
  • Key: `{action}.{base64_encoded_hash}` {action}.{base64_encoded_hash}
  • Value: Expiration timestamp


# `verify_nonce($action,


##verify_nonce($action,
$code, $protect = )` )## ====== Verify a nonce token.

Parameters:
  • `$action` $action (string): Action identifier that was used in `create_nonce()` create_nonce()
  • `$code` $code (string): Nonce token from user
  • `$protect` $protect (int): Protection level
  • ==== 3. IP Address Validation ====
    
    **Purpose:** Detect IP spoofing or hijacking
    
    **Behavior:**
      - Stores IP on first request
      - Compares on subsequent requests
      - Soft failure on mismatch: <!--markup:1:begin-->`destroy<!--markup:1:end--> <!--markup:2:begin-->##destroy<!--markup:2:end--> = <!--markup:1:begin-->1`<!--markup:1:end--> <!--markup:2:begin-->1##<!--markup:2:end--> (keeps regenerating)
      - Tracks IP changes in <!--markup:1:begin-->`sticky__ip`<!--markup:1:end--> <!--markup:2:begin-->##sticky__ip##<!--markup:2:end-->
    
    **Configuration:**
    <!--markup:1:begin-->```php<!--markup:1:end-->**
    <!--markup:2:begin-->		
    `php

**IP Change Tracking:**
<!--markup:1:begin-->```php<!--markup:1:end-->**
<!--markup:2:begin-->		


##set_cookie($name,
$value, $persistent = false): void## ====== Legacy cookie setter (alternative to setcookie()).

Parameters:
  • $name: Cookie name (prefix added)
  • $value: Cookie value
  • $persistent:
  • `false`: false: Session cookie (deleted on browser close)
  • Number: Days to persist
  • ==== 4. TLS/SSL Validation <!--markup:2:begin-->====<!--markup:2:end-->
    
    **Purpose:** Prevent protocol downgrade attacks
    
    **Behavior:**
      - Checks if connection transitioned from HTTPS to HTTP
      - Destroys session on mismatch
    
    **Configuration:**
    <!--markup:1:begin-->```php<!--markup:1:end-->**
    <!--markup:2:begin-->		
    `


# `delete_cookie($name): void`
`

###<!--markup:1:end-->
<!--markup:2:begin-->%%

====<!--markup:2:end--> 5. Anti-Replay Protection <!--markup:2:begin-->====<!--markup:2:end-->

**Purpose:** Prevent CSRF and replay attacks

**Mechanism:**
  - Generates unique "NoReplay" nonce on each request
  - Cookie-based nonce verification
  - Detects rapid-fire requests (AJAX attacks)

**Configuration:**
%%(hl php)
$session->cf_prevent_replay = 1;       // Enable (default)
$session->cf_prevent_replay = ;       // Disable if needed
%%

**How It Works:**
%%
Request 1: Generate nonce, send in cookie
Request 2: Client sends nonce back, verify & generate new one
Request 3: If old nonce used again → reject (replay detected)
%%

==== 6. Referer Validation (Optional) ====

**Purpose:** Prevent CSRF via header checking

**Configuration:**
%%(hl php)
$session->cf_referer_check = 'example.com';
// Session rejected if HTTP_REFERER doesn't contain this string
<!--markup:1:begin-->		
(hl php)
$session->delete_cookie('old_preference');
----

===<!--markup:2:end--> API Reference

<!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->===

====<!--markup:2:end--> Public Methods

<!--markup:1:begin-->####<!--markup:1:end--> <!--markup:2:begin-->====

=====<!--markup:2:end--> Lifecycle Management

<!--markup:1:begin-->##### `start($name<!--markup:1:end--> <!--markup:2:begin-->=====

====== ##start($name<!--markup:2:end--> = null, $id = null): bool## ======
Start or resume a session.

**Parameters:**
  - <!--markup:1:begin-->`$name`<!--markup:1:end--> <!--markup:2:begin-->##$name##<!--markup:2:end--> (string|null): Session name (cookie name base). Alphanumeric + underscore/dash. Defaults to 'sesid'
  - <!--markup:1:begin-->`$id`<!--markup:1:end--> <!--markup:2:begin-->##$id##<!--markup:2:end--> (string|null): Existing session ID to resume. If null, attempts to read from cookie

**Returns:** <!--markup:1:begin-->`true`<!--markup:1:end-->** <!--markup:2:begin-->##true##<!--markup:2:end--> if session started successfully, <!--markup:1:begin-->`false`<!--markup:1:end--> <!--markup:2:begin-->##false##<!--markup:2:end--> on error

**Side Effects:**
  - Sets headers (cookies, cache control)
  - Populates session data from storage
  - Performs security validations
  - May trigger session ID regeneration

**Example:**		
`php

`<!--markup:1:end-->
<!--markup:2:begin-->%%<!--markup:2:end-->

**Validation Steps:**
  1. Reject if headers already sent
  2. Validate session name format
  3. Retrieve ID from parameter or cookie
  4. Check Referer header (if configured)
  5. Validate ID format via <!--markup:1:begin-->`store_validate_id()`<!--markup:1:end--> <!--markup:2:begin-->##store_validate_id()##<!--markup:2:end-->
  6. Read session data from storage
  7. Verify nonces and timestamps
  8. Check user agent, IP, TLS
  9. Regenerate if needed

----

====== ##write_close(): void## ======
Save session data and close session.

**Side Effects:**
  - Calls <!--markup:1:begin-->`write_session()`<!--markup:1:end--> <!--markup:2:begin-->##write_session()##<!--markup:2:end--> to serialize and store data
  - Calls <!--markup:1:begin-->`store_close()`<!--markup:1:end--> <!--markup:2:begin-->##store_close()##<!--markup:2:end--> to close storage handler
  - Sets ##$active = false##

**Example:**
%%(hl php)
$session['key'] = 'value';
$session->write_close();  // Ensure data is saved
<!--markup:1:begin-->		


Protected Methods (For Store Implementation)
`regenerate_id($delete_old ====

##regenerate_id($delete_old
= false, $message = ''): bool## ===== Internal method to regenerate session ID (called automatically).

Protected - Usually called automatically, but can be overridden/called by subclasses


`store_generate_id(): string`


store_generate_id(): string

Generate a new session ID.

Default Implementation: Returns 21-character random alphanumeric string via `Ut::random_token(21)` Ut::random_token(21)
Override in subclass to customize:
----

====== ##restart(): bool## ======<!--markup:2:end-->
Destroy current session and create new one.

**Equivalent to:** ##regenerate_id(true) + clean_vars() + populate()##

**Returns:** <!--markup:1:begin-->`true`<!--markup:1:end-->** <!--markup:2:begin-->##true##<!--markup:2:end--> on success, <!--markup:1:begin-->`false`<!--markup:1:end--> <!--markup:2:begin-->##false##<!--markup:2:end--> on error

**Use Cases:**
  - User logout and new login
  - Security reset
  - Complete session refresh

**Example:**		
`

`store_validate_id($id): bool`

`

---

####<!--markup:1:end-->
<!--markup:2:begin-->%%

----

=====<!--markup:2:end--> Session Access

<!--markup:1:begin-->##### `id(): mixed`<!--markup:1:end--> <!--markup:2:begin-->=====

====== ##id(): mixed## ======<!--markup:2:end-->
Get current session ID.

**Returns:** Session ID string or null if not started

%%(hl php)
$sid = $session->id();  // "abc123xyz..."
%%

----

====== ##name(): string## ======
Get session name (cookie prefix).

**Returns:** Session name

<!--markup:1:begin-->		
(hl php)
protected function store_validate_id($id): bool {
return preg_match('/^[a-f-9]{64}$/', $id); // SHA256 format
}
PHP
<!--markup:2:end-->
$name = $session->name();  // "myapp"
(hl php)
protected function store_open($name): void {
$this->db = new PDO('sqlite::memory:');
}
PHP
if ($session->active()) {
    $session['key'] = 'value';
}
<!--markup:1:begin-->```

---

##### `message(): string|null`<!--markup:1:end-->
<!--markup:2:begin-->
(hl php)<!markup:2:end>
protected function store_read($id, $lock = false): string|false {
$data = file_get_contents("/tmp/sess_$id");
return $data ?: false;
}
PHP
$session->start('app');
if ($message = $session->message()) {
    error_log("Session issue: $message");
}
<!--markup:1:begin-->```

---

##### `toArray(): array`<!--markup:1:end-->
<!--markup:2:begin-->
(hl php)<!markup:2:end>
protected function store_write($id, $data): void {
file_put_contents("/tmp/sess_$id", $data);
}
PHP
$data = $session->toArray();
foreach ($data as $key => $value) {
    echo "$key => $value\n";
}
(hl php)<!markup:2:end>
protected function store_close(): void {
// Close database, file, etc.
}
PHP
$nonce = $session->create_nonce('form_submit', 36);
// Use in HTML: <input type="hidden" name="nonce" value="<?= $nonce ?>">
<!--markup:1:begin-->```<!--markup:1:end-->
<!--markup:2:begin-->
`
###

:<!--markup:1:end--> <!--markup:2:begin-->####:<!--markup:2:end--> Single-use nonce (consumed on first verification)
  - <!--markup:1:begin-->`1+`:<!--markup:1:end--> <!--markup:2:begin-->##1+##:<!--markup:2:end--> Protected nonce (can verify multiple times, prevents fast replays)

**Returns:**
  - <!--markup:1:begin-->`true`<!--markup:1:end--> <!--markup:2:begin-->##true##<!--markup:2:end--> (1): Nonce verified and valid
  - ##false## (): Nonce invalid or expired
  - ##-1##: Protected nonce used twice in quick succession (possible AJAX attack)

**Example:**
%%(hl php)
if ($nonce = $session->verify_nonce('form_submit', $_POST['nonce'])) {
    if ($nonce === -1) {
        // Possible replay, but might be legitimate AJAX
        $session->cf_prevent_replay = ;  // Disable for this request
    } else {
        // Safe to process
        process_form();
    }
}
%%

**Cleanup:**
  - Expired nonces automatically removed
  - Verified single-use nonces removed from storage

----

===== Cookie Management =====

====== ##setcookie($name, $value = null, $expires = , $path = null, $domain = null, $secure = null, $httponly = null, $samesite = null): bool## ======
Set a cookie with security headers.

**Parameters:**
  - ##$name##: Cookie name (automatically URL-encoded)
  - ##$value##: Cookie value (automatically URL-encoded, null to delete)
  - ##$expires##: Expiration timestamp ( = session cookie)
  - ##$path##: Cookie path (default: ##cf_cookie_path##)
  - ##$domain##: Cookie domain (default: ##cf_cookie_domain##)
  - ##$secure##: HTTPS only (default: ##cf_cookie_secure##)
  - ##$httponly##: Disable JS access (default: ##cf_cookie_httponly##)
  - ##$samesite##: SameSite attribute (default: ##cf_cookie_samesite##)

**Returns:** ##true## on success, ##false## if headers already sent

**Features:**
  - RFC 2616 2.2 token encoding for cookie name
  - RFC 6265 4.1.1 cookie-octet encoding for value
  - Removes duplicate cookie headers automatically
  - Adds all security attributes (secure, httponly, samesite)
  - Does NOT replace existing cookies (allows multiple Set-Cookie headers)

**Example:**
%%(hl php)
// Session cookie
$session->setcookie('user_pref', 'dark_mode');

// Persistent cookie (3 days)
$session->setcookie('remember_me', 'token123', time() + 3*864);

// Delete cookie
$session->setcookie('old_cookie', null);

// Secure cookie with SameSite
$session->setcookie('token', 'abc123', time() + 36, 
    path: '/', secure: true, httponly: true, samesite: 'Strict');
%%

----

====== ##get_cookie($name)## ======
Retrieve cookie value.

**Parameters:**
  - ##$name##: Cookie name (prefix automatically added)

**Returns:** Cookie value or null if not set

<!--markup:1:begin-->		
start() is called
├─ ID from cookie
├─ store_read(id) → serialized_data
├─ data = unserialize(data)
├─ exchangeArray(data)
├─ active = true
├─ Security checks:
│ ├─ Replay check
│ ├─ Timeout checks
│ ├─ UA/IP/TLS checks
│ └─ May trigger regenerate_id()
└─ return true
PHP
<!--markup:2:end-->
$value = $session->get_cookie('user_pref');  // Reads $_COOKIE['user_pref']
<!--markup:1:begin-->```

---

##### `set_cookie($name,<!--markup:1:end-->
<!--markup:2:begin-->
`
====
:<!--markup:1:end--> <!--markup:2:begin-->####:<!--markup:2:end--> Use ##cf_cookie_persistent## config

**Example:**
%%(hl php)
$session->set_cookie('theme', 'dark');  // Session cookie
$session->set_cookie('lang', 'en', 365);  // 1 year
<!--markup:1:begin-->		

Session Destruction
----

====== ##delete_cookie($name): void## ======<!--markup:2:end-->
Delete a cookie.

**Parameters:**
  - <!--markup:1:begin-->`$name`:<!--markup:1:end--> <!--markup:2:begin-->##$name##:<!--markup:2:end--> Cookie name (prefix added)

**Implementation:** Sets empty value with immediate expiration

<!--markup:1:begin-->```php<!--markup:1:end-->

<!--markup:2:begin-->		
`

##

`

---

##### `unsetcookie($name): void`<!--markup:1:end-->
<!--markup:2:begin-->%%

----

====== ##unsetcookie($name): void## ======<!--markup:2:end-->
Alias for <!--markup:1:begin-->`setcookie($name)`<!--markup:1:end--> <!--markup:2:begin-->##setcookie($name)##<!--markup:2:end--> with no value (convenience method).

<!--markup:1:begin-->		
(hl php) // Store flash message for next request
$session->set_flash('error', 'Username already exists', 1); // 1 request
$session->set_flash('info', 'Welcome back!', 2); // 2 requests

// In next request, data automatically available
echo $session['error']; // "Username already exists"
PHP
<!--markup:2:end-->
$session->unsetcookie('cookie_name');
<!--markup:1:begin-->```

---

###<!--markup:1:end-->
<!--markup:2:begin-->
(hl php)

foreach ($sticky__flash as $var => $age) {
if (!isset($session[$var])) {
unset($sticky__flash[$var]); // Already deleted
} else if ($age <= ) {
unset($session[$var]); // Expired, remove
unset($flash__flash[$var]);
} else {
$flash__flash[$var] = $age; // Decrement counter
}
}
<!markup:1:begin-->
`php<!--markup:1:end-->**
<!--markup:2:begin-->%%(hl php)<!--markup:2:end-->
protected function store_generate_id(): string {
    return hash('sha256', random_bytes(32));  // Your format
}
<!--markup:1:begin-->		
`


##<!markup:1:end>
----

===== ##store_validate_id($id): bool## =====<!--markup:2:end-->
Validate session ID format.

**Default Implementation:** Regex check: <!--markup:1:begin-->`/^[a-zA-Z\d]{21}$/`<!--markup:1:end--> <!--markup:2:begin-->##/^[a-zA-Z\d]{21}$/##<!--markup:2:end-->

**Override in subclass to match your format:**
<!--markup:1:begin-->```php<!--markup:1:end-->**
<!--markup:2:begin-->		
(hl php)
// 1. Display form with nonce
$nonce = $session->create_nonce('user_update', 36);
?>
<form method="POST" action="/update-profile">
<input type="hidden" name="nonce" value="<?= htmlspecialchars($nonce) ?>">
<input type="text" name="username" value="...">
<button type="submit">Update</button>
</form>

<?php
// 2. Process form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$session->verify_nonce('user_update', $_POST['nonce'] ?? '')) {
http_response_code(43);
die('Security check failed');
}

// Safe to process
update_user($_POST);
}
----

===== ##store_open($name): void## =====
Open session storage (called before first read/write).

**Subclass must implement** - Initialize storage handler

**Example:**		
`php
====
----

===== ##store_read($id, $lock = false): <!--markup:1:begin-->string|false`<!--markup:1:end--> <!--markup:2:begin-->string|false## =====<!--markup:2:end-->
Read session data from storage.

**Subclass must implement**

**Parameters:**
  - <!--markup:1:begin-->`$id`:<!--markup:1:end--> <!--markup:2:begin-->##$id##:<!--markup:2:end--> Session ID to read
  - <!--markup:1:begin-->`$lock`:<!--markup:1:end--> <!--markup:2:begin-->##$lock##:<!--markup:2:end--> If true, lock the session file for writing (create new)

**Returns:**
  - Serialized session data (string) if found and locked
  - Empty string <!--markup:1:begin-->(`''`)<!--markup:1:end--> <!--markup:2:begin-->(##''##)<!--markup:2:end--> if new session should be created
  - <!--markup:1:begin-->`false`<!--markup:1:end--> <!--markup:2:begin-->##false##<!--markup:2:end--> if session doesn't exist or read error

**Example:**
<!--markup:1:begin-->```php<!--markup:1:end-->**
<!--markup:2:begin-->		

Nonce Storage Format
----

===== ##store_write($id, $data): void## =====
Write session data to storage.

**Subclass must implement**

**Parameters:**
  - <!--markup:1:begin-->`$id`:<!--markup:1:end--> <!--markup:2:begin-->##$id##:<!--markup:2:end--> Session ID
  - <!--markup:1:begin-->`$data`:<!--markup:1:end--> <!--markup:2:begin-->##$data##:<!--markup:2:end--> Serialized session data (already processed by <!--markup:1:begin-->`Ut::serialize()`)<!--markup:1:end--> <!--markup:2:begin-->##Ut::serialize()##)<!--markup:2:end-->

**Example:**
<!--markup:1:begin-->```php<!--markup:1:end-->**
<!--markup:2:begin-->		
`
###

----

===== ##store_close(): void## =====
Close session storage.

**Subclass must implement** - Release resources

**Example:**
<!--markup:1:begin-->```php<!--markup:1:end-->**
<!--markup:2:begin-->		
(hl php)
// Cookie names: RFC 2616 2.2 token format
// Cookie values: RFC 6265 4.1.1 cookie-octet format
// Unsafe characters automatically URL-encoded
`

---

#### `store_gc(): void`<!--markup:1:end-->
<!--markup:2:begin-->%%

----

===== ##store_gc(): void## =====<!--markup:2:end-->
Perform garbage collection on old sessions.

**Subclass must implement** - Delete expired sessions

**Called During:**
  - Shutdown handler (probabilistic, based on <!--markup:1:begin-->`cf_gc_probability`)<!--markup:1:end--> <!--markup:2:begin-->##cf_gc_probability##)<!--markup:2:end-->

**Should Delete:**
  - Sessions older than ##cf_gc_maxlifetime## seconds

**Example:**
%%(hl php)
protected function store_gc(): void {
    $max_age = time() - $this->cf_gc_maxlifetime;
    // Delete files/records older than $max_age
}
%%

----

==== Private Methods (Internal Use)

<!--markup:1:begin-->##### `populate(): void`<!--markup:1:end--> <!--markup:2:begin-->====

====== ##populate(): void## ======<!--markup:2:end-->
Initialize session tracking variables on first request.

**Called by:** <!--markup:1:begin-->`start()`, `restart()`<!--markup:1:end-->** <!--markup:2:begin-->##start()##, ##restart()##<!--markup:2:end-->

**Initializes:**
  - <!--markup:1:begin-->`__started`:<!--markup:1:end--> <!--markup:2:begin-->##__started##:<!--markup:2:end--> Current timestamp
  - <!--markup:1:begin-->`__regenerated`:<!--markup:1:end--> <!--markup:2:begin-->##__regenerated##:<!--markup:2:end--> Current timestamp
  - <!--markup:1:begin-->`__user_agent`:<!--markup:1:end--> <!--markup:2:begin-->##__user_agent##:<!--markup:2:end--> Browser user agent
  - <!--markup:1:begin-->`__user_ip`:<!--markup:1:end--> <!--markup:2:begin-->##__user_ip##:<!--markup:2:end--> Client IP (if configured)
  - <!--markup:1:begin-->`__user_tls`:<!--markup:1:end--> <!--markup:2:begin-->##__user_tls##:<!--markup:2:end--> TLS status (if configured)
  - ##sticky__created##: Creation time (if not exists)

----

====== ##write_session(): void## ======
Serialize and write session data to storage.

**Called by:** ##regenerate_id()##, ##write_close()##, ##terminator()##

**Updates:**
  - <!--markup:1:begin-->`__updated`:<!--markup:1:end--> <!--markup:2:begin-->##__updated##:<!--markup:2:end--> Current timestamp
  - Calls ##store_write()## with serialized data

----

====== ##clean_vars(): void## ======
Remove non-sticky session variables.

**Called by:** <!--markup:1:begin-->`restart()`,<!--markup:1:end-->** <!--markup:2:begin-->##restart()##,<!--markup:2:end--> session validation failure

**Preserves:** Variables starting with <!--markup:1:begin-->`sticky_`

---

##### `prevent_replay(): void`<!--markup:1:end--> <!--markup:2:begin-->##sticky_##

----

====== ##prevent_replay(): void## ======<!--markup:2:end-->
Generate and send anti-replay nonce.

**Called by:** <!--markup:1:begin-->`populate()`<!--markup:1:end-->** <!--markup:2:begin-->##populate()##<!--markup:2:end-->

**Action:**
  - Creates 'NoReplay' nonce
  - Sends in cookie: <!--markup:1:begin-->`{cf_cookie_prefix}NoReplay`

---

##### `cache_limiter(): void`<!--markup:1:end--> <!--markup:2:begin-->##{cf_cookie_prefix}NoReplay##

----

====== ##cache_limiter(): void## ======<!--markup:2:end-->
Set HTTP cache control headers based on configuration.

**Called by:** <!--markup:1:begin-->`start()`<!--markup:1:end-->** <!--markup:2:begin-->##start()##<!--markup:2:end--> after session data loaded

**Modes:**
  - <!--markup:1:begin-->`'public'`:<!--markup:1:end--> <!--markup:2:begin-->##'public'##:<!--markup:2:end--> Cacheable, <!--markup:1:begin-->`Cache-Control:<!--markup:1:end--> <!--markup:2:begin-->##Cache-Control:<!--markup:2:end--> public, <!--markup:1:begin-->max-age=...`<!--markup:1:end--> <!--markup:2:begin-->max-age=...##<!--markup:2:end-->
  - <!--markup:1:begin-->`'private'`:<!--markup:1:end--> <!--markup:2:begin-->##'private'##:<!--markup:2:end--> Private, <!--markup:1:begin-->`Cache-Control:<!--markup:1:end--> <!--markup:2:begin-->##Cache-Control:<!--markup:2:end--> private, <!--markup:1:begin-->max-age=...`<!--markup:1:end--> <!--markup:2:begin-->max-age=...##<!--markup:2:end-->
  - ##'private_no_expire'##: Private no TTL
  - ##'nocache'##: No storage, ##Cache-Control: no-store##
  - <!--markup:1:begin-->`'none'`:<!--markup:1:end--> <!--markup:2:begin-->##'none'##:<!--markup:2:end--> No headers (default)

----

====== ##set_new_id(): void## ======
Generate and assign new session ID, send in cookie.

**Called by:** ##regenerate_id()##, ##start()## (for new sessions)

<!--markup:1:begin-->---

##### `remove_cookie($cookie): void`<!--markup:1:end-->

<!--markup:2:begin-->----

====== ##remove_cookie($cookie): void## ======<!--markup:2:end-->
Remove existing Set-Cookie header to avoid duplicates.

**Called by:** <!--markup:1:begin-->`setcookie()`<!--markup:1:end-->** <!--markup:2:begin-->##setcookie()##<!--markup:2:end--> before setting new value

----

====== ##nonce_index($action, $code): string## (static) <!--markup:2:begin-->======<!--markup:2:end-->
Generate storage key for nonce.

**Returns:** <!--markup:1:begin-->`{action}.{base64_encoded_hash}`

---

---

##<!--markup:1:end-->** <!--markup:2:begin-->##{action}.{base64_encoded_hash}##

----

----

===<!--markup:2:end--> Session Lifecycle

<!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->===

====<!--markup:2:end--> Complete Session Flow ====

%%
┌─ Browser Request
│
├─ Application Code
│  └─ $session->start('appname')
│     │
│     ├─ Check if headers sent
│     ├─ Validate/read session name
│     ├─ Get session ID from:
│     │  1. Parameter $id
│     │  2. Cookie: {prefix}appname
│     ├─ Validate referer (if cf_referer_check set)
│     ├─ Validate ID format via store_validate_id()
│     ├─ store_open(name)
│     ├─ store_read(id)
│     │  └─ If missing/invalid/expired:
│     │     └─ set_new_id()
│     │        └─ regenerate_id = 2 (NEW)
│     ├─ Deserialize session data
│     ├─ exchangeArray(data)
│     ├─ active = true
│     ├─ cache_limiter()
│     │
│     └─ Security Checks (if NOT first request):
│        ├─ Verify NoReplay nonce
│        ├─ Check expiration flags
│        ├─ Check max session time
│        ├─ Check max idle time
│        ├─ Compare user agent (95%+ similarity)
│        ├─ Compare TLS status
│        ├─ Compare IP address
│        │  ├─ Match: OK
│        │  └─ Mismatch: destroy=1, regenerate
│        └─ Check regen time/probability
│           └─ regenerate_id()
│
├─ Application Code
│  └─ $session['key'] = 'value'
│
└─ End of Request
   │
   └─ register_shutdown_function() → terminator()
      │
      ├─ Process flash data
      │  └─ Decrement lifetimes
      │  └─ Remove expired flash
      ├─ write_session()
      │  └─ store_write(id, serialized_data)
      ├─ store_close()
      ├─ Probabilistic garbage collection
      │  └─ store_gc() (cf_gc_probability % chance)
      │     └─ Delete old sessions
      └─ Output sent to browser
%%

==== First Request (New Session) ====

%%
start() is called
├─ No ID in cookie
├─ store_read(id) → false
├─ set_new_id()
│  └─ id = store_generate_id()
│  └─ send_cookie(name, id)
├─ data = []
├─ active = true
├─ populate()
│  ├─ __started = now
│  ├─ __regenerated = now
│  ├─ __user_agent = UA
│  └─ sticky__created = now
└─ return true
<!--markup:1:begin-->		
(hl php)
setcookie('auth', 'token',
expires: time() + 36,
secure: true, // HTTPS only
httponly: true, // Disable JavaScript
samesite: 'Strict' // CSRF protection
);
====<!--markup:2:end--> Subsequent Request (Resume Session)

<!--markup:1:begin-->```<!--markup:1:end--> <!--markup:2:begin-->====		
(hl php)
// Automatically removes old Set-Cookie header before setting new one
// Prevents cookie header duplication
remove_cookie($name) → clears old headers
setcookie() → sets new header
`

###<!--markup:1:end-->
<!--markup:2:begin-->%%

====<!--markup:2:end--> Session ID Regeneration

<!--markup:1:begin-->		
(hl php)
$session->cf_cookie_path = '/app'; // Path
$session->cf_cookie_domain = '.example.com'; // Domain
$session->cf_cookie_secure = true; // HTTPS
$session->cf_cookie_httponly = true; // No JS
$session->cf_cookie_samesite = 'Lax'; // SameSite
$session->cf_cookie_prefix = 'app_'; // Prefix

$session->setcookie('token', 'value');
// Uses all configured defaults
<!--markup:2:end-->
regenerate_id($delete_old, $message) is called
├─ Check not headers_sent()
├─ Check $active
├─ Check not already regenerated in this request
├─ write_session()  [Save current data]
├─ set __expire:
│  ├─ if $delete_old=: __expire = now + 5
│  └─ if $delete_old>: __expire = 
├─ Generate new ID:
│  └─ loop:
│      ├─ id = store_generate_id()
│      └─ while store_read(id) !== false  [Ensure unique]
├─ Lock new session: store_read(id, true)
├─ Set: __regenerated = now
├─ Set: regenerated = 1
├─ Log event: sticky__log[] = [now, message]
└─ return true
<!--markup:1:begin-->```

###<!--markup:1:end-->
<!--markup:2:begin-->		
(hl php)<!markup:2:end>
// Prevent XSS and CSRF
$session->cf_cookie_secure = true; // HTTPS only
$session->cf_cookie_httponly = true; // No JavaScript access
$session->cf_cookie_samesite = 'Strict'; // Strict CSRF protection

// Set scope
$session->cf_cookie_path = '/'; // Root path
$session->cf_cookie_domain = ''; // Current host only

// Session cookies (delete on browser close)
$session->cf_cookie_lifetime = ;
$session->cf_cookie_persistent = false;
`<!--markup:1:end--> <!--markup:2:begin-->====

%%<!--markup:2:end-->
Triggered by:
├─ restart() → regenerate_id(true)
├─ Validation failure (destroy=2)
│  └─ regenerate_id(2)
│  └─ clean_vars()  [Remove non-sticky data]
└─ Timeout or security violation
Results in:
├─ __expire =   [Immediate expiration]
├─ Non-sticky variables cleared
├─ sticky_ variables preserved
└─ New session ID generated
<!--markup:1:begin-->		
(hl php)
if (headers_sent($file, $line)) {
trigger_error("id regeneration requested after headers flushed at $file:$line",
E_USER_WARNING);
return false;
}
----

===<!--markup:2:end--> Flash Data ===

Flash data persists for a limited number of requests (typically 1-2) and is automatically removed.

<!--markup:1:begin-->###<!--markup:1:end-->

<!--markup:2:begin-->====<!--markup:2:end--> Usage ====		
(hl php)
if (headers_sent($file, $line)) {
trigger_error("cannot place session cookie $name=$value due to $file:$line",
E_USER_WARNING);
return;
}
==== How It Works <!--markup:2:begin-->====<!--markup:2:end-->
  1. **Storage:** Flash data stored in ##$session->sticky__flash##
  - Key: Variable name
  - Value: Lifetime in requests
  2. **Cleanup:** In <!--markup:1:begin-->`terminator()`<!--markup:1:end--> <!--markup:2:begin-->##terminator()##<!--markup:2:end--> (shutdown handler):
   <!--markup:1:begin-->```php<!--markup:1:end-->
   <!--markup:2:begin-->		
(hl php)
if ($this->store_read($this->id, true) !== '') {
// error! [comment indicates error, but continues]
}
`<!--markup:1:end-->
   <!--markup:2:begin-->%%<!--markup:2:end-->
  3. **Persistence:** Flash variables are kept in ##sticky__flash## even during session resets

==== Example: Login Flow ====

%%(hl php)
// POST /login
if ($credentials_valid) {
    $session->restart();  // New session
    $session['user_id'] = $user->id;
    $session->set_flash('success', 'Login successful!', 1);
    header('Location: /dashboard');
} else {
    $session->set_flash('error', 'Invalid credentials', 1);
    header('Location: /login');
}

// GET /dashboard (or /login on failure)
if ($message = $session['error'] ?? null) {
    echo "<div class='error'>$message</div>";
}
if ($message = $session['success'] ?? null) {
    echo "<div class='success'>$message</div>";
}
<!--markup:1:begin-->		
(hl php)
# Ut::dbg("regeneration failed by flush at $file:$line");
# Ut::dbg($destroy, $message);
# Ut::dbg("session setcookie $name failed by $file:$line");
----

===<!--markup:2:end--> Nonce System <!--markup:2:begin-->===<!--markup:2:end-->

Nonces provide CSRF protection and replay attack detection.

<!--markup:1:begin-->###<!--markup:1:end-->

<!--markup:2:begin-->====<!--markup:2:end--> Terminology <!--markup:2:begin-->====<!--markup:2:end-->
  - **Nonce:** Number used ONCE - cryptographic token for action verification
  - **Action:** Type of operation being protected (e.g., 'form_submit', 'delete_user')
  - **Protected Nonce:** Can be verified multiple times with protection against rapid reuse

==== Complete Example: Form Protection

<!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====		
(hl php)<!markup:2:end>
// Access session event history
if (isset($session->sticky__log)) {
foreach ($session->sticky__log as [$timestamp, $message]) {
echo "[$timestamp] $message\n";
}
}
`

###<!--markup:1:end-->
<!--markup:2:begin-->%%

====<!--markup:2:end--> Example: Protected Nonce (AJAX-Safe)

<!--markup:1:begin-->		
`

PHP
<!--markup:2:end-->
// Generate protected nonce (can verify multiple times)
$nonce = $session->create_nonce('ajax_action', 3);

// Verify with protection level 3 (3 seconds)
$result = $session->verify_nonce('ajax_action', $_POST['nonce'], 3);

if ($result === -1) {
    // Rapid reuse detected (possible attack, but might be AJAX)
    if (is_ajax_request()) {
        // AJAX is OK, disable replay protection this once
        $session->cf_prevent_replay = ;
    } else {
        // Likely attack
        http_response_code(43);
        die('Suspicious activity');
    }
} else if ($result === true) {
    // Safe to process
    process_ajax();
}
<!--markup:1:begin-->```

###<!--markup:1:end-->
<!--markup:2:begin-->
(hl php)
<?php

class DatabaseSession extends Session {
private PDO $pdo;

public function __construct(PDO $pdo) {
parent::__construct();
$this->pdo = $pdo;
$this->ensure_table();
}

private function ensure_table(): void {
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(21) PRIMARY KEY,
data LONGTEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
SQL;
$this->pdo->exec($sql);
}

protected function store_open($name): void {
// Database already connected
}

protected function store_read($id, $lock = false): string|false {
$stmt = $this->pdo->prepare('SELECT data FROM sessions WHERE id = ?');
$stmt->execute([$id]);

if ($result = $stmt->fetch(PDO::FETCH_ASSOC)) {
return $result['data'];
}

if ($lock) {
// Create new session
$stmt = $this->pdo->prepare('INSERT INTO sessions (id, data) VALUES (?, ?)');
$stmt->execute([$id, '']);
return '';
}

return false;
}

protected function store_write($id, $data): void {
$stmt = $this->pdo->prepare(
'INSERT INTO sessions (id, data) VALUES (?, ?)
ON DUPLICATE KEY UPDATE data = VALUES(data)'
);
$stmt->execute([$id, $data]);
}

protected function store_close(): void {
// Connection persists for application
}

protected function store_gc(): void {
$cutoff = time() - $this->cf_gc_maxlifetime;
$this->pdo->prepare('DELETE FROM sessions WHERE updated_at < FROM_UNIXTIME(?)')
->execute([$cutoff]);
}
}
`<!--markup:1:end--> <!--markup:2:begin-->====

%%<!--markup:2:end-->
Internal storage (__nonces array):
[
    "{action}.{hash}" => expiration_timestamp,
    "form_submit.AbCdEfGhIjK" => 123456789,
    "delete_user.XyZaBcDeFgH" => 123456789,
]
Where:
- action: Custom action identifier
- hash: First 11 chars of base64(sha1(code_bytes)) 
- expiration_timestamp: time() + lifetime
<!--markup:1:begin-->		
`php
=====
====<!--markup:2:end--> Security Properties <!--markup:2:begin-->====<!--markup:2:end-->
  - **CSRF Protection:** Nonce must match to process form
  - **One-Time Use:** Each nonce consumed after first verification (unless protected)
  - **Expiration:** Nonces automatically expire
  - **Action-Specific:** Each action has separate nonce space
  - **AJAX-Safe:** Protected nonces allow multiple quick verifications

----

=== Cookie Management

<!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->===

====<!--markup:2:end--> Security Features <!--markup:2:begin-->====<!--markup:2:end-->

The <!--markup:1:begin-->`setcookie()`<!--markup:1:end--> <!--markup:2:begin-->##setcookie()##<!--markup:2:end--> method implements comprehensive cookie security:

<!--markup:1:begin-->####<!--markup:1:end-->

<!--markup:2:begin-->=====<!--markup:2:end--> Encoding
<!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->=====		

==== Complete Integration Example <!--markup:1:begin-->
===== Security Attributes =====		
`php<!--markup:1:end--> <!--markup:2:begin-->====

===== No Duplicate Headers =====		



Security Checklist


Use this checklist when implementing sessions:
  • [ ] Use HTTPS only in production
  • [ ] Enable cf_cookie_secure
  • [ ] Enable cf_cookie_httponly
  • [ ] Set cf_cookie_samesite to 'Strict' or 'Lax'
  • [ ] Set appropriate cf_max_session timeout
  • [ ] Set appropriate cf_max_idle timeout
  • [ ] Enable cf_prevent_replay
  • [ ] Validate cf_ip if possible
  • [ ] Validate cf_tls on HTTPS sites
  • [ ] Use nonces for all state-changing forms
  • [ ] Implement proper logout (call `restart()`) <!markup:2:begin>restart())
  • [ ] Regenerate on privilege escalation (login)
  • [ ] Monitor `sticky__ip` sticky__ip for suspicious changes
  • [ ] Review `sticky__log` sticky__log for attack patterns
  • [ ] Implement garbage collection (`store_gc`) (store_gc)
  • [ ] Hash session IDs before storing (see TODOs)
  • [ ] Use secure random token generation


##



===
Common Patterns

### ===

Login Flow ====
==== Configuration-Driven Defaults ====		

<!markup:2:end> Logout Flow


==== Typical Secure Configuration

<!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====		

====<!--markup:2:end--> CSRF-Protected Form <!--markup:1:begin-->
----

=== Error Handling ===

==== Graceful Degradation ====

The Session class gracefully handles errors:

===== Headers Already Sent =====		
` ###<!--markup:1:end--> <!--markup:2:begin-->
**Impact:** Session ID cannot be regenerated, but session continues

===== Cookie Setting Failure =====		
(hl php) // After action $session->set_flash('info', 'Profile updated successfully', 1); // Display next page if (isset($session['info'])) { echo $session['info']; } <!--markup:1:begin-->
**Impact:** Cookie not set, but session data remains accessible

===== Storage Errors =====		
`php<!--markup:1:end--> <!--markup:2:begin-->====

**Impact:** Creates new session if storage returns error

<!--markup:1:begin-->###<!--markup:1:end-->

<!--markup:2:begin-->====<!--markup:2:end--> Debug Logging <!--markup:2:begin-->====<!--markup:2:end-->

The Session class includes commented debug statements:		

==== Cookie Not Setting <!--markup:1:begin-->
To enable: Uncomment lines and ensure <!--markup:1:begin-->`Ut::dbg()`<!--markup:1:end--> <!--markup:2:begin-->##Ut::dbg()##<!--markup:2:end--> function exists

<!--markup:1:begin-->###<!--markup:1:end-->

<!--markup:2:begin-->====<!--markup:2:end--> Event Logging <!--markup:2:begin-->====<!--markup:2:end-->

Session events tracked in <!--markup:1:begin-->`sticky__log`:

```php<!--markup:1:end--> <!--markup:2:begin-->##sticky__log##:		
`php<!--markup:1:end--> <!--markup:2:begin-->====

`<!--markup:1:end-->
<!--markup:2:begin-->%%<!--markup:2:end-->

**Logged Events:**
  - Session regeneration (with reason)
  - Limited to 15 most recent events (old entries archived as '...')

----

=== Implementation Guide ===

==== Creating a Concrete Session Class ====

You must implement the abstract storage methods. Choose your storage backend: files, database, cache, etc.

<!--markup:1:begin-->####<!--markup:1:end-->

<!--markup:2:begin-->=====<!--markup:2:end--> File-Based Storage =====

%%(hl php)
<?php

class FileSession extends Session {
    private $session_dir = '/tmp/sessions';
    private $file_handle = null;

    public function __construct() {
        parent::__construct();
        if (!is_dir($this->session_dir)) {
            mkdir($this->session_dir, 7, true);
        }
    }

    protected function store_open($name): void {
        // PHP sessions don't really "open", just prepare
        // In file mode, we could initialize directory
    }

    protected function store_read($id, $lock = false): string|false {
        $file = $this->session_dir . '/sess_' . preg_replace('/[^a-zA-Z-9]/', '', $id);

        if (!file_exists($file)) {
            if ($lock) {
                // Create new session file
                file_put_contents($file, '', LOCK_EX);
                return '';
            }
            return false;
        }

        if (filemtime($file) < time() - $this->cf_gc_maxlifetime) {
            unlink($file);  // Expired
            return false;
        }

        return file_get_contents($file);
    }

    protected function store_write($id, $data): void {
        $file = $this->session_dir . '/sess_' . preg_replace('/[^a-zA-Z-9]/', '', $id);
        file_put_contents($file, $data, LOCK_EX);
    }

    protected function store_close(): void {
        // No cleanup needed for file backend
    }

    protected function store_gc(): void {
        $cutoff = time() - $this->cf_gc_maxlifetime;
        foreach (glob($this->session_dir . '/sess_*') as $file) {
            if (filemtime($file) < $cutoff) {
                unlink($file);
            }
        }
    }
}
<!--markup:1:begin-->		

Nonce Verification Failing


=====<!--markup:2:end--> Database Storage (PDO)

<!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->=====		

<!markup:2:end> Session Data Lost


`

####<!--markup:1:end-->
<!--markup:2:begin-->%%

=====<!--markup:2:end--> Redis Storage

<!--markup:1:begin-->		



TODO Items (From Code Comments)

The following improvements are planned:
  1. Do not store session ID in filename or DB index - store hash instead
    • Improves security by not exposing IDs in storage layer
    • Would require hashing logic in store_* methods
  2. Log of IP changes and other possible security alerts
    • Track `sticky__ip` sticky__ip changes more comprehensively
    • Create security audit trail
  3. Allocate internal unique session which lives through lifetime of uber-session
    • Multi-session management (parent/child sessions)
    • Useful for complex user flows
  4. Do not delete old sessions, but use them as hijack pointers
    • Maintain session history for analysis
    • Detect potential session hijacking patterns
    • Implement session relationship tracking
  5. All SIDs used later than 5secs of regenerations is hijacks
    • Detect and block delayed session ID usage
    • Current implementation allows 5-second window
    • Could be more granular


References

Security Standards

  • RFC 2616: HTTP/1.1 (Cookie syntax)
  • RFC 6265: HTTP State Management Mechanism
  • RFC 6234: US Secure Hash and Message Authentication Code Algorithms
  • OWASP: Session Management Cheat Sheet
  • OWASP: Cross-Site Request Forgery (CSRF) Prevention

Related Code

  • Ut::serialize() / Ut::unserialize(): Session data serialization
  • `Ut::random_token()`: Ut::random_token(): Cryptographic token generation
  • Ut::http_date(): HTTP date formatting
  • Ut::urlencode(): Cookie-safe encoding
  • `Ut::is_empty()`: Ut::is_empty(): Empty value checking

###
==== See Also ====
  • `src/class/http.php`: src/class/http.php: HTTP request/response handling
  • `src/class/auth.php`: src/class/auth.php: Authentication (uses Session)
  • Session security best practices in OWASP documentation


## Version History


Version History

  • Current: Abstract session class with security features
  • Planned: Implementation of TODO items above




Documentation generated: 226-5-5
For latest updates, see: https://github.com/Trojer/wack[...]ION_DOCUMENTATION.md
PHP
<!--markup:2:end-->
<?php

class RedisSession extends Session {
    private Redis $redis;
    private string $prefix = 'sess:';

    public function __construct(Redis $redis) {
        parent::__construct();
        $this->redis = $redis;
    }

    protected function store_open($name): void {
        // Redis already connected
    }

    protected function store_read($id, $lock = false): string|false {
        $data = $this->redis->get($this->prefix . $id);

        if ($data !== false) {
            return $data;
        }

        if ($lock) {
            // Create new session
            $this->redis->set($this->prefix . $id, '', 
                            ['EX' => $this->cf_gc_maxlifetime]);
            return '';
        }

        return false;
    }

    protected function store_write($id, $data): void {
        $this->redis->set($this->prefix . $id, $data,
                        ['EX' => $this->cf_gc_maxlifetime]);
    }

    protected function store_close(): void {
        // Connection persists
    }

    protected function store_gc(): void {
        // Redis handles expiration automatically with TTL
    }
}