Gracefully handle validation errors in CSS and Emoji pages. (#71)

Handle CSS and Validation emoji errors so users get descriptive messages and are able to return to the application.

Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/71
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-14 19:44:41 +00:00 committed by greg
parent f96616bcef
commit dbd27b266d
5 changed files with 89 additions and 39 deletions

View File

@ -12,6 +12,9 @@
--color-flash-success: darkgreen;
--color-flash-success-bg: honeydew;
--color-flash-success-border-left: forestgreen;
--color-flash-warning: darkgoldenrod;
--color-flash-warning-bg: lightgoldenrodyellow;
--color-flash-warning-border-left: gold;
--color-mood-border: darkslateblue;
--color-mood-hover: lightsteelblue;
--color-mood-selected: lightblue;
@ -324,6 +327,12 @@ summary:focus,
color: var(--color-flash-error);
}
.flash-warning {
background-color: var(--color-flash-warning-bg);
border-left-color: var(--color-flash-warning-border-left);
color: var(--color-flash-warning);
}
.fieldset-items {
margin-bottom: 14px;
display: grid;

View File

@ -26,24 +26,30 @@ class CssController extends Controller {
$cssRow = $cssModel->getByFilename($filename);
if (!$cssRow){
http_response_code(404);
Log::error("Custom css file not in database: {$filename}");
exit;
$msg = "Custom css file not in database: {$filename}";
Log::error($msg);
Session::setFlashMessage('error', $msg);
return;
}
// Make sure the file exists on the filesystem and is readable
$filePath = CSS_UPLOAD_DIR . "/$filename";
if (!file_exists($filePath) || !is_readable($filePath)) {
http_response_code(404);
Log::error("Custom css file not found or not readable: $filePath");
exit;
$msg = "Custom css file not found or not readable: {$filePath}";
Log::error($msg);
Session::setFlashMessage('error', $msg);
return;
}
// Make sure the file has a .css extension
$ext = strToLower(pathinfo($filename, PATHINFO_EXTENSION));
if($ext != 'css'){
http_response_code(400);
Log::error("Invalid file type requested: $ext");
exit;
$msg = "Invalid file type requested: {$ext}";
Log::error($msg);
Session::setFlashMessage('error', $msg);
return;
}
// If we get here, serve the file
@ -61,8 +67,11 @@ class CssController extends Controller {
// Make sure the default CSS file exists and is readable
if (!file_exists($filePath) || !is_readable($filePath)) {
http_response_code(404);
Log::error("Default CSS file not found");
exit;
$msg = "Default CSS file not found";
Log::error($msg);
Session::setFlashMessage('error', $msg);
return;
}
// Serve the file
@ -96,7 +105,10 @@ class CssController extends Controller {
// Don't try to delete the default theme.
if (!$_POST['selectCssFile']){
http_response_code(400);
exit("Cannot delete default theme");
$msg = "Cannot delete default theme.";
Log::warning($msg);
Session::setFlashMessage('warning', $msg);
return;
}
// Get the data for the selected CSS file
@ -107,7 +119,10 @@ class CssController extends Controller {
// exit if the requested file isn't in the database
if (!$cssRow){
http_response_code(400);
exit("No entry found for css id $cssId");
$msg = "No entry found for css id {$cssId}.";
Log::warning($msg);
Session::setFlashMessage('warning', $msg);
return;
}
// get the filename
@ -115,8 +130,11 @@ class CssController extends Controller {
// delete the file from the database
if (!$cssModel->delete($cssId)){
http_response_code(400);
exit("Error deleting theme");
http_response_code(500);
$msg = "Error deleting theme {$cssId}.";
Log::error($msg);
Session::setFlashMessage('error', $msg);
return;
}
// Build the full path to the file
@ -125,27 +143,36 @@ class CssController extends Controller {
// Exit if the file doesn't exist or isn't readable
if (!file_exists($filePath) || !is_readable($filePath)) {
http_response_code(404);
exit("CSS file not found: $filePath");
$msg = "CSS file not found: {$filePath}";
Log::error($msg);
Session::setFlashMessage('error', $msg);
return;
}
// Delete the file
if (!unlink($filePath)){
http_response_code(400);
exit("Error deleting file: $filePath");
http_response_code(500);
$msg = "Error deleting file: {$filePath}";
Log::error($msg);
Session::setFlashMessage('error', $msg);
return;
}
// Set the theme back to default
try {
$app['settings']->cssId = null;
$app['settings'] = $app['settings']->save();
Session::setFlashMessage('success', 'Theme ' . $cssFilename . ' deleted.');
$msg = "Theme {$cssFilename} deleted.";
Log::debug($msg);
Session::setFlashMessage('success', $msg);
} catch (Exception $e) {
Log::error("Failed to update config after deleting theme: " . $e->getMessage());
Session::setFlashMessage('error', 'Theme deleted but failed to update settings');
$msg = "Failed to update config after deleting theme.";
Log::error($msg . ' ' . $e->getMessage());
Session::setFlashMessage('error', $msg);
}
}
private function handleSetTheme() {
private function handleSetTheme(): void {
global $app;
try {
@ -166,7 +193,7 @@ class CssController extends Controller {
}
}
private function handleUpload() {
private function handleUpload(): void {
try {
// Check if file was uploaded
if (!isset($_FILES['uploadCssFile']) || $_FILES['uploadCssFile']['error'] !== UPLOAD_ERR_OK) {
@ -221,12 +248,11 @@ class CssController extends Controller {
} catch (Exception $e) {
// Set error flash message
// Todo - don't do a global catch like this. Subclass Exception.
Session::setFlashMessage('error', 'Upload exception: ' . $e->getMessage());
}
}
private function validateCssContent($content) {
private function validateCssContent($content): void {
// Remove comments
$content = preg_replace('/\/\*.*?\*\//s', '', $content);
@ -247,7 +273,7 @@ class CssController extends Controller {
}
}
private function scanForMaliciousContent($content, $fileName) {
private function scanForMaliciousContent($content, $fileName): void {
// Check for suspicious patterns
$suspiciousPatterns = [
'/javascript:/i',
@ -286,12 +312,11 @@ class CssController extends Controller {
}
}
private function generateSafeFileName($originalName) {
private function generateSafeFileName($originalName): string {
// Remove path information and dangerous characters
$fileName = basename($originalName);
$fileName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $fileName);
return $fileName;
}
}

View File

@ -10,9 +10,10 @@ declare(strict_types=1);
$emojiModel = new EmojiModel($app['db']);
$emojiList = $emojiModel->getAll();
} catch (Exception $e) {
Log::error("Failed to load emoji list: " . $e->getMessage());
$emojiList = [];
Session::setFlashMessage('error', 'Failed to load custom emoji');
$msg = "Failed to load emoji list.";
Log::error($msg . " " . $e->getMessage());
Session::setFlashMessage('error', $msg);
}
$vars = [
@ -49,23 +50,31 @@ declare(strict_types=1);
// TODO - log a warning if mbstring isn't loaded
$charCount = mb_strlen($emoji, 'UTF-8');
if ($charCount !== 1) {
// TODO - handle error
$msg = "Emoji must be a single UTF-8 encoded character.";
Log::error($msg);
Session::setFlashMessage('error', $msg);
return false;
}
} else {
Log::warning("mbstring extension not loaded. Skipping emoji character count validation.");
}
// Validate the emoji is actually an emoji
$emojiPattern = '/^[\x{1F000}-\x{1F9FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F1E0}-\x{1F1FF}\x{1F900}-\x{1F9FF}\x{1FA70}-\x{1FAFF}]$/u';
if (!preg_match($emojiPattern, $emoji)) {
// TODO - handle error
$msg = "Character is not a valid emoji.";
Log::error($msg);
Session::setFlashMessage('error', $msg);
return false;
}
// emojis should have more bytes than characters
$byteCount = strlen($emoji);
if ($byteCount <= 1) {
// TODO - handle error
$msg = "Character is not a valid emoji (too few bytes).";
Log::error($msg);
Session::setFlashMessage('error', $msg);
return false;
}
@ -76,7 +85,7 @@ declare(strict_types=1);
global $app;
if (!$this->isValidEmoji($emoji)){
Session::setFlashMessage('error', 'Invalid emoji format');
// exceptions are handled in isValidEmoji
return;
}
@ -84,10 +93,14 @@ declare(strict_types=1);
try {
$emojiModel = new EmojiModel($app['db']);
$emojiModel->add($emoji, $description);
Session::setFlashMessage('success', 'Emoji added successfully');
$msg = "Emoji added: {$emoji} - {$description}";
Log::debug($msg);
Session::setFlashMessage('success', $msg);
} catch (Exception $e) {
Log::error("Failed to add emoji: " . $e->getMessage());
Session::setFlashMessage('error', 'Failed to add emoji');
$msg = "Failed to add emoji.";
Log::error($msg . " " . $e->getMessage());
Session::setFlashMessage('error', $msg);
}
}
@ -100,10 +113,14 @@ declare(strict_types=1);
try {
$emojiModel = new EmojiModel($app['db']);
$emojiModel->delete($ids);
Session::setFlashMessage('success', 'Emoji deleted successfully');
$msg = "Emoji deleted.";
Log::debug($msg);
Session::setFlashMessage('success', $msg);
} catch (Exception $e) {
Log::error("Failed to delete emoji: " . $e->getMessage());
Session::setFlashMessage('error', 'Failed to delete emoji');
$msg = "Failed to delete emoji.";
Log::error($msg . " " . $e->getMessage());
Session::setFlashMessage('error', $msg);
}
}
}

View File

@ -61,6 +61,6 @@ class TickController extends Controller{
// Redirect back to homepage
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, ''));
exit();
exit;
}
}

View File

@ -67,7 +67,6 @@ class Session {
// valid types are:
// - success
// - error
// - info
// - warning
public static function setFlashMessage(string $type, string $message): void {
if (!isset($_SESSION['flash'][$type])){