瀏覽代碼

- Persisted sessions by cookie

- Session keys moved to constants
- Adding indices to schema
- Posting with expired cookies will persist the post body for retrying after logging back in
master
Rocketsoup 3 年之前
父節點
當前提交
5f225e1a4e
共有 3 個文件被更改,包括 138 次插入15 次删除
  1. 124
    14
      htdocs/index.php
  2. 二進制
      journal.db
  3. 14
    1
      source/create-tables.sql

+ 124
- 14
htdocs/index.php 查看文件

@@ -39,16 +39,21 @@ define('BASE_URL', 'https://example.com/path/index.php');
39 39
 define('SCHEMA_VERSION', '20230107');
40 40
 define('RECENT_POSTS_PER_PAGE', 50);
41 41
 define('SESSION_COOKIE_TTL_SECONDS', 14 * 24 * 60 * 60); // 14 days
42
+define('SESSION_COOKIE_NAME', 'journal_token');
42 43
 /// If a new post matches an existing post created less than this many seconds
43 44
 /// ago, the new duplicate post will be ignored.
44 45
 define('DUPLICATE_POST_SECONDS', 10);
45 46
 define('TIME_ZONE_DEFAULT', 'America/Los_Angeles');
47
+define('TOKEN_BYTES', 16);
48
+define('SESSION_KEY_ERROR', 'error');
49
+define('SESSION_KEY_LAST_ACCESS', 'last_access');
50
+define('SESSION_KEY_POST_BODY', 'post_body');
51
+define('SESSION_KEY_USER_ID', 'user_id');
46 52
 
47 53
 // -- Core setup ------------------------------------------
48 54
 
49
-session_set_cookie_params(SESSION_COOKIE_TTL_SECONDS, httponly: true);
50 55
 session_start();
51
-$_SESSION['last_access'] = time(); // keep alive
56
+$_SESSION[SESSION_KEY_LAST_ACCESS] = time(); // keep alive
52 57
 
53 58
 function handle_error($error_level = E_WARNING, $error_message = '',
54 59
 		$error_file = '', $error_line = 0, $error_context = null): void {
@@ -65,7 +70,7 @@ error_reporting(E_ERROR);
65 70
 
66 71
 /// Logs a debug message.
67 72
 function trace(string $message): void {
68
-	//error_log($message, 0);
73
+	// error_log($message, 0);
69 74
 }
70 75
 
71 76
 // -- Data classes ----------------------------------------
@@ -185,13 +190,13 @@ class Post {
185 190
 
186 191
 /// Represents a user.
187 192
 class User {
193
+	public static ?User $current = null;
194
+
188 195
 	public int $user_id;
189 196
 	public string $username;
190 197
 	public string $password_hash;
191 198
 	public string $timezone;
192 199
 
193
-	public static ?User $current = null;
194
-
195 200
 	function __construct(array $row) {
196 201
 		$this->user_id = $row['user_id'];
197 202
 		$this->username = $row['username'];
@@ -260,15 +265,16 @@ class User {
260 265
 			return null;
261 266
 		}
262 267
 		trace("Login succeeded for {$username}.");
263
-		$_SESSION['user_id'] = $user->user_id;
268
+		$_SESSION[SESSION_KEY_USER_ID] = $user->user_id;
264 269
 		self::$current = $user;
270
+		Session::create($user->user_id);
265 271
 		return $user;
266 272
 	}
267 273
 
268 274
 	/// Updates self::$current from $_SESSION.
269 275
 	public static function update_current(): void {
270
-		if (self::any_exist() && array_key_exists('user_id', $_SESSION) && is_int($_SESSION['user_id'])) {
271
-			$id = $_SESSION['user_id'];
276
+		if (self::any_exist() && array_key_exists(SESSION_KEY_USER_ID, $_SESSION) && is_int($_SESSION[SESSION_KEY_USER_ID])) {
277
+			$id = $_SESSION[SESSION_KEY_USER_ID];
272 278
 			self::$current = User::get_by_id($id);
273 279
 		} else {
274 280
 			self::$current = null;
@@ -277,7 +283,103 @@ class User {
277 283
 
278 284
 	public static function sign_out(): void {
279 285
 		self::$current = null;
280
-		unset($_SESSION['user_id']);
286
+		Session::$current?->delete();
287
+		unset($_SESSION[SESSION_KEY_USER_ID]);
288
+	}
289
+}
290
+
291
+class Session {
292
+	public static ?Session $current = null;
293
+
294
+	public int $rowid;
295
+	public string $token;
296
+	public int $user_id;
297
+	public int $created;
298
+	public int $updated;
299
+
300
+	function __construct(array $row) {
301
+		$this->rowid = $row['rowid'];
302
+		$this->token = $row['token'];
303
+		$this->user_id = $row['user_id'];
304
+		$this->created = $row['created'];
305
+		$this->updated = $row['updated'];
306
+	}
307
+
308
+	/// Logs in from the journal token cookie if needed.
309
+	public static function check_cookie(): void {
310
+		if (User::$current) return;
311
+		if (!array_key_exists(SESSION_COOKIE_NAME, $_COOKIE)) return;
312
+		$token = $_COOKIE[SESSION_COOKIE_NAME];
313
+		trace("Found token cookie");
314
+		self::$current = self::get_by_token($token);
315
+		if (!self::$current) return;
316
+		$user = User::get_by_id(self::$current->user_id);
317
+		if (!$user) {
318
+			self::$current->delete();
319
+			return;
320
+		}
321
+		trace("Found user from cookie");
322
+		User::$current = $user;
323
+		$_SESSION[SESSION_KEY_USER_ID] = $user->user_id;
324
+		self::$current->touch();
325
+	}
326
+
327
+	public static function create(int $user_id): Session {
328
+		$token = bin2hex(random_bytes(TOKEN_BYTES));
329
+		$time = intval(time());
330
+		$sql = 'INSERT INTO sessions (token, user_id, created, updated) ' .
331
+			'VALUES (:token, :user_id, :created, :updated);';
332
+		$args = array(
333
+			':token' => $token,
334
+			':user_id' => $user_id,
335
+			':created' => $time,
336
+			':updated' => $time,
337
+		);
338
+		Database::query($sql, $args);
339
+		$session = self::get_by_token($token);
340
+		$session->touch(false);
341
+		return $session;
342
+	}
343
+
344
+	public function touch(bool $update_table = true): void {
345
+		$this->updated = time();
346
+		$secure = str_starts_with(BASE_URL, 'https:');
347
+		$expires = intval(time()) + SESSION_COOKIE_TTL_SECONDS;
348
+		trace('Updating cookie to ' . localized_date_string($expires));
349
+		setcookie(SESSION_COOKIE_NAME, $this->token, $expires, path: '/',
350
+			secure: $secure, httponly: true);
351
+		if ($update_table) {
352
+			$sql = 'UPDATE sessions SET updated=:updated WHERE rowid=:rowid;';
353
+			$args = array(':updated' => $this->updated, ':rowid' => $this->rowid);
354
+			Database::query($sql, $args);
355
+		}
356
+	}
357
+
358
+	public static function get_by_token(string $token): ?Session {
359
+		$sql = 'SELECT rowid, * FROM sessions WHERE token=:token LIMIT 1';
360
+		$args = array(':token' => $token);
361
+		return Database::query_object('Session', $sql, $args);
362
+	}
363
+
364
+	public static function get_by_user_id(int $user_id): array {
365
+		$sql = 'SELECT rowid, * FROM sessions WHERE user_id=:user_id;';
366
+		$args = array(':user_id' => $user_id);
367
+		return Database::query_objects('Session', $sql, $args);
368
+	}
369
+
370
+	public static function delete_all_for_user(int $user_id): void {
371
+		$sql = 'DELETE FROM sessions WHERE user_id=:user_id;';
372
+		$args = array(':user_id' => $user_id);
373
+		Database::query($sql, $args);
374
+	}
375
+
376
+	public function delete(): void {
377
+		$sql = 'DELETE FROM sessions WHERE rowid=:rowid;';
378
+		$args = array(':rowid' => $this->rowid);
379
+		Database::query($sql, $args);
380
+		$secure = str_starts_with(BASE_URL, 'https:');
381
+		setcookie(SESSION_COOKIE_NAME, '', 1, path: '/', secure: $secure, httponly: true);
382
+		unset($_COOKIE[SESSION_COOKIE_NAME]);
281 383
 	}
282 384
 }
283 385
 
@@ -484,7 +586,7 @@ function check_setup(): void {
484 586
 /// message.
485 587
 function fatal_error(string $message): never {
486 588
 	if ($_SERVER['REQUEST_METHOD'] != 'GET') {
487
-		$_SESSION['error'] = $message;
589
+		$_SESSION[SESSION_KEY_ERROR] = $message;
488 590
 		HTMLPage::redirect_home();
489 591
 	}
490 592
 	HTMLPage::render_page_start();
@@ -622,9 +724,9 @@ class HTMLPage {
622 724
 	}
623 725
 
624 726
 	public static function render_error_if_needed(): void {
625
-		if (array_key_exists('error', $_SESSION)) {
626
-			$e = $_SESSION['error'];
627
-			unset($_SESSION['error']);
727
+		if (array_key_exists(SESSION_KEY_ERROR, $_SESSION)) {
728
+			$e = $_SESSION[SESSION_KEY_ERROR];
729
+			unset($_SESSION[SESSION_KEY_ERROR]);
628 730
 			self::render_error($e);
629 731
 		}
630 732
 	}
@@ -634,9 +736,11 @@ class HTMLPage {
634 736
 	}
635 737
 
636 738
 	public static function render_post_form(): void {
739
+		$body = array_key_exists(SESSION_KEY_POST_BODY, $_SESSION) ? $_SESSION[SESSION_KEY_POST_BODY] : '';
740
+		unset($_SESSION[SESSION_KEY_POST_BODY]);
637 741
 		print(<<<HTML
638 742
 			<form id="post-form" method="POST">
639
-				<div class="text-container"><textarea name="body" placeholder="Your journal post here…"></textarea></div>
743
+				<div class="text-container"><textarea name="body" placeholder="Your journal post here…">$body</textarea></div>
640 744
 				<input type="hidden" name="action" value="post" />
641 745
 				<div class="submit-post"><input type="submit" value="Post" /></div>
642 746
 			</form>
@@ -751,6 +855,7 @@ class HTMLPage {
751 855
 // -- Main logic ------------------------------------------
752 856
 
753 857
 check_setup();
858
+Session::check_cookie();
754 859
 User::update_current();
755 860
 switch ($_SERVER['REQUEST_METHOD']) {
756 861
 	case 'GET':
@@ -778,6 +883,11 @@ switch ($_SERVER['REQUEST_METHOD']) {
778 883
 				$body = validate($_POST, 'body', $nonempty_str_type);
779 884
 				$author = User::$current->user_id;
780 885
 				$created = time();
886
+				if (!User::$current) {
887
+					// Not logged in. Save body for populating once they sign in.
888
+					$_SESSION[SESSION_KEY_POST_BODY] = $body;
889
+					fatal_error('Please sign in to post.');
890
+				}
781 891
 				Post::create($body, $author, $created);
782 892
 				break;
783 893
 			case 'createaccount':

二進制
journal.db 查看文件


+ 14
- 1
source/create-tables.sql 查看文件

@@ -7,6 +7,8 @@ CREATE TABLE config (
7 7
 	name TEXT UNIQUE,  -- variable name
8 8
 	value ANY          -- variable value
9 9
 );
10
+CREATE UNIQUE INDEX IF NOT EXISTS idx_config_name ON config (name);
11
+INSERT INTO config (name, value) VALUES ('schema_version', '20230107');
10 12
 
11 13
 DROP TABLE IF EXISTS users;
12 14
 CREATE TABLE users (
@@ -15,6 +17,7 @@ CREATE TABLE users (
15 17
 	password_hash TEXT,                          -- output of password_hash()
16 18
 	timezone TEXT DEFAULT 'America/Los_Angeles'  -- for date formatting
17 19
 );
20
+CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users (username COLLATE NOCASE);
18 21
 
19 22
 DROP TABLE IF EXISTS posts;
20 23
 CREATE TABLE posts (
@@ -23,7 +26,17 @@ CREATE TABLE posts (
23 26
 	author_id INTEGER NOT NULL,   -- user_id of author
24 27
 	FOREIGN KEY (author_id) REFERENCES users (user_id)
25 28
 );
29
+CREATE INDEX IF NOT EXISTS idx_posts_created ON posts (created DESC);
26 30
 
27
-INSERT INTO config (name, value) VALUES ('schema_version', '20230107');
31
+DROP TABLE IF EXISTS sessions;
32
+CREATE TABLE sessions (
33
+	token TEXT UNIQUE NOT NULL,
34
+	user_id INTEGER NOT NULL,
35
+	created INTEGER,
36
+	updated INTEGER,
37
+	FOREIGN KEY (user_id) REFERENCES users (user_id)
38
+);
39
+CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token ON sessions (token);
40
+CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions (user_id);
28 41
 
29 42
 COMMIT;

Loading…
取消
儲存