diff --git a/data/gui/plugins_window.ui b/data/gui/plugins_window.ui index 4560c2013..89e451a24 100644 --- a/data/gui/plugins_window.ui +++ b/data/gui/plugins_window.ui @@ -245,10 +245,47 @@ True 5 end + + + True + False + True + + + + True + + + True + gtk-apply + + + 0 + + + + + True + 0 + Install + + + 1 + + + + + + + False + False + 0 + + True - True + False True @@ -266,6 +303,7 @@ True + 0 Uninstall @@ -278,7 +316,7 @@ False False - 0 + 1 @@ -302,6 +340,7 @@ True + 0 Configure @@ -314,7 +353,7 @@ False False - 1 + 2 diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 164e755c4..df2511e73 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -61,7 +61,7 @@ from common.connection_handlers_events import * from common import ged from common import nec from common.nec import NetworkEvent -from plugins import GajimPlugin + if gajim.HAVE_FARSIGHT: from common.jingle import ConnectionJingle else: diff --git a/src/common/exceptions.py b/src/common/exceptions.py index 122262418..de8c3309d 100644 --- a/src/common/exceptions.py +++ b/src/common/exceptions.py @@ -131,3 +131,15 @@ class GajimGeneralException(Exception): def __str__(self): return self.text + +class PluginsystemError(Exception): + """ + Error in the pluginsystem + """ + + def __init__(self, text=''): + Exception.__init__(self) + self.text = text + + def __str__(self): + return self.text diff --git a/src/dialogs.py b/src/dialogs.py index f08e515f7..e9ca65d96 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -1349,11 +1349,11 @@ class FileChooserDialog(gtk.FileChooserDialog): Non-blocking FileChooser Dialog around gtk.FileChooserDialog """ def __init__(self, title_text, action, buttons, default_response, - select_multiple = False, current_folder = None, on_response_ok = None, - on_response_cancel = None): + select_multiple=False, current_folder=None, on_response_ok=None, + on_response_cancel=None): gtk.FileChooserDialog.__init__(self, title=title_text, action=action, - buttons=buttons) + buttons=buttons) self.set_default_response(default_response) self.set_select_multiple(select_multiple) @@ -1468,8 +1468,8 @@ class WarningDialog(HigDialog): """ def __init__(self, pritext, sectext=''): - HigDialog.__init__( self, None, - gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, pritext, sectext) + HigDialog.__init__(self, None, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, + pritext, sectext) self.set_modal(False) if hasattr(gajim.interface, 'roster') and gajim.interface.roster: self.set_transient_for(gajim.interface.roster.window) @@ -1505,13 +1505,12 @@ class YesNoDialog(HigDialog): """ def __init__(self, pritext, sectext='', checktext='', on_response_yes=None, - on_response_no=None): + on_response_no=None): self.user_response_yes = on_response_yes self.user_response_no = on_response_no - HigDialog.__init__( self, None, - gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, pritext, sectext, - on_response_yes=self.on_response_yes, - on_response_no=self.on_response_no) + HigDialog.__init__(self, None, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, + pritext, sectext, on_response_yes=self.on_response_yes, + on_response_no=self.on_response_no) if checktext: self.checkbutton = gtk.CheckButton(checktext) @@ -4442,6 +4441,52 @@ class AvatarChooserDialog(ImageChooserDialog): else: self.response_clear(widget) + +class ArchiveChooserDialog(FileChooserDialog): + def __init__(self, on_response_ok=None, on_response_cancel=None): + + def on_ok(widget, callback): + '''check if file exists and call callback''' + path_to_file = self.get_filename() + if not path_to_file: + return + path_to_file = gtkgui_helpers.decode_filechooser_file_paths( + (path_to_file,))[0] + if os.path.exists(path_to_file): + if isinstance(callback, tuple): + callback[0](path_to_file, *callback[1:]) + else: + callback(path_to_file) + self.destroy() + + path = helpers.get_documents_path() + + FileChooserDialog.__init__(self, + title_text=_('Choose Archive'), + action=gtk.FILE_CHOOSER_ACTION_OPEN, + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK), + default_response=gtk.RESPONSE_OK, + current_folder=path, + on_response_ok=(on_ok, on_response_ok), + on_response_cancel=on_response_cancel) + + if on_response_cancel: + self.connect('destroy', on_response_cancel) + + filter_ = gtk.FileFilter() + filter_.set_name(_('All files')) + filter_.add_pattern('*') + self.add_filter(filter_) + + filter_ = gtk.FileFilter() + filter_.set_name(_('Zip files')) + filter_.add_pattern('*.zip') + + self.add_filter(filter_) + self.set_filter(filter_) + + class AddSpecialNotificationDialog: def __init__(self, jid): """ diff --git a/src/plugins/gui.py b/src/plugins/gui.py index ab0281e1f..23de1fe84 100644 --- a/src/plugins/gui.py +++ b/src/plugins/gui.py @@ -30,9 +30,10 @@ import pango import gtk, gobject import gtkgui_helpers +import dialogs from common import gajim - from plugins.helpers import log_calls, log +from common.exceptions import PluginsystemError class PluginsWindow(object): '''Class for Plugins window''' @@ -44,15 +45,11 @@ class PluginsWindow(object): self.window = self.xml.get_object('plugins_window') self.window.set_transient_for(gajim.interface.roster.window) - widgets_to_extract = ('plugins_notebook', - 'plugin_name_label', - 'plugin_version_label', - 'plugin_authors_label', - 'plugin_homepage_linkbutton', - 'plugin_description_textview', - 'uninstall_plugin_button', - 'configure_plugin_button', - 'installed_plugins_treeview') + widgets_to_extract = ('plugins_notebook', 'plugin_name_label', + 'plugin_version_label', 'plugin_authors_label', + 'plugin_homepage_linkbutton', 'plugin_description_textview', + 'uninstall_plugin_button', 'configure_plugin_button', + 'installed_plugins_treeview') for widget_name in widgets_to_extract: setattr(self, widget_name, self.xml.get_object(widget_name)) @@ -62,8 +59,7 @@ class PluginsWindow(object): self.plugin_name_label.set_attributes(attr_list) self.installed_plugins_model = gtk.ListStore(gobject.TYPE_PYOBJECT, - gobject.TYPE_STRING, - gobject.TYPE_BOOLEAN) + gobject.TYPE_STRING, gobject.TYPE_BOOLEAN) self.installed_plugins_treeview.set_model(self.installed_plugins_model) renderer = gtk.CellRendererText() @@ -79,7 +75,7 @@ class PluginsWindow(object): # connect signal for selection change selection = self.installed_plugins_treeview.get_selection() selection.connect('changed', - self.installed_plugins_treeview_selection_changed) + self.installed_plugins_treeview_selection_changed) selection.set_mode(gtk.SELECTION_SINGLE) self._clear_installed_plugin_info() @@ -116,7 +112,8 @@ class PluginsWindow(object): desc_textbuffer = self.plugin_description_textview.get_buffer() desc_textbuffer.set_text(plugin.description) self.plugin_description_textview.set_property('sensitive', True) - self.uninstall_plugin_button.set_property('sensitive', True) + self.uninstall_plugin_button.set_property('sensitive', + gajim.PLUGINS_DIRS[1] in plugin.__path__) if plugin.config_dialog is None: self.configure_plugin_button.set_property('sensitive', False) else: @@ -143,9 +140,8 @@ class PluginsWindow(object): self.installed_plugins_model.set_sort_column_id(1, gtk.SORT_ASCENDING) for plugin in pm.plugins: - self.installed_plugins_model.append([plugin, - plugin.name, - plugin.active]) + self.installed_plugins_model.append([plugin, plugin.name, + plugin.active]) @log_calls('PluginsWindow') def installed_plugins_toggled_cb(self, cell, path): @@ -189,14 +185,66 @@ class PluginsWindow(object): @log_calls('PluginsWindow') def on_uninstall_plugin_button_clicked(self, widget): - pass + selection = self.installed_plugins_treeview.get_selection() + model, iter = selection.get_selected() + if iter: + plugin = model.get_value(iter, 0) + plugin_name = model.get_value(iter, 1).decode('utf-8') + is_active = model.get_value(iter, 2) + try: + gajim.plugin_manager.remove_plugin(plugin) + except PluginsystemError, e: + dialogs.WarningDialog(_('Unable to properly remove the plugin'), + str(e)) + return + model.remove(iter) + + @log_calls('PluginsWindow') + def on_install_plugin_button_clicked(self, widget): + def _on_plugin_exists(zip_filename): + def on_yes(is_checked): + plugin = gajim.plugin_manager.install_from_zip(zip_filename, + True) + model = self.installed_plugins_model + + for row in xrange(len(model)): + if plugin == model[row][0]: + model.remove(model.get_iter((row, 0))) + break + + iter_ = model.append([plugin, plugin.name, False]) + sel = self.installed_plugins_treeview.get_selection() + sel.select_iter(iter_) + + dialogs.YesNoDialog(_('Plugin already exists'), + sectext=_('Overwrite?'), on_response_yes=on_yes) + + def _try_install(zip_filename): + try: + plugin = gajim.plugin_manager.install_from_zip(zip_filename) + except PluginsystemError, er_type: + error_text = str(er_type) + if error_text == _('Plugin already exists'): + _on_plugin_exists(zip_filename) + return + + dialogs.WarningDialog(error_text, '"%s"' % zip_filename) + return + + model = self.installed_plugins_model + iter_ = model.append([plugin, plugin.name, False]) + sel = self.installed_plugins_treeview.get_selection() + sel.select_iter(iter_) + + self.dialog = dialogs.ArchiveChooserDialog(on_response_ok=_try_install) class GajimPluginConfigDialog(gtk.Dialog): @log_calls('GajimPluginConfigDialog') def __init__(self, plugin, **kwargs): - gtk.Dialog.__init__(self, '%s %s'%(plugin.name, _('Configuration')), **kwargs) + gtk.Dialog.__init__(self, '%s %s'%(plugin.name, _('Configuration')), + **kwargs) self.plugin = plugin self.add_button('gtk-close', gtk.RESPONSE_CLOSE) diff --git a/src/plugins/pluginmanager.py b/src/plugins/pluginmanager.py index a0708c89c..3560e4985 100644 --- a/src/plugins/pluginmanager.py +++ b/src/plugins/pluginmanager.py @@ -29,9 +29,12 @@ __all__ = ['PluginManager'] import os import sys import fnmatch +import zipfile +from shutil import rmtree from common import gajim from common import nec +from common.exceptions import PluginsystemError from plugins.helpers import log, log_calls, Singleton from plugins.plugin import GajimPlugin @@ -370,7 +373,7 @@ class PluginManager(object): @staticmethod @log_calls('PluginManager') - def scan_dir_for_plugins(path): + def scan_dir_for_plugins(path, scan_dirs=True): ''' Scans given directory for plugin classes. @@ -414,7 +417,7 @@ class PluginManager(object): pass #log.debug('Module not imported successfully. ImportError: %s'%(import_error)) - elif os.path.isdir(file_path): + elif os.path.isdir(file_path) and scan_dirs: module_name = elem_name file_path += os.path.sep #log.debug('Possible package detected.') @@ -454,3 +457,70 @@ class PluginManager(object): #log.debug(module) return plugins_found + + def install_from_zip(self, zip_filename, owerwrite=None): + ''' + Install plagin from zip and return plugin + ''' + try: + zip_file = zipfile.ZipFile(zip_filename) + except zipfile.BadZipfile, e: + # it is not zip file + raise PluginsystemError(_('Archive corrupted')) + except IOError,e: + raise PluginsystemError(_('Archive empty')) + + if zip_file.testzip(): + # CRC error + raise PluginsystemError(_('Archive corrupted')) + + dirs = [] + 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) + + if len(dirs) > 1: + # several directories in the root of the archive + raise PluginsystemError(_('Archive is malformed')) + + base_dir, user_dir = gajim.PLUGINS_DIRS + plugin_dir = os.path.join(user_dir, dirs[0]) + + if os.path.isdir(plugin_dir): + # Plugin already exists + if not owerwrite: + raise PluginsystemError(_('Plugin already exists')) + self.remove_plugin(self.get_plugin_by_path(plugin_dir)) + + zip_file.extractall(user_dir) + zip_file.close() + path = os.path.join(user_dir, dirs[0]) + self.add_plugin(self.scan_dir_for_plugins(plugin_dir, False)[0]) + plugin = self.plugins[-1] + return plugin + + def remove_plugin(self, plugin): + ''' + Deactivate and remove plugin from `plugins` list + ''' + 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]) + + if plugin.active: + self.deactivate_plugin(plugin) + rmtree(plugin.__path__, False, on_error) + self.plugins.remove(plugin) + + def get_plugin_by_path(self, plugin_dir): + for plugin in self.plugins: + if plugin.__path__ in plugin_dir: + return plugin