1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the Plugin Manager.
8"""
9
10import os
11import sys
12import zipfile
13import types
14import importlib
15import contextlib
16import logging
17
18from PyQt5.QtCore import (
19    pyqtSignal, QObject, QDate, QFile, QFileInfo, QUrl, QIODevice
20)
21from PyQt5.QtGui import QPixmap
22from PyQt5.QtNetwork import (
23    QNetworkAccessManager, QNetworkRequest, QNetworkReply
24)
25
26from E5Gui import E5MessageBox
27from E5Gui.E5Application import e5App
28
29from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired
30try:
31    from E5Network.E5SslErrorHandler import E5SslErrorHandler, E5SslErrorState
32    SSL_AVAILABLE = True
33except ImportError:
34    SSL_AVAILABLE = False
35
36from .PluginExceptions import (
37    PluginPathError, PluginModulesError, PluginLoadError,
38    PluginActivationError, PluginModuleFormatError, PluginClassFormatError
39)
40
41import UI.PixmapCache
42
43import Globals
44import Utilities
45import Preferences
46
47from eric6config import getConfig
48
49
50class PluginManager(QObject):
51    """
52    Class implementing the Plugin Manager.
53
54    @signal shutdown() emitted at shutdown of the IDE
55    @signal pluginAboutToBeActivated(modulName, pluginObject) emitted just
56        before a plugin is activated
57    @signal pluginActivated(moduleName, pluginObject) emitted just after
58        a plugin was activated
59    @signal allPlugginsActivated() emitted at startup after all plugins have
60        been activated
61    @signal pluginAboutToBeDeactivated(moduleName, pluginObject) emitted just
62        before a plugin is deactivated
63    @signal pluginDeactivated(moduleName, pluginObject) emitted just after
64        a plugin was deactivated
65    """
66    shutdown = pyqtSignal()
67    pluginAboutToBeActivated = pyqtSignal(str, object)
68    pluginActivated = pyqtSignal(str, object)
69    allPlugginsActivated = pyqtSignal()
70    pluginAboutToBeDeactivated = pyqtSignal(str, object)
71    pluginDeactivated = pyqtSignal(str, object)
72
73    def __init__(self, parent=None, disabledPlugins=None, doLoadPlugins=True,
74                 develPlugin=None):
75        """
76        Constructor
77
78        The Plugin Manager deals with three different plugin directories.
79        The first is the one, that is part of eric6 (eric6/Plugins). The
80        second one is the global plugin directory called 'eric6plugins',
81        which is located inside the site-packages directory. The last one
82        is the user plugin directory located inside the .eric6 directory
83        of the users home directory.
84
85        @param parent reference to the parent object
86        @type QObject
87        @param disabledPlugins list of plug-ins that have been disabled via
88            the command line parameters '--disable-plugin='
89        @type list of str
90        @param doLoadPlugins flag indicating, that plug-ins should
91            be loaded
92        @type bool
93        @param develPlugin filename of a plug-in to be loaded for
94            development
95        @type str
96        @exception PluginPathError raised to indicate an invalid plug-in path
97        @exception PluginModulesError raised to indicate the absence of
98            plug-in modules
99        """
100        super().__init__(parent)
101
102        self.__ui = parent
103        self.__develPluginFile = develPlugin
104        self.__develPluginName = None
105        if disabledPlugins is not None:
106            self.__disabledPlugins = disabledPlugins[:]
107        else:
108            self.__disabledPlugins = []
109
110        self.__inactivePluginsKey = "PluginManager/InactivePlugins"
111
112        self.pluginDirs = {
113            "eric6": os.path.join(getConfig('ericDir'), "Plugins"),
114            "global": os.path.join(Utilities.getPythonLibraryDirectory(),
115                                   "eric6plugins"),
116            "user": os.path.join(Utilities.getConfigDir(), "eric6plugins"),
117        }
118        self.__priorityOrder = ["eric6", "global", "user"]
119
120        self.__defaultDownloadDir = os.path.join(
121            Utilities.getConfigDir(), "Downloads")
122
123        self.__activePlugins = {}
124        self.__inactivePlugins = {}
125        self.__onDemandActivePlugins = {}
126        self.__onDemandInactivePlugins = {}
127        self.__activeModules = {}
128        self.__inactiveModules = {}
129        self.__onDemandActiveModules = {}
130        self.__onDemandInactiveModules = {}
131        self.__failedModules = {}
132
133        self.__foundCoreModules = []
134        self.__foundGlobalModules = []
135        self.__foundUserModules = []
136
137        self.__modulesCount = 0
138
139        pdirsExist, msg = self.__pluginDirectoriesExist()
140        if not pdirsExist:
141            raise PluginPathError(msg)
142
143        if doLoadPlugins:
144            if not self.__pluginModulesExist():
145                raise PluginModulesError
146
147            self.__insertPluginsPaths()
148
149            self.__loadPlugins()
150
151        self.__checkPluginsDownloadDirectory()
152
153        self.pluginRepositoryFile = os.path.join(Utilities.getConfigDir(),
154                                                 "PluginRepository")
155
156        # attributes for the network objects
157        self.__networkManager = QNetworkAccessManager(self)
158        self.__networkManager.proxyAuthenticationRequired.connect(
159            proxyAuthenticationRequired)
160        if SSL_AVAILABLE:
161            self.__sslErrorHandler = E5SslErrorHandler(self)
162            self.__networkManager.sslErrors.connect(self.__sslErrors)
163        self.__replies = []
164
165        with contextlib.suppress(AttributeError):
166            self.__ui.onlineStateChanged.connect(self.__onlineStateChanged)
167
168    def finalizeSetup(self):
169        """
170        Public method to finalize the setup of the plugin manager.
171        """
172        for module in (
173            list(self.__onDemandInactiveModules.values()) +
174            list(self.__onDemandActiveModules.values())
175        ):
176            if hasattr(module, "moduleSetup"):
177                module.moduleSetup()
178
179    def getPluginDir(self, key):
180        """
181        Public method to get the path of a plugin directory.
182
183        @param key key of the plug-in directory (string)
184        @return path of the requested plugin directory (string)
185        """
186        if key not in ["global", "user"]:
187            return None
188        else:
189            try:
190                return self.pluginDirs[key]
191            except KeyError:
192                return None
193
194    def __pluginDirectoriesExist(self):
195        """
196        Private method to check, if the plugin folders exist.
197
198        If the plugin folders don't exist, they are created (if possible).
199
200        @return tuple of a flag indicating existence of any of the plugin
201            directories (boolean) and a message (string)
202        """
203        if self.__develPluginFile:
204            path = Utilities.splitPath(self.__develPluginFile)[0]
205            fname = os.path.join(path, "__init__.py")
206            if not os.path.exists(fname):
207                try:
208                    with open(fname, "w"):
209                        pass
210                except OSError:
211                    return (
212                        False,
213                        self.tr("Could not create a package for {0}.")
214                            .format(self.__develPluginFile))
215
216        fname = os.path.join(self.pluginDirs["user"], "__init__.py")
217        if not os.path.exists(fname):
218            if not os.path.exists(self.pluginDirs["user"]):
219                os.mkdir(self.pluginDirs["user"], 0o755)
220            try:
221                with open(fname, "w"):
222                    pass
223            except OSError:
224                del self.pluginDirs["user"]
225
226        if not os.path.exists(self.pluginDirs["global"]):
227            try:
228                # create the global plugins directory
229                os.mkdir(self.pluginDirs["global"], 0o755)
230                fname = os.path.join(self.pluginDirs["global"], "__init__.py")
231                with open(fname, "w", encoding="utf-8") as f:
232                    f.write('# -*- coding: utf-8 -*-' + "\n")
233                    f.write("\n")
234                    f.write('"""' + "\n")
235                    f.write('Package containing the global plugins.' + "\n")
236                    f.write('"""' + "\n")
237            except OSError:
238                del self.pluginDirs["global"]
239
240        if not os.path.exists(self.pluginDirs["eric6"]):
241            return (
242                False,
243                self.tr(
244                    "The internal plugin directory <b>{0}</b>"
245                    " does not exits.").format(self.pluginDirs["eric6"]))
246
247        return (True, "")
248
249    def __pluginModulesExist(self):
250        """
251        Private method to check, if there are plugins available.
252
253        @return flag indicating the availability of plugins (boolean)
254        """
255        if (
256            self.__develPluginFile and
257            not os.path.exists(self.__develPluginFile)
258        ):
259            return False
260
261        self.__foundCoreModules = self.getPluginModules(
262            self.pluginDirs["eric6"])
263        if Preferences.getPluginManager("ActivateExternal"):
264            if "global" in self.pluginDirs:
265                self.__foundGlobalModules = self.getPluginModules(
266                    self.pluginDirs["global"])
267            if "user" in self.pluginDirs:
268                self.__foundUserModules = self.getPluginModules(
269                    self.pluginDirs["user"])
270
271        return len(self.__foundCoreModules + self.__foundGlobalModules +
272                   self.__foundUserModules) > 0
273
274    def getPluginModules(self, pluginPath):
275        """
276        Public method to get a list of plugin modules.
277
278        @param pluginPath name of the path to search (string)
279        @return list of plugin module names (list of string)
280        """
281        pluginFiles = [f[:-3] for f in os.listdir(pluginPath)
282                       if self.isValidPluginName(f)]
283        return pluginFiles[:]
284
285    def isValidPluginName(self, pluginName):
286        """
287        Public methode to check, if a file name is a valid plugin name.
288
289        Plugin modules must start with "Plugin" and have the extension ".py".
290
291        @param pluginName name of the file to be checked (string)
292        @return flag indicating a valid plugin name (boolean)
293        """
294        return pluginName.startswith("Plugin") and pluginName.endswith(".py")
295
296    def __insertPluginsPaths(self):
297        """
298        Private method to insert the valid plugin paths intos the search path.
299        """
300        for key in self.__priorityOrder:
301            if key in self.pluginDirs:
302                if self.pluginDirs[key] not in sys.path:
303                    sys.path.insert(2, self.pluginDirs[key])
304                UI.PixmapCache.addSearchPath(self.pluginDirs[key])
305
306        if self.__develPluginFile:
307            path = Utilities.splitPath(self.__develPluginFile)[0]
308            if path not in sys.path:
309                sys.path.insert(2, path)
310            UI.PixmapCache.addSearchPath(path)
311
312    def __loadPlugins(self):
313        """
314        Private method to load the plugins found.
315        """
316        develPluginName = ""
317        if self.__develPluginFile:
318            develPluginPath, develPluginName = Utilities.splitPath(
319                self.__develPluginFile)
320            if self.isValidPluginName(develPluginName):
321                develPluginName = develPluginName[:-3]
322
323        for pluginName in self.__foundGlobalModules:
324            # user and core plug-ins have priority
325            if (
326                pluginName not in self.__foundUserModules and
327                pluginName not in self.__foundCoreModules and
328                pluginName != develPluginName
329            ):
330                self.loadPlugin(pluginName, self.pluginDirs["global"])
331
332        for pluginName in self.__foundUserModules:
333            # core plug-ins have priority
334            if (
335                pluginName not in self.__foundCoreModules and
336                pluginName != develPluginName
337            ):
338                self.loadPlugin(pluginName, self.pluginDirs["user"])
339
340        for pluginName in self.__foundCoreModules:
341            # plug-in under development has priority
342            if pluginName != develPluginName:
343                self.loadPlugin(pluginName, self.pluginDirs["eric6"])
344
345        if develPluginName:
346            self.loadPlugin(develPluginName, develPluginPath)
347            self.__develPluginName = develPluginName
348
349    def loadDocumentationSetPlugins(self):
350        """
351        Public method to load just the documentation sets plugins.
352
353        @exception PluginModulesError raised to indicate the absence of
354            plug-in modules
355        """
356        if not self.__pluginModulesExist():
357            raise PluginModulesError
358
359        self.__insertPluginsPaths()
360
361        for pluginName in self.__foundGlobalModules:
362            # user and core plug-ins have priority
363            if (
364                pluginName not in self.__foundUserModules and
365                pluginName not in self.__foundCoreModules and
366                pluginName.startswith("PluginDocumentationSets")
367            ):
368                self.loadPlugin(pluginName, self.pluginDirs["global"])
369
370        for pluginName in self.__foundUserModules:
371            # core plug-ins have priority
372            if (
373                pluginName not in self.__foundCoreModules and
374                pluginName.startswith("PluginDocumentationSets")
375            ):
376                self.loadPlugin(pluginName, self.pluginDirs["user"])
377
378        for pluginName in self.__foundCoreModules:
379            # plug-in under development has priority
380            if pluginName.startswith("PluginDocumentationSets"):
381                self.loadPlugin(pluginName, self.pluginDirs["eric6"])
382
383    def loadPlugin(self, name, directory, reload_=False, install=False):
384        """
385        Public method to load a plugin module.
386
387        Initially all modules are inactive. Modules that are requested on
388        demand are sorted out and are added to the on demand list. Some
389        basic validity checks are performed as well. Modules failing these
390        checks are added to the failed modules list.
391
392        @param name name of the module to be loaded
393        @type str
394        @param directory name of the plugin directory
395        @type str
396        @param reload_ flag indicating to reload the module
397        @type bool
398        @param install flag indicating a load operation as part of an
399            installation process
400        @type bool
401        @exception PluginLoadError raised to indicate an issue loading
402            the plug-in
403        """
404        try:
405            fname = "{0}.py".format(os.path.join(directory, name))
406            spec = importlib.util.spec_from_file_location(name, fname)
407            module = importlib.util.module_from_spec(spec)
408            sys.modules[module.__name__] = module
409            spec.loader.exec_module(module)
410            if not hasattr(module, "autoactivate"):
411                module.error = self.tr(
412                    "Module is missing the 'autoactivate' attribute.")
413                logging.debug(
414                    "{0}: Module is missing the 'autoactivate' attribute."
415                    .format(name)
416                )
417                self.__failedModules[name] = module
418                raise PluginLoadError(name)
419            if getattr(module, "autoactivate", False):
420                self.__inactiveModules[name] = module
421            else:
422                if (
423                    not hasattr(module, "pluginType") or
424                    not hasattr(module, "pluginTypename")
425                ):
426                    module.error = self.tr(
427                        "Module is missing the 'pluginType' "
428                        "and/or 'pluginTypename' attributes."
429                    )
430                    logging.debug(
431                        "{0}: Module is missing the 'pluginType' "
432                        "and/or 'pluginTypename' attributes."
433                        .format(name)
434                    )
435                    self.__failedModules[name] = module
436                    raise PluginLoadError(name)
437                else:
438                    self.__onDemandInactiveModules[name] = module
439            module.eric6PluginModuleName = name
440            module.eric6PluginModuleFilename = fname
441            if install and hasattr(module, "installDependencies"):
442                # ask the module to install its dependencies
443                module.installDependencies(self.pipInstall)
444            self.__modulesCount += 1
445            if reload_:
446                importlib.reload(module)
447                self.initOnDemandPlugin(name)
448                with contextlib.suppress(KeyError, AttributeError):
449                    pluginObject = self.__onDemandInactivePlugins[name]
450                    pluginObject.initToolbar(
451                        self.__ui, e5App().getObject("ToolbarManager"))
452        except PluginLoadError:
453            print("Error loading plug-in module:", name)
454        except Exception as err:
455            module = types.ModuleType(name)
456            module.error = self.tr(
457                "Module failed to load. Error: {0}").format(str(err))
458            logging.debug(
459                "{0}: Module failed to load. Error: {1}"
460                .format(name, str(err))
461            )
462            self.__failedModules[name] = module
463            print("Error loading plug-in module:", name)
464            print(str(err))
465
466    def unloadPlugin(self, name):
467        """
468        Public method to unload a plugin module.
469
470        @param name name of the module to be unloaded (string)
471        @return flag indicating success (boolean)
472        """
473        if name in self.__onDemandActiveModules:
474            # cannot unload an ondemand plugin, that is in use
475            return False
476
477        if name in self.__activeModules:
478            self.deactivatePlugin(name)
479
480        if name in self.__inactiveModules:
481            with contextlib.suppress(KeyError):
482                pluginObject = self.__inactivePlugins[name]
483                with contextlib.suppress(AttributeError):
484                    pluginObject.prepareUnload()
485                del self.__inactivePlugins[name]
486            del self.__inactiveModules[name]
487        elif name in self.__onDemandInactiveModules:
488            with contextlib.suppress(KeyError):
489                pluginObject = self.__onDemandInactivePlugins[name]
490                with contextlib.suppress(AttributeError):
491                    pluginObject.prepareUnload()
492                del self.__onDemandInactivePlugins[name]
493            del self.__onDemandInactiveModules[name]
494        elif name in self.__failedModules:
495            del self.__failedModules[name]
496
497        self.__modulesCount -= 1
498        return True
499
500    def removePluginFromSysModules(self, pluginName, package,
501                                   internalPackages):
502        """
503        Public method to remove a plugin and all related modules from
504        sys.modules.
505
506        @param pluginName name of the plugin module (string)
507        @param package name of the plugin package (string)
508        @param internalPackages list of intenal packages (list of string)
509        @return flag indicating the plugin module was found in sys.modules
510            (boolean)
511        """
512        packages = [package] + internalPackages
513        found = False
514        if not package:
515            package = "__None__"
516        for moduleName in list(sys.modules.keys())[:]:
517            if (
518                moduleName == pluginName or
519                moduleName.split(".")[0] in packages
520            ):
521                found = True
522                del sys.modules[moduleName]
523        return found
524
525    def initOnDemandPlugins(self):
526        """
527        Public method to create plugin objects for all on demand plugins.
528
529        Note: The plugins are not activated.
530        """
531        names = sorted(self.__onDemandInactiveModules.keys())
532        for name in names:
533            self.initOnDemandPlugin(name)
534
535    def initOnDemandPlugin(self, name):
536        """
537        Public method to create a plugin object for the named on demand plugin.
538
539        Note: The plug-in is not activated.
540
541        @param name name of the plug-in (string)
542        @exception PluginActivationError raised to indicate an issue during the
543            plug-in activation
544        """
545        try:
546            try:
547                module = self.__onDemandInactiveModules[name]
548            except KeyError:
549                return
550
551            if not self.__canActivatePlugin(module):
552                raise PluginActivationError(module.eric6PluginModuleName)
553            version = getattr(module, "version", "0.0.0")
554            className = getattr(module, "className", "")
555            pluginClass = getattr(module, className)
556            pluginObject = None
557            if name not in self.__onDemandInactivePlugins:
558                pluginObject = pluginClass(self.__ui)
559                pluginObject.eric6PluginModule = module
560                pluginObject.eric6PluginName = className
561                pluginObject.eric6PluginVersion = version
562                self.__onDemandInactivePlugins[name] = pluginObject
563        except PluginActivationError:
564            return
565
566    def initPluginToolbars(self, toolbarManager):
567        """
568        Public method to initialize plug-in toolbars.
569
570        @param toolbarManager reference to the toolbar manager object
571            (E5ToolBarManager)
572        """
573        self.initOnDemandPlugins()
574        for pluginObject in self.__onDemandInactivePlugins.values():
575            with contextlib.suppress(AttributeError):
576                pluginObject.initToolbar(self.__ui, toolbarManager)
577
578    def activatePlugins(self):
579        """
580        Public method to activate all plugins having the "autoactivate"
581        attribute set to True.
582        """
583        savedInactiveList = Preferences.Prefs.settings.value(
584            self.__inactivePluginsKey)
585        inactiveList = self.__disabledPlugins[:]
586        if savedInactiveList is not None:
587            inactiveList += [p for p in savedInactiveList
588                             if p not in self.__disabledPlugins]
589        if (
590            self.__develPluginName is not None and
591            self.__develPluginName in inactiveList
592        ):
593            inactiveList.remove(self.__develPluginName)
594        names = sorted(self.__inactiveModules.keys())
595        for name in names:
596            if name not in inactiveList:
597                self.activatePlugin(name)
598        self.allPlugginsActivated.emit()
599
600    def activatePlugin(self, name, onDemand=False):
601        """
602        Public method to activate a plugin.
603
604        @param name name of the module to be activated
605        @param onDemand flag indicating activation of an
606            on demand plugin (boolean)
607        @return reference to the initialized plugin object
608        @exception PluginActivationError raised to indicate an issue during the
609            plug-in activation
610        """
611        try:
612            try:
613                module = (
614                    self.__onDemandInactiveModules[name]
615                    if onDemand else
616                    self.__inactiveModules[name]
617                )
618            except KeyError:
619                return None
620
621            if not self.__canActivatePlugin(module):
622                raise PluginActivationError(module.eric6PluginModuleName)
623            version = getattr(module, "version", "0.0.0")
624            className = getattr(module, "className", "")
625            pluginClass = getattr(module, className)
626            pluginObject = None
627            if onDemand and name in self.__onDemandInactivePlugins:
628                pluginObject = self.__onDemandInactivePlugins[name]
629            elif not onDemand and name in self.__inactivePlugins:
630                pluginObject = self.__inactivePlugins[name]
631            else:
632                pluginObject = pluginClass(self.__ui)
633            self.pluginAboutToBeActivated.emit(name, pluginObject)
634            try:
635                obj, ok = pluginObject.activate()
636            except TypeError:
637                module.error = self.tr(
638                    "Incompatible plugin activation method.")
639                logging.debug(
640                    "{0}: Incompatible plugin activation method."
641                    .format(name)
642                )
643                obj = None
644                ok = True
645            except Exception as err:
646                module.error = str(err)
647                logging.debug("{0}: {1}".format(name, str(err)))
648                obj = None
649                ok = False
650            if not ok:
651                return None
652
653            self.pluginActivated.emit(name, pluginObject)
654            pluginObject.eric6PluginModule = module
655            pluginObject.eric6PluginName = className
656            pluginObject.eric6PluginVersion = version
657
658            if onDemand:
659                self.__onDemandInactiveModules.pop(name)
660                with contextlib.suppress(KeyError):
661                    self.__onDemandInactivePlugins.pop(name)
662                self.__onDemandActivePlugins[name] = pluginObject
663                self.__onDemandActiveModules[name] = module
664            else:
665                self.__inactiveModules.pop(name)
666                with contextlib.suppress(KeyError):
667                    self.__inactivePlugins.pop(name)
668                self.__activePlugins[name] = pluginObject
669                self.__activeModules[name] = module
670            return obj
671        except PluginActivationError:
672            return None
673
674    def __canActivatePlugin(self, module):
675        """
676        Private method to check, if a plugin can be activated.
677
678        @param module reference to the module to be activated
679        @return flag indicating, if the module satisfies all requirements
680            for being activated (boolean)
681        @exception PluginModuleFormatError raised to indicate an invalid
682            plug-in module format
683        @exception PluginClassFormatError raised to indicate an invalid
684            plug-in class format
685        """
686        try:
687            if not hasattr(module, "version"):
688                raise PluginModuleFormatError(
689                    module.eric6PluginModuleName, "version")
690            if not hasattr(module, "className"):
691                raise PluginModuleFormatError(
692                    module.eric6PluginModuleName, "className")
693            className = getattr(module, "className", "")
694            if not className or not hasattr(module, className):
695                raise PluginModuleFormatError(
696                    module.eric6PluginModuleName, className)
697            pluginClass = getattr(module, className)
698            if not hasattr(pluginClass, "__init__"):
699                raise PluginClassFormatError(
700                    module.eric6PluginModuleName,
701                    className, "__init__")
702            if not hasattr(pluginClass, "activate"):
703                raise PluginClassFormatError(
704                    module.eric6PluginModuleName,
705                    className, "activate")
706            if not hasattr(pluginClass, "deactivate"):
707                raise PluginClassFormatError(
708                    module.eric6PluginModuleName,
709                    className, "deactivate")
710            return True
711        except PluginModuleFormatError as e:
712            print(repr(e))
713            return False
714        except PluginClassFormatError as e:
715            print(repr(e))
716            return False
717
718    def deactivatePlugin(self, name, onDemand=False):
719        """
720        Public method to deactivate a plugin.
721
722        @param name name of the module to be deactivated
723        @param onDemand flag indicating deactivation of an
724            on demand plugin (boolean)
725        """
726        try:
727            module = (
728                self.__onDemandActiveModules[name]
729                if onDemand else
730                self.__activeModules[name]
731            )
732        except KeyError:
733            return
734
735        if self.__canDeactivatePlugin(module):
736            pluginObject = None
737            if onDemand and name in self.__onDemandActivePlugins:
738                pluginObject = self.__onDemandActivePlugins[name]
739            elif not onDemand and name in self.__activePlugins:
740                pluginObject = self.__activePlugins[name]
741            if pluginObject:
742                self.pluginAboutToBeDeactivated.emit(name, pluginObject)
743                pluginObject.deactivate()
744                self.pluginDeactivated.emit(name, pluginObject)
745
746                if onDemand:
747                    self.__onDemandActiveModules.pop(name)
748                    self.__onDemandActivePlugins.pop(name)
749                    self.__onDemandInactivePlugins[name] = pluginObject
750                    self.__onDemandInactiveModules[name] = module
751                else:
752                    self.__activeModules.pop(name)
753                    with contextlib.suppress(KeyError):
754                        self.__activePlugins.pop(name)
755                    self.__inactivePlugins[name] = pluginObject
756                    self.__inactiveModules[name] = module
757
758    def __canDeactivatePlugin(self, module):
759        """
760        Private method to check, if a plugin can be deactivated.
761
762        @param module reference to the module to be deactivated
763        @return flag indicating, if the module satisfies all requirements
764            for being deactivated (boolean)
765        """
766        return getattr(module, "deactivateable", True)
767
768    def getPluginObject(self, type_, typename, maybeActive=False):
769        """
770        Public method to activate an ondemand plugin given by type and
771        typename.
772
773        @param type_ type of the plugin to be activated (string)
774        @param typename name of the plugin within the type category (string)
775        @param maybeActive flag indicating, that the plugin may be active
776            already (boolean)
777        @return reference to the initialized plugin object
778        """
779        for name, module in list(self.__onDemandInactiveModules.items()):
780            if (
781                getattr(module, "pluginType", "") == type_ and
782                getattr(module, "pluginTypename", "") == typename
783            ):
784                return self.activatePlugin(name, onDemand=True)
785
786        if maybeActive:
787            for name, module in list(self.__onDemandActiveModules.items()):
788                if (
789                    getattr(module, "pluginType", "") == type_ and
790                    getattr(module, "pluginTypename", "") == typename
791                ):
792                    self.deactivatePlugin(name, onDemand=True)
793                    return self.activatePlugin(name, onDemand=True)
794
795        return None
796
797    def getPluginInfos(self):
798        """
799        Public method to get infos about all loaded plug-ins.
800
801        @return list of dictionaries with keys "module_name", "plugin_name",
802            "version", "auto_activate", "active", "short_desc", "error"
803        @rtype list of dict ("module_name": str, "plugin_name": str,
804            "version": str, "auto_activate": bool, "active": bool,
805            "short_desc": str, "error": bool)
806        """
807        infos = []
808
809        # 1. active, non-on-demand modules
810        for name in list(self.__activeModules.keys()):
811            info = self.__getShortInfo(self.__activeModules[name])
812            info.update({
813                "module_name": name,
814                "auto_activate": True,
815                "active": True,
816            })
817            infos.append(info)
818
819        # 2. inactive, non-on-demand modules
820        for name in list(self.__inactiveModules.keys()):
821            info = self.__getShortInfo(self.__inactiveModules[name])
822            info.update({
823                "module_name": name,
824                "auto_activate": True,
825                "active": False,
826            })
827            infos.append(info)
828
829        # 3. active, on-demand modules
830        for name in list(self.__onDemandActiveModules.keys()):
831            info = self.__getShortInfo(self.__onDemandActiveModules[name])
832            info.update({
833                "module_name": name,
834                "auto_activate": False,
835                "active": True,
836            })
837            infos.append(info)
838
839        # 4. inactive, non-on-demand modules
840        for name in list(self.__onDemandInactiveModules.keys()):
841            info = self.__getShortInfo(self.__onDemandInactiveModules[name])
842            info.update({
843                "module_name": name,
844                "auto_activate": False,
845                "active": False,
846            })
847            infos.append(info)
848
849        # 5. failed modules
850        for name in list(self.__failedModules.keys()):
851            info = self.__getShortInfo(self.__failedModules[name])
852            info.update({
853                "module_name": name,
854                "auto_activate": False,
855                "active": False,
856            })
857            infos.append(info)
858
859        return infos
860
861    def __getShortInfo(self, module):
862        """
863        Private method to extract the short info from a module.
864
865        @param module module to extract short info from
866        @return dictionay containing plug-in data
867        @rtype dict ("plugin_name": str, "version": str, "short_desc": str,
868            "error": bool)
869        """
870        return {
871            "plugin_name": getattr(module, "name", ""),
872            "version": getattr(module, "version", ""),
873            "short_desc": getattr(module, "shortDescription", ""),
874            "error": bool(getattr(module, "error", "")),
875        }
876
877    def getPluginDetails(self, name):
878        """
879        Public method to get detailed information about a plugin.
880
881        @param name name of the module to get detailed infos about (string)
882        @return details of the plugin as a dictionary
883        """
884        details = {}
885
886        autoactivate = True
887        active = True
888
889        if name in self.__activeModules:
890            module = self.__activeModules[name]
891        elif name in self.__inactiveModules:
892            module = self.__inactiveModules[name]
893            active = False
894        elif name in self.__onDemandActiveModules:
895            module = self.__onDemandActiveModules[name]
896            autoactivate = False
897        elif name in self.__onDemandInactiveModules:
898            module = self.__onDemandInactiveModules[name]
899            autoactivate = False
900            active = False
901        elif name in self.__failedModules:
902            module = self.__failedModules[name]
903            autoactivate = False
904            active = False
905        elif "_" in name:
906            # try stripping of a postfix
907            return self.getPluginDetails(name.rsplit("_", 1)[0])
908        else:
909            # should not happen
910            return None
911
912        details["moduleName"] = name
913        details["moduleFileName"] = getattr(
914            module, "eric6PluginModuleFilename", "")
915        details["pluginName"] = getattr(module, "name", "")
916        details["version"] = getattr(module, "version", "")
917        details["author"] = getattr(module, "author", "")
918        details["description"] = getattr(module, "longDescription", "")
919        details["autoactivate"] = autoactivate
920        details["active"] = active
921        details["error"] = getattr(module, "error", "")
922
923        return details
924
925    def doShutdown(self):
926        """
927        Public method called to perform actions upon shutdown of the IDE.
928        """
929        names = []
930        for name in list(self.__inactiveModules.keys()):
931            names.append(name)
932        Preferences.Prefs.settings.setValue(self.__inactivePluginsKey, names)
933
934        self.shutdown.emit()
935
936    def getPluginDisplayStrings(self, type_):
937        """
938        Public method to get the display strings of all plugins of a specific
939        type.
940
941        @param type_ type of the plugins (string)
942        @return dictionary with name as key and display string as value
943            (dictionary of string)
944        """
945        pluginDict = {}
946
947        for module in (
948            list(self.__onDemandActiveModules.values()) +
949            list(self.__onDemandInactiveModules.values())
950        ):
951            if (
952                getattr(module, "pluginType", "") == type_ and
953                getattr(module, "error", "") == ""
954            ):
955                plugin_name = getattr(module, "pluginTypename", "")
956                if plugin_name:
957                    if hasattr(module, "displayString"):
958                        try:
959                            disp = module.displayString()
960                        except TypeError:
961                            disp = getattr(module, "displayString", "")
962                        if disp != "":
963                            pluginDict[plugin_name] = disp
964                    else:
965                        pluginDict[plugin_name] = plugin_name
966
967        return pluginDict
968
969    def getPluginPreviewPixmap(self, type_, name):
970        """
971        Public method to get a preview pixmap of a plugin of a specific type.
972
973        @param type_ type of the plugin (string)
974        @param name name of the plugin type (string)
975        @return preview pixmap (QPixmap)
976        """
977        for module in (
978            list(self.__onDemandActiveModules.values()) +
979            list(self.__onDemandInactiveModules.values())
980        ):
981            if (
982                getattr(module, "pluginType", "") == type_ and
983                getattr(module, "pluginTypename", "") == name
984            ):
985                if hasattr(module, "previewPix"):
986                    return module.previewPix()
987                else:
988                    return QPixmap()
989
990        return QPixmap()
991
992    def getPluginApiFiles(self, language):
993        """
994        Public method to get the list of API files installed by a plugin.
995
996        @param language language of the requested API files (string)
997        @return list of API filenames (list of string)
998        """
999        apis = []
1000
1001        for module in (
1002            list(self.__activeModules.values()) +
1003            list(self.__onDemandActiveModules.values())
1004        ):
1005            if hasattr(module, "apiFiles"):
1006                apis.extend(module.apiFiles(language))
1007
1008        return apis
1009
1010    def getPluginQtHelpFiles(self):
1011        """
1012        Public method to get the list of QtHelp documentation files provided
1013        by a plug-in.
1014
1015        @return dictionary with documentation type as key and list of files
1016            as value
1017        @rtype dict (key: str, value: list of str)
1018        """
1019        helpFiles = {}
1020        for module in (
1021            list(self.__activeModules.values()) +
1022            list(self.__onDemandActiveModules.values())
1023        ):
1024            if hasattr(module, "helpFiles"):
1025                helpFiles.update(module.helpFiles())
1026
1027        return helpFiles
1028
1029    def getPluginExeDisplayData(self):
1030        """
1031        Public method to get data to display information about a plugins
1032        external tool.
1033
1034        @return list of dictionaries containing the data. Each dictionary must
1035            either contain data for the determination or the data to be
1036            displayed.<br />
1037            A dictionary of the first form must have the following entries:
1038            <ul>
1039                <li>programEntry - indicator for this dictionary form
1040                   (boolean), always True</li>
1041                <li>header - string to be diplayed as a header (string)</li>
1042                <li>exe - the executable (string)</li>
1043                <li>versionCommand - commandline parameter for the exe
1044                    (string)</li>
1045                <li>versionStartsWith - indicator for the output line
1046                    containing the version (string)</li>
1047                <li>versionPosition - number of element containing the
1048                    version (integer)</li>
1049                <li>version - version to be used as default (string)</li>
1050                <li>versionCleanup - tuple of two integers giving string
1051                    positions start and stop for the version string
1052                    (tuple of integers)</li>
1053            </ul>
1054            A dictionary of the second form must have the following entries:
1055            <ul>
1056                <li>programEntry - indicator for this dictionary form
1057                    (boolean), always False</li>
1058                <li>header - string to be diplayed as a header (string)</li>
1059                <li>text - entry text to be shown (string)</li>
1060                <li>version - version text to be shown (string)</li>
1061            </ul>
1062        """
1063        infos = []
1064
1065        for module in (
1066            list(self.__activeModules.values()) +
1067            list(self.__inactiveModules.values())
1068        ):
1069            if hasattr(module, "exeDisplayDataList"):
1070                infos.extend(module.exeDisplayDataList())
1071            elif hasattr(module, "exeDisplayData"):
1072                infos.append(module.exeDisplayData())
1073        for module in (
1074            list(self.__onDemandActiveModules.values()) +
1075            list(self.__onDemandInactiveModules.values())
1076        ):
1077            if hasattr(module, "exeDisplayDataList"):
1078                infos.extend(module.exeDisplayDataList())
1079            elif hasattr(module, "exeDisplayData"):
1080                infos.append(module.exeDisplayData())
1081
1082        return infos
1083
1084    def getPluginConfigData(self):
1085        """
1086        Public method to get the config data of all active, non on-demand
1087        plugins used by the configuration dialog.
1088
1089        Plugins supporting this functionality must provide the plugin module
1090        function 'getConfigData' returning a dictionary with unique keys
1091        of lists with the following list contents:
1092        <dl>
1093          <dt>display string</dt>
1094          <dd>string shown in the selection area of the configuration page.
1095              This should be a localized string</dd>
1096          <dt>pixmap name</dt>
1097          <dd>filename of the pixmap to be shown next to the display
1098              string</dd>
1099          <dt>page creation function</dt>
1100          <dd>plugin module function to be called to create the configuration
1101              page. The page must be subclasses from
1102              Preferences.ConfigurationPages.ConfigurationPageBase and must
1103              implement a method called 'save' to save the settings. A parent
1104              entry will be created in the selection list, if this value is
1105              None.</dd>
1106          <dt>parent key</dt>
1107          <dd>dictionary key of the parent entry or None, if this defines a
1108              toplevel entry.</dd>
1109          <dt>reference to configuration page</dt>
1110          <dd>This will be used by the configuration dialog and must always
1111              be None</dd>
1112        </dl>
1113
1114        @return plug-in configuration data
1115        """
1116        configData = {}
1117        for module in (
1118            list(self.__activeModules.values()) +
1119            list(self.__onDemandActiveModules.values()) +
1120                list(self.__onDemandInactiveModules.values())
1121        ):
1122            if hasattr(module, 'getConfigData'):
1123                configData.update(module.getConfigData())
1124        return configData
1125
1126    def isPluginLoaded(self, pluginName):
1127        """
1128        Public method to check, if a certain plugin is loaded.
1129
1130        @param pluginName name of the plugin to check for (string)
1131        @return flag indicating, if the plugin is loaded (boolean)
1132        """
1133        return (
1134            pluginName in self.__activeModules or
1135            pluginName in self.__inactiveModules or
1136            pluginName in self.__onDemandActiveModules or
1137            pluginName in self.__onDemandInactiveModules
1138        )
1139
1140    def isPluginActive(self, pluginName):
1141        """
1142        Public method to check, if a certain plugin is active.
1143
1144        @param pluginName name of the plugin to check for (string)
1145        @return flag indicating, if the plugin is active (boolean)
1146        """
1147        return (
1148            pluginName in self.__activeModules or
1149            pluginName in self.__onDemandActiveModules
1150        )
1151
1152    ###########################################################################
1153    ## Specialized plug-in module handling methods below
1154    ###########################################################################
1155
1156    ###########################################################################
1157    ## VCS related methods below
1158    ###########################################################################
1159
1160    def getVcsSystemIndicators(self):
1161        """
1162        Public method to get the Vcs System indicators.
1163
1164        Plugins supporting this functionality must support the module function
1165        getVcsSystemIndicator returning a dictionary with indicator as key and
1166        a tuple with the vcs name (string) and vcs display string (string).
1167
1168        @return dictionary with indicator as key and a list of tuples as
1169            values. Each tuple contains the vcs name (string) and vcs display
1170            string (string).
1171        """
1172        vcsDict = {}
1173
1174        for module in (
1175            list(self.__onDemandActiveModules.values()) +
1176            list(self.__onDemandInactiveModules.values())
1177        ):
1178            if (
1179                getattr(module, "pluginType", "") == "version_control" and
1180                hasattr(module, "getVcsSystemIndicator")
1181            ):
1182                res = module.getVcsSystemIndicator()
1183                for indicator, vcsData in list(res.items()):
1184                    if indicator in vcsDict:
1185                        vcsDict[indicator].append(vcsData)
1186                    else:
1187                        vcsDict[indicator] = [vcsData]
1188
1189        return vcsDict
1190
1191    def deactivateVcsPlugins(self):
1192        """
1193        Public method to deactivated all activated VCS plugins.
1194        """
1195        for name, module in list(self.__onDemandActiveModules.items()):
1196            if getattr(module, "pluginType", "") == "version_control":
1197                self.deactivatePlugin(name, True)
1198
1199    ########################################################################
1200    ## Methods for the creation of the plug-ins download directory
1201    ########################################################################
1202
1203    def __checkPluginsDownloadDirectory(self):
1204        """
1205        Private slot to check for the existence of the plugins download
1206        directory.
1207        """
1208        downloadDir = Preferences.getPluginManager("DownloadPath")
1209        if not downloadDir:
1210            downloadDir = self.__defaultDownloadDir
1211
1212        if not os.path.exists(downloadDir):
1213            try:
1214                os.mkdir(downloadDir, 0o755)
1215            except OSError:
1216                # try again with (possibly) new default
1217                downloadDir = self.__defaultDownloadDir
1218                if not os.path.exists(downloadDir):
1219                    try:
1220                        os.mkdir(downloadDir, 0o755)
1221                    except OSError as err:
1222                        E5MessageBox.critical(
1223                            self.__ui,
1224                            self.tr("Plugin Manager Error"),
1225                            self.tr(
1226                                """<p>The plugin download directory"""
1227                                """ <b>{0}</b> could not be created. Please"""
1228                                """ configure it via the configuration"""
1229                                """ dialog.</p><p>Reason: {1}</p>""")
1230                            .format(downloadDir, str(err)))
1231                        downloadDir = ""
1232
1233        Preferences.setPluginManager("DownloadPath", downloadDir)
1234
1235    def preferencesChanged(self):
1236        """
1237        Public slot to react to changes in configuration.
1238        """
1239        self.__checkPluginsDownloadDirectory()
1240
1241    ########################################################################
1242    ## Methods for automatic plug-in update check below
1243    ########################################################################
1244
1245    def __onlineStateChanged(self, online):
1246        """
1247        Private slot handling changes in online state.
1248
1249        @param online flag indicating the online state
1250        @type bool
1251        """
1252        if online:
1253            self.checkPluginUpdatesAvailable()
1254
1255    def checkPluginUpdatesAvailable(self):
1256        """
1257        Public method to check the availability of updates of plug-ins.
1258        """
1259        period = Preferences.getPluginManager("UpdatesCheckInterval")
1260        if period == 0:
1261            return
1262        elif period in [1, 2, 3]:
1263            lastModified = QFileInfo(self.pluginRepositoryFile).lastModified()
1264            if lastModified.isValid() and lastModified.date().isValid():
1265                lastModifiedDate = lastModified.date()
1266                now = QDate.currentDate()
1267                if (
1268                    (period == 1 and lastModifiedDate.day() == now.day()) or
1269                    (period == 2 and lastModifiedDate.daysTo(now) < 7) or
1270                    (period == 3 and (lastModifiedDate.daysTo(now) <
1271                                      lastModifiedDate.daysInMonth()))
1272                ):
1273                    # daily, weekly, monthly
1274                    return
1275
1276        self.__updateAvailable = False
1277
1278        request = QNetworkRequest(
1279            QUrl(Preferences.getUI("PluginRepositoryUrl6")))
1280        request.setAttribute(
1281            QNetworkRequest.Attribute.CacheLoadControlAttribute,
1282            QNetworkRequest.CacheLoadControl.AlwaysNetwork)
1283        reply = self.__networkManager.get(request)
1284        reply.finished.connect(
1285            lambda: self.__downloadRepositoryFileDone(reply))
1286        self.__replies.append(reply)
1287
1288    def __downloadRepositoryFileDone(self, reply):
1289        """
1290        Private method called after the repository file was downloaded.
1291
1292        @param reply reference to the reply object of the download
1293        @type QNetworkReply
1294        """
1295        if reply in self.__replies:
1296            self.__replies.remove(reply)
1297
1298        if reply.error() != QNetworkReply.NetworkError.NoError:
1299            E5MessageBox.warning(
1300                None,
1301                self.tr("Error downloading file"),
1302                self.tr(
1303                    """<p>Could not download the requested file"""
1304                    """ from {0}.</p><p>Error: {1}</p>"""
1305                ).format(Preferences.getUI("PluginRepositoryUrl6"),
1306                         reply.errorString())
1307            )
1308            reply.deleteLater()
1309            return
1310
1311        ioDevice = QFile(self.pluginRepositoryFile + ".tmp")
1312        ioDevice.open(QIODevice.OpenModeFlag.WriteOnly)
1313        ioDevice.write(reply.readAll())
1314        ioDevice.close()
1315        if QFile.exists(self.pluginRepositoryFile):
1316            QFile.remove(self.pluginRepositoryFile)
1317        ioDevice.rename(self.pluginRepositoryFile)
1318        reply.deleteLater()
1319
1320        if os.path.exists(self.pluginRepositoryFile):
1321            f = QFile(self.pluginRepositoryFile)
1322            if f.open(QIODevice.OpenModeFlag.ReadOnly):
1323                # save current URL
1324                url = Preferences.getUI("PluginRepositoryUrl6")
1325
1326                # read the repository file
1327                from E5XML.PluginRepositoryReader import PluginRepositoryReader
1328                reader = PluginRepositoryReader(f, self.checkPluginEntry)
1329                reader.readXML()
1330                if url != Preferences.getUI("PluginRepositoryUrl6"):
1331                    # redo if it is a redirect
1332                    self.checkPluginUpdatesAvailable()
1333                    return
1334
1335                if self.__updateAvailable:
1336                    res = E5MessageBox.information(
1337                        None,
1338                        self.tr("New plugin versions available"),
1339                        self.tr("<p>There are new plug-ins or plug-in"
1340                                " updates available. Use the plug-in"
1341                                " repository dialog to get them.</p>"),
1342                        E5MessageBox.StandardButtons(
1343                            E5MessageBox.Ignore |
1344                            E5MessageBox.Open),
1345                        E5MessageBox.Open)
1346                    if res == E5MessageBox.Open:
1347                        self.__ui.showPluginsAvailable()
1348
1349    def checkPluginEntry(self, name, short, description, url, author, version,
1350                         filename, status):
1351        """
1352        Public method to check a plug-in's data for an update.
1353
1354        @param name data for the name field (string)
1355        @param short data for the short field (string)
1356        @param description data for the description field (list of strings)
1357        @param url data for the url field (string)
1358        @param author data for the author field (string)
1359        @param version data for the version field (string)
1360        @param filename data for the filename field (string)
1361        @param status status of the plugin (string [stable, unstable, unknown])
1362        """
1363        # ignore hidden plug-ins
1364        pluginName = os.path.splitext(url.rsplit("/", 1)[1])[0]
1365        if pluginName in Preferences.getPluginManager("HiddenPlugins"):
1366            return
1367
1368        archive = os.path.join(Preferences.getPluginManager("DownloadPath"),
1369                               filename)
1370
1371        # Check against installed/loaded plug-ins
1372        pluginDetails = self.getPluginDetails(pluginName)
1373        if pluginDetails is None:
1374            if not Preferences.getPluginManager("CheckInstalledOnly"):
1375                self.__updateAvailable = True
1376            return
1377
1378        versionTuple = Globals.versionToTuple(version)[:3]
1379        pluginVersionTuple = Globals.versionToTuple(
1380            pluginDetails["version"])[:3]
1381
1382        if pluginVersionTuple < versionTuple:
1383            self.__updateAvailable = True
1384            return
1385
1386        if not Preferences.getPluginManager("CheckInstalledOnly"):
1387            # Check against downloaded plugin archives
1388            # 1. Check, if the archive file exists
1389            if not os.path.exists(archive):
1390                if pluginDetails["moduleName"] != pluginName:
1391                    self.__updateAvailable = True
1392                return
1393
1394            # 2. Check, if the archive is a valid zip file
1395            if not zipfile.is_zipfile(archive):
1396                self.__updateAvailable = True
1397                return
1398
1399            # 3. Check the version of the archive file
1400            zipFile = zipfile.ZipFile(archive, "r")
1401            try:
1402                aversion = zipFile.read("VERSION").decode("utf-8")
1403            except KeyError:
1404                aversion = "0.0.0"
1405            zipFile.close()
1406
1407            aversionTuple = Globals.versionToTuple(aversion)[:3]
1408            if aversionTuple != versionTuple:
1409                self.__updateAvailable = True
1410
1411    def __sslErrors(self, reply, errors):
1412        """
1413        Private slot to handle SSL errors.
1414
1415        @param reply reference to the reply object (QNetworkReply)
1416        @param errors list of SSL errors (list of QSslError)
1417        """
1418        ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0]
1419        if ignored == E5SslErrorState.NOT_IGNORED:
1420            self.__downloadCancelled = True
1421
1422    ########################################################################
1423    ## Methods to clear private data of plug-ins below
1424    ########################################################################
1425
1426    def clearPluginsPrivateData(self, type_):
1427        """
1428        Public method to clear the private data of plug-ins of a specified
1429        type.
1430
1431        Plugins supporting this functionality must support the module function
1432        clearPrivateData() and have the module level attribute pluginType.
1433
1434        @param type_ type of the plugin to clear private data for (string)
1435        """
1436        for module in (
1437            list(self.__onDemandActiveModules.values()) +
1438            list(self.__onDemandInactiveModules.values()) +
1439            list(self.__activeModules.values()) +
1440            list(self.__inactiveModules.values())
1441        ):
1442            if (
1443                getattr(module, "pluginType", "") == type_ and
1444                hasattr(module, "clearPrivateData")
1445            ):
1446                module.clearPrivateData()
1447
1448    ########################################################################
1449    ## Methods to install a plug-in module dependency via pip
1450    ########################################################################
1451
1452    def pipInstall(self, packages):
1453        """
1454        Public method to install the given package via pip.
1455
1456        @param packages list of packages to install
1457        @type list of str
1458        """
1459        try:
1460            pip = e5App().getObject("Pip")
1461        except KeyError:
1462            # Installation is performed via the plug-in installation script.
1463            from PipInterface.Pip import Pip
1464            pip = Pip(self)
1465        pip.installPackages(packages, interpreter=sys.executable)
1466
1467#
1468# eflag: noqa = M801
1469