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 = ( + ''.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.