"""
/***************************************************************************
                            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 json
import os
import shutil
import zipfile
from functools import partial

from qgis.core import (
    Qgis,
    QgsApplication,
    QgsMessageLog,
    QgsNetworkAccessManager,
    QgsNetworkRequestParameters,
    QgsSettings,
    QgsSettingsTree,
)
from qgis.gui import QgsHelp, QgsMessageBar, QgsPasswordLineEdit
from qgis.PyQt import sip
from qgis.PyQt.QtCore import (
    QDateTime,
    QDir,
    QFile,
    QFileInfo,
    QObject,
    Qt,
    QTemporaryDir,
    QUrl,
)
from qgis.PyQt.QtNetwork import QNetworkRequest
from qgis.PyQt.QtWidgets import (
    QApplication,
    QDialog,
    QDialogButtonBox,
    QFrame,
    QLabel,
    QMessageBox,
    QPushButton,
    QVBoxLayout,
)
from qgis.utils import (
    HOME_PLUGIN_PATH,
    OverrideCursor,
    iface,
    isPluginLoaded,
    loadPlugin,
    plugins_metadata_parser,
    startPlugin,
    unloadPlugin,
    updateAvailablePlugins,
)

from .installer_data import officialRepo, plugins, removeDir, reposGroup, repositories
from .plugindependencies import find_dependencies
from .qgsplugindependenciesdialog import QgsPluginDependenciesDialog
from .qgsplugininstallerfetchingdialog import QgsPluginInstallerFetchingDialog
from .qgsplugininstallerinstallingdialog import QgsPluginInstallerInstallingDialog
from .qgsplugininstallerpluginerrordialog import QgsPluginInstallerPluginErrorDialog
from .qgsplugininstallerrepositorydialog import QgsPluginInstallerRepositoryDialog
from .unzip import unzip

# 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

        iface.mainWindow().pluginUpdatesAvailable.emit(updatable_plugin_names)

    # ----------------------------------------- #
    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 5 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 False

        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 False

        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)

        password = None
        infoString = None
        success = False
        keepTrying = True

        extractDir = QTemporaryDir()
        while keepTrying:
            try:
                # Test extraction. If fails, then exception will be raised and no removing occurs
                unzip(filePath, extractDir.path(), 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:
            # Removing old plugin files if exist
            # If the target directory already exists as a link,
            # the link should be removed without resolving
            infoString = removeDir(pluginDirectory)
            if infoString:
                success = False

            try:
                shutil.move(extractDir.filePath(pluginName), pluginsDirectory)
            except:
                infoString = (
                    self.tr("Could not store plugin to the plugin directory:")
                    + "\n"
                    + pluginDirectory
                )
                success = 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)
        return success

    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() == QDialog.DialogCode.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,
                            )
