diff --git a/lizmap/definitions/qgis_settings.py b/lizmap/definitions/qgis_settings.py
new file mode 100644
index 00000000..33c48462
--- /dev/null
+++ b/lizmap/definitions/qgis_settings.py
@@ -0,0 +1,26 @@
+"""Definitions for QgsSettings."""
+
+# TODO, use the settings API from QGIS 3.30 etc
+# Mail QGIS-Dev 24/10/2023
+
+__copyright__ = 'Copyright 2023, 3Liz'
+__license__ = 'GPL version 3'
+__email__ = 'info@3liz.org'
+
+KEY = 'lizmap'
+
+
+class Settings:
+
+ @classmethod
+ def key(cls, key):
+ return KEY + '/' + key
+
+ PreventEcw = 'prevent_ecw'
+ PreventPgAuthId = 'prevent_pg_auth_id'
+ PreventPgService = 'prevent_pg_service'
+ ForcePgUserPass = 'force_pg_user_password'
+ PreventNetworkDrive = 'prevent_network_drive'
+ AllowParentFolder = 'allow_parent_folder'
+ NumberParentFolder = 'number_parent_folder'
+ BeginnerMode = 'beginner_mode'
diff --git a/lizmap/dialogs/main.py b/lizmap/dialogs/main.py
index e0e291a1..2921b3d1 100755
--- a/lizmap/dialogs/main.py
+++ b/lizmap/dialogs/main.py
@@ -21,13 +21,14 @@
)
from qgis.utils import OverrideCursor, iface
+from lizmap.definitions.qgis_settings import Settings
from lizmap.log_panel import LogPanel
from lizmap.project_checker_tools import (
project_trust_layer_metadata,
simplify_provider_side,
use_estimated_metadata,
)
-from lizmap.saas import fix_ssl
+from lizmap.saas import SAAS_MAX_PARENT_FOLDER, SAAS_NAME, fix_ssl
try:
from qgis.PyQt.QtWebKitWidgets import QWebView
@@ -44,7 +45,12 @@
from lizmap.qgis_plugin_tools.tools.i18n import tr
from lizmap.qgis_plugin_tools.tools.resources import load_ui, resources_path
from lizmap.qt_style_sheets import COMPLETE_STYLE_SHEET
-from lizmap.tools import format_qgis_version, human_size, qgis_version
+from lizmap.tools import (
+ format_qgis_version,
+ human_size,
+ qgis_version,
+ relative_path,
+)
FORM_CLASS = load_ui('ui_lizmap.ui')
LOGGER = logging.getLogger("Lizmap")
@@ -171,6 +177,103 @@ def __init__(self, parent=None):
)
self.label_file_action.setOpenExternalLinks(True)
+ self.radio_beginner.setToolTip(
+ 'If one safeguard is not OK, the Lizmap configuration file is not going to be generated.'
+ )
+ self.radio_normal.setToolTip(
+ 'If one safeguard is not OK, only a warning will be displayed, not blocking the saving of the Lizmap '
+ 'configuration file.'
+ )
+
+ msg_parent_force_local = tr('Prevent file based layers to be in a parent folder')
+ self.radio_force_local_folder.setText(msg_parent_force_local)
+ self.radio_force_local_folder.setToolTip(tr(
+ 'Files must be located in {} or in a sub directory.'
+ ).format(self.project.absolutePath()))
+
+ msg_parent_folder = tr('Allow file based layers to be in a parent folder')
+ self.radio_allow_parent_folder.setText(msg_parent_folder)
+ self.radio_allow_parent_folder.setToolTip(tr(
+ 'Files can be located in a parent folder from {}, up to the setting below.'
+ ).format(self.project.absolutePath()))
+
+ msg_network_drive = tr('Prevent file based layers to be stored on another network drive')
+ self.safe_network_drive.setText(msg_network_drive)
+
+ msg_service = tr('Prevent PostgreSQL layers to use a service file')
+ self.safe_pg_service.setText(msg_service)
+
+ msg_auth_db = tr('Prevent PostgreSQL layers to use the authentication database')
+ self.safe_pg_auth_db.setText(msg_auth_db)
+
+ msg_pg_user_pass = tr(
+ 'PostgreSQL layers, if using a user and password, must have credentials saved in the datasource')
+ self.safe_pg_user_password.setText(msg_pg_user_pass)
+
+ msg_ecw = tr('Prevent from using a ECW raster')
+ self.safe_ecw.setText(msg_ecw)
+
+ # Normal / beginner
+ self.radio_normal.setChecked(
+ not QgsSettings().value(Settings.key(Settings.BeginnerMode), type=bool))
+ self.radio_beginner.setChecked(
+ QgsSettings().value(Settings.key(Settings.BeginnerMode), type=bool))
+ self.radio_normal.toggled.connect(self.radio_mode_normal_toggled)
+ self.radio_normal.toggled.connect(self.save_settings)
+ self.radio_mode_normal_toggled()
+
+ # Parent or subdirectory
+ self.radio_force_local_folder.setChecked(
+ not QgsSettings().value(Settings.key(Settings.AllowParentFolder), type=bool))
+ self.radio_allow_parent_folder.setChecked(
+ QgsSettings().value(Settings.key(Settings.AllowParentFolder), type=bool))
+ self.radio_allow_parent_folder.toggled.connect(self.radio_parent_folder_toggled)
+ self.radio_allow_parent_folder.toggled.connect(self.save_settings)
+ self.radio_parent_folder_toggled()
+
+ # Number
+ self.safe_number_parent.setValue(QgsSettings().value(Settings.key(Settings.NumberParentFolder)))
+ self.safe_number_parent.valueChanged.connect(self.save_settings)
+
+ # Network drive
+ self.safe_network_drive.setChecked(QgsSettings().value(Settings.key(Settings.PreventNetworkDrive), type=bool))
+ self.safe_network_drive.toggled.connect(self.save_settings)
+
+ # PG Service
+ self.safe_pg_service.setChecked(QgsSettings().value(Settings.key(Settings.PreventPgService), type=bool))
+ self.safe_pg_service.toggled.connect(self.save_settings)
+
+ # PG Auth DB
+ self.safe_pg_auth_db.setChecked(QgsSettings().value(Settings.key(Settings.PreventPgAuthId), type=bool))
+ self.safe_pg_auth_db.toggled.connect(self.save_settings)
+
+ # User password
+ self.safe_pg_user_password.setChecked(QgsSettings().value(Settings.key(Settings.ForcePgUserPass), type=bool))
+ self.safe_pg_user_password.toggled.connect(self.save_settings)
+
+ # ECW
+ self.safe_ecw.setChecked(QgsSettings().value(Settings.key(Settings.PreventEcw), type=bool))
+ self.safe_ecw.toggled.connect(self.save_settings)
+
+ self.label_safe_lizmap_cloud.setText(tr("Some safe guards are overridden by {}.").format(SAAS_NAME))
+ msg = (
+ '
'
+ '- {max_parent}
'
+ '- {network}
'
+ '- {auth_db}
'
+ '- {user_pass}
'
+ '- {ecw}
'
+ '
'.format(
+ max_parent=tr("Maximum of parent folder {} : {}").format(
+ SAAS_MAX_PARENT_FOLDER, relative_path(SAAS_MAX_PARENT_FOLDER)),
+ network=msg_network_drive,
+ auth_db=msg_auth_db,
+ user_pass=msg_pg_user_pass,
+ ecw=msg_ecw,
+ )
+ )
+ self.label_safe_lizmap_cloud.setToolTip(msg)
+
def check_api_key_address(self):
""" Check the API key is provided for the address search bar. """
provider = self.liExternalSearch.currentData()
@@ -723,6 +826,44 @@ def check_action_file_exists(self) -> bool:
self.label_file_action_found.setText("" + tr('Not found') + "")
return False
+ def radio_parent_folder_toggled(self):
+ """ When the parent allowed folder radio is toggled. """
+ parent_allowed = self.radio_allow_parent_folder.isChecked()
+ widgets = (
+ self.label_parent_folder,
+ self.safe_number_parent,
+ )
+ for widget in widgets:
+ widget.setEnabled(parent_allowed)
+
+ def radio_mode_normal_toggled(self):
+ """ When the beginner/normal radio are toggled. """
+ is_normal = self.radio_normal.isChecked()
+ widgets = (
+ self.group_file_layer,
+ self.safe_number_parent,
+ self.safe_network_drive,
+ self.safe_pg_service,
+ self.safe_pg_auth_db,
+ self.safe_pg_user_password,
+ self.safe_ecw,
+ self.label_parent_folder,
+ )
+ for widget in widgets:
+ widget.setEnabled(is_normal)
+ widget.setVisible(is_normal)
+
+ def save_settings(self):
+ """ Save settings checkboxes. """
+ QgsSettings().setValue(Settings.key(Settings.BeginnerMode), not self.radio_normal.isChecked())
+ QgsSettings().setValue(Settings.key(Settings.AllowParentFolder), self.radio_allow_parent_folder.isChecked())
+ QgsSettings().setValue(Settings.key(Settings.NumberParentFolder), self.safe_number_parent.value())
+ QgsSettings().setValue(Settings.key(Settings.PreventNetworkDrive), self.safe_network_drive.isChecked())
+ QgsSettings().setValue(Settings.key(Settings.PreventPgService), self.safe_pg_service.isChecked())
+ QgsSettings().setValue(Settings.key(Settings.PreventPgAuthId), self.safe_pg_auth_db.isChecked())
+ QgsSettings().setValue(Settings.key(Settings.ForcePgUserPass), self.safe_pg_user_password.isChecked())
+ QgsSettings().setValue(Settings.key(Settings.PreventEcw), self.safe_ecw.isChecked())
+
def allow_navigation(self, allow_navigation: bool, message: str = ''):
""" Allow the navigation or not in the UI. """
for i in range(1, self.mOptionsListWidget.count()):
diff --git a/lizmap/plugin.py b/lizmap/plugin.py
index 10e55904..dfc52dad 100755
--- a/lizmap/plugin.py
+++ b/lizmap/plugin.py
@@ -92,6 +92,7 @@
online_cloud_help,
online_lwc_help,
)
+from lizmap.definitions.qgis_settings import Settings
from lizmap.definitions.time_manager import TimeManagerDefinitions
from lizmap.definitions.tooltip import ToolTipDefinitions
from lizmap.definitions.warnings import Warnings
@@ -120,15 +121,16 @@
duplicated_layer_name_or_group,
duplicated_layer_with_filter,
invalid_int8_primary_key,
+ project_safeguards_checks,
project_trust_layer_metadata,
simplify_provider_side,
use_estimated_metadata,
)
from lizmap.saas import (
+ SAAS_MAX_PARENT_FOLDER,
SAAS_NAME,
check_project_ssl_postgis,
is_lizmap_cloud,
- valid_lizmap_cloud,
)
from lizmap.table_manager.base import TableManager
from lizmap.table_manager.dataviz import TableManagerDataviz
@@ -166,6 +168,7 @@
lizmap_user_folder,
next_git_tag,
qgis_version,
+ relative_path,
to_bool,
unaccent,
)
@@ -198,6 +201,39 @@ def __init__(self, iface):
# 04/01/2022
QgsSettings().remove('lizmap/instance_target_url_authid')
+ # Set some default settings when loading the plugin
+ beginner_mode = QgsSettings().value(Settings.key(Settings.BeginnerMode), defaultValue=None)
+ if beginner_mode is None:
+ QgsSettings().setValue(Settings.key(Settings.BeginnerMode), True)
+
+ prevent_ecw = QgsSettings().value(Settings.key(Settings.PreventEcw), defaultValue=None)
+ if prevent_ecw is None:
+ QgsSettings().setValue(Settings.key(Settings.PreventEcw), True)
+
+ prevent_auth_id = QgsSettings().value(Settings.key(Settings.PreventPgAuthId), defaultValue=None)
+ if prevent_auth_id is None:
+ QgsSettings().setValue(Settings.key(Settings.PreventPgAuthId), True)
+
+ prevent_service = QgsSettings().value(Settings.key(Settings.PreventPgService), defaultValue=None)
+ if prevent_service is None:
+ QgsSettings().setValue(Settings.key(Settings.PreventPgService), True)
+
+ force_pg_user_pass = QgsSettings().value(Settings.key(Settings.ForcePgUserPass), defaultValue=None)
+ if force_pg_user_pass is None:
+ QgsSettings().setValue(Settings.key(Settings.ForcePgUserPass), True)
+
+ prevent_network_drive = QgsSettings().value(Settings.key(Settings.PreventNetworkDrive), defaultValue=None)
+ if prevent_network_drive is None:
+ QgsSettings().setValue(Settings.key(Settings.PreventNetworkDrive), True)
+
+ allow_parent_folder = QgsSettings().value(Settings.key(Settings.AllowParentFolder), defaultValue=None)
+ if allow_parent_folder is None:
+ QgsSettings().setValue(Settings.key(Settings.AllowParentFolder), False)
+
+ parent_folder = QgsSettings().value(Settings.key(Settings.NumberParentFolder), defaultValue=None)
+ if parent_folder is None:
+ QgsSettings().setValue(Settings.key(Settings.NumberParentFolder), 2)
+
# Connect the current project filepath
self.current_path = None
# noinspection PyUnresolvedReferences
@@ -359,6 +395,7 @@ def write_log_message(message, tag, level):
self.lizmap_cloud = [
self.dlg.label_lizmap_search_grant,
+ self.dlg.label_safe_lizmap_cloud,
]
# Add widgets (not done in lizmap_var to avoid dependencies on ui)
@@ -2852,14 +2889,45 @@ def project_config_file(
server_metadata = self.dlg.server_combo.currentData(ServerComboData.JsonMetadata.value)
- if check_server and is_lizmap_cloud(server_metadata):
- results, more = valid_lizmap_cloud(self.project)
+ if check_server:
+
+ # Global checks config
+ prevent_ecw = QgsSettings().value(Settings.key(Settings.PreventEcw), True, bool)
+ prevent_auth_id = QgsSettings().value(Settings.key(Settings.PreventPgAuthId), True, bool)
+ prevent_service = QgsSettings().value(Settings.key(Settings.PreventPgService), True, bool)
+ force_pg_user_pass = QgsSettings().value(Settings.key(Settings.ForcePgUserPass), True, bool)
+ prevent_network_drive = QgsSettings().value(Settings.key(Settings.PreventNetworkDrive), True, bool)
+ allow_parent_folder = QgsSettings().value(Settings.key(Settings.AllowParentFolder), False, bool)
+ parent_folder = relative_path(QgsSettings().value(Settings.key(Settings.NumberParentFolder), 2, int))
+
+ beginner_mode = QgsSettings().value(Settings.key(Settings.BeginnerMode), True, bool)
+
+ lizmap_cloud = is_lizmap_cloud(server_metadata)
+ if lizmap_cloud:
+ # But Lizmap Cloud override some user globals checks
+ prevent_ecw = True
+ prevent_auth_id = True
+ force_pg_user_pass = True
+ prevent_network_drive = True
+ parent_folder = relative_path(SAAS_MAX_PARENT_FOLDER)
+ # prevent_service = False We encourage service
+ # allow_parent_folder = False Of course we can
+
+ results, more = project_safeguards_checks(
+ self.project,
+ prevent_ecw=prevent_ecw,
+ prevent_auth_id=prevent_auth_id,
+ prevent_service=prevent_service,
+ force_pg_user_pass=force_pg_user_pass,
+ prevent_network_drive=prevent_network_drive,
+ allow_parent_folder=allow_parent_folder,
+ parent_folder=parent_folder,
+ lizmap_cloud=lizmap_cloud,
+ )
if len(results):
show_log_panel = True
warnings.append(Warnings.SaasLizmapCloud.value)
- self.dlg.log_panel.append(tr(
- 'Some configurations are not valid with {} hosting'
- ).format(SAAS_NAME), Html.H2)
+ self.dlg.log_panel.append(tr('Some safeguards are not compatible'), Html.H2)
self.dlg.log_panel.append(warning_suggest, Html.P)
self.dlg.log_panel.append("
")
@@ -2876,9 +2944,18 @@ def project_config_file(
self.dlg.log_panel.append(more)
self.dlg.log_panel.append("
")
- self.dlg.log_panel.append(tr(
- "The process is continuing but expect these layers to not be visible in Lizmap Web Client."
- ), Html.P)
+ if beginner_mode:
+ error_cfg_saving = True
+ self.dlg.log_panel.append(tr(
+ "The process is stopping, the CFG file is not going to be generated because some safeguards "
+ "are not compatible and you are using the 'Beginner' mode. Either fix these issues or switch "
+ "to a 'Normal' mode if you know what you are doing."
+ ), Html.P, level=Qgis.Critical)
+ else:
+ self.dlg.log_panel.append(tr(
+ "The process is continuing but expect these layers to not be visible in Lizmap Web Client if "
+ "QGIS Server knows what to do with invalid layers."
+ ), Html.P)
if check_server:
diff --git a/lizmap/project_checker_tools.py b/lizmap/project_checker_tools.py
index f573ab89..36ca796c 100644
--- a/lizmap/project_checker_tools.py
+++ b/lizmap/project_checker_tools.py
@@ -2,24 +2,164 @@
__license__ = 'GPL version 3'
__email__ = 'info@3liz.org'
-from typing import List, Optional, Tuple
+from os.path import relpath
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
from qgis.core import (
QgsDataSourceUri,
QgsLayerTree,
QgsMapLayer,
QgsProject,
+ QgsProviderRegistry,
+ QgsRasterLayer,
QgsVectorLayer,
QgsWkbTypes,
)
from lizmap.qgis_plugin_tools.tools.i18n import tr
+from lizmap.saas import (
+ SAAS_DOMAIN,
+ SAAS_NAME,
+ edit_connection,
+ edit_connection_title,
+ right_click_step,
+)
+from lizmap.tools import is_vector_pg, update_uri
""" Some checks which can be done on a layer. """
# https://github.com/3liz/lizmap-web-client/issues/3692
+def project_safeguards_checks(
+ project: QgsProject,
+ prevent_ecw: bool,
+ prevent_auth_id: bool,
+ prevent_service: bool,
+ force_pg_user_pass: bool,
+ prevent_network_drive: bool,
+ allow_parent_folder: bool,
+ parent_folder: str,
+ lizmap_cloud: bool,
+) -> Tuple[Dict[str, str], str]:
+ """ Check the project about safeguards. """
+ # Do not use homePath, it's not designed for this if the user has set a custom home path
+ project_home = Path(project.absolutePath())
+ layer_error: Dict[str, str] = {}
+
+ connection_error = False
+ for layer in project.mapLayers().values():
+
+ if isinstance(layer, QgsRasterLayer):
+ if layer.source().lower().endswith('ecw') and prevent_ecw:
+ if lizmap_cloud:
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" is an ECW. Because of the ECW\'s licence, this format is not compatible with '
+ 'QGIS server. You should switch to a COG format.'
+ ).format(layer.name())
+ else:
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" is an ECW. You have activated a safeguard about preventing you using an ECW '
+ 'layer. Either switch to a COG format or disable this safeguard.'
+ ).format(layer.name())
+
+ if is_vector_pg(layer):
+ datasource = QgsDataSourceUri(layer.source())
+ if datasource.authConfigId() != '' and prevent_auth_id:
+ if lizmap_cloud:
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" is using the QGIS authentication database. You must either use a PostgreSQL '
+ 'service or store the login and password in the layer.'
+ ).format(layer.name())
+ else:
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" is using the QGIS authentication database. You have activated a safeguard '
+ 'preventing you using the QGIS authentication database. Either switch to another '
+ 'authentication mechanism or disable this safeguard.'
+ ).format(layer.name())
+ connection_error = True
+
+ if datasource.service() != '' and prevent_service:
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" is using the PostgreSQL service file. Using a service file can be recommended in '
+ 'many cases, but it requires a configuration step. If you have done the configuration (on the '
+ 'server side mainly), you can disable this safeguard.'
+ ).format(layer.name())
+
+ if datasource.host().endswith(SAAS_DOMAIN) or force_pg_user_pass:
+ if not datasource.username() or not datasource.password():
+ if lizmap_cloud:
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" is missing some credentials. Either the user and/or the password is not in '
+ 'the layer datasource.'
+ ).format(layer.name())
+ else:
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" is missing some credentials. Either the user and/or the password is not in '
+ 'the layer datasource, or disable the safeguard.'
+ ).format(layer.name())
+ connection_error = True
+
+ components = QgsProviderRegistry.instance().decodeUri(layer.dataProvider().name(), layer.source())
+ if 'path' not in components.keys():
+ # The layer is not file base.
+ continue
+
+ layer_path = Path(components['path'])
+ if not layer_path.exists():
+ # Let's skip, QGIS is already warning this layer
+ continue
+
+ try:
+ relative_path = relpath(layer_path, project_home)
+ except ValueError:
+ # https://docs.python.org/3/library/os.path.html#os.path.relpath
+ # On Windows, ValueError is raised when path and start are on different drives.
+ # For instance, H: and C:
+ # Lizmap Cloud message must be prioritized
+ if lizmap_cloud:
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" can not be hosted on {} because the layer is hosted on a different drive.'
+ ).format(layer.name(), SAAS_NAME)
+ continue
+ elif prevent_network_drive:
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" is on another drive. Either move this file based layer or disable this safeguard.'
+ ).format(layer.name())
+ continue
+
+ # Not sure what to do for now...
+ # We can't compute a relative path, but the user didn't enable the safety check, so we must still skip
+ continue
+
+ if parent_folder in relative_path and allow_parent_folder:
+ if lizmap_cloud:
+ # The layer can only be hosted the in "/qgis" directory
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" can not be hosted on {} because the layer is located in too many '
+ 'parent\'s folder. The current path from the project home path to the given layer is "{}".'
+ ).format(layer.name(), SAAS_NAME, relative_path)
+ else:
+ layer_error[layer.name()] = tr(
+ 'The layer "{}" is located in too many parent\'s folder. Either move this file based layer or '
+ 'disable this safeguard. The current path from the project home path to the given layer is "{}".'
+ ).format(layer.name(), relative_path)
+
+ more = ''
+ if connection_error:
+ more = edit_connection_title + " "
+ more += edit_connection + " "
+ more += '
'
+ more += right_click_step + " "
+ more += tr(
+ "When opening a QGIS project in your desktop, you mustn't have any "
+ "prompt for a user&password."
+ )
+
+ return layer_error, more
+
+
def auto_generated_primary_key_field(layer: QgsVectorLayer) -> Tuple[bool, Optional[str]]:
""" If the primary key has been detected as tid/ctid but the field does not exist. """
# Example
@@ -143,23 +283,6 @@ def duplicated_layer_with_filter(project: QgsProject) -> Optional[str]:
return text
-def is_vector_pg(layer: QgsMapLayer, geometry_check=False) -> bool:
- """ Return boolean if the layer is stored in PG and is a vector with a geometry. """
- if layer.type() != QgsMapLayer.VectorLayer:
- return False
-
- if layer.dataProvider().name() != 'postgres':
- return False
-
- if not geometry_check:
- return True
-
- if not layer.isSpatial():
- return False
-
- return True
-
-
def simplify_provider_side(project: QgsProject, fix=False) -> List[str]:
""" Return the list of layer name which can be simplified on the server side. """
results = []
@@ -208,13 +331,3 @@ def project_trust_layer_metadata(project: QgsProject, fix: bool = False) -> bool
project.setTrustLayerMetadata(True)
return True
-
-
-def update_uri(layer: QgsMapLayer, uri: QgsDataSourceUri):
- """ Set a new datasource URI on a layer. """
- layer.setDataSource(
- uri.uri(True),
- layer.name(),
- layer.dataProvider().name(),
- layer.dataProvider().ProviderOptions()
- )
diff --git a/lizmap/resources/ui/ui_lizmap.ui b/lizmap/resources/ui/ui_lizmap.ui
index 5f738c6f..c0a26bfd 100755
--- a/lizmap/resources/ui/ui_lizmap.ui
+++ b/lizmap/resources/ui/ui_lizmap.ui
@@ -4589,6 +4589,16 @@ This is different to the map maximum extent (defined in QGIS project properties,
-
-
+
+
+ Convert all PostgreSQL layers used in this project to use SSL
+
+
+ true
+
+
+
+ -
@@ -4601,17 +4611,17 @@ This is different to the map maximum extent (defined in QGIS project properties,
- -
-
+
-
+
- Convert all PostgreSQL layers used in this project to use SSL
+ Convert all PostgreSQL layers used in this project to 'estimated metadata'
true
- -
+
-
@@ -4624,52 +4634,180 @@ This is different to the map maximum extent (defined in QGIS project properties,
- -
-
+
-
+
- Convert all PostgreSQL layers used in this project to 'estimated metadata'
+ Enable the trust project option in the project properties dialog
true
- -
+
-
Trust project
- -
-
+
-
+
- Enable the trust project option in the project properties dialog
+ Enable provider geometry simplification when possible in the layer properties dialog
true
- -
+
-
Simplify geometry on the provider side
- -
-
+
+
+
+
+
+ -
+
+
+ Safe guards
+
+
+
-
+
+
+ Only if you are sure about your server and if you are more comfortable with Lizmap, you can disable some safe guards
+
+
+ true
+
+
+
+ -
+
+
+ SET IN PYTHON, HOSTING
+
+
+ true
+
+
+
+ -
+
+
-
+
- Enable provider geometry simplification when possible in the layer properties dialog
+ Beginner
-
- true
+
+
+ -
+
+
+ Normal
+ -
+
+
+ File based layer
+
+
+
-
+
+
+ SET PYTHON PREVENT FILE IN PARENT FOLDER
+
+
+
+ -
+
+
-
+
+
+ Levels of parent folder allowed
+
+
+
+ -
+
+
+ 1
+
+
+ 2
+
+
+
+ -
+
+
+ SET PYTHON ALLOW PARENT FOLDER
+
+
+
+
+
+
+
+
+ -
+
+
+ SET PYTHON PREVENT ANOTHER NETWORK DRIVE
+
+
+ false
+
+
+
+ -
+
+
+ SET PYTHON PREVENT PG SERVICE FILE
+
+
+ false
+
+
+
+ -
+
+
+ SET PYTHON PREVENT PG AUTH DB
+
+
+ false
+
+
+
+ -
+
+
+ SET PYTHON USER PASSWORD DATASOURCE
+
+
+ false
+
+
+
+ -
+
+
+ SET PYTHON PREVENT ECW
+
+
+
diff --git a/lizmap/saas.py b/lizmap/saas.py
index 80d5d531..3c656986 100644
--- a/lizmap/saas.py
+++ b/lizmap/saas.py
@@ -4,19 +4,12 @@
__license__ = 'GPL version 3'
__email__ = 'info@3liz.org'
-from os.path import relpath
-from pathlib import Path
-from typing import Dict, List, Tuple
-
-from qgis.core import (
- QgsDataSourceUri,
- QgsProject,
- QgsProviderRegistry,
- QgsRasterLayer,
-)
+from typing import List, Tuple
+
+from qgis.core import QgsDataSourceUri, QgsProject
-from lizmap.project_checker_tools import is_vector_pg, update_uri
from lizmap.qgis_plugin_tools.tools.i18n import tr
+from lizmap.tools import is_vector_pg, update_uri
edit_connection_title = tr("You must edit the database connection.")
edit_connection = tr(
@@ -31,6 +24,7 @@
SAAS_DOMAIN = 'lizmap.com'
SAAS_NAME = 'Lizmap Cloud'
+SAAS_MAX_PARENT_FOLDER = 2 # TODO Check COG, is-it 3 ?
def is_lizmap_cloud(metadata: dict) -> bool:
@@ -42,84 +36,6 @@ def is_lizmap_cloud(metadata: dict) -> bool:
return metadata.get('hosting', '') == SAAS_DOMAIN
-def valid_lizmap_cloud(project: QgsProject) -> Tuple[Dict[str, str], str]:
- """ Check the project when it's hosted on Lizmap Cloud. """
- # Do not use homePath, it's not designed for this if the user has set a custom home path
- project_home = Path(project.absolutePath())
- layer_error: Dict[str, str] = {}
-
- connection_error = False
- for layer in project.mapLayers().values():
-
- if isinstance(layer, QgsRasterLayer):
- if layer.source().lower().endswith('ecw'):
- layer_error[layer.name()] = tr(
- 'The layer "{}" is an ECW. Because of the ECW\'s licence, this format is not compatible with QGIS '
- 'server. You should switch to a COG format.').format(layer.name())
-
- if is_vector_pg(layer):
- datasource = QgsDataSourceUri(layer.source())
- if datasource.authConfigId() != '':
- layer_error[layer.name()] = tr(
- 'The layer "{}" is using the QGIS authentication database. You must either use a PostgreSQL '
- 'service or store the login and password in the layer.').format(layer.name())
- connection_error = True
-
- if datasource.service():
- # We trust the user about login, password etc ...
- continue
-
- # Users might be hosted on Lizmap Cloud but using an external database
- if datasource.host().endswith(SAAS_DOMAIN):
- if not datasource.username() or not datasource.password():
- layer_error[layer.name()] = tr(
- 'The layer "{}" is missing some credentials. Either the user and/or the password is not in '
- 'the layer datasource.'
- ).format(layer.name())
- connection_error = True
-
- components = QgsProviderRegistry.instance().decodeUri(layer.dataProvider().name(), layer.source())
- if 'path' not in components.keys():
- # The layer is not file base.
- continue
-
- layer_path = Path(components['path'])
- if not layer_path.exists():
- # Let's skip, QGIS is already warning this layer
- continue
-
- try:
- relative_path = relpath(layer_path, project_home)
- except ValueError:
- # https://docs.python.org/3/library/os.path.html#os.path.relpath
- # On Windows, ValueError is raised when path and start are on different drives.
- # For instance, H: and C:
- layer_error[layer.name()] = tr(
- 'The layer "{}" can not be hosted on {} because the layer is hosted on a different drive.'
- ).format(layer.name(), SAAS_NAME)
- continue
-
- if '../../..' in relative_path:
- # The layer can only be hosted the in "/qgis" directory
- layer_error[layer.name()] = tr(
- 'The layer "{}" can not be hosted on {} because the layer is located in too many '
- 'parent\'s folder. The current path from the project home path to the given layer is "{}".'
- ).format(layer.name(), SAAS_NAME, relative_path)
-
- more = ''
- if connection_error:
- more = edit_connection_title + " "
- more += edit_connection + " "
- more += '
'
- more += right_click_step + " "
- more += tr(
- "When opening a QGIS project in your desktop, you mustn't have any "
- "prompt for a user&password."
- )
-
- return layer_error, more
-
-
def check_project_ssl_postgis(project: QgsProject) -> Tuple[List[str], str]:
""" Check if the project is not using SSL on some PostGIS layers which are on a Lizmap Cloud database. """
layer_error: List[str] = []
diff --git a/lizmap/test/test_wizard_server.py b/lizmap/test/test_wizard_server.py
index 3ac082b6..7ae1c8eb 100644
--- a/lizmap/test/test_wizard_server.py
+++ b/lizmap/test/test_wizard_server.py
@@ -2,7 +2,7 @@
import os
import unittest
-from qgis._core import Qgis
+from qgis.core import Qgis
from qgis.PyQt.QtCore import QUrl
from qgis.PyQt.QtWidgets import QWizard
diff --git a/lizmap/tools.py b/lizmap/tools.py
index 874dba6a..d5f15a56 100755
--- a/lizmap/tools.py
+++ b/lizmap/tools.py
@@ -14,7 +14,14 @@
from pathlib import Path
from typing import List, Tuple, Union
-from qgis.core import Qgis, QgsApplication, QgsProviderRegistry, QgsVectorLayer
+from qgis.core import (
+ Qgis,
+ QgsApplication,
+ QgsDataSourceUri,
+ QgsMapLayer,
+ QgsProviderRegistry,
+ QgsVectorLayer,
+)
from qgis.PyQt.QtCore import QDateTime, QDir, Qt
from lizmap.definitions.definitions import LayerProperties
@@ -241,6 +248,39 @@ def merge_strings(string_1: str, string_2: str) -> str:
return string_1 + (string_2 if k is None else string_2[k:])
+def relative_path(max_parent: int) -> str:
+ """ Return the dot notation for a maximum parent folder. """
+ parent = ['..'] * max_parent
+ return '/'.join(parent)
+
+
+def update_uri(layer: QgsMapLayer, uri: QgsDataSourceUri):
+ """ Set a new datasource URI on a layer. """
+ layer.setDataSource(
+ uri.uri(True),
+ layer.name(),
+ layer.dataProvider().name(),
+ layer.dataProvider().ProviderOptions()
+ )
+
+
+def is_vector_pg(layer: QgsMapLayer, geometry_check=False) -> bool:
+ """ Return boolean if the layer is stored in PG and is a vector with a geometry. """
+ if layer.type() != QgsMapLayer.VectorLayer:
+ return False
+
+ if layer.dataProvider().name() != 'postgres':
+ return False
+
+ if not geometry_check:
+ return True
+
+ if not layer.isSpatial():
+ return False
+
+ return True
+
+
def convert_lizmap_popup(content: str, layer: QgsVectorLayer) -> Tuple[str, List[str]]:
""" Convert an HTML Lizmap popup to QGIS HTML Maptip.