From 74acbcca7cb77ddc95c2d8ce7e470d58689e2755 Mon Sep 17 00:00:00 2001 From: Pavel Baksy Date: Mon, 5 Jan 2026 11:38:58 +0100 Subject: [PATCH] Add input validation for project and request names --- src/window.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/window.py b/src/window.py index 5f5c53b..f5f55c3 100644 --- a/src/window.py +++ b/src/window.py @@ -758,6 +758,62 @@ class RosterWindow(Adw.ApplicationWindow): # Project Management Methods + def _validate_project_name(self, name: str) -> tuple[bool, str]: + """ + Validate project name. + + Args: + name: The project name to validate + + Returns: + Tuple of (is_valid, error_message) + """ + # Check if empty + if not name or not name.strip(): + return False, "Project name cannot be empty" + + # Check length (max 100 characters) + if len(name) > 100: + return False, "Project name is too long (max 100 characters)" + + # Check for invalid characters (file system unsafe characters) + invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'] + for char in invalid_chars: + if char in name: + return False, f"Project name cannot contain '{char}'" + + # Check if it's only whitespace + if name.strip() == '': + return False, "Project name cannot be only whitespace" + + return True, "" + + def _validate_request_name(self, name: str) -> tuple[bool, str]: + """ + Validate request name. + + Args: + name: The request name to validate + + Returns: + Tuple of (is_valid, error_message) + """ + # Check if empty + if not name or not name.strip(): + return False, "Request name cannot be empty" + + # Check length (max 200 characters) + if len(name) > 200: + return False, "Request name is too long (max 200 characters)" + + # Check for invalid characters (file system unsafe characters) + invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'] + for char in invalid_chars: + if char in name: + return False, f"Request name cannot contain '{char}'" + + return True, "" + def _load_projects(self) -> None: """Load and display projects.""" # Clear existing @@ -796,9 +852,13 @@ class RosterWindow(Adw.ApplicationWindow): def on_response(dlg, response): if response == "create": name = entry.get_text().strip() - if name: + is_valid, error_msg = self._validate_project_name(name) + if is_valid: self.project_manager.add_project(name) self._load_projects() + self._show_toast(f"Project '{name}' created") + else: + self._show_toast(error_msg) dialog.connect("response", on_response) dialog.present(self) @@ -848,9 +908,13 @@ class RosterWindow(Adw.ApplicationWindow): if response == "save": new_name = entry.get_text().strip() new_icon = icon_button.selected_icon - if new_name: + is_valid, error_msg = self._validate_project_name(new_name) + if is_valid: self.project_manager.update_project(project.id, new_name, new_icon) self._load_projects() + self._show_toast(f"Project updated") + else: + self._show_toast(error_msg) dialog.connect("response", on_response) dialog.present(self) @@ -982,7 +1046,8 @@ class RosterWindow(Adw.ApplicationWindow): def on_response(dlg, response): if response == "save": name = entry.get_text().strip() - if name: + is_valid, error_msg = self._validate_request_name(name) + if is_valid: selected = dropdown.get_selected() project = projects[selected] # Check for duplicate name @@ -998,6 +1063,8 @@ class RosterWindow(Adw.ApplicationWindow): # Clear modified flag on current tab self._mark_tab_as_saved(saved_request.id, name, request, scripts) + else: + self._show_toast(error_msg) dialog.connect("response", on_response) dialog.present(self) @@ -1094,7 +1161,8 @@ class RosterWindow(Adw.ApplicationWindow): def on_response(dlg, response): if response == "save": name = entry.get_text().strip() - if name: + is_valid, error_msg = self._validate_request_name(name) + if is_valid: # Check for duplicate name existing = self.project_manager.find_request_by_name(project.id, name) if existing: @@ -1108,6 +1176,8 @@ class RosterWindow(Adw.ApplicationWindow): # Clear modified flag on current tab self._mark_tab_as_saved(saved_request.id, name, request) + else: + self._show_toast(error_msg) dialog.connect("response", on_response) dialog.present(self)