|
|
@@ -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':
|