1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a dialog for plugin deinstallation.
8"""
9
10import sys
11import os
12import importlib
13import shutil
14import glob
15
16from PyQt5.QtCore import pyqtSlot, pyqtSignal
17from PyQt5.QtWidgets import QWidget, QDialog, QDialogButtonBox, QVBoxLayout
18
19from E5Gui import E5MessageBox
20from E5Gui.E5MainWindow import E5MainWindow
21from E5Gui.E5Application import e5App
22
23from .Ui_PluginUninstallDialog import Ui_PluginUninstallDialog
24
25import Preferences
26import UI.PixmapCache
27
28
29class PluginUninstallWidget(QWidget, Ui_PluginUninstallDialog):
30    """
31    Class implementing a dialog for plugin deinstallation.
32
33    @signal accepted() emitted to indicate the removal of a plug-in
34    """
35    accepted = pyqtSignal()
36
37    def __init__(self, pluginManager, parent=None):
38        """
39        Constructor
40
41        @param pluginManager reference to the plugin manager object
42        @param parent parent of this dialog (QWidget)
43        """
44        super().__init__(parent)
45        self.setupUi(self)
46
47        if pluginManager is None:
48            # started as external plugin deinstaller
49            from .PluginManager import PluginManager
50            self.__pluginManager = PluginManager(doLoadPlugins=False)
51            self.__external = True
52        else:
53            self.__pluginManager = pluginManager
54            self.__external = False
55
56        self.pluginDirectoryCombo.addItem(
57            self.tr("User plugins directory"),
58            self.__pluginManager.getPluginDir("user"))
59
60        globalDir = self.__pluginManager.getPluginDir("global")
61        if globalDir is not None and os.access(globalDir, os.W_OK):
62            self.pluginDirectoryCombo.addItem(
63                self.tr("Global plugins directory"),
64                globalDir)
65
66        msh = self.minimumSizeHint()
67        self.resize(max(self.width(), msh.width()), msh.height())
68
69    @pyqtSlot(int)
70    def on_pluginDirectoryCombo_currentIndexChanged(self, index):
71        """
72        Private slot to populate the plugin name combo upon a change of the
73        plugin area.
74
75        @param index index of the selected item (integer)
76        """
77        pluginDirectory = self.pluginDirectoryCombo.itemData(index)
78        pluginNames = sorted(self.__pluginManager.getPluginModules(
79            pluginDirectory))
80        self.pluginNameCombo.clear()
81        for pluginName in pluginNames:
82            fname = "{0}.py".format(os.path.join(pluginDirectory, pluginName))
83            self.pluginNameCombo.addItem(pluginName, fname)
84        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(
85            self.pluginNameCombo.currentText() != "")
86
87    @pyqtSlot()
88    def on_buttonBox_accepted(self):
89        """
90        Private slot to handle the accepted signal of the button box.
91        """
92        if self.__uninstallPlugin():
93            self.accepted.emit()
94
95    def __uninstallPlugin(self):
96        """
97        Private slot to uninstall the selected plugin.
98
99        @return flag indicating success (boolean)
100        """
101        pluginDirectory = self.pluginDirectoryCombo.itemData(
102            self.pluginDirectoryCombo.currentIndex())
103        pluginName = self.pluginNameCombo.currentText()
104        pluginFile = self.pluginNameCombo.itemData(
105            self.pluginNameCombo.currentIndex())
106
107        if not self.__pluginManager.unloadPlugin(pluginName):
108            E5MessageBox.critical(
109                self,
110                self.tr("Plugin Uninstallation"),
111                self.tr(
112                    """<p>The plugin <b>{0}</b> could not be unloaded."""
113                    """ Aborting...</p>""").format(pluginName))
114            return False
115
116        if pluginDirectory not in sys.path:
117            sys.path.insert(2, pluginDirectory)
118        spec = importlib.util.spec_from_file_location(pluginName, pluginFile)
119        module = importlib.util.module_from_spec(spec)
120        spec.loader.exec_module(module)
121        if not hasattr(module, "packageName"):
122            E5MessageBox.critical(
123                self,
124                self.tr("Plugin Uninstallation"),
125                self.tr(
126                    """<p>The plugin <b>{0}</b> has no 'packageName'"""
127                    """ attribute. Aborting...</p>""").format(pluginName))
128            return False
129
130        package = getattr(module, "packageName", None)
131        if package is None:
132            package = "None"
133            packageDir = ""
134        else:
135            packageDir = os.path.join(pluginDirectory, package)
136        if (
137            hasattr(module, "prepareUninstall") and
138            not self.keepConfigurationCheckBox.isChecked()
139        ):
140            module.prepareUninstall()
141        internalPackages = []
142        if hasattr(module, "internalPackages"):
143            # it is a comma separated string
144            internalPackages = [p.strip() for p in
145                                module.internalPackages.split(",")]
146        del module
147
148        # clean sys.modules
149        self.__pluginManager.removePluginFromSysModules(
150            pluginName, package, internalPackages)
151
152        try:
153            if packageDir and os.path.exists(packageDir):
154                shutil.rmtree(packageDir)
155
156            fnameo = "{0}o".format(pluginFile)
157            if os.path.exists(fnameo):
158                os.remove(fnameo)
159
160            fnamec = "{0}c".format(pluginFile)
161            if os.path.exists(fnamec):
162                os.remove(fnamec)
163
164            pluginDirCache = os.path.join(
165                os.path.dirname(pluginFile), "__pycache__")
166            if os.path.exists(pluginDirCache):
167                pluginFileName = os.path.splitext(
168                    os.path.basename(pluginFile))[0]
169                for fnameo in glob.glob(os.path.join(
170                        pluginDirCache, "{0}*.pyo".format(pluginFileName))):
171                    os.remove(fnameo)
172                for fnamec in glob.glob(os.path.join(
173                        pluginDirCache, "{0}*.pyc".format(pluginFileName))):
174                    os.remove(fnamec)
175
176            os.remove(pluginFile)
177        except OSError as err:
178            E5MessageBox.critical(
179                self,
180                self.tr("Plugin Uninstallation"),
181                self.tr(
182                    """<p>The plugin package <b>{0}</b> could not be"""
183                    """ removed. Aborting...</p>"""
184                    """<p>Reason: {1}</p>""").format(packageDir, str(err)))
185            return False
186
187        if not self.__external:
188            ui = e5App().getObject("UserInterface")
189            ui.showNotification(
190                UI.PixmapCache.getPixmap("plugin48"),
191                self.tr("Plugin Uninstallation"),
192                self.tr(
193                    """<p>The plugin <b>{0}</b> was uninstalled"""
194                    """ successfully from {1}.</p>""")
195                .format(pluginName, pluginDirectory))
196            return True
197
198        E5MessageBox.information(
199            self,
200            self.tr("Plugin Uninstallation"),
201            self.tr(
202                """<p>The plugin <b>{0}</b> was uninstalled successfully"""
203                """ from {1}.</p>""")
204            .format(pluginName, pluginDirectory))
205        return True
206
207
208class PluginUninstallDialog(QDialog):
209    """
210    Class for the dialog variant.
211    """
212    def __init__(self, pluginManager, parent=None):
213        """
214        Constructor
215
216        @param pluginManager reference to the plugin manager object
217        @param parent reference to the parent widget (QWidget)
218        """
219        super().__init__(parent)
220        self.setSizeGripEnabled(True)
221
222        self.__layout = QVBoxLayout(self)
223        self.__layout.setContentsMargins(0, 0, 0, 0)
224        self.setLayout(self.__layout)
225
226        self.cw = PluginUninstallWidget(pluginManager, self)
227        size = self.cw.size()
228        self.__layout.addWidget(self.cw)
229        self.resize(size)
230        self.setWindowTitle(self.cw.windowTitle())
231
232        self.cw.buttonBox.accepted.connect(self.accept)
233        self.cw.buttonBox.rejected.connect(self.reject)
234
235
236class PluginUninstallWindow(E5MainWindow):
237    """
238    Main window class for the standalone dialog.
239    """
240    def __init__(self, parent=None):
241        """
242        Constructor
243
244        @param parent reference to the parent widget (QWidget)
245        """
246        super().__init__(parent)
247        self.cw = PluginUninstallWidget(None, self)
248        size = self.cw.size()
249        self.setCentralWidget(self.cw)
250        self.resize(size)
251        self.setWindowTitle(self.cw.windowTitle())
252
253        self.setStyle(Preferences.getUI("Style"),
254                      Preferences.getUI("StyleSheet"))
255
256        self.cw.buttonBox.accepted.connect(self.close)
257        self.cw.buttonBox.rejected.connect(self.close)
258