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