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