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:
parent
f96616bcef
commit
dbd27b266d
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,6 @@ class TickController extends Controller{
|
||||
|
||||
// Redirect back to homepage
|
||||
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, ''));
|
||||
exit();
|
||||
exit;
|
||||
}
|
||||
}
|
@ -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])){
|
||||
|
Loading…
x
Reference in New Issue
Block a user