Refactor bootstrap. Use global database.
This commit is contained in:
		
							parent
							
								
									eb025c8d3c
								
							
						
					
					
						commit
						3fbeaf87e3
					
				
							
								
								
									
										208
									
								
								config/bootstrap.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								config/bootstrap.php
									
									
									
									
									
										Normal 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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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
 | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
| @ -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"); | ||||
|  | ||||
| @ -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 = ?"); | ||||
|  | ||||
| @ -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"); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user