From 3fbeaf87e38cf754438785693e69e6851f9ccef1 Mon Sep 17 00:00:00 2001 From: Greg Sarjeant <1686767+gsarjeant@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:47:03 -0400 Subject: [PATCH] Refactor bootstrap. Use global database. --- config/bootstrap.php | 208 ++++++++++++++++++ public/index.php | 42 ++-- .../AuthController/AuthController.php | 3 +- .../CssController/CssController.php | 3 - src/Framework/Util/Util.php | 85 ------- src/Model/ConfigModel/ConfigModel.php | 4 +- src/Model/CssModel/CssModel.php | 12 +- src/Model/UserModel/UserModel.php | 6 +- 8 files changed, 234 insertions(+), 129 deletions(-) create mode 100644 config/bootstrap.php diff --git a/config/bootstrap.php b/config/bootstrap.php new file mode 100644 index 0000000..47a27d5 --- /dev/null +++ b/config/bootstrap.php @@ -0,0 +1,208 @@ +setupIssue = $setupIssue; + } + + public function getSetupIssue(): string { + return $this->setupIssue; + } +} + +// Main validation function +// Any failures will throw a SetupException +function confirm_setup(): void { + validate_storage_dir(); + validate_storage_subdirs(); + validate_tables(); + validate_table_contents(); +} + +// Make sure the storage/ directory exists and is writable +function validate_storage_dir(): void{ + if (!is_dir(STORAGE_DIR)) { + throw new SetupException( + STORAGE_DIR . "does not exist. Please check your installation.", + 'storage_missing' + ); + } + + if (!is_writable(STORAGE_DIR)) { + throw new SetupException( + STORAGE_DIR . "is not writable. Exiting.", + 'storage_permissions' + ); + } +} + +function validate_storage_subdirs(): void { + $storageSubdirs = array(); + $storageSubdirs[] = CSS_UPLOAD_DIR; + $storageSubdirs[] = DATA_DIR; + $storageSubdirs[] = TICKS_DIR; + + foreach($storageSubdirs as $storageSubdir){ + if (!is_dir($storageSubdir)) { + if (!mkdir($dir, 0770, true)) { + throw new SetupException( + "Failed to create required directory: $dir", + 'directory_creation' + ); + } + } + + if (!is_writable($storageSubdir)) { + if (!chmod($storageSubdir, 0770)) { + throw new SetupException( + "Required directory is not writable: $dir", + 'directory_permissions' + ); + } + } + } +} + +// Verify that the requested directory exists +// and optionally create it if it doesn't. + +function get_db(): PDO { + try { + // SQLite will just create this if it doesn't exist. + $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) { + throw new SetupException( + "Database connection failed: " . $e->getMessage(), + 'database_connection', + 0, + $e + ); + } + + return $db; +} + +function create_tables(): void { + $db = get_db(); + + try { + // user table + $db->exec("CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + display_name TEXT NOT NULL, + password_hash TEXT NULL, + about TEXT NULL, + website TEXT NULL, + mood TEXT NULL + )"); + + // settings table + $db->exec("CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY, + site_title TEXT NOT NULL, + site_description TEXT NULL, + base_url TEXT NOT NULL, + base_path TEXT NOT NULL, + items_per_page INTEGER NOT NULL, + css_id INTEGER NULL + )"); + + // css table + $db->exec("CREATE TABLE IF NOT EXISTS css ( + id INTEGER PRIMARY KEY, + filename TEXT NOT NULL, + description TEXT NULL + )"); + + // mood table + $db->exec("CREATE TABLE IF NOT EXISTS mood ( + id INTEGER PRIMARY KEY, + emoji TEXT NOT NULL, + description TEXT NOT NULL + )"); + } catch (PDOException $e) { + throw new SetupException( + "Table creation failed: " . $e->getMessage(), + 'table_creation', + 0, + $e + ); + } +} + +function validate_tables(): void { + $appTables = array(); + $appTables[] = "settings"; + $appTables[] = "user"; + $appTables[] = "css"; + $appTables[] = "mood"; + + $db = get_db(); + + foreach ($appTables as $appTable){ + $stmt = $db->prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?"); + $stmt->execute([$appTable]); + if (!$stmt->fetch()){ + // At least one table doesn't exist. + // Try creating tables (hacky, but I have 4 total tables) + // Will throw an exception if it fails + create_tables(); + } + } +} + +function validate_table_contents(): void { + $db = get_db(); + + // make sure required tables (user, settings) are populated + $user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); + $settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); + + // If either required table has no records and we aren't on /admin, + // redirect to /admin to complete setup + if ($user_count === 0 || $settings_count === 0){ + throw new SetupException( + "Required tables aren't populated. Please complete setup", + 'table_contents', + ); + }; +} + +// Load all classes from the src/ directory +function load_classes(): void { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator(SRC_DIR) + ); + + // load base classes first + require_once SRC_DIR . '/Controller/Controller.php'; + + // load everything else + foreach ($iterator as $file) { + if ($file->isFile() && fnmatch('*.php', $file->getFilename())) { + require_once $file; + } + } +} diff --git a/public/index.php b/public/index.php index 03ea1af..9c55648 100644 --- a/public/index.php +++ b/public/index.php @@ -11,48 +11,32 @@ if (preg_match('/\.php$/', $path)) { exit; } -define('APP_ROOT', dirname(dirname(__FILE__))); +// Define base paths and load classes +include_once(dirname(dirname(__FILE__)) . "/config/bootstrap.php"); +load_classes(); -// Define all the important paths -define('SRC_DIR', APP_ROOT . '/src'); -define('STORAGE_DIR', APP_ROOT . '/storage'); -define('TEMPLATES_DIR', APP_ROOT . '/templates'); -define('TICKS_DIR', STORAGE_DIR . '/ticks'); -define('DATA_DIR', STORAGE_DIR . '/db'); -define('CSS_UPLOAD_DIR', STORAGE_DIR . '/upload/css'); -define('DB_FILE', DATA_DIR . '/tkr.sqlite'); - -// Load all classes from the src/ directory -function loadClasses(): void { - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator(SRC_DIR) - ); - - // load base classes first - require_once SRC_DIR . '/Controller/Controller.php'; - - // load everything else - foreach ($iterator as $file) { - if ($file->isFile() && fnmatch('*.php', $file->getFilename())) { - require_once $file; - } - } +// Make sure the initial setup is complete +try { + confirm_setup(); +} catch (SetupException $e) { + // TODO - pass to exception handler (maybe also defined in bootstrap to keep this smaller) + echo $e->getMessage(); + exit; } -loadClasses(); - // Everything's loaded. Now we can start ticking. -Util::confirm_setup(); +global $db; +$db = get_db(); $config = ConfigModel::load(); Session::start(); Session::generateCsrfToken(); // Remove the base path from the URL -// and strip the trailing slash from the resulting route if (strpos($path, $config->basePath) === 0) { $path = substr($path, strlen($config->basePath)); } +// strip the trailing slash from the resulting route $path = trim($path, '/'); // Main router function diff --git a/src/Controller/AuthController/AuthController.php b/src/Controller/AuthController/AuthController.php index 445676a..c2894ff 100644 --- a/src/Controller/AuthController/AuthController.php +++ b/src/Controller/AuthController/AuthController.php @@ -27,7 +27,8 @@ class AuthController extends Controller { $password = $_POST['password'] ?? ''; // TODO: move into user model - $db = Util::get_db(); + global $db; + //$db = Util::get_db(); $stmt = $db->prepare("SELECT id, username, password_hash FROM user WHERE username = ?"); $stmt->execute([$username]); $user = $stmt->fetch(); diff --git a/src/Controller/CssController/CssController.php b/src/Controller/CssController/CssController.php index 7c4fa97..4d597de 100644 --- a/src/Controller/CssController/CssController.php +++ b/src/Controller/CssController/CssController.php @@ -163,9 +163,6 @@ class CssController extends Controller { // Scan for malicious content $this->scanForMaliciousContent($fileContent, $filename); - // Create upload directory if it doesn't exist - Util::verify_storage_dir(CSS_UPLOAD_DIR, true); - // Generate safe filename $safeFilename = $this->generateSafeFileName($filename); $uploadPath = CSS_UPLOAD_DIR . '/' . $safeFilename; diff --git a/src/Framework/Util/Util.php b/src/Framework/Util/Util.php index c356cc2..18751dc 100644 --- a/src/Framework/Util/Util.php +++ b/src/Framework/Util/Util.php @@ -39,77 +39,6 @@ class Util { return $diff->s . ' second' . ($diff->s != 1 ? 's' : '') . ' ago'; } - public static function verify_storage_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(); - - // user table - $db->exec("CREATE TABLE IF NOT EXISTS user ( - id INTEGER PRIMARY KEY, - username TEXT NOT NULL, - display_name TEXT NOT NULL, - password_hash TEXT NULL, - about TEXT NULL, - website TEXT NULL, - mood TEXT NULL - )"); - - // settings table - $db->exec("CREATE TABLE IF NOT EXISTS settings ( - id INTEGER PRIMARY KEY, - site_title TEXT NOT NULL, - site_description TEXT NULL, - base_url TEXT NOT NULL, - base_path TEXT NOT NULL, - items_per_page INTEGER NOT NULL, - css_id INTEGER NULL - )"); - - // css table - $db->exec("CREATE TABLE IF NOT EXISTS css ( - id INTEGER PRIMARY KEY, - filename TEXT NOT NULL, - description TEXT 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(); - $config = ConfigModel::load(); - - // If either table has no records and we aren't on /admin, - // redirect to /admin to complete setup - if ($user_count === 0 || $settings_count === 0){ - if (basename($_SERVER['PHP_SELF']) !== 'admin'){ - header('Location: ' . $config->basePath . 'admin'); - exit; - } - }; - } - public static function tick_time_to_tick_path($tickTime){ [$date, $time] = explode(' ', $tickTime); $dateParts = explode('-', $date); @@ -120,18 +49,4 @@ class Util { return "$year/$month/$day/$hour/$minute/$second"; } - - public static function get_db(): PDO { - Util::verify_storage_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/ConfigModel/ConfigModel.php b/src/Model/ConfigModel/ConfigModel.php index b7ae943..8d08457 100644 --- a/src/Model/ConfigModel/ConfigModel.php +++ b/src/Model/ConfigModel/ConfigModel.php @@ -24,7 +24,7 @@ class ConfigModel { $c->baseUrl = ($c->baseUrl === '') ? $init['base_url'] : $c->baseUrl; $c->basePath = ($c->basePath === '') ? $init['base_path'] : $c->basePath; - $db = Util::get_db(); + global $db; $stmt = $db->query("SELECT site_title, site_description, base_url, base_path, items_per_page, css_id FROM settings WHERE id=1"); $row = $stmt->fetch(PDO::FETCH_ASSOC); @@ -53,7 +53,7 @@ class ConfigModel { } public function save(): self { - $db = Util::get_db(); + global $db; if (!ConfigModel::isFirstSetup()){ $stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_url=?, base_path=?, items_per_page=?, css_id=? WHERE id=1"); diff --git a/src/Model/CssModel/CssModel.php b/src/Model/CssModel/CssModel.php index 12d495e..b801eb6 100644 --- a/src/Model/CssModel/CssModel.php +++ b/src/Model/CssModel/CssModel.php @@ -1,7 +1,7 @@ prepare("SELECT id, filename, description FROM css ORDER BY filename"); $stmt->execute(); @@ -9,31 +9,31 @@ class CssModel { } public function getById(int $id): Array{ - $db = Util::get_db(); + global $db; $stmt = $db->prepare("SELECT id, filename, description FROM css WHERE id=?"); $stmt->execute([$id]); return $stmt->fetch(PDO::FETCH_ASSOC); } public function getByFilename(string $filename): Array{ - $db = Util::get_db(); + global $db; $stmt = $db->prepare("SELECT id, filename, description FROM css WHERE filename=?"); $stmt->execute([$filename]); return $stmt->fetch(PDO::FETCH_ASSOC); } public function delete(int $id): bool{ - $db = Util::get_db(); + global $db; $stmt = $db->prepare("DELETE FROM css WHERE id=?"); return $stmt->execute([$id]); } public function save(string $filename, ?string $description = null): void { - $db = Util::get_db(); + global $db; $stmt = $db->prepare("SELECT COUNT(id) FROM css WHERE filename = ?"); $stmt->execute([$filename]); - $fileExists = $stmt->fetchColumn(); + $fileExists = $stmt->fetch(); if ($fileExists) { $stmt = $db->prepare("UPDATE css SET description = ? WHERE filename = ?"); diff --git a/src/Model/UserModel/UserModel.php b/src/Model/UserModel/UserModel.php index 480312a..d5bdf29 100644 --- a/src/Model/UserModel/UserModel.php +++ b/src/Model/UserModel/UserModel.php @@ -9,7 +9,7 @@ class UserModel { // load user settings from sqlite database public static function load(): self { - $db = Util::get_db(); + global $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 UserModel { } public function save(): self { - $db = Util::get_db(); + global $db; if (!ConfigModel::isFirstSetup()){ $stmt = $db->prepare("UPDATE user SET username=?, display_name=?, about=?, website=?, mood=? WHERE id=1"); @@ -44,7 +44,7 @@ class UserModel { // Making this a separate function to avoid // loading the password into memory public function set_password(string $password): void { - $db = Util::get_db(); + global $db; $hash = password_hash($password, PASSWORD_DEFAULT); $stmt = $db->prepare("UPDATE user SET password_hash=? WHERE id=1");