From 407cb1b1da86b06bfcdd5915767895a8e3a60516 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Fri, 5 Sep 2025 14:41:46 +0200 Subject: [PATCH 1/9] Attention required banner redirect to current workspace --- Mergin/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From 21b744d73fbb31643aacb7de0e222aa3a884da36 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 11 Sep 2025 15:45:05 +0200 Subject: [PATCH 2/9] Check default filenames --- Mergin/validation.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/Mergin/validation.py b/Mergin/validation.py index 32831611..cf1dd0b5 100644 --- a/Mergin/validation.py +++ b/Mergin/validation.py @@ -6,6 +6,7 @@ from enum import Enum from collections import defaultdict from pathlib import Path +from pathvalidate import is_valid_filename from qgis.core import ( QgsMapLayerType, @@ -29,8 +30,9 @@ get_layer_by_path, ) -INVALID_CHARS = re.compile('[\\\/\(\)\[\]\{\}"\n\r]') +INVALID_FIELD_NAME_CHARS = re.compile('[\\\/\(\)\[\]\{\}"\n\r]') PROJECT_VARS = re.compile("\@project_home|\@project_path|\@project_folder") +DISALLOWED_FILENAME_EXPRESSIONS = ["now()"] class Warning(Enum): @@ -62,6 +64,7 @@ class Warning(Enum): EDITOR_JSON_CONFIG_CHANGE = 26 EDITOR_DIFFBASED_FILE_REMOVED = 27 PROJECT_HOME_PATH = 28 + INVALID_FILENAME_CHARS = 29 class MultipleLayersWarning: @@ -128,6 +131,7 @@ def run_checks(self): self.check_datum_shift_grids() self.check_svgs_embedded() self.check_editor_perms() + self.check_default_filenames() return self.issues @@ -345,7 +349,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 +456,26 @@ 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_default_filenames(self): + """Checks that file names which will be created by the app will contain valid characters. + Rationale: when there is a default value set up for a photo field with `now()` or using characters that are not + allowed in filenames (e.g. ':'). The server would refuse whole sync when the app tries to push these photos.""" + for lid, layer in self.layers.items(): + if lid not in self.editable: + continue + fields = layer.fields() + for i in range(fields.count()): + default_def = layer.defaultValueDefinition(i) + expr_str = default_def.expression() # returns string or empty if none + if not expr_str: + continue + if expr_str.lower() in DISALLOWED_FILENAME_EXPRESSIONS: + self.issues.append(SingleLayerWarning(lid, Warning.INVALID_FILENAME_CHARS)) + break + if not is_valid_filename(expr_str): + self.issues.append(SingleLayerWarning(lid, Warning.INVALID_FILENAME_CHARS)) + break + def warning_display_string(warning_id, url=None): """Returns a display string for a corresponding warning""" @@ -516,3 +540,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_FILENAME_CHARS: + return "You use invalid file name characters in some of your field's default expression. Files with invalid names cannot be upload to the cloud." From 17f22e5d2b12df768bb076487b4cb1001d519ef7 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 11 Sep 2025 16:30:25 +0200 Subject: [PATCH 3/9] added files' name validation --- Mergin/validation.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Mergin/validation.py b/Mergin/validation.py index cf1dd0b5..cdcba93d 100644 --- a/Mergin/validation.py +++ b/Mergin/validation.py @@ -64,7 +64,8 @@ class Warning(Enum): EDITOR_JSON_CONFIG_CHANGE = 26 EDITOR_DIFFBASED_FILE_REMOVED = 27 PROJECT_HOME_PATH = 28 - INVALID_FILENAME_CHARS = 29 + INVALID_DEFAULT_FILENAME = 29 + INVALID_ADDED_FILENAME = 30 class MultipleLayersWarning: @@ -132,6 +133,7 @@ def run_checks(self): self.check_svgs_embedded() self.check_editor_perms() self.check_default_filenames() + self.check_filenames() return self.issues @@ -470,12 +472,17 @@ def check_default_filenames(self): if not expr_str: continue if expr_str.lower() in DISALLOWED_FILENAME_EXPRESSIONS: - self.issues.append(SingleLayerWarning(lid, Warning.INVALID_FILENAME_CHARS)) + self.issues.append(SingleLayerWarning(lid, Warning.INVALID_DEFAULT_FILENAME)) break if not is_valid_filename(expr_str): - self.issues.append(SingleLayerWarning(lid, Warning.INVALID_FILENAME_CHARS)) + self.issues.append(SingleLayerWarning(lid, Warning.INVALID_DEFAULT_FILENAME)) break + 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 not is_valid_filename(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""" @@ -540,5 +547,7 @@ 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_FILENAME_CHARS: + elif warning_id == Warning.INVALID_DEFAULT_FILENAME: return "You use invalid file name characters in some of your field's default expression. Files with invalid names cannot be upload to the cloud." + elif warning_id == Warning.INVALID_ADDED_FILENAME: + return f"You cannot upload a file with invalid characters in it's name. Please sanitize the name of this file '{url}'" From fb00e04cf7f42642bae77881c00c5713581f1f8d Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 11 Sep 2025 16:31:53 +0200 Subject: [PATCH 4/9] black --- Mergin/validation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Mergin/validation.py b/Mergin/validation.py index cdcba93d..e3e6a7bc 100644 --- a/Mergin/validation.py +++ b/Mergin/validation.py @@ -484,6 +484,7 @@ def check_filenames(self): if not is_valid_filename(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""" help_mgr = MerginHelp() From ee4b8a2852a85287045346c5cb824e96d6eae6b6 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 11 Sep 2025 16:49:14 +0200 Subject: [PATCH 5/9] text --- Mergin/validation.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Mergin/validation.py b/Mergin/validation.py index e3e6a7bc..eaa1ed82 100644 --- a/Mergin/validation.py +++ b/Mergin/validation.py @@ -471,11 +471,12 @@ def check_default_filenames(self): expr_str = default_def.expression() # returns string or empty if none if not expr_str: continue + field_name = fields[i].name() if expr_str.lower() in DISALLOWED_FILENAME_EXPRESSIONS: - self.issues.append(SingleLayerWarning(lid, Warning.INVALID_DEFAULT_FILENAME)) + self.issues.append(SingleLayerWarning(lid, Warning.INVALID_DEFAULT_FILENAME, field_name)) break if not is_valid_filename(expr_str): - self.issues.append(SingleLayerWarning(lid, Warning.INVALID_DEFAULT_FILENAME)) + self.issues.append(SingleLayerWarning(lid, Warning.INVALID_DEFAULT_FILENAME, field_name)) break def check_filenames(self): @@ -549,6 +550,6 @@ def warning_display_string(warning_id, url=None): 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_DEFAULT_FILENAME: - return "You use invalid file name characters in some of your field's default expression. Files with invalid names cannot be upload to the cloud." + return f"The default expression set in field '{url}' will lead to invalid file name that cannot be synchronized. Please sanitize the expression." elif warning_id == Warning.INVALID_ADDED_FILENAME: - return f"You cannot upload a file with invalid characters in it's name. Please sanitize the name of this file '{url}'" + return f"You cannot synchronize a file with invalid characters in it's name. Please sanitize the name of this file '{url}'" From 4858e6f0f3c7f93e38456b105c7e5e521c11d969 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Fri, 12 Sep 2025 14:56:22 +0200 Subject: [PATCH 6/9] Check photo names in project settings --- Mergin/project_settings_widget.py | 17 ++++++++++++----- Mergin/ui/ui_project_config.ui | 27 ++++++++++++++++++++------- Mergin/validation.py | 27 +-------------------------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/Mergin/project_settings_widget.py b/Mergin/project_settings_widget.py index b1753a03..a68c1f9e 100644 --- a/Mergin/project_settings_widget.py +++ b/Mergin/project_settings_widget.py @@ -4,6 +4,7 @@ import json import os import typing +import re from qgis.PyQt import uic from qgis.PyQt.QtGui import QIcon, QColor from qgis.PyQt.QtCore import Qt @@ -34,6 +35,7 @@ ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_config.ui") ProjectConfigUiWidget, _ = uic.loadUiType(ui_file) +illegal_filename_chars = re.compile(r'[\x00-\x19<>:|?*"]') class MerginProjectConfigFactory(QgsOptionsWidgetFactory): @@ -224,14 +226,19 @@ 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 + match = illegal_filename_chars.search(val) + if match: + self.label_preview.setText(f"The file name '{val}.jpg' contains an invalid character. Do not use '{match.group()}' 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 +247,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..3659367a 100644 --- a/Mergin/ui/ui_project_config.ui +++ b/Mergin/ui/ui_project_config.ui @@ -9,8 +9,8 @@ 0 0 - 642 - 556 + 822 + 1005 @@ -44,9 +44,9 @@ 0 - -888 - 628 - 1444 + 0 + 808 + 1396 @@ -193,6 +193,11 @@ 0 + + + true + + @@ -214,14 +219,22 @@ - 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> + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Photo name format</span></p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Set up custom photo names format based on an expression. Make sure that the name is unique for each photo.</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">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 style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">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 style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Use only allowed filename characters A-z, 0-9, ' ', '-' and '_'. <br />'/' can be used for subfolders.</p></body></html> true diff --git a/Mergin/validation.py b/Mergin/validation.py index eaa1ed82..2637b213 100644 --- a/Mergin/validation.py +++ b/Mergin/validation.py @@ -64,8 +64,7 @@ class Warning(Enum): EDITOR_JSON_CONFIG_CHANGE = 26 EDITOR_DIFFBASED_FILE_REMOVED = 27 PROJECT_HOME_PATH = 28 - INVALID_DEFAULT_FILENAME = 29 - INVALID_ADDED_FILENAME = 30 + INVALID_ADDED_FILENAME = 29 class MultipleLayersWarning: @@ -132,7 +131,6 @@ def run_checks(self): self.check_datum_shift_grids() self.check_svgs_embedded() self.check_editor_perms() - self.check_default_filenames() self.check_filenames() return self.issues @@ -458,27 +456,6 @@ 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_default_filenames(self): - """Checks that file names which will be created by the app will contain valid characters. - Rationale: when there is a default value set up for a photo field with `now()` or using characters that are not - allowed in filenames (e.g. ':'). The server would refuse whole sync when the app tries to push these photos.""" - for lid, layer in self.layers.items(): - if lid not in self.editable: - continue - fields = layer.fields() - for i in range(fields.count()): - default_def = layer.defaultValueDefinition(i) - expr_str = default_def.expression() # returns string or empty if none - if not expr_str: - continue - field_name = fields[i].name() - if expr_str.lower() in DISALLOWED_FILENAME_EXPRESSIONS: - self.issues.append(SingleLayerWarning(lid, Warning.INVALID_DEFAULT_FILENAME, field_name)) - break - if not is_valid_filename(expr_str): - self.issues.append(SingleLayerWarning(lid, Warning.INVALID_DEFAULT_FILENAME, field_name)) - break - 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"]: @@ -549,7 +526,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_DEFAULT_FILENAME: - return f"The default expression set in field '{url}' will lead to invalid file name that cannot be synchronized. Please sanitize the expression." 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}'" From d637223078c0f42e12a249005f301c9319cf92ff Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Fri, 12 Sep 2025 14:57:01 +0200 Subject: [PATCH 7/9] black --- Mergin/project_settings_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Mergin/project_settings_widget.py b/Mergin/project_settings_widget.py index a68c1f9e..f8326611 100644 --- a/Mergin/project_settings_widget.py +++ b/Mergin/project_settings_widget.py @@ -237,7 +237,9 @@ def update_preview(self, expression, layer, field_name): # check if evaluated expression contains invalid filename characters match = illegal_filename_chars.search(val) if match: - self.label_preview.setText(f"The file name '{val}.jpg' contains an invalid character. Do not use '{match.group()}' character in the file name.") + self.label_preview.setText( + f"The file name '{val}.jpg' contains an invalid character. Do not use '{match.group()}' character in the file name." + ) return config = layer.fields().field(field_name).editorWidgetSetup().config() target_dir = resolve_target_dir(layer, config) From 20ca0f7909268c46fcf0aa332e4f612c6966b336 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Mon, 15 Sep 2025 09:02:58 +0200 Subject: [PATCH 8/9] invalid_filename_character() in utils --- Mergin/project_settings_widget.py | 9 ++++----- Mergin/ui/ui_project_config.ui | 20 ++++++-------------- Mergin/utils.py | 9 +++++++++ Mergin/validation.py | 4 ++-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Mergin/project_settings_widget.py b/Mergin/project_settings_widget.py index f8326611..07e32da5 100644 --- a/Mergin/project_settings_widget.py +++ b/Mergin/project_settings_widget.py @@ -4,7 +4,6 @@ import json import os import typing -import re from qgis.PyQt import uic from qgis.PyQt.QtGui import QIcon, QColor from qgis.PyQt.QtCore import Qt @@ -31,11 +30,11 @@ 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") ProjectConfigUiWidget, _ = uic.loadUiType(ui_file) -illegal_filename_chars = re.compile(r'[\x00-\x19<>:|?*"]') class MerginProjectConfigFactory(QgsOptionsWidgetFactory): @@ -235,10 +234,10 @@ def update_preview(self, expression, layer, field_name): return if val: # check if evaluated expression contains invalid filename characters - match = illegal_filename_chars.search(val) - if match: + 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 '{match.group()}' character in the file name." + 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() diff --git a/Mergin/ui/ui_project_config.ui b/Mergin/ui/ui_project_config.ui index 3659367a..694d7f9c 100644 --- a/Mergin/ui/ui_project_config.ui +++ b/Mergin/ui/ui_project_config.ui @@ -9,8 +9,8 @@ 0 0 - 822 - 1005 + 642 + 556 @@ -44,9 +44,9 @@ 0 - 0 - 808 - 1396 + -253 + 687 + 1447 @@ -226,15 +226,7 @@ - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Photo name format</span></p> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Set up custom photo names format based on an expression. Make sure that the name is unique for each photo.</p> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">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 style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">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 style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Use only allowed filename characters A-z, 0-9, ' ', '-' and '_'. <br />'/' can be used for subfolders.</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 2637b213..a5bff6cf 100644 --- a/Mergin/validation.py +++ b/Mergin/validation.py @@ -28,11 +28,11 @@ QGIS_NET_PROVIDERS, is_versioned_file, get_layer_by_path, + invalid_filename_character, ) INVALID_FIELD_NAME_CHARS = re.compile('[\\\/\(\)\[\]\{\}"\n\r]') PROJECT_VARS = re.compile("\@project_home|\@project_path|\@project_folder") -DISALLOWED_FILENAME_EXPRESSIONS = ["now()"] class Warning(Enum): @@ -459,7 +459,7 @@ def check_editor_perms(self): 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 not is_valid_filename(file["path"]): + if invalid_filename_character(file["path"]): self.issues.append(MultipleLayersWarning(Warning.INVALID_ADDED_FILENAME, file["path"])) From 8c73727b3a583384117bf9973757d35eae9e69e5 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Mon, 15 Sep 2025 09:05:23 +0200 Subject: [PATCH 9/9] cleanup --- Mergin/ui/ui_project_config.ui | 6 +++--- Mergin/validation.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Mergin/ui/ui_project_config.ui b/Mergin/ui/ui_project_config.ui index 694d7f9c..d2da2177 100644 --- a/Mergin/ui/ui_project_config.ui +++ b/Mergin/ui/ui_project_config.ui @@ -44,9 +44,9 @@ 0 - -253 - 687 - 1447 + -888 + 628 + 1444 diff --git a/Mergin/validation.py b/Mergin/validation.py index a5bff6cf..0f2e3eb9 100644 --- a/Mergin/validation.py +++ b/Mergin/validation.py @@ -6,7 +6,6 @@ from enum import Enum from collections import defaultdict from pathlib import Path -from pathvalidate import is_valid_filename from qgis.core import ( QgsMapLayerType,