mirror of
				https://github.com/qgis/QGIS.git
				synced 2025-10-31 00:06:02 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			948 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			948 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """
 | |
| /***************************************************************************
 | |
|                             Plugin Installer module
 | |
|                              -------------------
 | |
|     Date                 : May 2013
 | |
|     Copyright            : (C) 2013 by Borys Jurgiel
 | |
|     Email                : info at borysjurgiel dot pl
 | |
| 
 | |
|     This module is based on former plugin_installer plugin:
 | |
|       Copyright (C) 2007-2008 Matthew Perry
 | |
|       Copyright (C) 2008-2013 Borys Jurgiel
 | |
| 
 | |
|  ***************************************************************************/
 | |
| 
 | |
| /***************************************************************************
 | |
|  *                                                                         *
 | |
|  *   This program is free software; you can redistribute it and/or modify  *
 | |
|  *   it under the terms of the GNU General Public License as published by  *
 | |
|  *   the Free Software Foundation; either version 2 of the License, or     *
 | |
|  *   (at your option) any later version.                                   *
 | |
|  *                                                                         *
 | |
|  ***************************************************************************/
 | |
| """
 | |
| 
 | |
| import os
 | |
| import json
 | |
| import zipfile
 | |
| from functools import partial
 | |
| 
 | |
| from qgis.PyQt import sip
 | |
| from qgis.PyQt.QtCore import Qt, QObject, QDateTime, QDir, QUrl, QFileInfo, QFile
 | |
| from qgis.PyQt.QtWidgets import (
 | |
|     QApplication,
 | |
|     QDialog,
 | |
|     QDialogButtonBox,
 | |
|     QFrame,
 | |
|     QMessageBox,
 | |
|     QLabel,
 | |
|     QVBoxLayout,
 | |
|     QPushButton,
 | |
| )
 | |
| from qgis.PyQt.QtNetwork import QNetworkRequest
 | |
| 
 | |
| from qgis.core import (
 | |
|     Qgis,
 | |
|     QgsApplication,
 | |
|     QgsMessageLog,
 | |
|     QgsNetworkAccessManager,
 | |
|     QgsSettings,
 | |
|     QgsSettingsTree,
 | |
|     QgsNetworkRequestParameters,
 | |
| )
 | |
| from qgis.gui import QgsMessageBar, QgsPasswordLineEdit, QgsHelp
 | |
| from qgis.utils import (
 | |
|     iface,
 | |
|     startPlugin,
 | |
|     unloadPlugin,
 | |
|     loadPlugin,
 | |
|     OverrideCursor,
 | |
|     updateAvailablePlugins,
 | |
|     plugins_metadata_parser,
 | |
|     isPluginLoaded,
 | |
|     HOME_PLUGIN_PATH,
 | |
| )
 | |
| from .installer_data import repositories, plugins, officialRepo, reposGroup, removeDir
 | |
| from .qgsplugininstallerinstallingdialog import QgsPluginInstallerInstallingDialog
 | |
| from .qgsplugininstallerpluginerrordialog import QgsPluginInstallerPluginErrorDialog
 | |
| from .qgsplugininstallerfetchingdialog import QgsPluginInstallerFetchingDialog
 | |
| from .qgsplugininstallerrepositorydialog import QgsPluginInstallerRepositoryDialog
 | |
| from .unzip import unzip
 | |
| from .plugindependencies import find_dependencies
 | |
| from .qgsplugindependenciesdialog import QgsPluginDependenciesDialog
 | |
| 
 | |
| 
 | |
| # public instances:
 | |
| pluginInstaller = None
 | |
| 
 | |
| 
 | |
| def initPluginInstaller():
 | |
|     global pluginInstaller
 | |
|     pluginInstaller = QgsPluginInstaller()
 | |
| 
 | |
| 
 | |
| # -------------------------------------------------------- #
 | |
| class QgsPluginInstaller(QObject):
 | |
|     """The main class for managing the plugin installer stuff"""
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def __init__(self):
 | |
|         """Initialize data objects, starts fetching if appropriate, and warn about/removes obsolete plugins"""
 | |
| 
 | |
|         QObject.__init__(self)  # initialize QObject in order to to use self.tr()
 | |
|         repositories.load()
 | |
|         plugins.getAllInstalled()
 | |
| 
 | |
|         self.message_bar_widget = None
 | |
| 
 | |
|         if (
 | |
|             repositories.checkingOnStart()
 | |
|             and repositories.timeForChecking()
 | |
|             and repositories.allEnabled()
 | |
|         ):
 | |
|             # start fetching repositories
 | |
|             repositories.checkingDone.connect(self.checkingDone)
 | |
|             for key in repositories.allEnabled():
 | |
|                 repositories.requestFetching(key)
 | |
|         else:
 | |
|             # no fetching at start, so mark all enabled repositories as requesting to be fetched.
 | |
|             for key in repositories.allEnabled():
 | |
|                 repositories.setRepositoryData(key, "state", 3)
 | |
| 
 | |
|         # look for obsolete plugins updates (the user-installed one is older than the core one)
 | |
|         for key in plugins.obsoletePlugins:
 | |
|             plugin = plugins.localCache[key]
 | |
|             msg = QMessageBox()
 | |
|             msg.setIcon(QMessageBox.Icon.Warning)
 | |
|             msg.setWindowTitle(self.tr("QGIS Python Plugin Installer"))
 | |
|             msg.addButton(
 | |
|                 self.tr("Uninstall (recommended)"), QMessageBox.ButtonRole.AcceptRole
 | |
|             )
 | |
|             msg.addButton(
 | |
|                 self.tr("I will uninstall it later"), QMessageBox.ButtonRole.RejectRole
 | |
|             )
 | |
|             msg.setText(
 | |
|                 "{} <b>{}</b><br/><br/>{}".format(
 | |
|                     self.tr("Obsolete plugin:"),
 | |
|                     plugin["name"],
 | |
|                     self.tr(
 | |
|                         "QGIS has detected an obsolete plugin that masks its more recent version shipped with this copy of QGIS. This is likely due to files associated with a previous installation of QGIS. Do you want to remove the old plugin right now and unmask the more recent version?"
 | |
|                     ),
 | |
|                 )
 | |
|             )
 | |
|             msg.exec()
 | |
|             if not msg.result():
 | |
|                 settings = QgsSettings()
 | |
|                 plugin_is_active = settings.value(
 | |
|                     "/PythonPlugins/" + key, False, type=bool
 | |
|                 )
 | |
| 
 | |
|                 # uninstall the update, update utils and reload if enabled
 | |
|                 self.uninstallPlugin(key, quiet=True)
 | |
|                 updateAvailablePlugins()
 | |
|                 if plugin_is_active:
 | |
|                     settings.setValue(
 | |
|                         "/PythonPlugins/watchDogTimestamp/" + key,
 | |
|                         QDateTime.currentDateTime().toSecsSinceEpoch(),
 | |
|                     )
 | |
|                     loadPlugin(key)
 | |
|                     startPlugin(key)
 | |
|                     settings.remove("/PythonPlugins/watchDogTimestamp/" + key)
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def fetchAvailablePlugins(self, reloadMode):
 | |
|         """Fetch plugins from all enabled repositories."""
 | |
|         """  reloadMode = true:  Fully refresh data from QgsSettings to mRepositories  """
 | |
|         """  reloadMode = false: Fetch unready repositories only """
 | |
|         if reloadMode:
 | |
|             repositories.load()
 | |
|             plugins.clearRepoCache()
 | |
|             plugins.getAllInstalled()
 | |
| 
 | |
|         for key in repositories.allEnabled():
 | |
|             if (
 | |
|                 reloadMode or repositories.all()[key]["state"] == 3
 | |
|             ):  # if state = 3 (error or not fetched yet), try to fetch once again
 | |
|                 repositories.requestFetching(key, force_reload=reloadMode)
 | |
| 
 | |
|         if repositories.fetchingInProgress():
 | |
|             fetchDlg = QgsPluginInstallerFetchingDialog(iface.mainWindow())
 | |
|             fetchDlg.exec()
 | |
|             del fetchDlg
 | |
|             for key in repositories.all():
 | |
|                 repositories.killConnection(key)
 | |
| 
 | |
|         # display error messages for every unavailable repository, unless Shift pressed nor all repositories are unavailable
 | |
|         keepQuiet = QgsApplication.keyboardModifiers() == Qt.KeyboardModifiers(
 | |
|             Qt.KeyboardModifier.ShiftModifier
 | |
|         )
 | |
|         if (
 | |
|             repositories.allUnavailable()
 | |
|             and repositories.allUnavailable() != repositories.allEnabled()
 | |
|         ):
 | |
|             for key in repositories.allUnavailable():
 | |
|                 if not keepQuiet:
 | |
|                     QMessageBox.warning(
 | |
|                         iface.mainWindow(),
 | |
|                         self.tr("QGIS Python Plugin Installer"),
 | |
|                         self.tr("Error reading repository:")
 | |
|                         + " "
 | |
|                         + key
 | |
|                         + "\n\n"
 | |
|                         + repositories.all()[key]["error"],
 | |
|                     )
 | |
|                 if QgsApplication.keyboardModifiers() == Qt.KeyboardModifiers(
 | |
|                     Qt.KeyboardModifier.ShiftModifier
 | |
|                 ):
 | |
|                     keepQuiet = True
 | |
|         # finally, rebuild plugins from the caches
 | |
|         plugins.rebuild()
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def checkingDone(self):
 | |
|         """Remove the "Looking for new plugins..." label and display a notification instead if any updates available"""
 | |
| 
 | |
|         # rebuild plugins cache
 | |
|         plugins.rebuild()
 | |
|         # look for news in the repositories
 | |
|         plugins.markNews()
 | |
|         # then check for updates (and eventually overwrite status)
 | |
|         updatable_plugin_names = []
 | |
|         for _, properties in plugins.all().items():
 | |
|             if properties["status"] == "upgradeable":
 | |
|                 updatable_plugin_names.append(properties["name"])
 | |
| 
 | |
|         if not updatable_plugin_names:
 | |
|             return
 | |
| 
 | |
|         if len(updatable_plugin_names) >= 2:
 | |
|             status = self.tr("Multiple plugin updates are available")
 | |
|         else:
 | |
|             status = self.tr("An update to the {} plugin is available").format(
 | |
|                 updatable_plugin_names[0]
 | |
|             )
 | |
| 
 | |
|         QgsMessageLog.logMessage(
 | |
|             "Plugin update(s) available : {}".format(",".join(updatable_plugin_names)),
 | |
|             self.tr("Plugins"),
 | |
|         )
 | |
| 
 | |
|         bar = iface.messageBar()
 | |
|         self.message_bar_widget = bar.createMessage("", status)
 | |
|         update_button = QPushButton(self.tr("Install Updates…"))
 | |
|         tab_index = 3  # PLUGMAN_TAB_UPGRADEABLE
 | |
|         update_button.pressed.connect(
 | |
|             partial(self.showPluginManagerWhenReady, tab_index)
 | |
|         )
 | |
|         self.message_bar_widget.layout().addWidget(update_button)
 | |
|         bar.pushWidget(self.message_bar_widget, Qgis.MessageLevel.Info)
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def exportRepositoriesToManager(self):
 | |
|         """Update manager's repository tree widget with current data"""
 | |
|         iface.pluginManagerInterface().clearRepositoryList()
 | |
|         for key in repositories.all():
 | |
|             url = repositories.all()[key]["url"] + repositories.urlParams()
 | |
|             if repositories.inspectionFilter():
 | |
|                 enabled = key == repositories.inspectionFilter()
 | |
|             else:
 | |
|                 enabled = repositories.all()[key]["enabled"]
 | |
|             iface.pluginManagerInterface().addToRepositoryList(
 | |
|                 {
 | |
|                     "name": key,
 | |
|                     "url": url,
 | |
|                     "enabled": enabled and "true" or "false",
 | |
|                     "valid": repositories.all()[key]["valid"] and "true" or "false",
 | |
|                     "state": str(repositories.all()[key]["state"]),
 | |
|                     "error": repositories.all()[key]["error"],
 | |
|                     "inspection_filter": repositories.inspectionFilter()
 | |
|                     and "true"
 | |
|                     or "false",
 | |
|                 }
 | |
|             )
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def exportPluginsToManager(self):
 | |
|         """Insert plugins metadata to QgsMetadataRegistry"""
 | |
|         iface.pluginManagerInterface().clearPythonPluginMetadata()
 | |
|         for key in plugins.all():
 | |
|             plugin = plugins.all()[key]
 | |
|             iface.pluginManagerInterface().addPluginMetadata(
 | |
|                 {
 | |
|                     "id": key,
 | |
|                     "plugin_id": plugin["plugin_id"] or "",
 | |
|                     "name": plugin["name"],
 | |
|                     "description": plugin["description"],
 | |
|                     "about": plugin["about"],
 | |
|                     "category": plugin["category"],
 | |
|                     "tags": plugin["tags"],
 | |
|                     "changelog": plugin["changelog"],
 | |
|                     "author_name": plugin["author_name"],
 | |
|                     "author_email": plugin["author_email"],
 | |
|                     "homepage": plugin["homepage"],
 | |
|                     "tracker": plugin["tracker"],
 | |
|                     "code_repository": plugin["code_repository"],
 | |
|                     "version_installed": plugin["version_installed"],
 | |
|                     "library": plugin["library"],
 | |
|                     "icon": plugin["icon"],
 | |
|                     "readonly": plugin["readonly"] and "true" or "false",
 | |
|                     "installed": plugin["installed"] and "true" or "false",
 | |
|                     "available": plugin["available"] and "true" or "false",
 | |
|                     "status": plugin["status"],
 | |
|                     "status_exp": plugin["status_exp"],
 | |
|                     "error": plugin["error"],
 | |
|                     "error_details": plugin["error_details"],
 | |
|                     "create_date": plugin["create_date"],
 | |
|                     "update_date": plugin["update_date"],
 | |
|                     "create_date_stable": plugin["create_date_stable"],
 | |
|                     "update_date_stable": plugin["update_date_stable"],
 | |
|                     "create_date_experimental": plugin["create_date_experimental"],
 | |
|                     "update_date_experimental": plugin["update_date_experimental"],
 | |
|                     "experimental": plugin["experimental"] and "true" or "false",
 | |
|                     "deprecated": plugin["deprecated"] and "true" or "false",
 | |
|                     "trusted": plugin["trusted"] and "true" or "false",
 | |
|                     "version_available": plugin["version_available"],
 | |
|                     "version_available_stable": plugin["version_available_stable"]
 | |
|                     or "",
 | |
|                     "version_available_experimental": plugin[
 | |
|                         "version_available_experimental"
 | |
|                     ]
 | |
|                     or "",
 | |
|                     "zip_repository": plugin["zip_repository"],
 | |
|                     "download_url": plugin["download_url"],
 | |
|                     "download_url_stable": plugin["download_url_stable"],
 | |
|                     "download_url_experimental": plugin["download_url_experimental"],
 | |
|                     "filename": plugin["filename"],
 | |
|                     "downloads": plugin["downloads"],
 | |
|                     "average_vote": plugin["average_vote"],
 | |
|                     "rating_votes": plugin["rating_votes"],
 | |
|                     "plugin_dependencies": plugin.get("plugin_dependencies", None),
 | |
|                     "pythonic": "true",
 | |
|                 }
 | |
|             )
 | |
|         iface.pluginManagerInterface().reloadModel()
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def reloadAndExportData(self):
 | |
|         """Reload All repositories and export data to the Plugin Manager"""
 | |
|         self.fetchAvailablePlugins(reloadMode=True)
 | |
|         self.exportRepositoriesToManager()
 | |
|         self.exportPluginsToManager()
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def showPluginManagerWhenReady(self, *params):
 | |
|         """Open the plugin manager window. If fetching is still in progress, it shows the progress window first"""
 | |
|         """ Optionally pass the index of tab to be opened in params """
 | |
|         if self.message_bar_widget:
 | |
|             if not sip.isdeleted(self.message_bar_widget):
 | |
|                 iface.messageBar().popWidget(self.message_bar_widget)
 | |
|             self.message_bar_widget = None
 | |
| 
 | |
|         self.fetchAvailablePlugins(reloadMode=False)
 | |
|         self.exportRepositoriesToManager()
 | |
|         self.exportPluginsToManager()
 | |
| 
 | |
|         # finally, show the plugin manager window
 | |
|         tabIndex = -1
 | |
|         if len(params) == 1:
 | |
|             indx = str(params[0])
 | |
|             if indx.isdigit() and int(indx) > -1 and int(indx) < 7:
 | |
|                 tabIndex = int(indx)
 | |
|         iface.pluginManagerInterface().showPluginManager(tabIndex)
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def onManagerClose(self):
 | |
|         """Call this method when closing manager window - it resets last-use-dependent values."""
 | |
|         plugins.updateSeenPluginsList()
 | |
|         repositories.saveCheckingOnStartLastDate()
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def exportSettingsGroup(self):
 | |
|         """Return QgsSettings settingsGroup value"""
 | |
|         # todo QGIS 4 remove
 | |
|         return "plugin-manager"
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def upgradeAllUpgradeable(self):
 | |
|         """Reinstall all upgradeable plugins"""
 | |
|         for key in plugins.allUpgradeable():
 | |
|             self.installPlugin(key, quiet=True)
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def installPlugin(self, key, quiet=False, stable=True):
 | |
|         """Install given plugin"""
 | |
|         error = False
 | |
|         status_key = "status" if stable else "status_exp"
 | |
|         infoString = ("", "")
 | |
|         plugin = plugins.all()[key]
 | |
|         previousStatus = plugin[status_key]
 | |
|         if not plugin:
 | |
|             return
 | |
|         if (
 | |
|             plugin[status_key] == "newer" and not plugin["error"]
 | |
|         ):  # ask for confirmation if user downgrades an usable plugin
 | |
|             if (
 | |
|                 QMessageBox.warning(
 | |
|                     iface.mainWindow(),
 | |
|                     self.tr("QGIS Python Plugin Installer"),
 | |
|                     self.tr(
 | |
|                         "Are you sure you want to downgrade the plugin to the latest available version? The installed one is newer!"
 | |
|                     ),
 | |
|                     QMessageBox.StandardButton.Yes,
 | |
|                     QMessageBox.StandardButton.No,
 | |
|                 )
 | |
|                 == QMessageBox.StandardButton.No
 | |
|             ):
 | |
|                 return
 | |
| 
 | |
|         # if plugin is active, unload it before update, see https://github.com/qgis/QGIS/issues/54968
 | |
|         pluginWasLoaded = isPluginLoaded(plugin["id"])
 | |
|         if pluginWasLoaded:
 | |
|             unloadPlugin(plugin["id"])
 | |
| 
 | |
|         dlg = QgsPluginInstallerInstallingDialog(
 | |
|             iface.mainWindow(), plugin, stable=stable
 | |
|         )
 | |
|         dlg.exec()
 | |
| 
 | |
|         plugin_path = HOME_PLUGIN_PATH + "/" + key
 | |
|         if dlg.result():
 | |
|             error = True
 | |
|             infoString = (self.tr("Plugin installation failed"), dlg.result())
 | |
|             # download failed or aborted. If plugin was active before the update, let's try to load it back
 | |
|             if pluginWasLoaded and loadPlugin(plugin["id"]):
 | |
|                 startPlugin(plugin["id"])
 | |
|         elif not QDir(plugin_path).exists():
 | |
|             error = True
 | |
|             infoString = (
 | |
|                 self.tr("Plugin has disappeared"),
 | |
|                 self.tr(
 | |
|                     'The plugin seems to have been installed but it\'s not possible to know where. The directory "{}" '
 | |
|                     "has not been found. Probably the plugin package contained a wrong named directory.\nPlease search "
 | |
|                     "the list of installed plugins. You should find the plugin there, but it's not possible to "
 | |
|                     "determine which of them it is and it's also not possible to inform you about available updates. "
 | |
|                     "Please contact the plugin author and submit this issue."
 | |
|                 ).format(plugin_path),
 | |
|             )
 | |
|             with OverrideCursor(Qt.CursorShape.WaitCursor):
 | |
|                 plugins.getAllInstalled()
 | |
|                 plugins.rebuild()
 | |
|                 self.exportPluginsToManager()
 | |
|         else:
 | |
|             QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
 | |
|             # update the list of plugins in plugin handling routines
 | |
|             updateAvailablePlugins()
 | |
|             self.processDependencies(plugin["id"])
 | |
|             # try to load the plugin
 | |
|             loadPlugin(plugin["id"])
 | |
|             plugins.getAllInstalled()
 | |
|             plugins.rebuild()
 | |
|             plugin = plugins.all()[key]
 | |
|             if not plugin["error"]:
 | |
|                 if previousStatus in ["not installed", "new"]:
 | |
|                     infoString = (self.tr("Plugin installed successfully"), "")
 | |
|                     if startPlugin(plugin["id"]):
 | |
|                         settings = QgsSettings()
 | |
|                         settings.setValue("/PythonPlugins/" + plugin["id"], True)
 | |
|                 else:
 | |
|                     infoString = (self.tr("Plugin reinstalled successfully"), "")
 | |
|                     if pluginWasLoaded:
 | |
|                         loadPlugin(plugin["id"])
 | |
|                         startPlugin(plugin["id"])
 | |
|                     else:
 | |
|                         unloadPlugin(
 | |
|                             key
 | |
|                         )  # Just for a case. Will exit quietly if really not loaded
 | |
|                         loadPlugin(key)
 | |
|                 if quiet:
 | |
|                     infoString = (None, None)
 | |
|                 QApplication.restoreOverrideCursor()
 | |
|             else:
 | |
|                 QApplication.restoreOverrideCursor()
 | |
|                 if plugin["error"] == "incompatible":
 | |
|                     message = self.tr(
 | |
|                         "The plugin is not compatible with this version of QGIS. It's designed for QGIS versions:"
 | |
|                     )
 | |
|                     message += " <b>" + plugin["error_details"] + "</b>"
 | |
|                 elif plugin["error"] == "dependent":
 | |
|                     message = self.tr(
 | |
|                         "The plugin depends on some components missing on your system. You need to install the following Python module in order to enable it:"
 | |
|                     )
 | |
|                     message += "<b> " + plugin["error_details"] + "</b>"
 | |
|                 else:
 | |
|                     message = self.tr("The plugin is broken. Python said:")
 | |
|                     message += "<br><b>" + plugin["error_details"] + "</b>"
 | |
|                 dlg = QgsPluginInstallerPluginErrorDialog(iface.mainWindow(), message)
 | |
|                 dlg.exec()
 | |
|                 if dlg.result():
 | |
|                     # revert installation
 | |
|                     pluginDir = HOME_PLUGIN_PATH + "/" + plugin["id"]
 | |
|                     result = removeDir(pluginDir)
 | |
|                     if QDir(pluginDir).exists():
 | |
|                         error = True
 | |
|                         infoString = (self.tr("Plugin uninstall failed"), result)
 | |
|                         try:
 | |
|                             exec("sys.path_importer_cache.clear()")
 | |
|                             exec("import %s" % plugin["id"])
 | |
|                             exec("reload (%s)" % plugin["id"])
 | |
|                         except:
 | |
|                             pass
 | |
|                     else:
 | |
|                         try:
 | |
|                             exec("del sys.modules[%s]" % plugin["id"])
 | |
|                         except:
 | |
|                             pass
 | |
|                     plugins.getAllInstalled()
 | |
|                     plugins.rebuild()
 | |
| 
 | |
|             self.exportPluginsToManager()
 | |
| 
 | |
|         if infoString[0]:
 | |
|             level = error and Qgis.MessageLevel.Critical or Qgis.MessageLevel.Info
 | |
|             msg = "<b>%s</b>" % infoString[0]
 | |
|             if infoString[1]:
 | |
|                 msg += "<b>:</b> %s" % infoString[1]
 | |
|             iface.pluginManagerInterface().pushMessage(msg, level)
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def uninstallPlugin(self, key, quiet=False):
 | |
|         """Uninstall given plugin"""
 | |
|         if key in plugins.all():
 | |
|             plugin = plugins.all()[key]
 | |
|         else:
 | |
|             plugin = plugins.localCache[key]
 | |
|         if not plugin:
 | |
|             return
 | |
|         if not quiet:
 | |
|             warning = (
 | |
|                 self.tr("Are you sure you want to uninstall the following plugin?")
 | |
|                 + "\n("
 | |
|                 + plugin["name"]
 | |
|                 + ")"
 | |
|             )
 | |
|             if (
 | |
|                 plugin["status"] == "orphan"
 | |
|                 and plugin["status_exp"] == "orphan"
 | |
|                 and not plugin["error"]
 | |
|             ):
 | |
|                 warning += "\n\n" + self.tr(
 | |
|                     "Warning: this plugin isn't available in any accessible repository!"
 | |
|                 )
 | |
|             if (
 | |
|                 QMessageBox.warning(
 | |
|                     iface.mainWindow(),
 | |
|                     self.tr("QGIS Python Plugin Installer"),
 | |
|                     warning,
 | |
|                     QMessageBox.StandardButton.Yes,
 | |
|                     QMessageBox.StandardButton.No,
 | |
|                 )
 | |
|                 == QMessageBox.StandardButton.No
 | |
|             ):
 | |
|                 return
 | |
|         # unload the plugin
 | |
|         QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
 | |
|         try:
 | |
|             unloadPlugin(key)
 | |
|         except:
 | |
|             pass
 | |
|         pluginDir = HOME_PLUGIN_PATH + "/" + plugin["id"]
 | |
|         result = removeDir(pluginDir)
 | |
|         if result:
 | |
|             QApplication.restoreOverrideCursor()
 | |
|             msg = "<b>{}:</b>{}".format(self.tr("Plugin uninstall failed"), result)
 | |
|             iface.pluginManagerInterface().pushMessage(msg, Qgis.MessageLevel.Critical)
 | |
|         else:
 | |
|             # safe remove
 | |
|             try:
 | |
|                 unloadPlugin(plugin["id"])
 | |
|             except:
 | |
|                 pass
 | |
|             try:
 | |
|                 exec("plugins[%s].unload()" % plugin["id"])
 | |
|                 exec("del plugins[%s]" % plugin["id"])
 | |
|             except:
 | |
|                 pass
 | |
|             try:
 | |
|                 exec("del sys.modules[%s]" % plugin["id"])
 | |
|             except:
 | |
|                 pass
 | |
|             try:
 | |
|                 exec("del plugins_metadata_parser[%s]" % plugin["id"])
 | |
|             except:
 | |
|                 pass
 | |
| 
 | |
|             plugins.getAllInstalled()
 | |
|             plugins.rebuild()
 | |
|             self.exportPluginsToManager()
 | |
|             QApplication.restoreOverrideCursor()
 | |
|             iface.pluginManagerInterface().pushMessage(
 | |
|                 self.tr("Plugin uninstalled successfully"), Qgis.MessageLevel.Success
 | |
|             )
 | |
| 
 | |
|             settings = QgsSettings()
 | |
|             settings.remove("/PythonPlugins/" + key)
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def addRepository(self):
 | |
|         """add new repository connection"""
 | |
|         dlg = QgsPluginInstallerRepositoryDialog(iface.mainWindow())
 | |
|         dlg.editParams.setText(repositories.urlParams())
 | |
|         dlg.checkBoxEnabled.setCheckState(Qt.CheckState.Checked)
 | |
|         if not dlg.exec():
 | |
|             return
 | |
|         for i in list(repositories.all().values()):
 | |
|             if dlg.editURL.text().strip() == i["url"]:
 | |
|                 iface.pluginManagerInterface().pushMessage(
 | |
|                     self.tr("Unable to add another repository with the same URL!"),
 | |
|                     Qgis.MessageLevel.Warning,
 | |
|                 )
 | |
|                 return
 | |
|         settings = QgsSettings()
 | |
|         settings.beginGroup(reposGroup)
 | |
|         reposName = dlg.editName.text()
 | |
|         reposURL = dlg.editURL.text().strip()
 | |
|         if reposName in repositories.all():
 | |
|             reposName = reposName + "(2)"
 | |
|         # add to settings
 | |
|         settings.setValue(reposName + "/url", reposURL)
 | |
|         settings.setValue(reposName + "/authcfg", dlg.configId().strip())
 | |
|         settings.setValue(
 | |
|             reposName + "/enabled", bool(dlg.checkBoxEnabled.checkState())
 | |
|         )
 | |
|         # refresh lists and populate widgets
 | |
|         plugins.removeRepository(reposName)
 | |
|         self.reloadAndExportData()
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def editRepository(self, reposName):
 | |
|         """edit repository connection"""
 | |
|         if not reposName:
 | |
|             return
 | |
|         checkState = {False: Qt.CheckState.Unchecked, True: Qt.CheckState.Checked}
 | |
|         dlg = QgsPluginInstallerRepositoryDialog(iface.mainWindow())
 | |
|         dlg.editName.setText(reposName)
 | |
|         dlg.editURL.setText(repositories.all()[reposName]["url"])
 | |
|         dlg.editAuthCfgWgt.setConfigId(repositories.all()[reposName]["authcfg"])
 | |
|         dlg.editParams.setText(repositories.urlParams())
 | |
|         dlg.checkBoxEnabled.setCheckState(
 | |
|             checkState[repositories.all()[reposName]["enabled"]]
 | |
|         )
 | |
|         if repositories.all()[reposName]["valid"]:
 | |
|             dlg.checkBoxEnabled.setEnabled(True)
 | |
|             dlg.labelInfo.setText("")
 | |
|         else:
 | |
|             dlg.checkBoxEnabled.setEnabled(False)
 | |
|             dlg.labelInfo.setText(
 | |
|                 self.tr(
 | |
|                     "This repository is blocked due to incompatibility with your QGIS version"
 | |
|                 )
 | |
|             )
 | |
|             dlg.labelInfo.setFrameShape(QFrame.Shape.Box)
 | |
|         if not dlg.exec():
 | |
|             return  # nothing to do if canceled
 | |
|         for i in list(repositories.all().values()):
 | |
|             if (
 | |
|                 dlg.editURL.text().strip() == i["url"]
 | |
|                 and dlg.editURL.text().strip() != repositories.all()[reposName]["url"]
 | |
|             ):
 | |
|                 iface.pluginManagerInterface().pushMessage(
 | |
|                     self.tr("Unable to add another repository with the same URL!"),
 | |
|                     Qgis.MessageLevel.Warning,
 | |
|                 )
 | |
|                 return
 | |
|         # delete old repo from QgsSettings and create new one
 | |
|         settings = QgsSettings()
 | |
|         settings.beginGroup(reposGroup)
 | |
|         settings.remove(reposName)
 | |
|         newName = dlg.editName.text()
 | |
|         if newName in repositories.all() and newName != reposName:
 | |
|             newName = newName + "(2)"
 | |
|         settings.setValue(newName + "/url", dlg.editURL.text().strip())
 | |
|         settings.setValue(newName + "/authcfg", dlg.configId().strip())
 | |
|         settings.setValue(newName + "/enabled", bool(dlg.checkBoxEnabled.checkState()))
 | |
|         if (
 | |
|             dlg.editAuthCfgWgt.configId().strip()
 | |
|             != repositories.all()[reposName]["authcfg"]
 | |
|         ):
 | |
|             repositories.all()[reposName][
 | |
|                 "authcfg"
 | |
|             ] = dlg.editAuthCfgWgt.configId().strip()
 | |
|         if (
 | |
|             dlg.editURL.text().strip() == repositories.all()[reposName]["url"]
 | |
|             and dlg.checkBoxEnabled.checkState()
 | |
|             == checkState[repositories.all()[reposName]["enabled"]]
 | |
|         ):
 | |
|             repositories.rename(reposName, newName)
 | |
|             self.exportRepositoriesToManager()
 | |
|             return  # nothing else to do if only repository name was changed
 | |
|         plugins.removeRepository(reposName)
 | |
|         self.reloadAndExportData()
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def deleteRepository(self, reposName: str):
 | |
|         """delete repository connection"""
 | |
|         if not reposName:
 | |
|             return
 | |
|         settings = QgsSettings()
 | |
|         settings.beginGroup(reposGroup)
 | |
|         if settings.value(reposName + "/url", "", type=str) == officialRepo[1]:
 | |
|             iface.pluginManagerInterface().pushMessage(
 | |
|                 self.tr(
 | |
|                     "You can't remove the official QGIS Plugin Repository. You can disable it if needed."
 | |
|                 ),
 | |
|                 Qgis.MessageLevel.Warning,
 | |
|             )
 | |
|             return
 | |
|         warning = (
 | |
|             self.tr("Are you sure you want to remove the following repository?")
 | |
|             + "\n"
 | |
|             + reposName
 | |
|         )
 | |
|         if (
 | |
|             QMessageBox.warning(
 | |
|                 iface.mainWindow(),
 | |
|                 self.tr("QGIS Python Plugin Installer"),
 | |
|                 warning,
 | |
|                 QMessageBox.StandardButton.Yes,
 | |
|                 QMessageBox.StandardButton.No,
 | |
|             )
 | |
|             == QMessageBox.StandardButton.No
 | |
|         ):
 | |
|             return
 | |
|         # delete from the settings, refresh data and repopulate all the widgets
 | |
|         settings.remove(reposName)
 | |
|         repositories.remove(reposName)
 | |
|         plugins.removeRepository(reposName)
 | |
|         self.reloadAndExportData()
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def setRepositoryInspectionFilter(self, reposName=None):
 | |
|         """temporarily block another repositories to fetch only one for inspection"""
 | |
|         repositories.setInspectionFilter(reposName)
 | |
|         self.reloadAndExportData()
 | |
| 
 | |
|     # ----------------------------------------- #
 | |
|     def sendVote(self, plugin_id, vote):
 | |
|         """send vote via the RPC"""
 | |
| 
 | |
|         if not plugin_id or not vote:
 | |
|             return False
 | |
|         url = "https://plugins.qgis.org/plugins/RPC2/"
 | |
|         params = {
 | |
|             "id": "djangorpc",
 | |
|             "method": "plugin.vote",
 | |
|             "params": [str(plugin_id), str(vote)],
 | |
|         }
 | |
|         req = QNetworkRequest(QUrl(url))
 | |
|         req.setAttribute(
 | |
|             QNetworkRequest.Attribute(
 | |
|                 QgsNetworkRequestParameters.RequestAttributes.AttributeInitiatorClass
 | |
|             ),
 | |
|             "QgsPluginInstaller",
 | |
|         )
 | |
|         req.setAttribute(
 | |
|             QNetworkRequest.Attribute(
 | |
|                 QgsNetworkRequestParameters.RequestAttributes.AttributeInitiatorRequestId
 | |
|             ),
 | |
|             "sendVote",
 | |
|         )
 | |
|         req.setRawHeader(b"Content-Type", b"application/json")
 | |
|         reply = QgsNetworkAccessManager.instance().blockingPost(
 | |
|             req, bytes(json.dumps(params), "utf-8")
 | |
|         )
 | |
|         if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) == 200:
 | |
|             return True
 | |
|         else:
 | |
|             return False
 | |
| 
 | |
|     def installFromZipFile(self, filePath):
 | |
|         if not os.path.isfile(filePath):
 | |
|             return
 | |
| 
 | |
|         QgsSettingsTree.node("plugin-manager").childSetting(
 | |
|             "last-zip-directory"
 | |
|         ).setValue(QFileInfo(filePath).absoluteDir().absolutePath())
 | |
| 
 | |
|         pluginName = None
 | |
|         with zipfile.ZipFile(filePath, "r") as zf:
 | |
|             # search for metadata.txt. In case of multiple files, we can assume that
 | |
|             # the shortest path relates <pluginname>/metadata.txt
 | |
|             metadatafiles = sorted(
 | |
|                 f for f in zf.namelist() if f.endswith("metadata.txt")
 | |
|             )
 | |
|             if len(metadatafiles) > 0:
 | |
|                 pluginName = os.path.split(metadatafiles[0])[0]
 | |
| 
 | |
|         pluginFileName = os.path.splitext(os.path.basename(filePath))[0]
 | |
| 
 | |
|         if not pluginName:
 | |
|             msg_box = QMessageBox()
 | |
|             msg_box.setIcon(QMessageBox.Icon.Warning)
 | |
|             msg_box.setWindowTitle(
 | |
|                 self.tr("QGIS Python Install from ZIP Plugin Installer")
 | |
|             )
 | |
|             msg_box.setText(
 | |
|                 self.tr(
 | |
|                     "The Zip file is not a valid QGIS python plugin. No root folder was found inside."
 | |
|                 )
 | |
|             )
 | |
|             msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
 | |
|             more_info_btn = msg_box.addButton(
 | |
|                 self.tr("More Information"), QMessageBox.ButtonRole.HelpRole
 | |
|             )
 | |
|             msg_box.exec()
 | |
|             if msg_box.clickedButton() == more_info_btn:
 | |
|                 QgsHelp.openHelp("plugins/plugins.html#the-install-from-zip-tab")
 | |
|             return
 | |
| 
 | |
|         pluginsDirectory = HOME_PLUGIN_PATH
 | |
|         if not QDir(pluginsDirectory).exists():
 | |
|             QDir().mkpath(pluginsDirectory)
 | |
| 
 | |
|         pluginDirectory = QDir.cleanPath(os.path.join(pluginsDirectory, pluginName))
 | |
| 
 | |
|         # if plugin is active, unload it before update, see https://github.com/qgis/QGIS/issues/54968
 | |
|         if isPluginLoaded(pluginName):
 | |
|             unloadPlugin(pluginName)
 | |
| 
 | |
|         # If the target directory already exists as a link,
 | |
|         # remove the link without resolving
 | |
|         QFile(pluginDirectory).remove()
 | |
| 
 | |
|         password = None
 | |
|         infoString = None
 | |
|         success = False
 | |
|         keepTrying = True
 | |
| 
 | |
|         while keepTrying:
 | |
|             try:
 | |
|                 # Test extraction. If fails, then exception will be raised and no removing occurs
 | |
|                 unzip(filePath, pluginsDirectory, password)
 | |
|                 # Removing old plugin files if exist
 | |
|                 removeDir(pluginDirectory)
 | |
|                 # Extract new files
 | |
|                 unzip(filePath, pluginsDirectory, password)
 | |
|                 keepTrying = False
 | |
|                 success = True
 | |
|             except Exception as e:
 | |
|                 success = False
 | |
|                 if "password" in str(e):
 | |
|                     infoString = self.tr("Aborted by user")
 | |
|                     if "Bad password" in str(e):
 | |
|                         msg = self.tr(
 | |
|                             "Wrong password. Please enter a correct password to the zip file."
 | |
|                         )
 | |
|                     else:
 | |
|                         msg = self.tr(
 | |
|                             "The zip file is encrypted. Please enter password."
 | |
|                         )
 | |
|                     # Display a password dialog with QgsPasswordLineEdit
 | |
|                     dlg = QDialog()
 | |
|                     dlg.setWindowTitle(self.tr("Enter password"))
 | |
|                     buttonBox = QDialogButtonBox(
 | |
|                         QDialogButtonBox.StandardButton.Ok
 | |
|                         | QDialogButtonBox.StandardButton.Cancel,
 | |
|                         Qt.Orientation.Horizontal,
 | |
|                     )
 | |
|                     buttonBox.rejected.connect(dlg.reject)
 | |
|                     buttonBox.accepted.connect(dlg.accept)
 | |
|                     lePass = QgsPasswordLineEdit()
 | |
|                     layout = QVBoxLayout()
 | |
|                     layout.addWidget(QLabel(msg))
 | |
|                     layout.addWidget(lePass)
 | |
|                     layout.addWidget(buttonBox)
 | |
|                     dlg.setLayout(layout)
 | |
|                     keepTrying = dlg.exec()
 | |
|                     password = lePass.text()
 | |
|                 else:
 | |
|                     infoString = self.tr(
 | |
|                         f"Failed to unzip the plugin package\n{filePath}.\nProbably it is broken"
 | |
|                     )
 | |
|                     keepTrying = False
 | |
| 
 | |
|         if success:
 | |
|             with OverrideCursor(Qt.CursorShape.WaitCursor):
 | |
|                 updateAvailablePlugins()
 | |
|                 self.processDependencies(pluginName)
 | |
|                 loadPlugin(pluginName)
 | |
|                 plugins.getAllInstalled()
 | |
|                 plugins.rebuild()
 | |
| 
 | |
|                 settings = QgsSettings()
 | |
|                 if settings.contains(
 | |
|                     "/PythonPlugins/" + pluginName
 | |
|                 ):  # Plugin was available?
 | |
|                     unloadPlugin(pluginName)
 | |
|                     loadPlugin(pluginName)
 | |
|                     if settings.value(
 | |
|                         "/PythonPlugins/" + pluginName, False, bool
 | |
|                     ):  # Plugin was also active?
 | |
|                         startPlugin(pluginName)
 | |
|                 else:
 | |
|                     if startPlugin(pluginName):
 | |
|                         settings.setValue("/PythonPlugins/" + pluginName, True)
 | |
| 
 | |
|             self.exportPluginsToManager()
 | |
|             msg = "<b>%s</b>" % self.tr("Plugin installed successfully")
 | |
|         else:
 | |
|             msg = "<b>{}:</b> {}".format(
 | |
|                 self.tr("Plugin installation failed"), infoString
 | |
|             )
 | |
| 
 | |
|         level = Qgis.MessageLevel.Success if success else Qgis.MessageLevel.Critical
 | |
|         iface.pluginManagerInterface().pushMessage(msg, level)
 | |
| 
 | |
|     def processDependencies(self, plugin_id):
 | |
|         """Processes plugin dependencies
 | |
| 
 | |
|         :param plugin_id: plugin id
 | |
|         :type plugin_id: str
 | |
|         """
 | |
| 
 | |
|         to_install, to_upgrade, not_found = find_dependencies(plugin_id)
 | |
|         if to_install or to_upgrade or not_found:
 | |
|             dlg = QgsPluginDependenciesDialog(
 | |
|                 plugin_id, to_install, to_upgrade, not_found
 | |
|             )
 | |
|             if dlg.exec() == QgsPluginDependenciesDialog.Accepted:
 | |
|                 actions = dlg.actions()
 | |
|                 for dependency_plugin_id, action_data in actions.items():
 | |
|                     try:
 | |
|                         self.installPlugin(
 | |
|                             dependency_plugin_id,
 | |
|                             stable=action_data["use_stable_version"],
 | |
|                         )
 | |
|                         if action_data["action"] == "install":
 | |
|                             iface.pluginManagerInterface().pushMessage(
 | |
|                                 self.tr(
 | |
|                                     "Plugin dependency <b>%s</b> successfully installed"
 | |
|                                 )
 | |
|                                 % dependency_plugin_id,
 | |
|                                 Qgis.MessageLevel.Success,
 | |
|                             )
 | |
|                         else:
 | |
|                             iface.pluginManagerInterface().pushMessage(
 | |
|                                 self.tr(
 | |
|                                     "Plugin dependency <b>%s</b> successfully upgraded"
 | |
|                                 )
 | |
|                                 % dependency_plugin_id,
 | |
|                                 Qgis.MessageLevel.Success,
 | |
|                             )
 | |
|                     except Exception as ex:
 | |
|                         if action_data["action"] == "install":
 | |
|                             iface.pluginManagerInterface().pushMessage(
 | |
|                                 self.tr(
 | |
|                                     "Error installing plugin dependency <b>%s</b>: %s"
 | |
|                                 )
 | |
|                                 % (dependency_plugin_id, ex),
 | |
|                                 Qgis.MessageLevel.Warning,
 | |
|                             )
 | |
|                         else:
 | |
|                             iface.pluginManagerInterface().pushMessage(
 | |
|                                 self.tr(
 | |
|                                     "Error upgrading plugin dependency <b>%s</b>: %s"
 | |
|                                 )
 | |
|                                 % (dependency_plugin_id, ex),
 | |
|                                 Qgis.MessageLevel.Warning,
 | |
|                             )
 |