diff --git a/Mergin/plugin.py b/Mergin/plugin.py index c1ccd045..4af81232 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -412,7 +412,7 @@ def set_current_workspace(self, workspace): iface.messageBar().pushMessage( "Mergin Maps", "Your attention is required. Please visit the " - f"" + f"" "Mergin dashboard", level=Qgis.Critical, duration=0, diff --git a/Mergin/project_settings_widget.py b/Mergin/project_settings_widget.py index b1753a03..07e32da5 100644 --- a/Mergin/project_settings_widget.py +++ b/Mergin/project_settings_widget.py @@ -30,6 +30,7 @@ set_tracking_layer_flags, is_experimental_plugin_enabled, remove_prefix, + invalid_filename_character, ) ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_config.ui") @@ -224,14 +225,21 @@ def update_preview(self, expression, layer, field_name): exp = QgsExpression(expression) exp.prepare(context) if exp.hasParserError(): - self.label_preview.setText(f"{exp.parserErrorString()}") + self.label_preview.setText(f"{exp.parserErrorString()}") return val = exp.evaluate(context) if exp.hasEvalError(): - self.label_preview.setText(f"{exp.evalErrorString()}") + self.label_preview.setText(f"{exp.evalErrorString()}") return - + if val: + # check if evaluated expression contains invalid filename characters + invalid_char = invalid_filename_character(val) + if invalid_char: + self.label_preview.setText( + f"The file name '{val}.jpg' contains an invalid character. Do not use '{invalid_char}' character in the file name." + ) + return config = layer.fields().field(field_name).editorWidgetSetup().config() target_dir = resolve_target_dir(layer, config) prefix = prefix_for_relative_path( @@ -240,9 +248,9 @@ def update_preview(self, expression, layer, field_name): target_dir, ) if prefix: - self.label_preview.setText(f"{remove_prefix(prefix, QgsProject.instance().homePath())}/{val}.jpg") + self.label_preview.setText(f"{remove_prefix(prefix, QgsProject.instance().homePath())}/{val}.jpg") else: - self.label_preview.setText(f"{val}.jpg") + self.label_preview.setText(f"{val}.jpg") def check_project(self, state): """ diff --git a/Mergin/ui/ui_project_config.ui b/Mergin/ui/ui_project_config.ui index cac9754e..d2da2177 100644 --- a/Mergin/ui/ui_project_config.ui +++ b/Mergin/ui/ui_project_config.ui @@ -193,6 +193,11 @@ 0 + + + true + + @@ -214,14 +219,14 @@ - Preview + Preview: - <html><head/><body><p><span style=" font-weight:600;">Photo name format</span></p><p>Set up custom photo names format based on an expression. Make sure that the name is unique for each photo.</p><p>Pro tip: use variables like <span style=" font-family:'Noto Sans Mono';">@mm_user_email</span>, <span style=" font-family:'Noto Sans Mono';">@layer_name</span> or layer fields in combination with <span style=" font-family:'Noto Sans Mono';">now()</span> (to get the current time) in order to generate unique photo names.</p><p>For further details, please refer to <a href="https://merginmaps.com/docs/layer/settingup_forms_photo/#customising-photo-name-format-with-expressions"><span style=" text-decoration: underline; color:#1d99f3;">our documentation</span></a>.</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Photo name format</span></p><p>Set up custom photo names format based on an expression. Make sure that the name is unique for each photo.</p><p>Pro tip: use variables like <span style=" font-family:'Noto Sans Mono';">@mm_user_email</span>, <span style=" font-family:'Noto Sans Mono';">@layer_name</span> or layer fields in combination with <span style=" font-family:'Noto Sans Mono';">now()</span> (to get the current time) in order to generate unique photo names.</p><p>For further details, please refer to <a href="https://merginmaps.com/docs/layer/settingup_forms_photo/#customising-photo-name-format-with-expressions"><span style=" text-decoration: underline; color:#1d99f3;">our documentation</span></a>.</p><p>File names may include letters (A–Z, a–z), numbers (0–9), spaces, dashes (-), and underscores (_).<br>Use '/' only to separate subfolders.</p></body></html> true diff --git a/Mergin/utils.py b/Mergin/utils.py index da2c9c00..51395f2a 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1656,3 +1656,12 @@ def is_experimental_plugin_enabled() -> bool: else: value = settings.value("plugin-manager/allow-experimental", False) return value + + +def invalid_filename_character(filename: str) -> str: + """Returns invalid character for the filename""" + illegal_filename_chars = re.compile(r'[\x00-\x19<>:|?*"]') + + match = illegal_filename_chars.search(filename) + if match: + return match.group() diff --git a/Mergin/validation.py b/Mergin/validation.py index 32831611..0f2e3eb9 100644 --- a/Mergin/validation.py +++ b/Mergin/validation.py @@ -27,9 +27,10 @@ QGIS_NET_PROVIDERS, is_versioned_file, get_layer_by_path, + invalid_filename_character, ) -INVALID_CHARS = re.compile('[\\\/\(\)\[\]\{\}"\n\r]') +INVALID_FIELD_NAME_CHARS = re.compile('[\\\/\(\)\[\]\{\}"\n\r]') PROJECT_VARS = re.compile("\@project_home|\@project_path|\@project_folder") @@ -62,6 +63,7 @@ class Warning(Enum): EDITOR_JSON_CONFIG_CHANGE = 26 EDITOR_DIFFBASED_FILE_REMOVED = 27 PROJECT_HOME_PATH = 28 + INVALID_ADDED_FILENAME = 29 class MultipleLayersWarning: @@ -128,6 +130,7 @@ def run_checks(self): self.check_datum_shift_grids() self.check_svgs_embedded() self.check_editor_perms() + self.check_filenames() return self.issues @@ -345,7 +348,7 @@ def check_field_names(self): if dp.storageType() == "GPKG": fields = layer.fields() for f in fields: - if INVALID_CHARS.search(f.name()): + if INVALID_FIELD_NAME_CHARS.search(f.name()): self.issues.append(SingleLayerWarning(lid, Warning.INCORRECT_FIELD_NAME)) def check_snapping(self): @@ -452,6 +455,12 @@ def check_editor_perms(self): url = f"reset_file?layer={path}" self.issues.append(SingleLayerWarning(layer.id(), Warning.EDITOR_DIFFBASED_FILE_REMOVED, url)) + def check_filenames(self): + """Checks that files to upload have valid filenames. Otherwise, push will be refused by the server.""" + for file in self.changes["added"]: + if invalid_filename_character(file["path"]): + self.issues.append(MultipleLayersWarning(Warning.INVALID_ADDED_FILENAME, file["path"])) + def warning_display_string(warning_id, url=None): """Returns a display string for a corresponding warning""" @@ -516,3 +525,5 @@ def warning_display_string(warning_id, url=None): return f"You don't have permission to remove this layer. Reset the layer to be able to sync changes." elif warning_id == Warning.PROJECT_HOME_PATH: return "QGIS Project Home Path is specified. Quick fix the issue. (This will unset project home)" + elif warning_id == Warning.INVALID_ADDED_FILENAME: + return f"You cannot synchronize a file with invalid characters in it's name. Please sanitize the name of this file '{url}'"