=== 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
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-->====
`
###<!--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-->
<!--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--> 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
`'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-->=====
`$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[]
`$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-->
----
===<!--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
:<!--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
`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">
----
===== ##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)
// 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-->
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]) {
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(?)')
====<!--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-->
====<!--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-->