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