simplify bootstrap. move validation to classes.
This commit is contained in:
		
							parent
							
								
									cdc1153af4
								
							
						
					
					
						commit
						5998e7f7d3
					
				| @ -1,9 +1,7 @@ | ||||
| <?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
 | ||||
| // - set up autoloader
 | ||||
| 
 | ||||
| // Define all the important paths
 | ||||
| define('APP_ROOT', dirname(dirname(__FILE__))); | ||||
| @ -16,63 +14,17 @@ 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; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Exception handler
 | ||||
| function handle_setup_exception(SetupException $e){ | ||||
|     switch ($e->getSetupIssue()){ | ||||
|         case 'storage_missing': | ||||
|         case 'storage_permissions': | ||||
|         case 'directory_creation': | ||||
|         case 'directory_permissions': | ||||
|         case 'database_connection': | ||||
|         case 'load_classes': | ||||
|         case 'table_creation': | ||||
|             // Unrecoverable errors.
 | ||||
|             // Show error message and exit
 | ||||
|             http_response_code(500); | ||||
|             echo "<h1>Configuration Error</h1>"; | ||||
|             echo "<p>" . Util::escape_html($e->getSetupIssue) . '-' . Util::escape_html($e->getMessage()) . "</p>"; | ||||
|             exit; | ||||
|         case 'table_contents': | ||||
|             // Recoverable error.
 | ||||
|             // Redirect to setup if we aren't already headed there.
 | ||||
|             // NOTE: Just read directly from init.php instead of
 | ||||
|             //       trying to use the config object. This is the initial
 | ||||
|             //       setup. It shouldn't assume anything can be loaded.
 | ||||
|             $init = require APP_ROOT . '/config/init.php'; | ||||
|             $currentPath = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'); | ||||
| 
 | ||||
|             if (strpos($currentPath, 'setup') === false) { | ||||
|                 header('Location: ' . $init['base_path'] . 'setup'); | ||||
|                 exit; | ||||
|             } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Janky autoloader function
 | ||||
| // This is a bit more consistent with current frameworks
 | ||||
| function autoloader($className) { | ||||
|     $classFilename = $className . '.php'; | ||||
| 
 | ||||
|     $iterator = new RecursiveIteratorIterator( | ||||
|     $files = new RecursiveIteratorIterator( | ||||
|         new RecursiveDirectoryIterator(SRC_DIR, RecursiveDirectoryIterator::SKIP_DOTS), | ||||
|         RecursiveIteratorIterator::LEAVES_ONLY | ||||
|     ); | ||||
| 
 | ||||
|     foreach ($iterator as $file) { | ||||
|     foreach ($files as $file) { | ||||
|         if ($file->getFilename() === $classFilename) { | ||||
|             include_once $file->getPathname(); | ||||
|             return; | ||||
| @ -87,287 +39,3 @@ function autoloader($className) { | ||||
| 
 | ||||
| // Register the autoloader
 | ||||
| spl_autoload_register('autoloader'); | ||||
| 
 | ||||
| // Main validation function
 | ||||
| // Any failures will throw a SetupException
 | ||||
| function confirm_setup(): void { | ||||
|     validate_storage_dir(); | ||||
|     validate_storage_subdirs(); | ||||
|     validate_tables(); | ||||
|     validate_table_contents(); | ||||
|     migrate_db(); | ||||
|     migrate_tick_files(); | ||||
| } | ||||
| 
 | ||||
| // 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' | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // validate that the required storage subdirectories exist
 | ||||
| // attempt to create them if they don't
 | ||||
| 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($storageSubdir, 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' | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function migrate_tick_files() { | ||||
|     $files = new RecursiveIteratorIterator( | ||||
|         new RecursiveDirectoryIterator(TICKS_DIR, RecursiveDirectoryIterator::SKIP_DOTS) | ||||
|     ); | ||||
| 
 | ||||
|     foreach ($files as $file) { | ||||
|         if ($file->isFile() && str_ends_with($file->getFilename(), '.txt')) { | ||||
|             migrate_tick_file($file->getPathname()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function migrate_tick_file($filepath) { | ||||
|     $lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); | ||||
|     $modified = false; | ||||
| 
 | ||||
|     foreach ($lines as &$line) { | ||||
|         $fields = explode('|', $line); | ||||
|         if (count($fields) === 2) { | ||||
|             // Convert id|text to id|emoji|text
 | ||||
|             $line = $fields[0] . '||' . $fields[1]; | ||||
|             $modified = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if ($modified) { | ||||
|         file_put_contents($filepath, implode("\n", $lines) . "\n"); | ||||
|         // TODO: log properly
 | ||||
|         //echo "Migrated: " . basename($filepath) . "\n";
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
| } | ||||
| 
 | ||||
| // The database version will just be an int
 | ||||
| // stored as PRAGMA user_version. It will
 | ||||
| // correspond to the most recent migration file applied to the db.
 | ||||
| function get_db_version(): int { | ||||
|     $db = get_db(); | ||||
| 
 | ||||
|     return $db->query("PRAGMA user_version")->fetchColumn() ?? 0; | ||||
| } | ||||
| 
 | ||||
| function migration_number_from_file(string $filename): int { | ||||
|     $basename = basename($filename, '.sql'); | ||||
|     $parts = explode('_', $basename); | ||||
|     return (int) $parts[0]; | ||||
| } | ||||
| 
 | ||||
| function set_db_version(int $newVersion): void { | ||||
|     $currentVersion = get_db_version(); | ||||
| 
 | ||||
|     if ($newVersion <= $currentVersion){ | ||||
|         throw new SetupException( | ||||
|             "New version ($newVersion) must be greater than current version ($currentVersion)", | ||||
|             'db_migration' | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     $db = get_db(); | ||||
|     $db->exec("PRAGMA user_version = $newVersion"); | ||||
| } | ||||
| 
 | ||||
| function get_pending_migrations(): array { | ||||
|     $currentVersion = get_db_version(); | ||||
|     $files = glob(CONFIG_DIR . '/migrations/*.sql'); | ||||
| 
 | ||||
|     $pending = []; | ||||
|     foreach ($files as $file) { | ||||
|         $version = migration_number_from_file($file); | ||||
|         if ($version > $currentVersion) { | ||||
|             $pending[$version] = $file; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ksort($pending); | ||||
|     return $pending; | ||||
| } | ||||
| 
 | ||||
| function migrate_db(): void { | ||||
|     $migrations = get_pending_migrations(); | ||||
| 
 | ||||
|     if (empty($migrations)) { | ||||
|         # TODO: log
 | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     $db = get_db(); | ||||
|     $db->beginTransaction(); | ||||
| 
 | ||||
|     try { | ||||
|         foreach ($migrations as $version => $file) { | ||||
|             $filename = basename($file); | ||||
|             // TODO: log properly
 | ||||
| 
 | ||||
|             $sql = file_get_contents($file); | ||||
|             if ($sql === false) { | ||||
|                 throw new Exception("Could not read migration file: $file"); | ||||
|             } | ||||
| 
 | ||||
|             // Execute the migration SQL
 | ||||
|             $db->exec($sql); | ||||
|         } | ||||
| 
 | ||||
|         // Update db version
 | ||||
|         $db->commit(); | ||||
|         set_db_version($version); | ||||
|         //TODO: log properly
 | ||||
|         //echo "All migrations completed successfully.\n";
 | ||||
| 
 | ||||
|     } catch (Exception $e) { | ||||
|         $db->rollBack(); | ||||
|         throw new SetupException( | ||||
|             "Migration failed: $filename", | ||||
|             'db_migration', | ||||
|             0, | ||||
|             $e | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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 UNIQUE NOT NULL, | ||||
|             description TEXT NULL | ||||
|         )");
 | ||||
| 
 | ||||
|         // mood table
 | ||||
|         $db->exec("CREATE TABLE IF NOT EXISTS emoji(
 | ||||
|             id INTEGER PRIMARY KEY, | ||||
|             emoji TEXT UNIQUE NOT NULL, | ||||
|             description TEXT NOT NULL | ||||
|         )");
 | ||||
|     } catch (PDOException $e) { | ||||
|         throw new SetupException( | ||||
|             "Table creation failed: " . $e->getMessage(), | ||||
|             'table_creation', | ||||
|             0, | ||||
|             $e | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // make sure all tables exist
 | ||||
| // attempt to create them if they don't
 | ||||
| function validate_tables(): void { | ||||
|     $appTables = array(); | ||||
|     $appTables[] = "settings"; | ||||
|     $appTables[] = "user"; | ||||
|     $appTables[] = "css"; | ||||
|     $appTables[] = "emoji"; | ||||
| 
 | ||||
|     $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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // make sure tables that need to be seeded have been
 | ||||
| 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', | ||||
|         ); | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @ -13,25 +13,27 @@ if (preg_match('/\.php$/', $path)) { | ||||
| 
 | ||||
| // Define base paths and load classes
 | ||||
| include_once(dirname(dirname(__FILE__)) . "/config/bootstrap.php"); | ||||
| //load_classes();
 | ||||
| 
 | ||||
| // Make sure the initial setup is complete
 | ||||
| // unless we're already heading to setup
 | ||||
| if (!(preg_match('/setup$/', $path))) { | ||||
|     try { | ||||
|         confirm_setup(); | ||||
|         // filesystem validation
 | ||||
|         $fsMgr = new Filesystem(); | ||||
|         $fsMgr->validate(); | ||||
| 
 | ||||
|         // database validation
 | ||||
|         $dbMgr = new Database(); | ||||
|         $dbMgr->validate(); | ||||
|     } catch (SetupException $e) { | ||||
|         handle_setup_exception($e); | ||||
|         $e->handle(); | ||||
|         exit; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // initialize the database
 | ||||
| global $db; | ||||
| $db = get_db(); | ||||
| 
 | ||||
| // Everything's loaded and setup is confirmed.
 | ||||
| // Let's start ticking.
 | ||||
| $db = Database::get(); | ||||
| 
 | ||||
| // Initialize core entities
 | ||||
| // Defining these as globals isn't great practice,
 | ||||
|  | ||||
							
								
								
									
										203
									
								
								src/Framework/Database/Database.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/Framework/Database/Database.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,203 @@ | ||||
| <?php | ||||
| class Database{ | ||||
|     public static function get(): 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; | ||||
|     } | ||||
| 
 | ||||
|     public function validate(): void{ | ||||
|         $this->validateTables(); | ||||
|         $this->validateTableContents(); | ||||
|         $this->migrate(); | ||||
|     } | ||||
| 
 | ||||
|     // The database version will just be an int
 | ||||
|     // stored as PRAGMA user_version. It will
 | ||||
|     // correspond to the most recent migration file applied to the db.
 | ||||
|     private function getVersion(): int { | ||||
|         $db = self::get(); | ||||
| 
 | ||||
|         return $db->query("PRAGMA user_version")->fetchColumn() ?? 0; | ||||
|     } | ||||
| 
 | ||||
|     private function migrationNumberFromFile(string $filename): int { | ||||
|         $basename = basename($filename, '.sql'); | ||||
|         $parts = explode('_', $basename); | ||||
|         return (int) $parts[0]; | ||||
|     } | ||||
| 
 | ||||
|     private function setVersion(int $newVersion): void { | ||||
|         $currentVersion = $this->getVersion(); | ||||
| 
 | ||||
|         if ($newVersion <= $currentVersion){ | ||||
|             throw new SetupException( | ||||
|                 "New version ($newVersion) must be greater than current version ($currentVersion)", | ||||
|                 'db_migration' | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         $db = self::get(); | ||||
|         $db->exec("PRAGMA user_version = $newVersion"); | ||||
|     } | ||||
| 
 | ||||
|     private function getPendingMigrations(): array { | ||||
|         $currentVersion = $this->getVersion(); | ||||
|         $files = glob(CONFIG_DIR . '/migrations/*.sql'); | ||||
| 
 | ||||
|         $pending = []; | ||||
|         foreach ($files as $file) { | ||||
|             $version = $this->migrationNumberFromFile($file); | ||||
|             if ($version > $currentVersion) { | ||||
|                 $pending[$version] = $file; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         ksort($pending); | ||||
|         return $pending; | ||||
|     } | ||||
| 
 | ||||
|     private function migrate(): void { | ||||
|         $migrations = $this->getPendingMigrations(); | ||||
| 
 | ||||
|         if (empty($migrations)) { | ||||
|             # TODO: log
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         $db = get_db(); | ||||
|         $db->beginTransaction(); | ||||
| 
 | ||||
|         try { | ||||
|             foreach ($migrations as $version => $file) { | ||||
|                 $filename = basename($file); | ||||
|                 // TODO: log properly
 | ||||
| 
 | ||||
|                 $sql = file_get_contents($file); | ||||
|                 if ($sql === false) { | ||||
|                     throw new Exception("Could not read migration file: $file"); | ||||
|                 } | ||||
| 
 | ||||
|                 // Execute the migration SQL
 | ||||
|                 $db->exec($sql); | ||||
|             } | ||||
| 
 | ||||
|             // Update db version
 | ||||
|             $db->commit(); | ||||
|             $this->setVersion($version); | ||||
|             //TODO: log properly
 | ||||
|             //echo "All migrations completed successfully.\n";
 | ||||
| 
 | ||||
|         } catch (Exception $e) { | ||||
|             $db->rollBack(); | ||||
|             throw new SetupException( | ||||
|                 "Migration failed: $filename", | ||||
|                 'db_migration', | ||||
|                 0, | ||||
|                 $e | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private function createTables(): void { | ||||
|         $db = self::get(); | ||||
| 
 | ||||
|         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 UNIQUE NOT NULL, | ||||
|                 description TEXT NULL | ||||
|             )");
 | ||||
| 
 | ||||
|             // mood table
 | ||||
|             $db->exec("CREATE TABLE IF NOT EXISTS emoji(
 | ||||
|                 id INTEGER PRIMARY KEY, | ||||
|                 emoji TEXT UNIQUE NOT NULL, | ||||
|                 description TEXT NOT NULL | ||||
|             )");
 | ||||
|         } catch (PDOException $e) { | ||||
|             throw new SetupException( | ||||
|                 "Table creation failed: " . $e->getMessage(), | ||||
|                 'table_creation', | ||||
|                 0, | ||||
|                 $e | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // make sure all tables exist
 | ||||
|     // attempt to create them if they don't
 | ||||
|     private function validateTables(): void { | ||||
|         $appTables = array(); | ||||
|         $appTables[] = "settings"; | ||||
|         $appTables[] = "user"; | ||||
|         $appTables[] = "css"; | ||||
|         $appTables[] = "emoji"; | ||||
| 
 | ||||
|         $db = self::get(); | ||||
| 
 | ||||
|         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
 | ||||
|                 $this->createTables(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // make sure tables that need to be seeded have been
 | ||||
|     private function validateTableContents(): void { | ||||
|         $db = self::get(); | ||||
| 
 | ||||
|         // 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', | ||||
|             ); | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/Framework/Exception/SetupException.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/Framework/Exception/SetupException.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| <?php | ||||
| // 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; | ||||
|     } | ||||
| 
 | ||||
|     // Exception handler
 | ||||
|     // Exceptions don't generally define their own handlers,
 | ||||
|     // but this is a very specific case.
 | ||||
|     public function handle(){ | ||||
|         switch ($this->setupIssue()){ | ||||
|             case 'storage_missing': | ||||
|             case 'storage_permissions': | ||||
|             case 'directory_creation': | ||||
|             case 'directory_permissions': | ||||
|             case 'database_connection': | ||||
|             case 'load_classes': | ||||
|             case 'table_creation': | ||||
|                 // Unrecoverable errors.
 | ||||
|                 // Show error message and exit
 | ||||
|                 http_response_code(500); | ||||
|                 echo "<h1>Configuration Error</h1>"; | ||||
|                 echo "<p>" . Util::escape_html($this->setupIssue) . '-' . Util::escape_html($this->getMessage()) . "</p>"; | ||||
|                 exit; | ||||
|             case 'table_contents': | ||||
|                 // Recoverable error.
 | ||||
|                 // Redirect to setup if we aren't already headed there.
 | ||||
|                 // NOTE: Just read directly from init.php instead of
 | ||||
|                 //       trying to use the config object. This is the initial
 | ||||
|                 //       setup. It shouldn't assume anything can be loaded.
 | ||||
|                 $init = require APP_ROOT . '/config/init.php'; | ||||
|                 $currentPath = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'); | ||||
| 
 | ||||
|                 if (strpos($currentPath, 'setup') === false) { | ||||
|                     header('Location: ' . $init['base_path'] . 'setup'); | ||||
|                     exit; | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										91
									
								
								src/Framework/Filesystem/Filesystem.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/Framework/Filesystem/Filesystem.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | ||||
| <?php | ||||
| // Validates that required directories exists
 | ||||
| // and files have correct formats
 | ||||
| class Filesystem { | ||||
|     public function validate(): void{ | ||||
|         $this->validateStorageDir(); | ||||
|         $this->validateStorageSubdirs(); | ||||
|         $this->migrateTickFiles(); | ||||
|     } | ||||
| 
 | ||||
|     // Make sure the storage/ directory exists and is writable
 | ||||
|     private function validateStorageDir(): 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' | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // validate that the required storage subdirectories exist
 | ||||
|     // attempt to create them if they don't
 | ||||
|     private function validateStorageSubdirs(): void { | ||||
|         $storageSubdirs = array(); | ||||
|         $storageSubdirs[] = CSS_UPLOAD_DIR; | ||||
|         $storageSubdirs[] = DATA_DIR; | ||||
|         $storageSubdirs[] = TICKS_DIR; | ||||
| 
 | ||||
|         foreach($storageSubdirs as $storageSubdir){ | ||||
|             if (!is_dir($storageSubdir)) { | ||||
|                 if (!mkdir($storageSubdir, 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' | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // TODO: Delete this sometime before 1.0
 | ||||
|     // Add mood to tick files
 | ||||
|     private function migrateTickFiles(): void { | ||||
|         $files = new RecursiveIteratorIterator( | ||||
|             new RecursiveDirectoryIterator(TICKS_DIR, RecursiveDirectoryIterator::SKIP_DOTS) | ||||
|         ); | ||||
| 
 | ||||
|         foreach ($files as $file) { | ||||
|             if ($file->isFile() && str_ends_with($file->getFilename(), '.txt')) { | ||||
|                 $this->migrateTickFile($file->getPathname()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // TODO: Delete this sometime before 1.0
 | ||||
|     // Add mood field to tick files if necessary
 | ||||
|     private function migrateTickFile($filepath): void { | ||||
|         $lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); | ||||
|         $modified = false; | ||||
| 
 | ||||
|         foreach ($lines as &$line) { | ||||
|             $fields = explode('|', $line); | ||||
|             if (count($fields) === 2) { | ||||
|                 // Convert id|text to id|emoji|text
 | ||||
|                 $line = $fields[0] . '||' . $fields[1]; | ||||
|                 $modified = true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if ($modified) { | ||||
|             file_put_contents($filepath, implode("\n", $lines) . "\n"); | ||||
|             // TODO: log properly
 | ||||
|         } | ||||
|     } | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user