Difference between revisions for Users / Eo Ny
|
← Previous edit
|
Next edit →
|
| Version1 | Version2 | ||
|---|---|---|---|
| 1 | == Session Management Technical Documentation == | 1 | == Session Management Technical Documentation == |
| 2 | {{toc numerate=1}} | ||
| 2 | 3 | ||
| 3 | === Overview === | 4 | === Overview === |
| 4 | 5 | ||
| … | … | … | … |
| 36 | 37 | ||
| 37 | ==== Session Data Storage ==== | 38 | ==== Session Data Storage ==== |
| 38 | Session data is stored as an array accessible through ##ArrayObject## interface: | 39 | Session data is stored as an array accessible through ##ArrayObject## interface: |
| 39 |
%% |
40 | %%(hl php) |
| 40 | $session['user_id'] = 123; // Set data | 41 | $session['user_id'] = 123; // Set data |
| 41 | echo $session['user_id']; // Get data | 42 | echo $session['user_id']; // Get data |
| 42 | %% | 43 | %% |
| … | … | … | … |
| 112 | All configuration properties are prefixed with ##cf_## (config) and can be set before calling ##start()##: | 113 | All configuration properties are prefixed with ##cf_## (config) and can be set before calling ##start()##: |
| 113 | 114 | ||
| 114 | ===== Session Behavior ===== | 115 | ===== Session Behavior ===== |
| 115 |
%% |
116 | %%(hl php) |
| 116 | $session->cf_static = 0; // Disable regenerations (e.g., for CAPTCHA) | 117 | $session->cf_static = 0; // Disable regenerations (e.g., for CAPTCHA) |
| 117 | $session->cf_max_session = 7200; // Max session lifetime (seconds) | 118 | $session->cf_max_session = 7200; // Max session lifetime (seconds) |
| 118 | $session->cf_max_idle = 1440; // Max idle time before destruction (seconds) | 119 | $session->cf_max_idle = 1440; // Max idle time before destruction (seconds) |
| … | … | … | … |
| 121 | %% | 122 | %% |
| 122 | 123 | ||
| 123 | ===== Nonce & Replay Protection ===== | 124 | ===== Nonce & Replay Protection ===== |
| 124 |
%% |
125 | %%(hl php) |
| 125 | $session->cf_secret = 'adyaiD9+255JeiskPybgisby'; // Secret for nonce generation | 126 | $session->cf_secret = 'adyaiD9+255JeiskPybgisby'; // Secret for nonce generation |
| 126 | $session->cf_nonce_lifetime = 7200; // Nonce expiration (seconds) | 127 | $session->cf_nonce_lifetime = 7200; // Nonce expiration (seconds) |
| 127 | $session->cf_prevent_replay = 1; // Enable replay attack prevention | 128 | $session->cf_prevent_replay = 1; // Enable replay attack prevention |
| 128 | %% | 129 | %% |
| 129 | 130 | ||
| 130 | ===== Garbage Collection ===== | 131 | ===== Garbage Collection ===== |
| 131 |
%% |
132 | %%(hl php) |
| 132 | $session->cf_gc_probability = 2; // Probability of GC on shutdown (0-100) | 133 | $session->cf_gc_probability = 2; // Probability of GC on shutdown (0-100) |
| 133 | $session->cf_gc_maxlifetime = 1440; // Max session file lifetime (seconds) | 134 | $session->cf_gc_maxlifetime = 1440; // Max session file lifetime (seconds) |
| 134 | %% | 135 | %% |
| 135 | 136 | ||
| 136 | ===== Cookie Settings ===== | 137 | ===== Cookie Settings ===== |
| 137 |
%% |
138 | %%(hl php) |
| 138 | $session->cf_cookie_prefix = ''; // Prefix for all cookies | 139 | $session->cf_cookie_prefix = ''; // Prefix for all cookies |
| 139 | $session->cf_cookie_persistent = false; // Make cookies persistent | 140 | $session->cf_cookie_persistent = false; // Make cookies persistent |
| 140 | $session->cf_cookie_lifetime = 0; // Cookie lifetime (0 = session cookie) | 141 | $session->cf_cookie_lifetime = 0; // Cookie lifetime (0 = session cookie) |
| … | … | … | … |
| 146 | %% | 147 | %% |
| 147 | 148 | ||
| 148 | ===== Cache Control ===== | 149 | ===== Cache Control ===== |
| 149 |
%% |
150 | %%(hl php) |
| 150 | $session->cf_cache_limiter = 'none'; // Cache control mode (public|private|nocache|none) | 151 | $session->cf_cache_limiter = 'none'; // Cache control mode (public|private|nocache|none) |
| 151 | $session->cf_cache_expire = 180*60; // Cache TTL (seconds) | 152 | $session->cf_cache_expire = 180*60; // Cache TTL (seconds) |
| 152 | $session->cf_cache_mtime = 0; // Modify time for Last-Modified header | 153 | $session->cf_cache_mtime = 0; // Modify time for Last-Modified header |
| 153 | %% | 154 | %% |
| 154 | 155 | ||
| 155 | ===== Security Validation ===== | 156 | ===== Security Validation ===== |
| 156 |
%% |
157 | %%(hl php) |
| 157 | $session->cf_referer_check = ''; // Check HTTP Referer header | 158 | $session->cf_referer_check = ''; // Check HTTP Referer header |
| 158 | %% | 159 | %% |
| 159 | 160 | ||
| 160 | ===== HTTP Context (Set by HTTP class) ===== | 161 | ===== HTTP Context (Set by HTTP class) ===== |
| 161 |
%% |
162 | %%(hl php) |
| 162 | $session->cf_ip; // Client IP address | 163 | $session->cf_ip; // Client IP address |
| 163 | $session->cf_tls; // TLS/SSL connection indicator | 164 | $session->cf_tls; // TLS/SSL connection indicator |
| 164 | %% | 165 | %% |
| … | … | … | … |
| 169 | 170 | ||
| 170 | ==== Basic Session Setup ==== | 171 | ==== Basic Session Setup ==== |
| 171 | 172 | ||
| 172 |
%% |
173 | %%(hl php) |
| 173 | // Create a concrete session implementation | 174 | // Create a concrete session implementation |
| 174 | class MySession extends Session { | 175 | class MySession extends Session { |
| 175 | // Implement abstract store_* methods | 176 | // Implement abstract store_* methods |
| … | … | … | … |
| 202 | 203 | ||
| 203 | ==== Session Data Access ==== | 204 | ==== Session Data Access ==== |
| 204 | 205 | ||
| 205 |
%% |
206 | %%(hl php) |
| 206 | // Array-like access (via ArrayObject) | 207 | // Array-like access (via ArrayObject) |
| 207 | $session['user_id'] = 123; | 208 | $session['user_id'] = 123; |
| 208 | echo $session['user_id']; | 209 | echo $session['user_id']; |
| … | … | … | … |
| 215 | 216 | ||
| 216 | ==== Session ID Management ==== | 217 | ==== Session ID Management ==== |
| 217 | 218 | ||
| 218 |
%% |
219 | %%(hl php) |
| 219 | // Get current session ID | 220 | // Get current session ID |
| 220 | $id = $session->id(); // Returns: e.g., "abc123xyz..." | 221 | $id = $session->id(); // Returns: e.g., "abc123xyz..." |
| 221 | 222 | ||
| … | … | … | … |
| 228 | 229 | ||
| 229 | ==== Session State ==== | 230 | ==== Session State ==== |
| 230 | 231 | ||
| 231 |
%% |
232 | %%(hl php) |
| 232 | // Check if session is active | 233 | // Check if session is active |
| 233 | if ($session->active()) { | 234 | if ($session->active()) { |
| 234 | // Session is running | 235 | // Session is running |
| … | … | … | … |
| 256 | - Session validation failures | 257 | - Session validation failures |
| 257 | 258 | ||
| 258 | **Manual Trigger:** | 259 | **Manual Trigger:** |
| 259 |
%% |
260 | %%(hl php) |
| 260 | $session->regenerate_id($delete_old = false, $message = 'custom_reason'); | 261 | $session->regenerate_id($delete_old = false, $message = 'custom_reason'); |
| 261 | %% | 262 | %% |
| 262 | 263 | ||
| … | … | … | … |
| 274 | - Single regeneration per request (checked via ##$this->regenerated## flag) | 275 | - Single regeneration per request (checked via ##$this->regenerated## flag) |
| 275 | - Logged in ##sticky__log## for debugging (max 15 entries) | 276 | - Logged in ##sticky__log## for debugging (max 15 entries) |
| 276 | 277 | ||
| 277 |
%% |
278 | %%(hl php) |
| 278 | // Example: Force regeneration on login | 279 | // Example: Force regeneration on login |
| 279 | $session->start('myapp'); | 280 | $session->start('myapp'); |
| 280 | if ($user_authenticated) { | 281 | if ($user_authenticated) { |
| … | … | … | … |
| 294 | - Useful against bot attacks or stolen sessions | 295 | - Useful against bot attacks or stolen sessions |
| 295 | 296 | ||
| 296 | **Configuration:** | 297 | **Configuration:** |
| 297 |
%% |
298 | %%(hl php) |
| 298 | // Automatic on each request (if enabled in code logic) | 299 | // Automatic on each request (if enabled in code logic) |
| 299 | // Triggers session destruction if UA changes significantly | 300 | // Triggers session destruction if UA changes significantly |
| 300 | %% | 301 | %% |
| … | … | … | … |
| 310 | - Tracks IP changes in ##sticky__ip## | 311 | - Tracks IP changes in ##sticky__ip## |
| 311 | 312 | ||
| 312 | **Configuration:** | 313 | **Configuration:** |
| 313 |
%% |
314 | %%(hl php) |
| 314 | $session->cf_ip = $_SERVER['REMOTE_ADDR']; // Set by HTTP class | 315 | $session->cf_ip = $_SERVER['REMOTE_ADDR']; // Set by HTTP class |
| 315 | // Validation happens automatically during start() | 316 | // Validation happens automatically during start() |
| 316 | %% | 317 | %% |
| 317 | 318 | ||
| 318 | **IP Change Tracking:** | 319 | **IP Change Tracking:** |
| 319 |
%% |
320 | %%(hl php) |
| 320 | // Access IP change history | 321 | // Access IP change history |
| 321 | $ip_history = $session->sticky__ip; // Array of [ip => change_count] | 322 | $ip_history = $session->sticky__ip; // Array of [ip => change_count] |
| 322 | %% | 323 | %% |
| … | … | … | … |
| 330 | - Destroys session on mismatch | 331 | - Destroys session on mismatch |
| 331 | 332 | ||
| 332 | **Configuration:** | 333 | **Configuration:** |
| 333 |
%% |
334 | %%(hl php) |
| 334 | $session->cf_tls = !empty($_SERVER['HTTPS']); // Set by HTTP class | 335 | $session->cf_tls = !empty($_SERVER['HTTPS']); // Set by HTTP class |
| 335 | // Validation happens automatically during start() | 336 | // Validation happens automatically during start() |
| 336 | %% | 337 | %% |
| … | … | … | … |
| 345 | - Detects rapid-fire requests (AJAX attacks) | 346 | - Detects rapid-fire requests (AJAX attacks) |
| 346 | 347 | ||
| 347 | **Configuration:** | 348 | **Configuration:** |
| 348 |
%% |
349 | %%(hl php) |
| 349 | $session->cf_prevent_replay = 1; // Enable (default) | 350 | $session->cf_prevent_replay = 1; // Enable (default) |
| 350 | $session->cf_prevent_replay = 0; // Disable if needed | 351 | $session->cf_prevent_replay = 0; // Disable if needed |
| 351 | %% | 352 | %% |
| … | … | … | … |
| 362 | **Purpose:** Prevent CSRF via header checking | 363 | **Purpose:** Prevent CSRF via header checking |
| 363 | 364 | ||
| 364 | **Configuration:** | 365 | **Configuration:** |
| 365 |
%% |
366 | %%(hl php) |
| 366 | $session->cf_referer_check = 'example.com'; | 367 | $session->cf_referer_check = 'example.com'; |
| 367 | // Session rejected if HTTP_REFERER doesn't contain this string | 368 | // Session rejected if HTTP_REFERER doesn't contain this string |
| 368 | %% | 369 | %% |
| … | … | … | … |
| 391 | - May trigger session ID regeneration | 392 | - May trigger session ID regeneration |
| 392 | 393 | ||
| 393 | **Example:** | 394 | **Example:** |
| 394 |
%% |
395 | %%(hl php) |
| 395 | if ($session->start('webapp', $_COOKIE['sess_id'] ?? null)) { | 396 | if ($session->start('webapp', $_COOKIE['sess_id'] ?? null)) { |
| 396 | // Session ready | 397 | // Session ready |
| 397 | } else { | 398 | } else { |
| … | … | … | … |
| 421 | - Sets ##$active = false## | 422 | - Sets ##$active = false## |
| 422 | 423 | ||
| 423 | **Example:** | 424 | **Example:** |
| 424 |
%% |
425 | %%(hl php) |
| 425 | $session['key'] = 'value'; | 426 | $session['key'] = 'value'; |
| 426 | $session->write_close(); // Ensure data is saved | 427 | $session->write_close(); // Ensure data is saved |
| 427 | %% | 428 | %% |
| … | … | … | … |
| 441 | - Complete session refresh | 442 | - Complete session refresh |
| 442 | 443 | ||
| 443 | **Example:** | 444 | **Example:** |
| 444 |
%% |
445 | %%(hl php) |
| 445 | $session->restart(); | 446 | $session->restart(); |
| 446 | // New session created, old data cleared, sticky_ vars preserved | 447 | // New session created, old data cleared, sticky_ vars preserved |
| 447 | %% | 448 | %% |
| … | … | … | … |
| 455 | 456 | ||
| 456 | **Returns:** Session ID string or null if not started | 457 | **Returns:** Session ID string or null if not started |
| 457 | 458 | ||
| 458 |
%% |
459 | %%(hl php) |
| 459 | $sid = $session->id(); // "abc123xyz..." | 460 | $sid = $session->id(); // "abc123xyz..." |
| 460 | %% | 461 | %% |
| 461 | 462 | ||
| … | … | … | … |
| 466 | 467 | ||
| 467 | **Returns:** Session name | 468 | **Returns:** Session name |
| 468 | 469 | ||
| 469 |
%% |
470 | %%(hl php) |
| 470 | $name = $session->name(); // "myapp" | 471 | $name = $session->name(); // "myapp" |
| 471 | %% | 472 | %% |
| 472 | 473 | ||
| … | … | … | … |
| 477 | 478 | ||
| 478 | **Returns:** ##true## if session is started and active, ##false## otherwise | 479 | **Returns:** ##true## if session is started and active, ##false## otherwise |
| 479 | 480 | ||
| 480 |
%% |
481 | %%(hl php) |
| 481 | if ($session->active()) { | 482 | if ($session->active()) { |
| 482 | $session['key'] = 'value'; | 483 | $session['key'] = 'value'; |
| 483 | } | 484 | } |
| … | … | … | … |
| 503 | - ##null##: No state change | 504 | - ##null##: No state change |
| 504 | 505 | ||
| 505 | **Example:** | 506 | **Example:** |
| 506 |
%% |
507 | %%(hl php) |
| 507 | $session->start('app'); | 508 | $session->start('app'); |
| 508 | if ($message = $session->message()) { | 509 | if ($message = $session->message()) { |
| 509 | error_log("Session issue: $message"); | 510 | error_log("Session issue: $message"); |
| … | … | … | … |
| 519 | 520 | ||
| 520 | **Note:** This is a direct call to ##ArrayObject::getArrayCopy()## | 521 | **Note:** This is a direct call to ##ArrayObject::getArrayCopy()## |
| 521 | 522 | ||
| 522 |
%% |
523 | %%(hl php) |
| 523 | $data = $session->toArray(); | 524 | $data = $session->toArray(); |
| 524 | foreach ($data as $key => $value) { | 525 | foreach ($data as $key => $value) { |
| 525 | echo "$key => $value\n"; | 526 | echo "$key => $value\n"; |
| … | … | … | … |
| 540 | **Returns:** Nonce token string (11 characters) | 541 | **Returns:** Nonce token string (11 characters) |
| 541 | 542 | ||
| 542 | **Example:** | 543 | **Example:** |
| 543 |
%% |
544 | %%(hl php) |
| 544 | $nonce = $session->create_nonce('form_submit', 3600); | 545 | $nonce = $session->create_nonce('form_submit', 3600); |
| 545 | // Use in HTML: <input type="hidden" name="nonce" value="<?= $nonce ?>"> | 546 | // Use in HTML: <input type="hidden" name="nonce" value="<?= $nonce ?>"> |
| 546 | %% | 547 | %% |
| … | … | … | … |
| 568 | - ##-1##: Protected nonce used twice in quick succession (possible AJAX attack) | 569 | - ##-1##: Protected nonce used twice in quick succession (possible AJAX attack) |
| 569 | 570 | ||
| 570 | **Example:** | 571 | **Example:** |
| 571 |
%% |
572 | %%(hl php) |
| 572 | if ($nonce = $session->verify_nonce('form_submit', $_POST['nonce'])) { | 573 | if ($nonce = $session->verify_nonce('form_submit', $_POST['nonce'])) { |
| 573 | if ($nonce === -1) { | 574 | if ($nonce === -1) { |
| 574 | // Possible replay, but might be legitimate AJAX | 575 | // Possible replay, but might be legitimate AJAX |
| … | … | … | … |
| 611 | - Does NOT replace existing cookies (allows multiple Set-Cookie headers) | 612 | - Does NOT replace existing cookies (allows multiple Set-Cookie headers) |
| 612 | 613 | ||
| 613 | **Example:** | 614 | **Example:** |
| 614 |
%% |
615 | %%(hl php) |
| 615 | // Session cookie | 616 | // Session cookie |
| 616 | $session->setcookie('user_pref', 'dark_mode'); | 617 | $session->setcookie('user_pref', 'dark_mode'); |
| 617 | 618 | ||
| … | … | … | … |
| 636 | 637 | ||
| 637 | **Returns:** Cookie value or null if not set | 638 | **Returns:** Cookie value or null if not set |
| 638 | 639 | ||
| 639 |
%% |
640 | %%(hl php) |
| 640 | $value = $session->get_cookie('user_pref'); // Reads $_COOKIE['user_pref'] | 641 | $value = $session->get_cookie('user_pref'); // Reads $_COOKIE['user_pref'] |
| 641 | %% | 642 | %% |
| 642 | 643 | ||
| … | … | … | … |
| 654 | - ##0##: Use ##cf_cookie_persistent## config | 655 | - ##0##: Use ##cf_cookie_persistent## config |
| 655 | 656 | ||
| 656 | **Example:** | 657 | **Example:** |
| 657 |
%% |
658 | %%(hl php) |
| 658 | $session->set_cookie('theme', 'dark'); // Session cookie | 659 | $session->set_cookie('theme', 'dark'); // Session cookie |
| 659 | $session->set_cookie('lang', 'en', 365); // 1 year | 660 | $session->set_cookie('lang', 'en', 365); // 1 year |
| 660 | %% | 661 | %% |
| … | … | … | … |
| 669 | 670 | ||
| 670 | **Implementation:** Sets empty value with immediate expiration | 671 | **Implementation:** Sets empty value with immediate expiration |
| 671 | 672 | ||
| 672 |
%% |
673 | %%(hl php) |
| 673 | $session->delete_cookie('old_preference'); | 674 | $session->delete_cookie('old_preference'); |
| 674 | %% | 675 | %% |
| 675 | 676 | ||
| … | … | … | … |
| 678 | ====== ##unsetcookie($name): void## ====== | 679 | ====== ##unsetcookie($name): void## ====== |
| 679 | Alias for ##setcookie($name)## with no value (convenience method). | 680 | Alias for ##setcookie($name)## with no value (convenience method). |
| 680 | 681 | ||
| 681 |
%% |
682 | %%(hl php) |
| 682 | $session->unsetcookie('cookie_name'); | 683 | $session->unsetcookie('cookie_name'); |
| 683 | %% | 684 | %% |
| 684 | 685 | ||
| … | … | … | … |
| 699 | **Default Implementation:** Returns 21-character random alphanumeric string via ##Ut::random_token(21)## | 700 | **Default Implementation:** Returns 21-character random alphanumeric string via ##Ut::random_token(21)## |
| 700 | 701 | ||
| 701 | **Override in subclass to customize:** | 702 | **Override in subclass to customize:** |
| 702 |
%% |
703 | %%(hl php) |
| 703 | protected function store_generate_id(): string { | 704 | protected function store_generate_id(): string { |
| 704 | return hash('sha256', random_bytes(32)); // Your format | 705 | return hash('sha256', random_bytes(32)); // Your format |
| 705 | } | 706 | } |
| … | … | … | … |
| 713 | **Default Implementation:** Regex check: ##/^[a-zA-Z\d]{21}$/## | 714 | **Default Implementation:** Regex check: ##/^[a-zA-Z\d]{21}$/## |
| 714 | 715 | ||
| 715 | **Override in subclass to match your format:** | 716 | **Override in subclass to match your format:** |
| 716 |
%% |
717 | %%(hl php) |
| 717 | protected function store_validate_id($id): bool { | 718 | protected function store_validate_id($id): bool { |
| 718 | return preg_match('/^[a-f0-9]{64}$/', $id); // SHA256 format | 719 | return preg_match('/^[a-f0-9]{64}$/', $id); // SHA256 format |
| 719 | } | 720 | } |
| … | … | … | … |
| 727 | **Subclass must implement** - Initialize storage handler | 728 | **Subclass must implement** - Initialize storage handler |
| 728 | 729 | ||
| 729 | **Example:** | 730 | **Example:** |
| 730 |
%% |
731 | %%(hl php) |
| 731 | protected function store_open($name): void { | 732 | protected function store_open($name): void { |
| 732 | $this->db = new PDO('sqlite::memory:'); | 733 | $this->db = new PDO('sqlite::memory:'); |
| 733 | } | 734 | } |
| … | … | … | … |
| 750 | - ##false## if session doesn't exist or read error | 751 | - ##false## if session doesn't exist or read error |
| 751 | 752 | ||
| 752 | **Example:** | 753 | **Example:** |
| 753 |
%% |
754 | %%(hl php) |
| 754 | protected function store_read($id, $lock = false): string|false { | 755 | protected function store_read($id, $lock = false): string|false { |
| 755 | $data = file_get_contents("/tmp/sess_$id"); | 756 | $data = file_get_contents("/tmp/sess_$id"); |
| 756 | return $data ?: false; | 757 | return $data ?: false; |
| … | … | … | … |
| 769 | - ##$data##: Serialized session data (already processed by ##Ut::serialize()##) | 770 | - ##$data##: Serialized session data (already processed by ##Ut::serialize()##) |
| 770 | 771 | ||
| 771 | **Example:** | 772 | **Example:** |
| 772 |
%% |
773 | %%(hl php) |
| 773 | protected function store_write($id, $data): void { | 774 | protected function store_write($id, $data): void { |
| 774 | file_put_contents("/tmp/sess_$id", $data); | 775 | file_put_contents("/tmp/sess_$id", $data); |
| 775 | } | 776 | } |
| … | … | … | … |
| 783 | **Subclass must implement** - Release resources | 784 | **Subclass must implement** - Release resources |
| 784 | 785 | ||
| 785 | **Example:** | 786 | **Example:** |
| 786 |
%% |
787 | %%(hl php) |
| 787 | protected function store_close(): void { | 788 | protected function store_close(): void { |
| 788 | // Close database, file, etc. | 789 | // Close database, file, etc. |
| 789 | } | 790 | } |
| … | … | … | … |
| 803 | - Sessions older than ##cf_gc_maxlifetime## seconds | 804 | - Sessions older than ##cf_gc_maxlifetime## seconds |
| 804 | 805 | ||
| 805 | **Example:** | 806 | **Example:** |
| 806 |
%% |
807 | %%(hl php) |
| 807 | protected function store_gc(): void { | 808 | protected function store_gc(): void { |
| 808 | $max_age = time() - $this->cf_gc_maxlifetime; | 809 | $max_age = time() - $this->cf_gc_maxlifetime; |
| 809 | // Delete files/records older than $max_age | 810 | // Delete files/records older than $max_age |
| … | … | … | … |
| 1038 | 1039 | ||
| 1039 | ==== Usage ==== | 1040 | ==== Usage ==== |
| 1040 | 1041 | ||
| 1041 |
%% |
1042 | %%(hl php) |
| 1042 | // Store flash message for next request | 1043 | // Store flash message for next request |
| 1043 | $session->set_flash('error', 'Username already exists', 1); // 1 request | 1044 | $session->set_flash('error', 'Username already exists', 1); // 1 request |
| 1044 | $session->set_flash('info', 'Welcome back!', 2); // 2 requests | 1045 | $session->set_flash('info', 'Welcome back!', 2); // 2 requests |
| … | … | … | … |
| 1052 | - Key: Variable name | 1053 | - Key: Variable name |
| 1053 | - Value: Lifetime in requests | 1054 | - Value: Lifetime in requests |
| 1054 | 2. **Cleanup:** In ##terminator()## (shutdown handler): | 1055 | 2. **Cleanup:** In ##terminator()## (shutdown handler): |
| 1055 |
%% |
1056 | %%(hl php) |
| 1056 | foreach ($sticky__flash as $var => $age) { | 1057 | foreach ($sticky__flash as $var => $age) { |
| 1057 | if (!isset($session[$var])) { | 1058 | if (!isset($session[$var])) { |
| 1058 | unset($sticky__flash[$var]); // Already deleted | 1059 | unset($sticky__flash[$var]); // Already deleted |
| … | … | … | … |
| 1068 | 1069 | ||
| 1069 | ==== Example: Login Flow ==== | 1070 | ==== Example: Login Flow ==== |
| 1070 | 1071 | ||
| 1071 |
%% |
1072 | %%(hl php) |
| 1072 | // POST /login | 1073 | // POST /login |
| 1073 | if ($credentials_valid) { | 1074 | if ($credentials_valid) { |
| 1074 | $session->restart(); // New session | 1075 | $session->restart(); // New session |
| … | … | … | … |
| 1102 | 1103 | ||
| 1103 | ==== Complete Example: Form Protection ==== | 1104 | ==== Complete Example: Form Protection ==== |
| 1104 | 1105 | ||
| 1105 |
%% |
1106 | %%(hl php) |
| 1106 | // 1. Display form with nonce | 1107 | // 1. Display form with nonce |
| 1107 | $nonce = $session->create_nonce('user_update', 3600); | 1108 | $nonce = $session->create_nonce('user_update', 3600); |
| 1108 | ?> | 1109 | ?> |
| … | … | … | … |
| 1127 | 1128 | ||
| 1128 | ==== Example: Protected Nonce (AJAX-Safe) ==== | 1129 | ==== Example: Protected Nonce (AJAX-Safe) ==== |
| 1129 | 1130 | ||
| 1130 |
%% |
1131 | %%(hl php) |
| 1131 | // Generate protected nonce (can verify multiple times) | 1132 | // Generate protected nonce (can verify multiple times) |
| 1132 | $nonce = $session->create_nonce('ajax_action', 300); | 1133 | $nonce = $session->create_nonce('ajax_action', 300); |
| 1133 | 1134 | ||
| … | … | … | … |
| 1181 | The ##setcookie()## method implements comprehensive cookie security: | 1182 | The ##setcookie()## method implements comprehensive cookie security: |
| 1182 | 1183 | ||
| 1183 | ===== Encoding ===== | 1184 | ===== Encoding ===== |
| 1184 |
%% |
1185 | %%(hl php) |
| 1185 | // Cookie names: RFC 2616 2.2 token format | 1186 | // Cookie names: RFC 2616 2.2 token format |
| 1186 | // Cookie values: RFC 6265 4.1.1 cookie-octet format | 1187 | // Cookie values: RFC 6265 4.1.1 cookie-octet format |
| 1187 | // Unsafe characters automatically URL-encoded | 1188 | // Unsafe characters automatically URL-encoded |
| 1188 | %% | 1189 | %% |
| 1189 | 1190 | ||
| 1190 | ===== Security Attributes ===== | 1191 | ===== Security Attributes ===== |
| 1191 |
%% |
1192 | %%(hl php) |
| 1192 | setcookie('auth', 'token', | 1193 | setcookie('auth', 'token', |
| 1193 | expires: time() + 3600, | 1194 | expires: time() + 3600, |
| 1194 | secure: true, // HTTPS only | 1195 | secure: true, // HTTPS only |
| … | … | … | … |
| 1198 | %% | 1199 | %% |
| 1199 | 1200 | ||
| 1200 | ===== No Duplicate Headers ===== | 1201 | ===== No Duplicate Headers ===== |
| 1201 |
%% |
1202 | %%(hl php) |
| 1202 | // Automatically removes old Set-Cookie header before setting new one | 1203 | // Automatically removes old Set-Cookie header before setting new one |
| 1203 | // Prevents cookie header duplication | 1204 | // Prevents cookie header duplication |
| 1204 | remove_cookie($name) → clears old headers | 1205 | remove_cookie($name) → clears old headers |
| … | … | … | … |
| 1207 | 1208 | ||
| 1208 | ==== Configuration-Driven Defaults ==== | 1209 | ==== Configuration-Driven Defaults ==== |
| 1209 | 1210 | ||
| 1210 |
%% |
1211 | %%(hl php) |
| 1211 | $session->cf_cookie_path = '/app'; // Path | 1212 | $session->cf_cookie_path = '/app'; // Path |
| 1212 | $session->cf_cookie_domain = '.example.com'; // Domain | 1213 | $session->cf_cookie_domain = '.example.com'; // Domain |
| 1213 | $session->cf_cookie_secure = true; // HTTPS | 1214 | $session->cf_cookie_secure = true; // HTTPS |
| … | … | … | … |
| 1221 | 1222 | ||
| 1222 | ==== Typical Secure Configuration ==== | 1223 | ==== Typical Secure Configuration ==== |
| 1223 | 1224 | ||
| 1224 |
%% |
1225 | %%(hl php) |
| 1225 | // Prevent XSS and CSRF | 1226 | // Prevent XSS and CSRF |
| 1226 | $session->cf_cookie_secure = true; // HTTPS only | 1227 | $session->cf_cookie_secure = true; // HTTPS only |
| 1227 | $session->cf_cookie_httponly = true; // No JavaScript access | 1228 | $session->cf_cookie_httponly = true; // No JavaScript access |
| … | … | … | … |
| 1245 | The Session class gracefully handles errors: | 1246 | The Session class gracefully handles errors: |
| 1246 | 1247 | ||
| 1247 | ===== Headers Already Sent ===== | 1248 | ===== Headers Already Sent ===== |
| 1248 |
%% |
1249 | %%(hl php) |
| 1249 | if (headers_sent($file, $line)) { | 1250 | if (headers_sent($file, $line)) { |
| 1250 | trigger_error("id regeneration requested after headers flushed at $file:$line", | 1251 | trigger_error("id regeneration requested after headers flushed at $file:$line", |
| 1251 | E_USER_WARNING); | 1252 | E_USER_WARNING); |
| … | … | … | … |
| 1256 | **Impact:** Session ID cannot be regenerated, but session continues | 1257 | **Impact:** Session ID cannot be regenerated, but session continues |
| 1257 | 1258 | ||
| 1258 | ===== Cookie Setting Failure ===== | 1259 | ===== Cookie Setting Failure ===== |
| 1259 |
%% |
1260 | %%(hl php) |
| 1260 | if (headers_sent($file, $line)) { | 1261 | if (headers_sent($file, $line)) { |
| 1261 | trigger_error("cannot place session cookie $name=$value due to $file:$line", | 1262 | trigger_error("cannot place session cookie $name=$value due to $file:$line", |
| 1262 | E_USER_WARNING); | 1263 | E_USER_WARNING); |
| … | … | … | … |
| 1267 | **Impact:** Cookie not set, but session data remains accessible | 1268 | **Impact:** Cookie not set, but session data remains accessible |
| 1268 | 1269 | ||
| 1269 | ===== Storage Errors ===== | 1270 | ===== Storage Errors ===== |
| 1270 |
%% |
1271 | %%(hl php) |
| 1271 | if ($this->store_read($this->id, true) !== '') { | 1272 | if ($this->store_read($this->id, true) !== '') { |
| 1272 | // error! [comment indicates error, but continues] | 1273 | // error! [comment indicates error, but continues] |
| 1273 | } | 1274 | } |
| … | … | … | … |
| 1279 | 1280 | ||
| 1280 | The Session class includes commented debug statements: | 1281 | The Session class includes commented debug statements: |
| 1281 | 1282 | ||
| 1282 |
%% |
1283 | %%(hl php) |
| 1283 | # Ut::dbg("regeneration failed by flush at $file:$line"); | 1284 | # Ut::dbg("regeneration failed by flush at $file:$line"); |
| 1284 | # Ut::dbg($destroy, $message); | 1285 | # Ut::dbg($destroy, $message); |
| 1285 | # Ut::dbg("session setcookie $name failed by $file:$line"); | 1286 | # Ut::dbg("session setcookie $name failed by $file:$line"); |
| … | … | … | … |
| 1291 | 1292 | ||
| 1292 | Session events tracked in ##sticky__log##: | 1293 | Session events tracked in ##sticky__log##: |
| 1293 | 1294 | ||
| 1294 |
%% |
1295 | %%(hl php) |
| 1295 | // Access session event history | 1296 | // Access session event history |
| 1296 | if (isset($session->sticky__log)) { | 1297 | if (isset($session->sticky__log)) { |
| 1297 | foreach ($session->sticky__log as [$timestamp, $message]) { | 1298 | foreach ($session->sticky__log as [$timestamp, $message]) { |
| … | … | … | … |
| 1314 | 1315 | ||
| 1315 | ===== File-Based Storage ===== | 1316 | ===== File-Based Storage ===== |
| 1316 | 1317 | ||
| 1317 |
%% |
1318 | %%(hl php) |
| 1318 | <?php | 1319 | <?php |
| 1319 | 1320 | ||
| 1320 | class FileSession extends Session { | 1321 | class FileSession extends Session { |
| … | … | … | … |
| 1375 | 1376 | ||
| 1376 | ===== Database Storage (PDO) ===== | 1377 | ===== Database Storage (PDO) ===== |
| 1377 | 1378 | ||
| 1378 |
%% |
1379 | %%(hl php) |
| 1379 | <?php | 1380 | <?php |
| 1380 | 1381 | ||
| 1381 | class DatabaseSession extends Session { | 1382 | class DatabaseSession extends Session { |
| … | … | … | … |
| 1443 | 1444 | ||
| 1444 | ===== Redis Storage ===== | 1445 | ===== Redis Storage ===== |
| 1445 | 1446 | ||
| 1446 |
%% |
1447 | %%(hl php) |
| 1447 | <?php | 1448 | <?php |
| 1448 | 1449 | ||
| 1449 | class RedisSession extends Session { | 1450 | class RedisSession extends Session { |
| … | … | … | … |
| 1493 | 1494 | ||
| 1494 | ==== Complete Integration Example ==== | 1495 | ==== Complete Integration Example ==== |
| 1495 | 1496 | ||
| 1496 |
%% |
1497 | %%(hl php) |
| 1497 | <?php | 1498 | <?php |
| 1498 | 1499 | ||
| 1499 | // Initialize session with configuration | 1500 | // Initialize session with configuration |
| … | … | … | … |
| 1543 | 1544 | ||
| 1544 | ==== Configuration Best Practices ==== | 1545 | ==== Configuration Best Practices ==== |
| 1545 | 1546 | ||
| 1546 |
%% |
1547 | %%(hl php) |
| 1547 | <?php | 1548 | <?php |
| 1548 | 1549 | ||
| 1549 | class SessionConfig { | 1550 | class SessionConfig { |
| … | … | … | … |
| 1584 | 1585 | ||
| 1585 | ==== Testing Tips ==== | 1586 | ==== Testing Tips ==== |
| 1586 | 1587 | ||
| 1587 |
%% |
1588 | %%(hl php) |
| 1588 | <?php | 1589 | <?php |
| 1589 | 1590 | ||
| 1590 | // Test nonce generation and verification | 1591 | // Test nonce generation and verification |
| … | … | … | … |
| 1643 | 1644 | ||
| 1644 | ==== Login Flow ==== | 1645 | ==== Login Flow ==== |
| 1645 | 1646 | ||
| 1646 |
%% |
1647 | %%(hl php) |
| 1647 | if ($_POST['action'] === 'login') { | 1648 | if ($_POST['action'] === 'login') { |
| 1648 | $user = authenticate($_POST['username'], $_POST['password']); | 1649 | $user = authenticate($_POST['username'], $_POST['password']); |
| 1649 | if ($user) { | 1650 | if ($user) { |
| … | … | … | … |
| 1661 | 1662 | ||
| 1662 | ==== Logout Flow ==== | 1663 | ==== Logout Flow ==== |
| 1663 | 1664 | ||
| 1664 |
%% |
1665 | %%(hl php) |
| 1665 | if ($_GET['action'] === 'logout') { | 1666 | if ($_GET['action'] === 'logout') { |
| 1666 | $session->restart(); // Complete reset | 1667 | $session->restart(); // Complete reset |
| 1667 | header('Location: /'); | 1668 | header('Location: /'); |
| … | … | … | … |
| 1670 | 1671 | ||
| 1671 | ==== CSRF-Protected Form ==== | 1672 | ==== CSRF-Protected Form ==== |
| 1672 | 1673 | ||
| 1673 |
%% |
1674 | %%(hl php) |
| 1674 | // Display form | 1675 | // Display form |
| 1675 | $csrf = $session->create_nonce('form_' . $form_id, 3600); | 1676 | $csrf = $session->create_nonce('form_' . $form_id, 3600); |
| 1676 | echo '<form method="POST">'; | 1677 | echo '<form method="POST">'; |
| … | … | … | … |
| 1689 | 1690 | ||
| 1690 | ==== Permission Check with Session Regeneration ==== | 1691 | ==== Permission Check with Session Regeneration ==== |
| 1691 | 1692 | ||
| 1692 |
%% |
1693 | %%(hl php) |
| 1693 | if ($user->privilege_level < ADMIN_LEVEL && $promoted_to_admin) { | 1694 | if ($user->privilege_level < ADMIN_LEVEL && $promoted_to_admin) { |
| 1694 | $session->regenerate_id(false, 'privilege_escalation'); | 1695 | $session->regenerate_id(false, 'privilege_escalation'); |
| 1695 | $session['is_admin'] = true; | 1696 | $session['is_admin'] = true; |
| … | … | … | … |
| 1698 | 1699 | ||
| 1699 | ==== Session Messages/Flash ==== | 1700 | ==== Session Messages/Flash ==== |
| 1700 | 1701 | ||
| 1701 |
%% |
1702 | %%(hl php) |
| 1702 | // After action | 1703 | // After action |
| 1703 | $session->set_flash('info', 'Profile updated successfully', 1); | 1704 | $session->set_flash('info', 'Profile updated successfully', 1); |
| 1704 | 1705 | ||
| … | … | … | … |
| 1745 | 1746 | ||
| 1746 | ==== Session Not Starting ==== | 1747 | ==== Session Not Starting ==== |
| 1747 | 1748 | ||
| 1748 |
%% |
1749 | %%(hl php) |
| 1749 | if (!$session->start('myapp')) { | 1750 | if (!$session->start('myapp')) { |
| 1750 | // Check reasons: | 1751 | // Check reasons: |
| 1751 | // 1. Headers already sent? | 1752 | // 1. Headers already sent? |
| … | … | … | … |
| 1757 | 1758 | ||
| 1758 | ==== Cookie Not Setting ==== | 1759 | ==== Cookie Not Setting ==== |
| 1759 | 1760 | ||
| 1760 |
%% |
1761 | %%(hl php) |
| 1761 | // If setcookie() returns false: | 1762 | // If setcookie() returns false: |
| 1762 | // - Check if headers_sent() | 1763 | // - Check if headers_sent() |
| 1763 | // - Check if cookie name is RFC 2616 compliant | 1764 | // - Check if cookie name is RFC 2616 compliant |
| … | … | … | … |
| 1766 | 1767 | ||
| 1767 | ==== Session ID Not Regenerating ==== | 1768 | ==== Session ID Not Regenerating ==== |
| 1768 | 1769 | ||
| 1769 |
%% |
1770 | %%(hl php) |
| 1770 | // If regenerate_id() returns false: | 1771 | // If regenerate_id() returns false: |
| 1771 | // - Headers might be sent | 1772 | // - Headers might be sent |
| 1772 | // - $active might be false | 1773 | // - $active might be false |
| … | … | … | … |
| 1778 | 1779 | ||
| 1779 | ==== Nonce Verification Failing ==== | 1780 | ==== Nonce Verification Failing ==== |
| 1780 | 1781 | ||
| 1781 |
%% |
1782 | %%(hl php) |
| 1782 | // If verify_nonce() returns false: | 1783 | // If verify_nonce() returns false: |
| 1783 | // 1. Nonce might be expired | 1784 | // 1. Nonce might be expired |
| 1784 | // 2. Nonce might be for different action | 1785 | // 2. Nonce might be for different action |
| … | … | … | … |
| 1791 | 1792 | ||
| 1792 | ==== Session Data Lost ==== | 1793 | ==== Session Data Lost ==== |
| 1793 | 1794 | ||
| 1794 |
%% |
1795 | %%(hl php) |
| 1795 | // Possible causes: | 1796 | // Possible causes: |
| 1796 | // 1. write_close() not called (usually automatic via shutdown) | 1797 | // 1. write_close() not called (usually automatic via shutdown) |
| 1797 | // 2. Storage backend failing silently | 1798 | // 2. Storage backend failing silently |