Refactor bootstrap. Use global database.

This commit is contained in:
Greg Sarjeant 2025-06-12 16:47:03 -04:00
parent eb025c8d3c
commit 3fbeaf87e3
8 changed files with 234 additions and 129 deletions

208
config/bootstrap.php Normal file
View File

@ -0,0 +1,208 @@
<?php
// This is the initialization code that needs to be run before anything else.
// - define paths
// - confirm /storage directory exists and is writable
// - make sure database is ready
// - load classes
// Define all the important paths
define('APP_ROOT', dirname(dirname(__FILE__)));
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');
// Define an exception for validation errors
class SetupException extends Exception {
private $setupIssue;
public function __construct(string $message, string $setupIssue = '', int $code = 0, Throwable $previous = null) {
parent::__construct($message, $code, $previous);
$this->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;
}
}
}

View File

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

View File

@ -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();

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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");

View File

@ -1,7 +1,7 @@
<?php
class CssModel {
public static function load(): Array {
$db = Util::get_db();
global $db;
$stmt = $db->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 = ?");

View File

@ -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");