807 lines
		
	
	
	
		
			32 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			807 lines
		
	
	
	
		
			32 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# This file is part of Gajim.
 | 
						|
#
 | 
						|
# Gajim 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; version 3 only.
 | 
						|
#
 | 
						|
# Gajim is distributed in the hope that it will be useful,
 | 
						|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						|
# GNU General Public License for more details.
 | 
						|
#
 | 
						|
# You should have received a copy of the GNU General Public License
 | 
						|
# along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
 | 
						|
 | 
						|
'''
 | 
						|
Plug-in management related classes.
 | 
						|
 | 
						|
:author: Mateusz Biliński <mateusz@bilinski.it>
 | 
						|
:since: 30th May 2008
 | 
						|
:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it>
 | 
						|
:license: GPL
 | 
						|
'''
 | 
						|
 | 
						|
__all__ = ['PluginManager']
 | 
						|
 | 
						|
import os
 | 
						|
import sys
 | 
						|
import fnmatch
 | 
						|
import zipfile
 | 
						|
from shutil import rmtree, move
 | 
						|
import configparser
 | 
						|
from pkg_resources import parse_version
 | 
						|
 | 
						|
import gajim
 | 
						|
from gajim.common import app
 | 
						|
from gajim.common import nec
 | 
						|
from gajim.common import configpaths
 | 
						|
from gajim.common import modules
 | 
						|
from gajim.common.i18n import _
 | 
						|
from gajim.common.exceptions import PluginsystemError
 | 
						|
from gajim.common.helpers import Singleton
 | 
						|
from gajim.plugins import plugins_i18n
 | 
						|
 | 
						|
from gajim.plugins.helpers import log, log_calls
 | 
						|
from gajim.plugins.helpers import GajimPluginActivateException
 | 
						|
from gajim.plugins.gajimplugin import GajimPlugin, GajimPluginException
 | 
						|
 | 
						|
class PluginManager(metaclass=Singleton):
 | 
						|
    '''
 | 
						|
    Main plug-in management class.
 | 
						|
 | 
						|
    Currently:
 | 
						|
            - scans for plugins
 | 
						|
            - activates them
 | 
						|
            - handles GUI extension points, when called by GUI objects after
 | 
						|
                plugin is activated (by dispatching info about call to handlers
 | 
						|
                in plugins)
 | 
						|
 | 
						|
    :todo: add more info about how GUI extension points work
 | 
						|
    :todo: add list of available GUI extension points
 | 
						|
    :todo: implement mechanism to dynamically load plugins where GUI extension
 | 
						|
               points have been already called (i.e. when plugin is activated
 | 
						|
               after GUI object creation). [DONE?]
 | 
						|
    :todo: implement mechanism to dynamically deactivate plugins (call plugin's
 | 
						|
               deactivation handler) [DONE?]
 | 
						|
    :todo: when plug-in is deactivated all GUI extension points are removed
 | 
						|
               from `PluginManager.gui_extension_points_handlers`. But when
 | 
						|
               object that invoked GUI extension point is abandoned by Gajim,
 | 
						|
               eg. closed ChatControl object, the reference to called GUI
 | 
						|
               extension points is still in `PluginManager.gui_extension_points`
 | 
						|
               These should be removed, so that object can be destroyed by
 | 
						|
               Python.
 | 
						|
               Possible solution: add call to clean up method in classes
 | 
						|
               'destructors' (classes that register GUI extension points)
 | 
						|
    '''
 | 
						|
 | 
						|
    #@log_calls('PluginManager')
 | 
						|
    def __init__(self):
 | 
						|
        self.plugins = []
 | 
						|
        '''
 | 
						|
        Detected plugin classes.
 | 
						|
 | 
						|
        Each class object in list is `GajimPlugin` subclass.
 | 
						|
 | 
						|
        :type: [] of class objects
 | 
						|
        '''
 | 
						|
        self.active_plugins = []
 | 
						|
        '''
 | 
						|
        Instance objects of active plugins.
 | 
						|
 | 
						|
        These are object instances of classes held `plugins`, but only those
 | 
						|
        that were activated.
 | 
						|
 | 
						|
        :type: [] of `GajimPlugin` based objects
 | 
						|
        '''
 | 
						|
        self.gui_extension_points = {}
 | 
						|
        '''
 | 
						|
        Registered GUI extension points.
 | 
						|
        '''
 | 
						|
 | 
						|
        self.gui_extension_points_handlers = {}
 | 
						|
        '''
 | 
						|
        Registered handlers of GUI extension points.
 | 
						|
        '''
 | 
						|
 | 
						|
        self.encryption_plugins = {}
 | 
						|
        '''
 | 
						|
        Registered names with instances of encryption Plugins.
 | 
						|
        '''
 | 
						|
 | 
						|
        self.update_plugins()
 | 
						|
 | 
						|
        for path in reversed(configpaths.get_plugin_dirs()):
 | 
						|
            pc = self.scan_dir_for_plugins(path)
 | 
						|
            self.add_plugins(pc)
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def _plugin_has_entry_in_global_config(self, plugin):
 | 
						|
        if app.config.get_per('plugins', plugin.short_name) is None:
 | 
						|
            return False
 | 
						|
        return True
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def _create_plugin_entry_in_global_config(self, plugin):
 | 
						|
        app.config.add_per('plugins', plugin.short_name)
 | 
						|
 | 
						|
    def _remove_plugin_entry_in_global_config(self, plugin):
 | 
						|
        app.config.del_per('plugins', plugin.short_name)
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def update_plugins(self, replace=True, activate=False, plugin_name=None):
 | 
						|
        '''
 | 
						|
        Move plugins from the downloaded folder to the user plugin folder
 | 
						|
 | 
						|
        :param replace: replace plugin files if they already exist.
 | 
						|
        :type replace: boolean
 | 
						|
        :param activate: load and activate the plugin
 | 
						|
        :type activate: boolean
 | 
						|
        :param plugin_name: if provided, update only this plugin
 | 
						|
        :type plugin_name: str
 | 
						|
        :return: list of updated plugins (files have been installed)
 | 
						|
        :rtype: [] of str
 | 
						|
        '''
 | 
						|
        updated_plugins = []
 | 
						|
        user_dir = configpaths.get('PLUGINS_USER')
 | 
						|
        dl_dir = configpaths.get('PLUGINS_DOWNLOAD')
 | 
						|
        to_update = [plugin_name] if plugin_name else next(os.walk(dl_dir))[1]
 | 
						|
        for directory in to_update:
 | 
						|
            src_dir = os.path.join(dl_dir, directory)
 | 
						|
            dst_dir = os.path.join(user_dir, directory)
 | 
						|
            try:
 | 
						|
                if os.path.exists(dst_dir):
 | 
						|
                    if not replace:
 | 
						|
                        continue
 | 
						|
                    self.delete_plugin_files(dst_dir)
 | 
						|
                move(src_dir, dst_dir)
 | 
						|
            except Exception:
 | 
						|
                log.exception('Upgrade of plugin %s failed. Impossible to move '
 | 
						|
                    'files from "%s" to "%s"', directory, src_dir, dst_dir)
 | 
						|
                continue
 | 
						|
            updated_plugins.append(directory)
 | 
						|
            if activate:
 | 
						|
                pc = self.scan_dir_for_plugins(dst_dir, scan_dirs=True,
 | 
						|
                    package=True)
 | 
						|
                if not pc:
 | 
						|
                    continue
 | 
						|
                self.add_plugin(pc[0])
 | 
						|
                plugin = self.plugins[-1]
 | 
						|
                self.activate_plugin(plugin)
 | 
						|
        return updated_plugins
 | 
						|
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def init_plugins(self):
 | 
						|
        self._activate_all_plugins_from_global_config()
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def add_plugin(self, plugin_class):
 | 
						|
        '''
 | 
						|
        :todo: what about adding plug-ins that are already added? Module reload
 | 
						|
        and adding class from reloaded module or ignoring adding plug-in?
 | 
						|
        '''
 | 
						|
        try:
 | 
						|
            plugin = plugin_class()
 | 
						|
        except Exception:
 | 
						|
            log.exception('Error while loading a plugin')
 | 
						|
            return
 | 
						|
 | 
						|
        if plugin not in self.plugins:
 | 
						|
            if not self._plugin_has_entry_in_global_config(plugin):
 | 
						|
                self._create_plugin_entry_in_global_config(plugin)
 | 
						|
 | 
						|
            self.plugins.append(plugin)
 | 
						|
            plugin.active = False
 | 
						|
        else:
 | 
						|
            log.info('Not loading plugin %s v%s from module %s '
 | 
						|
                     '(identified by short name: %s). Plugin already loaded.',
 | 
						|
                     plugin.name, plugin.version,
 | 
						|
                     plugin.__module__, plugin.short_name)
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def remove_plugin(self, plugin):
 | 
						|
        '''
 | 
						|
        removes the plugin from the plugin list and deletes all loaded modules
 | 
						|
        from sys. This way we will have a fresh start when the plugin gets added
 | 
						|
        again.
 | 
						|
        '''
 | 
						|
        if plugin.active:
 | 
						|
            self.deactivate_plugin(plugin)
 | 
						|
 | 
						|
        self.plugins.remove(plugin)
 | 
						|
 | 
						|
        # remove modules from cache
 | 
						|
        base_package = plugin.__module__.split('.')[0]
 | 
						|
        # get the subpackages/-modules of the base_package. Add a dot to the
 | 
						|
        # name to avoid name problems (removing module_abc if base_package is
 | 
						|
        # module_ab)
 | 
						|
        modules_to_remove = [module for module in sys.modules
 | 
						|
                             if module.startswith('{}.'.format(base_package))]
 | 
						|
        # remove the base_package itself
 | 
						|
        if base_package in sys.modules:
 | 
						|
            modules_to_remove.append(base_package)
 | 
						|
 | 
						|
        for module_to_remove in modules_to_remove:
 | 
						|
            del sys.modules[module_to_remove]
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def add_plugins(self, plugin_classes):
 | 
						|
        for plugin_class in plugin_classes:
 | 
						|
            self.add_plugin(plugin_class)
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def get_active_plugin(self, plugin_name):
 | 
						|
        for plugin in self.active_plugins:
 | 
						|
            if plugin.short_name == plugin_name:
 | 
						|
                return plugin
 | 
						|
        return None
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def extension_point(self, gui_extpoint_name, *args):
 | 
						|
        '''
 | 
						|
        Invokes all handlers (from plugins) for a particular extension point, but
 | 
						|
        doesn't add it to collection for further processing.
 | 
						|
        For example if you pass a message for encryption via extension point to a
 | 
						|
        plugin, its undesired that the call is stored and replayed on activating the
 | 
						|
        plugin. For example after an update.
 | 
						|
 | 
						|
        :param gui_extpoint_name: name of GUI extension point.
 | 
						|
        :type gui_extpoint_name: str
 | 
						|
        :param args: parameters to be passed to extension point handlers
 | 
						|
                (typically and object that invokes `gui_extension_point`;
 | 
						|
                however, this can be practically anything)
 | 
						|
        :type args: tuple
 | 
						|
        '''
 | 
						|
 | 
						|
        self._execute_all_handlers_of_gui_extension_point(gui_extpoint_name,
 | 
						|
            *args)
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def gui_extension_point(self, gui_extpoint_name, *args):
 | 
						|
        '''
 | 
						|
        Invokes all handlers (from plugins) for particular GUI extension point
 | 
						|
        and adds it to collection for further processing (eg. by plugins not
 | 
						|
        active yet).
 | 
						|
 | 
						|
        :param gui_extpoint_name: name of GUI extension point.
 | 
						|
        :type gui_extpoint_name: str
 | 
						|
        :param args: parameters to be passed to extension point handlers
 | 
						|
                (typically and object that invokes `gui_extension_point`;
 | 
						|
                however, this can be practically anything)
 | 
						|
        :type args: tuple
 | 
						|
 | 
						|
        :todo: GUI extension points must be documented well - names with
 | 
						|
                parameters that will be passed to handlers (in plugins). Such
 | 
						|
                documentation must be obeyed both in core and in plugins. This
 | 
						|
                is a loosely coupled approach and is pretty natural in Python.
 | 
						|
 | 
						|
        :bug: what if only some handlers are successfully connected? we should
 | 
						|
                revert all those connections that where successfully made. Maybe
 | 
						|
                call 'self._deactivate_plugin()' or sth similar.
 | 
						|
                Looking closer - we only rewrite tuples here. Real check should
 | 
						|
                be made in method that invokes gui_extpoints handlers.
 | 
						|
        '''
 | 
						|
 | 
						|
        self._add_gui_extension_point_call_to_list(gui_extpoint_name, *args)
 | 
						|
        self._execute_all_handlers_of_gui_extension_point(gui_extpoint_name,
 | 
						|
            *args)
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def remove_gui_extension_point(self, gui_extpoint_name, *args):
 | 
						|
        '''
 | 
						|
        Removes GUI extension point from collection held by `PluginManager`.
 | 
						|
 | 
						|
        From this point this particular extension point won't be visible
 | 
						|
        to plugins (eg. it won't invoke any handlers when plugin is activated).
 | 
						|
 | 
						|
        GUI extension point is removed completely (there is no way to recover it
 | 
						|
        from inside `PluginManager`).
 | 
						|
 | 
						|
        Removal is needed when instance object that given extension point was
 | 
						|
        connect with is destroyed (eg. ChatControl is closed or context menu
 | 
						|
        is hidden).
 | 
						|
 | 
						|
        Each `PluginManager.gui_extension_point` call should have a call of
 | 
						|
        `PluginManager.remove_gui_extension_point` related to it.
 | 
						|
 | 
						|
        :note: in current implementation different arguments mean different
 | 
						|
                extension points. The same arguments and the same name mean
 | 
						|
                the same extension point.
 | 
						|
        :todo: instead of using argument to identify which extpoint should be
 | 
						|
                removed, maybe add additional 'id' argument - this would work
 | 
						|
                similar hash in Python objects. 'id' would be calculated based
 | 
						|
                on arguments passed or on anything else (even could be constant)
 | 
						|
                This would give core developers (that add new extpoints) more
 | 
						|
                freedom, but is this necessary?
 | 
						|
 | 
						|
        :param gui_extpoint_name: name of GUI extension point.
 | 
						|
        :type gui_extpoint_name: str
 | 
						|
        :param args: arguments that `PluginManager.gui_extension_point` was
 | 
						|
                called with for this extension point. This is used (along with
 | 
						|
                extension point name) to identify element to be removed.
 | 
						|
        :type args: tuple
 | 
						|
        '''
 | 
						|
        if gui_extpoint_name in self.gui_extension_points:
 | 
						|
            extension_points = list(self.gui_extension_points[gui_extpoint_name])
 | 
						|
            for ext_point in extension_points:
 | 
						|
                if args[0] in ext_point:
 | 
						|
                    self.gui_extension_points[gui_extpoint_name].remove(
 | 
						|
                        ext_point)
 | 
						|
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def _add_gui_extension_point_call_to_list(self, gui_extpoint_name, *args):
 | 
						|
        '''
 | 
						|
        Adds GUI extension point call to list of calls.
 | 
						|
 | 
						|
        This is done only if such call hasn't been added already
 | 
						|
        (same extension point name and same arguments).
 | 
						|
 | 
						|
        :note: This is assumption that GUI extension points are different only
 | 
						|
        if they have different name or different arguments.
 | 
						|
 | 
						|
        :param gui_extpoint_name: GUI extension point name used to identify it
 | 
						|
                by plugins.
 | 
						|
        :type gui_extpoint_name: str
 | 
						|
 | 
						|
        :param args: parameters to be passed to extension point handlers
 | 
						|
                (typically and object that invokes `gui_extension_point`;
 | 
						|
                however, this can be practically anything)
 | 
						|
        :type args: tuple
 | 
						|
 | 
						|
        '''
 | 
						|
        if ((gui_extpoint_name not in self.gui_extension_points)
 | 
						|
        or (args not in self.gui_extension_points[gui_extpoint_name])):
 | 
						|
            self.gui_extension_points.setdefault(gui_extpoint_name, []).append(
 | 
						|
                args)
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def _execute_all_handlers_of_gui_extension_point(self, gui_extpoint_name,
 | 
						|
    *args):
 | 
						|
        if gui_extpoint_name in self.gui_extension_points_handlers:
 | 
						|
            for handlers in self.gui_extension_points_handlers[
 | 
						|
            gui_extpoint_name]:
 | 
						|
                try:
 | 
						|
                    handlers[0](*args)
 | 
						|
                except Exception:
 | 
						|
                    log.warning('Error executing %s',
 | 
						|
                                handlers[0], exc_info=True)
 | 
						|
 | 
						|
    def _register_events_handlers_in_ged(self, plugin):
 | 
						|
        for event_name, handler in plugin.events_handlers.items():
 | 
						|
            priority = handler[0]
 | 
						|
            handler_function = handler[1]
 | 
						|
            app.ged.register_event_handler(event_name, priority,
 | 
						|
                handler_function)
 | 
						|
 | 
						|
    def _remove_events_handler_from_ged(self, plugin):
 | 
						|
        for event_name, handler in plugin.events_handlers.items():
 | 
						|
            priority = handler[0]
 | 
						|
            handler_function = handler[1]
 | 
						|
            app.ged.remove_event_handler(event_name, priority,
 | 
						|
                handler_function)
 | 
						|
 | 
						|
    def _register_network_events_in_nec(self, plugin):
 | 
						|
        for event_class in plugin.events:
 | 
						|
            setattr(event_class, 'plugin', plugin)
 | 
						|
            if issubclass(event_class, nec.NetworkIncomingEvent):
 | 
						|
                app.nec.register_incoming_event(event_class)
 | 
						|
            elif issubclass(event_class, nec.NetworkOutgoingEvent):
 | 
						|
                app.nec.register_outgoing_event(event_class)
 | 
						|
 | 
						|
    def _remove_network_events_from_nec(self, plugin):
 | 
						|
        for event_class in plugin.events:
 | 
						|
            if issubclass(event_class, nec.NetworkIncomingEvent):
 | 
						|
                app.nec.unregister_incoming_event(event_class)
 | 
						|
            elif issubclass(event_class, nec.NetworkOutgoingEvent):
 | 
						|
                app.nec.unregister_outgoing_event(event_class)
 | 
						|
 | 
						|
    def _remove_name_from_encryption_plugins(self, plugin):
 | 
						|
        if plugin.encryption_name:
 | 
						|
            del self.encryption_plugins[plugin.encryption_name]
 | 
						|
 | 
						|
    def _register_modules_with_handlers(self, plugin):
 | 
						|
        if not hasattr(plugin, 'modules'):
 | 
						|
            return
 | 
						|
        for con in app.connections.values():
 | 
						|
            for module in plugin.modules:
 | 
						|
                if not module.zeroconf and con.name == 'Local':
 | 
						|
                    continue
 | 
						|
                instance, name = module.get_instance(con)
 | 
						|
                modules.register_single_module(con, instance, name)
 | 
						|
 | 
						|
                # If handlers have been registered, register the
 | 
						|
                # plugin handlers. Otherwise this will be done
 | 
						|
                # automatically on connecting
 | 
						|
                # in connection_handlers._register_handlers()
 | 
						|
                if con.handlers_registered:
 | 
						|
                    for handler in instance.handlers:
 | 
						|
                        con.connection.RegisterHandler(*handler)
 | 
						|
 | 
						|
    def _unregister_modules_with_handlers(self, plugin):
 | 
						|
        if not hasattr(plugin, 'modules'):
 | 
						|
            return
 | 
						|
        for con in app.connections.values():
 | 
						|
            for module in plugin.modules:
 | 
						|
                instance = con.get_module(module.name)
 | 
						|
                modules.unregister_single_module(con, module.name)
 | 
						|
 | 
						|
                # Account is still connected and handlers are registered
 | 
						|
                # So just unregister the plugin handlers
 | 
						|
                if con.handlers_registered:
 | 
						|
                    for handler in instance.handlers:
 | 
						|
                        if len(handler) > 4:
 | 
						|
                            handler = handler[:4]
 | 
						|
                        con.connection.UnregisterHandler(*handler)
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def activate_plugin(self, plugin):
 | 
						|
        '''
 | 
						|
        :param plugin: plugin to be activated
 | 
						|
        :type plugin: class object of `GajimPlugin` subclass
 | 
						|
        '''
 | 
						|
        if not plugin.active and plugin.activatable:
 | 
						|
 | 
						|
            self._add_gui_extension_points_handlers_from_plugin(plugin)
 | 
						|
            self._add_encryption_name_from_plugin(plugin)
 | 
						|
            self._handle_all_gui_extension_points_with_plugin(plugin)
 | 
						|
            self._register_events_handlers_in_ged(plugin)
 | 
						|
            self._register_network_events_in_nec(plugin)
 | 
						|
            self._register_modules_with_handlers(plugin)
 | 
						|
 | 
						|
            self.active_plugins.append(plugin)
 | 
						|
            try:
 | 
						|
                plugin.activate()
 | 
						|
            except GajimPluginException as e:
 | 
						|
                self.deactivate_plugin(plugin)
 | 
						|
                raise GajimPluginActivateException(str(e))
 | 
						|
            self._set_plugin_active_in_global_config(plugin)
 | 
						|
            plugin.active = True
 | 
						|
 | 
						|
    def deactivate_plugin(self, plugin):
 | 
						|
        # remove GUI extension points handlers (provided by plug-in) from
 | 
						|
        # handlers list
 | 
						|
        for gui_extpoint_name, gui_extpoint_handlers in \
 | 
						|
        plugin.gui_extension_points.items():
 | 
						|
            self.gui_extension_points_handlers[gui_extpoint_name].remove(
 | 
						|
                gui_extpoint_handlers)
 | 
						|
 | 
						|
        # detaching plug-in from handler GUI extension points (calling
 | 
						|
        # cleaning up method that must be provided by plug-in developer
 | 
						|
        # for each handled GUI extension point)
 | 
						|
        for gui_extpoint_name, gui_extpoint_handlers in \
 | 
						|
        plugin.gui_extension_points.items():
 | 
						|
            if gui_extpoint_name in self.gui_extension_points:
 | 
						|
                for gui_extension_point_args in self.gui_extension_points[
 | 
						|
                gui_extpoint_name]:
 | 
						|
                    handler = gui_extpoint_handlers[1]
 | 
						|
                    if handler:
 | 
						|
                        try:
 | 
						|
                            handler(*gui_extension_point_args)
 | 
						|
                        except Exception:
 | 
						|
                            log.warning('Error executing %s',
 | 
						|
                                        handler, exc_info=True)
 | 
						|
 | 
						|
        self._remove_events_handler_from_ged(plugin)
 | 
						|
        self._remove_network_events_from_nec(plugin)
 | 
						|
        self._remove_name_from_encryption_plugins(plugin)
 | 
						|
        self._unregister_modules_with_handlers(plugin)
 | 
						|
 | 
						|
        # removing plug-in from active plug-ins list
 | 
						|
        plugin.deactivate()
 | 
						|
        self.active_plugins.remove(plugin)
 | 
						|
        self._set_plugin_active_in_global_config(plugin, False)
 | 
						|
        plugin.active = False
 | 
						|
 | 
						|
    def _deactivate_all_plugins(self):
 | 
						|
        for plugin_object in self.active_plugins:
 | 
						|
            self.deactivate_plugin(plugin_object)
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def _add_gui_extension_points_handlers_from_plugin(self, plugin):
 | 
						|
        for gui_extpoint_name, gui_extpoint_handlers in \
 | 
						|
        plugin.gui_extension_points.items():
 | 
						|
            self.gui_extension_points_handlers.setdefault(gui_extpoint_name,
 | 
						|
                []).append(gui_extpoint_handlers)
 | 
						|
 | 
						|
    def _add_encryption_name_from_plugin(self, plugin):
 | 
						|
        if plugin.encryption_name:
 | 
						|
            self.encryption_plugins[plugin.encryption_name] = plugin
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def _handle_all_gui_extension_points_with_plugin(self, plugin):
 | 
						|
        for gui_extpoint_name, gui_extpoint_handlers in \
 | 
						|
        plugin.gui_extension_points.items():
 | 
						|
            if gui_extpoint_name in self.gui_extension_points:
 | 
						|
                for gui_extension_point_args in self.gui_extension_points[
 | 
						|
                gui_extpoint_name]:
 | 
						|
                    handler = gui_extpoint_handlers[0]
 | 
						|
                    if handler:
 | 
						|
                        try:
 | 
						|
                            handler(*gui_extension_point_args)
 | 
						|
                        except Exception:
 | 
						|
                            log.warning('Error executing %s',
 | 
						|
                                        handler, exc_info=True)
 | 
						|
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def _activate_all_plugins(self):
 | 
						|
        '''
 | 
						|
        Activates all plugins in `plugins`.
 | 
						|
 | 
						|
        Activated plugins are appended to `active_plugins` list.
 | 
						|
        '''
 | 
						|
        for plugin in self.plugins:
 | 
						|
            try:
 | 
						|
                self.activate_plugin(plugin)
 | 
						|
            except GajimPluginActivateException:
 | 
						|
                pass
 | 
						|
 | 
						|
    def _activate_all_plugins_from_global_config(self):
 | 
						|
        for plugin in self.plugins:
 | 
						|
            if self._plugin_is_active_in_global_config(plugin) and \
 | 
						|
            plugin.activatable:
 | 
						|
                try:
 | 
						|
                    self.activate_plugin(plugin)
 | 
						|
                except GajimPluginActivateException:
 | 
						|
                    pass
 | 
						|
 | 
						|
    def register_modules_for_account(self, con):
 | 
						|
        '''
 | 
						|
        A new account has been added, register modules
 | 
						|
        of all active plugins
 | 
						|
        '''
 | 
						|
        for plugin in self.plugins:
 | 
						|
            if not plugin.active:
 | 
						|
                return
 | 
						|
 | 
						|
            if not hasattr(plugin, 'modules'):
 | 
						|
                return
 | 
						|
 | 
						|
            for module in plugin.modules:
 | 
						|
                instance, name = module.get_instance(con)
 | 
						|
                if not module.zeroconf and con.name == 'Local':
 | 
						|
                    continue
 | 
						|
                modules.register_single_module(con, instance, name)
 | 
						|
 | 
						|
    def _plugin_is_active_in_global_config(self, plugin):
 | 
						|
        return app.config.get_per('plugins', plugin.short_name, 'active')
 | 
						|
 | 
						|
    def _set_plugin_active_in_global_config(self, plugin, active=True):
 | 
						|
        app.config.set_per('plugins', plugin.short_name, 'active', active)
 | 
						|
 | 
						|
    @log_calls('PluginManager')
 | 
						|
    def scan_dir_for_plugins(self, path, scan_dirs=True, package=False):
 | 
						|
        r'''
 | 
						|
        Scans given directory for plugin classes.
 | 
						|
 | 
						|
        :param path: directory to scan for plugins
 | 
						|
        :type path: str
 | 
						|
 | 
						|
        :param scan_dirs: folders inside path are processed as modules
 | 
						|
        :type scan_dirs: boolean
 | 
						|
 | 
						|
        :param package: if path points to a single package folder
 | 
						|
        :type package: boolean
 | 
						|
 | 
						|
        :return: list of found plugin classes (subclasses of `GajimPlugin`
 | 
						|
        :rtype: [] of class objects
 | 
						|
 | 
						|
        :note: currently it only searches for plugin classes in '\*.py' files
 | 
						|
                present in given directory `path` (no recursion here)
 | 
						|
 | 
						|
        :todo: add scanning zipped modules
 | 
						|
        '''
 | 
						|
        plugins_found = []
 | 
						|
        conf = configparser.ConfigParser()
 | 
						|
        fields = ('name', 'short_name', 'version', 'description', 'authors',
 | 
						|
            'homepage')
 | 
						|
        if not os.path.isdir(path):
 | 
						|
            return plugins_found
 | 
						|
 | 
						|
        if package:
 | 
						|
            path, package_name = os.path.split(path)
 | 
						|
            dir_list = [package_name]
 | 
						|
        else:
 | 
						|
            dir_list = os.listdir(path)
 | 
						|
 | 
						|
        sys.path.insert(0, path)
 | 
						|
 | 
						|
        for elem_name in dir_list:
 | 
						|
            file_path = os.path.join(path, elem_name)
 | 
						|
 | 
						|
            if os.path.isfile(file_path) and fnmatch.fnmatch(file_path, '*.py'):
 | 
						|
                module_name = os.path.splitext(elem_name)[0]
 | 
						|
            elif os.path.isdir(file_path) and scan_dirs:
 | 
						|
                module_name = elem_name
 | 
						|
                file_path += os.path.sep
 | 
						|
            else:
 | 
						|
                continue
 | 
						|
 | 
						|
            manifest_path = os.path.join(os.path.dirname(file_path),
 | 
						|
                'manifest.ini')
 | 
						|
            if scan_dirs and (not os.path.isfile(manifest_path)):
 | 
						|
                continue
 | 
						|
 | 
						|
            # read metadata from manifest.ini
 | 
						|
            conf.remove_section('info')
 | 
						|
            with open(manifest_path, encoding='utf-8') as conf_file:
 | 
						|
                try:
 | 
						|
                    conf.read_file(conf_file)
 | 
						|
                except configparser.Error:
 | 
						|
                    log.warning('Plugin %s not loaded, error loading manifest',
 | 
						|
                                elem_name, exc_info=True)
 | 
						|
                    continue
 | 
						|
 | 
						|
            short_name = conf.get('info', 'short_name', fallback=None)
 | 
						|
            if short_name is None:
 | 
						|
                log.error('No short_name defined for %s', elem_name)
 | 
						|
 | 
						|
            # Check if the plugin is already loaded
 | 
						|
            try:
 | 
						|
                for plugin in self.plugins:
 | 
						|
                    if plugin.short_name == short_name:
 | 
						|
                        raise PluginAlreadyLoaded(
 | 
						|
                            'Skip Plugin %s because its '
 | 
						|
                            'already loaded' % elem_name)
 | 
						|
            except PluginAlreadyLoaded as error:
 | 
						|
                log.warning(error)
 | 
						|
                continue
 | 
						|
 | 
						|
            min_v = conf.get('info', 'min_gajim_version', fallback=None)
 | 
						|
            max_v = conf.get('info', 'max_gajim_version', fallback=None)
 | 
						|
 | 
						|
            if min_v is None or max_v is None:
 | 
						|
                log.warning('Plugin without min/max version: %s', elem_name)
 | 
						|
                continue
 | 
						|
 | 
						|
            gajim_v = gajim.__version__.split('+', 1)[0]
 | 
						|
            gajim_v_cmp = parse_version(gajim_v)
 | 
						|
 | 
						|
            if min_v and gajim_v_cmp < parse_version(min_v):
 | 
						|
                log.warning('Plugin %s not loaded, newer version of'
 | 
						|
                            'gajim required: %s < %s',
 | 
						|
                            elem_name, gajim_v, min_v)
 | 
						|
                continue
 | 
						|
            if max_v and gajim_v_cmp > parse_version(max_v):
 | 
						|
                log.warning('Plugin %s not loaded, plugin incompatible '
 | 
						|
                            'with current version of gajim: '
 | 
						|
                            '%s > %s', elem_name, gajim_v, max_v)
 | 
						|
                continue
 | 
						|
 | 
						|
            module = None
 | 
						|
            try:
 | 
						|
                log.info('Loading %s', module_name)
 | 
						|
                module = __import__(module_name)
 | 
						|
            except Exception:
 | 
						|
                log.warning(
 | 
						|
                    'While trying to load %s, exception occurred',
 | 
						|
                    elem_name, exc_info=True)
 | 
						|
                continue
 | 
						|
 | 
						|
            if module is None:
 | 
						|
                continue
 | 
						|
 | 
						|
            log.debug('Attributes processing started')
 | 
						|
            for module_attr_name in [attr_name for attr_name in dir(module)
 | 
						|
            if not (attr_name.startswith('__') or attr_name.endswith('__'))]:
 | 
						|
                module_attr = getattr(module, module_attr_name)
 | 
						|
                log.debug('%s: %s', module_attr_name, module_attr)
 | 
						|
 | 
						|
                try:
 | 
						|
                    if not issubclass(module_attr, GajimPlugin) or \
 | 
						|
                    module_attr is GajimPlugin:
 | 
						|
                        continue
 | 
						|
                    log.debug('is subclass of GajimPlugin')
 | 
						|
                    module_attr.__path__ = os.path.abspath(
 | 
						|
                        os.path.dirname(file_path))
 | 
						|
 | 
						|
                    for option in fields:
 | 
						|
                        if conf.get('info', option) == '':
 | 
						|
                            raise configparser.NoOptionError(option, 'info')
 | 
						|
                        if option == 'description':
 | 
						|
                            setattr(module_attr, option, plugins_i18n._(conf.get('info', option)))
 | 
						|
                            continue
 | 
						|
                        setattr(module_attr, option, conf.get('info', option))
 | 
						|
 | 
						|
                    plugins_found.append(module_attr)
 | 
						|
                except TypeError:
 | 
						|
                    pass
 | 
						|
                except configparser.NoOptionError:
 | 
						|
                    # all fields are required
 | 
						|
                    log.debug(
 | 
						|
                        '%s: wrong manifest file. all fields are required!',
 | 
						|
                        module_attr_name)
 | 
						|
                except configparser.NoSectionError:
 | 
						|
                    # info section are required
 | 
						|
                    log.debug(
 | 
						|
                        '%s: wrong manifest file. info section are required!',
 | 
						|
                        module_attr_name)
 | 
						|
                except configparser.MissingSectionHeaderError:
 | 
						|
                    # info section are required
 | 
						|
                    log.debug('%s: wrong manifest file. section are required!',
 | 
						|
                              module_attr_name)
 | 
						|
 | 
						|
        sys.path.remove(path)
 | 
						|
        return plugins_found
 | 
						|
 | 
						|
    def install_from_zip(self, zip_filename, owerwrite=None):
 | 
						|
        '''
 | 
						|
        Install plugin from zip and return plugin
 | 
						|
        '''
 | 
						|
        try:
 | 
						|
            zip_file = zipfile.ZipFile(zip_filename)
 | 
						|
        except zipfile.BadZipfile:
 | 
						|
            # it is not zip file
 | 
						|
            raise PluginsystemError(_('Archive corrupted'))
 | 
						|
        except IOError:
 | 
						|
            raise PluginsystemError(_('Archive empty'))
 | 
						|
 | 
						|
        if zip_file.testzip():
 | 
						|
            # CRC error
 | 
						|
            raise PluginsystemError(_('Archive corrupted'))
 | 
						|
 | 
						|
        dirs = []
 | 
						|
        manifest = None
 | 
						|
        for filename in zip_file.namelist():
 | 
						|
            if filename.startswith('.') or filename.startswith('/') or \
 | 
						|
            ('/' not in filename):
 | 
						|
                # members not safe
 | 
						|
                raise PluginsystemError(_('Archive is malformed'))
 | 
						|
            if filename.endswith('/') and filename.find('/', 0, -1) < 0:
 | 
						|
                dirs.append(filename.strip('/'))
 | 
						|
            if 'manifest.ini' in filename.split('/')[1]:
 | 
						|
                manifest = True
 | 
						|
        if not manifest:
 | 
						|
            return
 | 
						|
        if len(dirs) > 1:
 | 
						|
            raise PluginsystemError(_('Archive is malformed'))
 | 
						|
 | 
						|
        user_dir = configpaths.get('PLUGINS_USER')
 | 
						|
        plugin_dir = os.path.join(user_dir, dirs[0])
 | 
						|
 | 
						|
        if os.path.isdir(plugin_dir):
 | 
						|
        # Plugin dir already exists
 | 
						|
            if not owerwrite:
 | 
						|
                raise PluginsystemError(_('Plugin already exists'))
 | 
						|
            self.uninstall_plugin(self.get_plugin_by_path(plugin_dir))
 | 
						|
 | 
						|
        zip_file.extractall(user_dir)
 | 
						|
        zip_file.close()
 | 
						|
 | 
						|
        plugins = self.scan_dir_for_plugins(plugin_dir, package=True)
 | 
						|
        if not plugins:
 | 
						|
            return
 | 
						|
        self.add_plugin(plugins[0])
 | 
						|
        plugin = self.plugins[-1]
 | 
						|
        return plugin
 | 
						|
 | 
						|
    def delete_plugin_files(self, plugin_path):
 | 
						|
        def on_error(func, path, error):
 | 
						|
            if func == os.path.islink:
 | 
						|
            # if symlink
 | 
						|
                os.unlink(path)
 | 
						|
                return
 | 
						|
            # access is denied or other
 | 
						|
            raise PluginsystemError(error[1][1])
 | 
						|
 | 
						|
        rmtree(plugin_path, False, on_error)
 | 
						|
 | 
						|
    def uninstall_plugin(self, plugin):
 | 
						|
        '''
 | 
						|
        Deactivate and remove plugin from `plugins` list
 | 
						|
        '''
 | 
						|
        if plugin:
 | 
						|
            self.remove_plugin(plugin)
 | 
						|
            self.delete_plugin_files(plugin.__path__)
 | 
						|
            if self._plugin_has_entry_in_global_config(plugin):
 | 
						|
                self._remove_plugin_entry_in_global_config(plugin)
 | 
						|
 | 
						|
    def get_plugin_by_path(self, plugin_dir):
 | 
						|
        for plugin in self.plugins:
 | 
						|
            if plugin.__path__ in plugin_dir:
 | 
						|
                return plugin
 | 
						|
 | 
						|
 | 
						|
class PluginAlreadyLoaded(Exception):
 | 
						|
    pass
 |