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: darkgreen;
|
||||||
--color-flash-success-bg: honeydew;
|
--color-flash-success-bg: honeydew;
|
||||||
--color-flash-success-border-left: forestgreen;
|
--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-border: darkslateblue;
|
||||||
--color-mood-hover: lightsteelblue;
|
--color-mood-hover: lightsteelblue;
|
||||||
--color-mood-selected: lightblue;
|
--color-mood-selected: lightblue;
|
||||||
@ -324,6 +327,12 @@ summary:focus,
|
|||||||
color: var(--color-flash-error);
|
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 {
|
.fieldset-items {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -26,24 +26,30 @@ class CssController extends Controller {
|
|||||||
$cssRow = $cssModel->getByFilename($filename);
|
$cssRow = $cssModel->getByFilename($filename);
|
||||||
if (!$cssRow){
|
if (!$cssRow){
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
Log::error("Custom css file not in database: {$filename}");
|
$msg = "Custom css file not in database: {$filename}";
|
||||||
exit;
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the file exists on the filesystem and is readable
|
// Make sure the file exists on the filesystem and is readable
|
||||||
$filePath = CSS_UPLOAD_DIR . "/$filename";
|
$filePath = CSS_UPLOAD_DIR . "/$filename";
|
||||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
Log::error("Custom css file not found or not readable: $filePath");
|
$msg = "Custom css file not found or not readable: {$filePath}";
|
||||||
exit;
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the file has a .css extension
|
// Make sure the file has a .css extension
|
||||||
$ext = strToLower(pathinfo($filename, PATHINFO_EXTENSION));
|
$ext = strToLower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
if($ext != 'css'){
|
if($ext != 'css'){
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
Log::error("Invalid file type requested: $ext");
|
$msg = "Invalid file type requested: {$ext}";
|
||||||
exit;
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, serve the file
|
// 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
|
// Make sure the default CSS file exists and is readable
|
||||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
Log::error("Default CSS file not found");
|
$msg = "Default CSS file not found";
|
||||||
exit;
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve the file
|
// Serve the file
|
||||||
@ -96,7 +105,10 @@ class CssController extends Controller {
|
|||||||
// Don't try to delete the default theme.
|
// Don't try to delete the default theme.
|
||||||
if (!$_POST['selectCssFile']){
|
if (!$_POST['selectCssFile']){
|
||||||
http_response_code(400);
|
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
|
// 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
|
// exit if the requested file isn't in the database
|
||||||
if (!$cssRow){
|
if (!$cssRow){
|
||||||
http_response_code(400);
|
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
|
// get the filename
|
||||||
@ -115,8 +130,11 @@ class CssController extends Controller {
|
|||||||
|
|
||||||
// delete the file from the database
|
// delete the file from the database
|
||||||
if (!$cssModel->delete($cssId)){
|
if (!$cssModel->delete($cssId)){
|
||||||
http_response_code(400);
|
http_response_code(500);
|
||||||
exit("Error deleting theme");
|
$msg = "Error deleting theme {$cssId}.";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the full path to the file
|
// 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
|
// Exit if the file doesn't exist or isn't readable
|
||||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||||
http_response_code(404);
|
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
|
// Delete the file
|
||||||
if (!unlink($filePath)){
|
if (!unlink($filePath)){
|
||||||
http_response_code(400);
|
http_response_code(500);
|
||||||
exit("Error deleting file: $filePath");
|
$msg = "Error deleting file: {$filePath}";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the theme back to default
|
// Set the theme back to default
|
||||||
try {
|
try {
|
||||||
$app['settings']->cssId = null;
|
$app['settings']->cssId = null;
|
||||||
$app['settings'] = $app['settings']->save();
|
$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) {
|
} catch (Exception $e) {
|
||||||
Log::error("Failed to update config after deleting theme: " . $e->getMessage());
|
$msg = "Failed to update config after deleting theme.";
|
||||||
Session::setFlashMessage('error', 'Theme deleted but failed to update settings');
|
Log::error($msg . ' ' . $e->getMessage());
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function handleSetTheme() {
|
private function handleSetTheme(): void {
|
||||||
global $app;
|
global $app;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -166,7 +193,7 @@ class CssController extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function handleUpload() {
|
private function handleUpload(): void {
|
||||||
try {
|
try {
|
||||||
// Check if file was uploaded
|
// Check if file was uploaded
|
||||||
if (!isset($_FILES['uploadCssFile']) || $_FILES['uploadCssFile']['error'] !== UPLOAD_ERR_OK) {
|
if (!isset($_FILES['uploadCssFile']) || $_FILES['uploadCssFile']['error'] !== UPLOAD_ERR_OK) {
|
||||||
@ -221,12 +248,11 @@ class CssController extends Controller {
|
|||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// Set error flash message
|
// Set error flash message
|
||||||
// Todo - don't do a global catch like this. Subclass Exception.
|
|
||||||
Session::setFlashMessage('error', 'Upload exception: ' . $e->getMessage());
|
Session::setFlashMessage('error', 'Upload exception: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateCssContent($content) {
|
private function validateCssContent($content): void {
|
||||||
// Remove comments
|
// Remove comments
|
||||||
$content = preg_replace('/\/\*.*?\*\//s', '', $content);
|
$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
|
// Check for suspicious patterns
|
||||||
$suspiciousPatterns = [
|
$suspiciousPatterns = [
|
||||||
'/javascript:/i',
|
'/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
|
// Remove path information and dangerous characters
|
||||||
$fileName = basename($originalName);
|
$fileName = basename($originalName);
|
||||||
$fileName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $fileName);
|
$fileName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $fileName);
|
||||||
|
|
||||||
return $fileName;
|
return $fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,10 @@ declare(strict_types=1);
|
|||||||
$emojiModel = new EmojiModel($app['db']);
|
$emojiModel = new EmojiModel($app['db']);
|
||||||
$emojiList = $emojiModel->getAll();
|
$emojiList = $emojiModel->getAll();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error("Failed to load emoji list: " . $e->getMessage());
|
|
||||||
$emojiList = [];
|
$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 = [
|
$vars = [
|
||||||
@ -49,23 +50,31 @@ declare(strict_types=1);
|
|||||||
// TODO - log a warning if mbstring isn't loaded
|
// TODO - log a warning if mbstring isn't loaded
|
||||||
$charCount = mb_strlen($emoji, 'UTF-8');
|
$charCount = mb_strlen($emoji, 'UTF-8');
|
||||||
if ($charCount !== 1) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log::warning("mbstring extension not loaded. Skipping emoji character count validation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the emoji is actually an emoji
|
// 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';
|
$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)) {
|
if (!preg_match($emojiPattern, $emoji)) {
|
||||||
// TODO - handle error
|
$msg = "Character is not a valid emoji.";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// emojis should have more bytes than characters
|
// emojis should have more bytes than characters
|
||||||
$byteCount = strlen($emoji);
|
$byteCount = strlen($emoji);
|
||||||
if ($byteCount <= 1) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +85,7 @@ declare(strict_types=1);
|
|||||||
global $app;
|
global $app;
|
||||||
|
|
||||||
if (!$this->isValidEmoji($emoji)){
|
if (!$this->isValidEmoji($emoji)){
|
||||||
Session::setFlashMessage('error', 'Invalid emoji format');
|
// exceptions are handled in isValidEmoji
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,10 +93,14 @@ declare(strict_types=1);
|
|||||||
try {
|
try {
|
||||||
$emojiModel = new EmojiModel($app['db']);
|
$emojiModel = new EmojiModel($app['db']);
|
||||||
$emojiModel->add($emoji, $description);
|
$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) {
|
} catch (Exception $e) {
|
||||||
Log::error("Failed to add emoji: " . $e->getMessage());
|
$msg = "Failed to add emoji.";
|
||||||
Session::setFlashMessage('error', 'Failed to add emoji');
|
Log::error($msg . " " . $e->getMessage());
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,10 +113,14 @@ declare(strict_types=1);
|
|||||||
try {
|
try {
|
||||||
$emojiModel = new EmojiModel($app['db']);
|
$emojiModel = new EmojiModel($app['db']);
|
||||||
$emojiModel->delete($ids);
|
$emojiModel->delete($ids);
|
||||||
Session::setFlashMessage('success', 'Emoji deleted successfully');
|
$msg = "Emoji deleted.";
|
||||||
|
|
||||||
|
Log::debug($msg);
|
||||||
|
Session::setFlashMessage('success', $msg);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error("Failed to delete emoji: " . $e->getMessage());
|
$msg = "Failed to delete emoji.";
|
||||||
Session::setFlashMessage('error', '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
|
// Redirect back to homepage
|
||||||
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, ''));
|
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, ''));
|
||||||
exit();
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -67,7 +67,6 @@ class Session {
|
|||||||
// valid types are:
|
// valid types are:
|
||||||
// - success
|
// - success
|
||||||
// - error
|
// - error
|
||||||
// - info
|
|
||||||
// - warning
|
// - warning
|
||||||
public static function setFlashMessage(string $type, string $message): void {
|
public static function setFlashMessage(string $type, string $message): void {
|
||||||
if (!isset($_SESSION['flash'][$type])){
|
if (!isset($_SESSION['flash'][$type])){
|
||||||
|
Loading…
x
Reference in New Issue
Block a user