From 9b6a42de7eca7d8c752e737114ceab3edcd4d8bc Mon Sep 17 00:00:00 2001 From: Greg Sarjeant <1686767+gsarjeant@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:28:28 -0400 Subject: [PATCH] add Session, Util, Controller classes --- public/index.php | 29 ++----- src/Controller/Admin/Admin.php | 4 +- src/Controller/Auth/Auth.php | 21 +++-- src/Controller/Controller.php | 15 ++++ src/Controller/Feed/Feed.php | 6 +- src/Controller/Home/Home.php | 8 +- src/Controller/Mood/Mood.php | 6 +- src/Framework/Session/Session.php | 31 +++++++ src/Framework/Util/Util.php | 121 ++++++++++++++++++++++++++ src/Model/Config/Config.php | 4 +- src/Model/User/User.php | 6 +- src/View/Home/Home.php | 4 +- src/lib/util.php | 139 ------------------------------ templates/home.php | 6 +- 14 files changed, 206 insertions(+), 194 deletions(-) create mode 100644 src/Controller/Controller.php create mode 100644 src/Framework/Session/Session.php create mode 100644 src/Framework/Util/Util.php delete mode 100644 src/lib/util.php diff --git a/public/index.php b/public/index.php index 86194e2..f2b192d 100644 --- a/public/index.php +++ b/public/index.php @@ -1,26 +1,4 @@ render("admin.php", $vars); } // POST handler diff --git a/src/Controller/Auth/Auth.php b/src/Controller/Auth/Auth.php index b61b04b..0bbbfde 100644 --- a/src/Controller/Auth/Auth.php +++ b/src/Controller/Auth/Auth.php @@ -1,8 +1,8 @@ $config, @@ -10,7 +10,7 @@ class AuthController { 'error' => $error, ]; - echo render_template(TEMPLATES_DIR . '/login.php', $vars); + $this->render('login.php', $vars); } function handleLogin(){ @@ -19,24 +19,25 @@ class AuthController { $error = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (!validateCsrfToken($_POST['csrf_token'])) { + if (!Session::validateCsrfToken($_POST['csrf_token'])) { die('Invalid CSRF token'); } - // TODO: move into session.php $username = $_POST['username'] ?? ''; $password = $_POST['password'] ?? ''; - - $db = get_db(); + + // TODO: move into user model + $db = Util::get_db(); $stmt = $db->prepare("SELECT id, username, password_hash FROM user WHERE username = ?"); $stmt->execute([$username]); $user = $stmt->fetch(); if ($user && password_verify($password, $user['password_hash'])) { session_regenerate_id(true); + // TODO: move into session.php $_SESSION['user_id'] = $user['id']; $_SESSION['username'] = $user['username']; - $_SESSION['csrf_token'] = generate_csrf_token(true); + Session::generateCsrfToken(true); header('Location: ' . $config->basePath); exit; } else { @@ -46,9 +47,7 @@ class AuthController { } function handleLogout(){ - $_SESSION = []; - session_destroy(); - + Session::end(); $config = Config::load(); header('Location: ' . $config->basePath); exit; diff --git a/src/Controller/Controller.php b/src/Controller/Controller.php new file mode 100644 index 0000000..8d4af6c --- /dev/null +++ b/src/Controller/Controller.php @@ -0,0 +1,15 @@ +vars); + $this->render("feed/rss.php", $this->vars); } public function atom(){ - echo render_template(TEMPLATES_DIR . "/feed/atom.php", $this->vars); + $this->render("feed/atom.php", $this->vars); } } diff --git a/src/Controller/Home/Home.php b/src/Controller/Home/Home.php index eaecfae..554d7ac 100644 --- a/src/Controller/Home/Home.php +++ b/src/Controller/Home/Home.php @@ -1,5 +1,7 @@ $tickList, ]; - echo render_template(TEMPLATES_DIR . "/home.php", $vars); + $this->render("home.php", $vars); } // POST handler @@ -30,7 +32,7 @@ class HomeController{ public function handleTick(){ if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['tick'])) { // ensure that the session is valid before proceeding - if (!validateCsrfToken($_POST['csrf_token'])) { + if (!Session::validateCsrfToken($_POST['csrf_token'])) { // TODO: maybe redirect to login? Maybe with tick preserved? die('Invalid CSRF token'); } diff --git a/src/Controller/Mood/Mood.php b/src/Controller/Mood/Mood.php index 5838ea2..6352b31 100644 --- a/src/Controller/Mood/Mood.php +++ b/src/Controller/Mood/Mood.php @@ -1,5 +1,5 @@ $moodPicker, ]; - echo render_template(TEMPLATES_DIR . "/mood.php", $vars); + $this->render("mood.php", $vars); } public function handleMood(){ if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['mood'])) { // ensure that the session is valid before proceeding - if (!validateCsrfToken($_POST['csrf_token'])) { + if (!Session::validateCsrfToken($_POST['csrf_token'])) { die('Invalid CSRF token'); } diff --git a/src/Framework/Session/Session.php b/src/Framework/Session/Session.php new file mode 100644 index 0000000..cc7b85e --- /dev/null +++ b/src/Framework/Session/Session.php @@ -0,0 +1,31 @@ +"\'()]+)~i', + fn($matches) => '' . $matches[1] . '', + $safe + ); + + return $safe; + } + + // For relative time display, compare the stored time to the current time + // and display it as "X second/minutes/hours/days etc. "ago + public static function relative_time(string $tickTime): string { + $datetime = new DateTime($tickTime); + $now = new DateTime('now', $datetime->getTimezone()); + $diff = $now->diff($datetime); + + if ($diff->y > 0) { + return $diff->y . ' year' . ($diff->y > 1 ? 's' : '') . ' ago'; + } + if ($diff->m > 0) { + return $diff->m . ' month' . ($diff->m > 1 ? 's' : '') . ' ago'; + } + if ($diff->d > 0) { + return $diff->d . ' day' . ($diff->d > 1 ? 's' : '') . ' ago'; + } + if ($diff->h > 0) { + return $diff->h . ' hour' . ($diff->h > 1 ? 's' : '') . ' ago'; + } + if ($diff->i > 0) { + return $diff->i . ' minute' . ($diff->i > 1 ? 's' : '') . ' ago'; + } + return $diff->s . ' second' . ($diff->s != 1 ? 's' : '') . ' ago'; + } + + public static function verify_data_dir(string $dir, bool $allow_create = false): void { + if (!is_dir($dir)) { + if ($allow_create) { + if (!mkdir($dir, 0770, true)) { + http_response_code(500); + echo "Failed to create required directory: $dir"; + exit; + } + } else { + http_response_code(500); + echo "Required directory does not exist: $dir"; + exit; + } + } + + if (!is_writable($dir)) { + http_response_code(500); + echo "Directory is not writable: $dir"; + exit; + } + } + + // Verify that setup is complete (i.e. the databse is populated). + // Redirect to setup.php if it isn't. + public static function confirm_setup(): void { + $db = Util::get_db(); + + // Ensure required tables exist + $db->exec("CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + display_name TEXT NOT NULL, + password_hash TEXT NOT NULL, + about TEXT NULL, + website TEXT NULL, + mood TEXT NULL + )"); + + $db->exec("CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY, + site_title TEXT NOT NULL, + site_description TEXT NULL, + base_path TEXT NOT NULL, + items_per_page INTEGER NOT NULL + )"); + + // See if there's any data in the tables + $user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); + $settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); + + // If either table has no records and we aren't on setup.php, redirect to setup.php + if ($user_count === 0 || $settings_count === 0){ + if (basename($_SERVER['PHP_SELF']) !== 'setup.php'){ + header('Location: setup.php'); + exit; + } + } else { + // If setup is complete and we are on setup.php, redirect to index.php. + if (basename($_SERVER['PHP_SELF']) === 'setup.php'){ + header('Location: index.php'); + exit; + } + }; + } + + // TODO: Move to model base class? + public static function get_db(): PDO { + Util::verify_data_dir(DATA_DIR, true); + + try { + $db = new PDO("sqlite:" . DB_FILE); + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + } catch (PDOException $e) { + die("Database connection failed: " . $e->getMessage()); + } + + return $db; + } +} \ No newline at end of file diff --git a/src/Model/Config/Config.php b/src/Model/Config/Config.php index 1b37166..9ad9e19 100644 --- a/src/Model/Config/Config.php +++ b/src/Model/Config/Config.php @@ -10,7 +10,7 @@ class Config { // load config from sqlite database public static function load(): self { - $db = get_db(); + $db = Util::get_db(); $stmt = $db->query("SELECT site_title, site_description, base_path, items_per_page FROM settings WHERE id=1"); $row = $stmt->fetch(PDO::FETCH_ASSOC); $c = new self(); @@ -26,7 +26,7 @@ class Config { } public function save(): self { - $db = get_db(); + $db = Util::get_db(); $stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_path=?, items_per_page=? WHERE id=1"); $stmt->execute([$this->siteTitle, $this->siteDescription, $this->basePath, $this->itemsPerPage]); diff --git a/src/Model/User/User.php b/src/Model/User/User.php index 44f8975..59b31e9 100644 --- a/src/Model/User/User.php +++ b/src/Model/User/User.php @@ -9,7 +9,7 @@ class User { // load user settings from sqlite database public static function load(): self { - $db = get_db(); + $db = Util::get_db(); // There's only ever one user. I'm just leaning into that. $stmt = $db->query("SELECT username, display_name, about, website, mood FROM user WHERE id=1"); @@ -28,7 +28,7 @@ class User { } public function save(): self { - $db = get_db(); + $db = Util::get_db(); $stmt = $db->prepare("UPDATE user SET username=?, display_name=?, about=?, website=?, mood=? WHERE id=1"); $stmt->execute([$this->username, $this->displayName, $this->about, $this->website, $this->mood]); @@ -39,7 +39,7 @@ class User { // Making this a separate function to avoid // loading the password into memory public function set_password(string $password): void { - $db = get_db(); + $db = Util::get_db(); $hash = password_hash($password, PASSWORD_DEFAULT); $stmt = $db->prepare("UPDATE user SET password_hash=? WHERE id=1"); diff --git a/src/View/Home/Home.php b/src/View/Home/Home.php index 4ef7694..8bdcfff 100644 --- a/src/View/Home/Home.php +++ b/src/View/Home/Home.php @@ -11,8 +11,8 @@ class HomeView {
-
- +
+
diff --git a/src/lib/util.php b/src/lib/util.php deleted file mode 100644 index beffc38..0000000 --- a/src/lib/util.php +++ /dev/null @@ -1,139 +0,0 @@ -"\'()]+)~i', - fn($matches) => '' . $matches[1] . '', - $safe - ); - - return $safe; -} - -// For relative time display, compare the stored time to the current time -// and display it as "X second/minutes/hours/days etc. "ago -function relative_time(string $tickTime): string { - $datetime = new DateTime($tickTime); - $now = new DateTime('now', $datetime->getTimezone()); - $diff = $now->diff($datetime); - - if ($diff->y > 0) { - return $diff->y . ' year' . ($diff->y > 1 ? 's' : '') . ' ago'; - } - if ($diff->m > 0) { - return $diff->m . ' month' . ($diff->m > 1 ? 's' : '') . ' ago'; - } - if ($diff->d > 0) { - return $diff->d . ' day' . ($diff->d > 1 ? 's' : '') . ' ago'; - } - if ($diff->h > 0) { - return $diff->h . ' hour' . ($diff->h > 1 ? 's' : '') . ' ago'; - } - if ($diff->i > 0) { - return $diff->i . ' minute' . ($diff->i > 1 ? 's' : '') . ' ago'; - } - return $diff->s . ' second' . ($diff->s != 1 ? 's' : '') . ' ago'; -} - -function verify_data_dir(string $dir, bool $allow_create = false): void { - if (!is_dir($dir)) { - if ($allow_create) { - if (!mkdir($dir, 0770, true)) { - http_response_code(500); - echo "Failed to create required directory: $dir"; - exit; - } - } else { - http_response_code(500); - echo "Required directory does not exist: $dir"; - exit; - } - } - - if (!is_writable($dir)) { - http_response_code(500); - echo "Directory is not writable: $dir"; - exit; - } -} - -// Verify that setup is complete (i.e. the databse is populated). -// Redirect to setup.php if it isn't. -function confirm_setup(): void { - $db = get_db(); - - // Ensure required tables exist - $db->exec("CREATE TABLE IF NOT EXISTS user ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL, - display_name TEXT NOT NULL, - password_hash TEXT NOT NULL, - about TEXT NULL, - website TEXT NULL, - mood TEXT NULL - )"); - - $db->exec("CREATE TABLE IF NOT EXISTS settings ( - id INTEGER PRIMARY KEY, - site_title TEXT NOT NULL, - site_description TEXT NULL, - base_path TEXT NOT NULL, - items_per_page INTEGER NOT NULL - )"); - - // See if there's any data in the tables - $user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); - $settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); - - // If either table has no records and we aren't on setup.php, redirect to setup.php - if ($user_count === 0 || $settings_count === 0){ - if (basename($_SERVER['PHP_SELF']) !== 'setup.php'){ - header('Location: setup.php'); - exit; - } - } else { - // If setup is complete and we are on setup.php, redirect to index.php. - if (basename($_SERVER['PHP_SELF']) === 'setup.php'){ - header('Location: index.php'); - exit; - } - }; -} - -function get_db(): PDO { - verify_data_dir(DATA_DIR, true); - - try { - $db = new PDO("sqlite:" . DB_FILE); - $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); - } catch (PDOException $e) { - die("Database connection failed: " . $e->getMessage()); - } - - return $db; -} - -// TODO - Maybe this is the sort of thing that would be good -// in a Controller base class. -function render_template(string $templateFile, array $vars = []): string { - if (!file_exists($templateFile)) { - throw new RuntimeException("Template not found: $templatePath"); - } - - // Extract variables into local scope - extract($vars, EXTR_SKIP); - - // Start output buffering - ob_start(); - - // Include the template (with extracted variables in scope) - include $templateFile; - - // Return rendered output - return ob_get_clean(); -} \ No newline at end of file diff --git a/templates/home.php b/templates/home.php index a785527..fca3a70 100644 --- a/templates/home.php +++ b/templates/home.php @@ -13,8 +13,8 @@
- rss - atom + rss + atom login @@ -28,7 +28,7 @@

Hi, I'm displayName ?>

about ?>

-

Website: website) ?>

+

Website: website) ?>

Current mood: mood ?>