Add error handling for external dependencies. (#55)

Protect functions that interact with databases and filesystems.

Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/55
Co-authored-by: Greg Sarjeant <greg@subcultureofone.org>
Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
This commit is contained in:
Greg Sarjeant 2025-08-04 11:46:55 +00:00 committed by greg
parent 51abf33ad1
commit d9d0ed9571
4 changed files with 87 additions and 51 deletions

View File

@ -34,6 +34,7 @@ $db = $prerequisites->getDatabase();
// Make sure the initial setup is complete unless we're already heading to setup // Make sure the initial setup is complete unless we're already heading to setup
if (!(preg_match('/setup$/', $path))) { if (!(preg_match('/setup$/', $path))) {
try {
// Make sure required tables (user, settings) are populated // Make sure required tables (user, settings) are populated
$user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); $user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn();
$settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); $settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
@ -43,7 +44,16 @@ if (!(preg_match('/setup$/', $path))) {
$init = require APP_ROOT . '/config/init.php'; $init = require APP_ROOT . '/config/init.php';
header('Location: ' . $init['base_path'] . 'setup'); header('Location: ' . $init['base_path'] . 'setup');
exit; exit;
}; }
} catch (Exception $e) {
// Database error during setup validation - show error page
error_log("Database error during setup validation: " . $e->getMessage());
http_response_code(500);
echo "<h1>Database Error</h1>";
echo "<p>Cannot validate setup status. The database may be corrupted or locked.</p>";
echo "<p>Please check your installation or contact your hosting provider.</p>";
exit;
}
} }
/* /*
@ -83,7 +93,7 @@ if ($method === 'POST' && $path != 'setup') {
if (!Session::isValid($_POST['csrf_token'])) { if (!Session::isValid($_POST['csrf_token'])) {
// Invalid session - redirect to /login // Invalid session - redirect to /login
Log::info('Attempt to POST with invalid session. Redirecting to login.'); Log::info('Attempt to POST with invalid session. Redirecting to login.');
header('Location: ' . Util::buildRelativeUrl($app->config->basePath, 'login')); header('Location: ' . Util::buildRelativeUrl($app['config']->basePath, 'login'));
exit; exit;
} }
} else { } else {

View File

@ -112,16 +112,20 @@ class CssController extends Controller {
} }
// Set the theme back to default // Set the theme back to default
try {
$app['config']->cssId = null; $app['config']->cssId = null;
$app['config'] = $app['config']->save(); $app['config'] = $app['config']->save();
// Set flash message
Session::setFlashMessage('success', 'Theme ' . $cssFilename . ' deleted.'); Session::setFlashMessage('success', 'Theme ' . $cssFilename . ' deleted.');
} catch (Exception $e) {
Log::error("Failed to update config after deleting theme: " . $e->getMessage());
Session::setFlashMessage('error', 'Theme deleted but failed to update settings');
}
} }
private function handleSetTheme() { private function handleSetTheme() {
global $app; global $app;
try {
if ($_POST['selectCssFile']){ if ($_POST['selectCssFile']){
// Set custom theme // Set custom theme
$app['config']->cssId = $_POST['selectCssFile']; $app['config']->cssId = $_POST['selectCssFile'];
@ -132,9 +136,11 @@ class CssController extends Controller {
// Update the site theme // Update the site theme
$app['config'] = $app['config']->save(); $app['config'] = $app['config']->save();
// Set flash message
Session::setFlashMessage('success', 'Theme applied.'); Session::setFlashMessage('success', 'Theme applied.');
} catch (Exception $e) {
Log::error("Failed to save theme setting: " . $e->getMessage());
Session::setFlashMessage('error', 'Failed to apply theme');
}
} }
private function handleUpload() { private function handleUpload() {

View File

@ -5,10 +5,15 @@ class FeedController extends Controller {
public function __construct() { public function __construct() {
global $app; global $app;
try {
$tickModel = new TickModel($app['db'], $app['config']); $tickModel = new TickModel($app['db'], $app['config']);
$this->ticks = $tickModel->getPage($app['config']->itemsPerPage); $this->ticks = $tickModel->getPage($app['config']->itemsPerPage);
Log::debug("Loaded " . count($this->ticks) . " ticks for feeds"); Log::debug("Loaded " . count($this->ticks) . " ticks for feeds");
} catch (Exception $e) {
Log::error("Failed to load ticks for feed: " . $e->getMessage());
// Provide empty feed rather than crashing - RSS readers can handle this
$this->ticks = [];
}
} }
public function rss(){ public function rss(){

View File

@ -19,7 +19,12 @@ class Log {
// (should be handled by Prerequisites, but doesn't hurt) // (should be handled by Prerequisites, but doesn't hurt)
$logDir = dirname(self::$logFile); $logDir = dirname(self::$logFile);
if (!is_dir($logDir)) { if (!is_dir($logDir)) {
try {
mkdir($logDir, 0770, true); mkdir($logDir, 0770, true);
} catch (Exception $e) {
// Fall back to error_log if we can't create log directory
error_log("Failed to create log directory {$logDir}: " . $e->getMessage());
}
} }
} }
@ -61,6 +66,7 @@ class Log {
$logEntry = "[{$timestamp}] {$level}: " . Util::getClientIp() . "{$context} - {$message}\n"; $logEntry = "[{$timestamp}] {$level}: " . Util::getClientIp() . "{$context} - {$message}\n";
// Rotate if we're at the max file size (1000 lines) // Rotate if we're at the max file size (1000 lines)
try {
if (file_exists(self::$logFile)) { if (file_exists(self::$logFile)) {
$lineCount = count(file(self::$logFile)); $lineCount = count(file(self::$logFile));
if ($lineCount >= self::$maxLines) { if ($lineCount >= self::$maxLines) {
@ -70,9 +76,14 @@ class Log {
} }
file_put_contents(self::$logFile, $logEntry, FILE_APPEND | LOCK_EX); file_put_contents(self::$logFile, $logEntry, FILE_APPEND | LOCK_EX);
} catch (Exception $e) {
// Fall back to error_log if file operations fail
error_log("Log write failed: " . $e->getMessage() . " - Original message: " . trim($logEntry));
}
} }
private static function rotate() { private static function rotate() {
try {
// Rotate existing history files: tkr.4.log -> tkr.5.log, etc. // Rotate existing history files: tkr.4.log -> tkr.5.log, etc.
for ($i = self::$maxFiles - 1; $i >= 1; $i--) { for ($i = self::$maxFiles - 1; $i >= 1; $i--) {
$oldFile = self::$logFile . '.' . $i; $oldFile = self::$logFile . '.' . $i;
@ -91,5 +102,9 @@ class Log {
if (file_exists(self::$logFile)) { if (file_exists(self::$logFile)) {
rename(self::$logFile, self::$logFile . '.1'); rename(self::$logFile, self::$logFile . '.1');
} }
} catch (Exception $e) {
// Log rotation failure - not critical, just continue
error_log("Log rotation failed: " . $e->getMessage());
}
} }
} }