diff --git a/plugin_installer/DST_Root_CA_X3.pem b/plugin_installer/DST_Root_CA_X3.pem new file mode 100644 index 0000000..300cd7d --- /dev/null +++ b/plugin_installer/DST_Root_CA_X3.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- \ No newline at end of file diff --git a/plugin_installer/config_dialog.ui b/plugin_installer/config_dialog.ui index fe7764c..33077dc 100644 --- a/plugin_installer/config_dialog.ui +++ b/plugin_installer/config_dialog.ui @@ -1,326 +1,338 @@ - + - - + + False - - 800 + True - True - 340 - True + False + 12 + 12 + 12 + 12 - + + Check update after start True False + False + False + True + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + False + gtk-refresh + + + False + + + True + True + + + True + False + + + False + True + end + + + 0 + 1 + + True True + True 6 never True True + True + plugin_store + False 1 - + + + + + + + Plugin + True + + + + 0 + + + + + + 2 + + + + + + + Installed + + + + 3 + + + + + + + Available + + + + 4 + + + + + + + Install / +Upgrade + 0.5 + + + + + + 5 + + + - True - True - 0 - - - - - False - end - - - False - True - 1 + 0 + 0 - False + True False - + + 350 True False 6 - vertical - 6 + 3 + 5 - - True + False - &lt;empty&gt; + True + start True - 0 - False - True - 0 + 0 + 0 + 2 - + True False - 6 - - - True - False - Version: - - - False - True - 0 - - - - - True - False - <empty> - True - 0 - - - True - True - 1 - - + start + Version: - False - True - 1 + 0 + 1 - + True False - 6 - - - True - False - Authors: - 0 - - - False - True - 0 - - - - - True - False - <empty> - word-char - True - end - 0 - 0 - - - True - True - 1 - - + start + start + Authors: - False - True - 2 + 0 + 2 - + True False + start + Homepage: + + + 0 + 3 + + + + + True + False + start + Description: + + + 0 + 4 + 2 + + + + + True + False + start + False + True + + + 1 + 1 + + + + + True + False + start + start + word-char + True + end + + + 1 + 2 + + + + + True + False + True + True + start + True + none + + + 1 + 3 + + + + + True + False + end + 2 + 2 + vertical + start - - True - False - Homepage: - - - False - True - 0 - - - - + + Install/Upgrade True True True - none - False - 0 + start + False + refresh + True + True True - 1 - - - - - False - True - 3 - - - - - True - False - vertical - - - True - False - - - True - False - Description: - - - False - True - 0 - - - - - - - - False - True 0 - - - True - True - in - - - - - - True - True - 1 - - - True - True - 4 + 0 + 6 + 2 - + True - False + True + True + True + in - - - True - False - end - - - False - True - False - True - True - - - - True - False - - - True - False - gtk-refresh - - - True - True - 0 - - - - - True - False - Install/Upgrade - 0 - - - True - True - 1 - - - - - - - False - False - 0 - - - - - False - False - end - 1 - - - False - False - 5 + 0 + 5 + 2 @@ -332,87 +344,4 @@ - - False - - - True - False - 6 - - - True - False - 6 - - - True - False - FTP Server: - 0 - - - False - True - 0 - - - - - True - True - - - - True - True - 1 - - - - - False - True - 0 - - - - - Check update after start - False - True - False - False - False - 0.5 - True - - - - False - True - 1 - - - - - Check update every 24 hours - False - True - False - False - False - 0.5 - True - - - - False - True - 2 - - - - - diff --git a/plugin_installer/manifest.ini b/plugin_installer/manifest.ini index 4e61f46..70ae7c8 100644 --- a/plugin_installer/manifest.ini +++ b/plugin_installer/manifest.ini @@ -1,10 +1,11 @@ [info] name: Plugin Installer short_name: plugin_installer -version: 0.17 +version: 0.18 description: Install and upgrade plugins from ftp authors: Denis Fomin Yann Leboulanger Thilo Molitor -homepage: http://trac-plugins.gajim.org/wiki/PluginInstallerPlugin + Philipp Hörist +homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/PluginInstallerPlugin min_gajim_version: 0.16.10 diff --git a/plugin_installer/plugin_installer.py b/plugin_installer/plugin_installer.py index e0197fb..7e65850 100644 --- a/plugin_installer/plugin_installer.py +++ b/plugin_installer/plugin_installer.py @@ -4,6 +4,7 @@ ## ## Copyright (C) 2010-2012 Denis Fomin ## Copyright (C) 2011-2012 Yann Leboulanger +## Copyright (C) 2017 Philipp Hörist ## ## This file is part of Gajim. ## @@ -23,311 +24,227 @@ from gi.repository import Gtk from gi.repository import GdkPixbuf from gi.repository import Pango from gi.repository import GLib -from gi.repository import GObject -import ftplib import io import threading import configparser import os -import fnmatch -import sys -import zipfile import ssl import logging +import posixpath +import urllib.error +from zipfile import ZipFile +from distutils.version import LooseVersion as V +from urllib.request import urlopen from common import gajim from plugins import GajimPlugin -from plugins.helpers import log_calls, log from htmltextview import HtmlTextView from dialogs import WarningDialog, HigDialog, YesNoDialog from plugins.gui import GajimPluginConfigDialog +from enum import IntEnum +from gtkgui_helpers import get_action log = logging.getLogger('gajim.plugin_system.plugin_installer') -( -C_PIXBUF, -C_DIR, -C_NAME, -C_LOCAL_VERSION, -C_VERSION, -C_UPGRADE, -C_DESCRIPTION, -C_AUTHORS, -C_HOMEPAGE -) = range(9) +PLUGINS_URL = 'https://ftp.gajim.org/plugins_1/' +MANIFEST_URL = 'https://ftp.gajim.org/plugins_1/manifests.zip' +MANIFEST_IMAGE_URL = 'https://ftp.gajim.org/plugins_1/manifests_images.zip' +MANDATORY_FIELDS = ['name', 'version', 'description', 'authors', 'homepage'] + + +class Column(IntEnum): + PIXBUF = 0 + DIR = 1 + NAME = 2 + LOCAL_VERSION = 3 + VERSION = 4 + UPGRADE = 5 + DESCRIPTION = 6 + AUTHORS = 7 + HOMEPAGE = 8 + + +def get_local_version(plugin_name): + for plugin in gajim.plugin_manager.plugins: + if plugin.name == plugin_name: + return plugin.version -def convert_version_to_list(version_str): - version_list = version_str.split('.') - l = [] - while len(version_list): - l.append(int(version_list.pop(0))) - return l class PluginInstaller(GajimPlugin): - - @log_calls('PluginInstallerPlugin') def init(self): - self.description = _('Install and upgrade plugins from ftp') + self.description = _('Install and Upgrade Plugins') self.config_dialog = PluginInstallerPluginConfigDialog(self) - self.config_default_values = {'ftp_server': ('ftp.gajim.org', ''), - 'check_update': (True, ''), - 'check_update_periodically': (True, '')} + self.config_default_values = {'check_update': (True, '')} self.window = None self.progressbar = None self.available_plugins_model = None - self.upgrading = False # True when opened from upgrade popup dialog self.timeout_id = 0 self.connected_ids = {} icon = Gtk.Image() - self.def_icon = icon.render_icon(Gtk.STOCK_PREFERENCES, - Gtk.IconSize.MENU) - if gajim.version.startswith('0.15'): - self.server_folder = 'plugins_0.15' - elif gajim.version.startswith('0.16.10'): - self.server_folder = 'plugins_1' - else: - self.server_folder = 'plugins_0.16' + self.def_icon = icon.render_icon( + Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU) - @log_calls('PluginInstallerPlugin') def activate(self): - self.pl_menuitem = gajim.interface.roster.xml.get_object( - 'plugins_menuitem') - self.id_ = self.pl_menuitem.connect_after('activate', self.on_activate) - if 'plugins' in gajim.interface.instances: - self.on_activate(None) if self.config['check_update']: self.timeout_id = GLib.timeout_add_seconds(30, self.check_update) + if 'plugins' in gajim.interface.instances: + self.on_activate(gajim.interface.instances['plugins']) - @log_calls('PluginInstallerPlugin') def warn_update(self, plugins): def open_update(dummy): - self.upgrading = True - self.pl_menuitem.activate() - nb = gajim.interface.instances['plugins'].plugins_notebook - page = nb.page_num(self.hpaned) - GLib.idle_add(nb.set_current_page, page) + get_action('plugins').activate() + page = self.notebook.page_num(self.paned) + self.notebook.set_current_page(page) if plugins: - plugins_str = '\n'.join(plugins) - YesNoDialog(_('Plugins updates'), _('Some updates are available for' - ' your installer plugins. Do you want to update those plugins:' - '\n%s') % plugins_str, on_response_yes=open_update) - - def ftp_connect(self): - if os.name == 'nt': - ctx = ssl.create_default_context() - con = ftplib.FTP_TLS(self.config['ftp_server'], context=ctx) + plugins_str = '\n' + '\n'.join(plugins) + YesNoDialog( + _('Plugins updates'), + _('Some updates are available for your installer plugins. ' + 'Do you want to update those plugins:\n%s') + % plugins_str, on_response_yes=open_update) else: - con = ftplib.FTP_TLS(self.config['ftp_server']) + log.info('No updates found') + if hasattr(self, 'thread'): + del self.thread - con.login() - con.prot_p() - return con - - @log_calls('PluginInstallerPlugin') def check_update(self): - def _run(): - try: - to_update = [] - con = self.ftp_connect() - con.cwd(self.server_folder) - con.retrbinary('RETR manifests.zip', ftp.handleDownload) - zip_file = zipfile.ZipFile(ftp.buffer_) - manifest_list = zip_file.namelist() - for filename in manifest_list: - config = configparser.ConfigParser() - conf_file = zip_file.open(filename) - config.read_file(io.TextIOWrapper(conf_file, encoding='utf-8')) - conf_file.close() - if not config.has_section('info'): - continue - opts = config.options('info') - if 'name' not in opts or 'version' not in opts or \ - 'description' not in opts or 'authors' not in opts or \ - 'homepage' not in opts: - continue - local_version = ftp.get_plugin_version(config.get( - 'info', 'name')) - if local_version: - local = convert_version_to_list(local_version) - remote = convert_version_to_list(config.get('info', - 'version')) - if remote > local: - to_update.append(config.get('info', 'name')) - con.quit() - GLib.idle_add(self.warn_update, to_update) - # check for updates at least once every 24 hours - if self.config['check_update_periodically']: - self.timeout_id = GLib.timeout_add_seconds(24*3600, self.check_update) - except Exception as e: - log.debug('Ftp error when check updates: %s' % str(e)) - ftp = Ftp(self) - ftp.run = _run - ftp.start() + if hasattr(self, 'thread'): + return + log.info('Checking for Updates...') + self.start_download(check_update=True) self.timeout_id = 0 - @log_calls('PluginInstallerPlugin') def deactivate(self): - self.pl_menuitem.disconnect(self.id_) - if hasattr(self, 'page_num'): - self.notebook.remove_page(self.notebook.page_num(self.hpaned)) + if hasattr(self, 'available_page'): + self.notebook.remove_page(self.notebook.page_num(self.paned)) self.notebook.set_current_page(0) for id_, widget in list(self.connected_ids.items()): widget.disconnect(id_) - del self.page_num - if hasattr(self, 'ftp'): - del self.ftp + del self.available_page + if hasattr(self, 'thread'): + del self.thread if self.timeout_id > 0: GLib.source_remove(self.timeout_id) self.timeout_id = 0 - def on_activate(self, widget): - if 'plugins' not in gajim.interface.instances: - return - if hasattr(self, 'page_num'): + def on_activate(self, plugin_win): + if hasattr(self, 'available_page'): # 'Available' tab exists return - self.installed_plugins_model = gajim.interface.instances[ - 'plugins'].installed_plugins_model - self.notebook = gajim.interface.instances['plugins'].plugins_notebook + if hasattr(self, 'thread'): + del self.thread + self.installed_plugins_model = plugin_win.installed_plugins_model + self.notebook = plugin_win.plugins_notebook id_ = self.notebook.connect('switch-page', self.on_notebook_switch_page) self.connected_ids[id_] = self.notebook - self.window = gajim.interface.instances['plugins'].window + self.window = plugin_win.window id_ = self.window.connect('destroy', self.on_win_destroy) self.connected_ids[id_] = self.window self.Gtk_BUILDER_FILE_PATH = self.local_file_path('config_dialog.ui') self.xml = Gtk.Builder() self.xml.set_translation_domain('gajim_plugins') - self.xml.add_objects_from_file(self.Gtk_BUILDER_FILE_PATH, ['hpaned2']) - self.hpaned = self.xml.get_object('hpaned2') - self.page_num = self.notebook.append_page(self.hpaned, - Gtk.Label.new(_('Available'))) + self.xml.add_objects_from_file(self.Gtk_BUILDER_FILE_PATH, + ['refresh', 'paned', 'plugin_store']) widgets_to_extract = ( - 'plugin_name_label', 'available_treeview', 'progressbar', - 'inslall_upgrade_button', 'plugin_authors_label', - 'plugin_homepage_linkbutton', 'plugin_version_label') + 'name_label', 'available_treeview', 'progressbar', 'paned', + 'install_button', 'authors_label', 'homepage_linkbutton', + 'version_label', 'scrolled_description_window') for widget_name in widgets_to_extract: setattr(self, widget_name, self.xml.get_object(widget_name)) - self.available_plugins_model = Gtk.ListStore(GdkPixbuf.Pixbuf, - object, str, str, str, bool,object, object, object) - self.available_treeview.set_model(self.available_plugins_model) - self.available_treeview.set_rules_hint(True) - self.available_plugins_model.set_sort_column_id(2, Gtk.SortType.ASCENDING) + # Make Link in LinkButton not centered + style_provider = Gtk.CssProvider() + css = '.link { padding-left: 0px; padding-right: 0px; }' + style_provider.load_from_data(css.encode()) + context = self.homepage_linkbutton.get_style_context() + context.add_provider(style_provider, + Gtk.STYLE_PROVIDER_PRIORITY_USER) - self.progressbar.set_property('no-show-all', True) - renderer = Gtk.CellRendererText() - col = Gtk.TreeViewColumn(_('Plugin')) - cell = Gtk.CellRendererPixbuf() - col.pack_start(cell, False) - col.add_attribute(cell, 'pixbuf', C_PIXBUF) - col.pack_start(renderer, True) - col.add_attribute(renderer, 'text', C_NAME) - col.set_resizable(True) - col.set_property('expand', True) - col.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY) - self.available_treeview.append_column(col) - col = Gtk.TreeViewColumn(_('Installed\nversion'), renderer, - text=C_LOCAL_VERSION) - self.available_treeview.append_column(col) - col = Gtk.TreeViewColumn(_('Available\nversion'), renderer, - text=C_VERSION) - col.set_property('expand', False) - self.available_treeview.append_column(col) + self.available_page = self.notebook.append_page( + self.paned, Gtk.Label.new(_('Available'))) - renderer = Gtk.CellRendererToggle() - renderer.set_property('activatable', True) - renderer.connect('toggled', self.available_plugins_toggled_cb) - col = Gtk.TreeViewColumn(_('Install /\nUpgrade'), renderer, - active=C_UPGRADE) - self.available_treeview.append_column(col) + self.available_plugins_model = self.xml.get_object('plugin_store') + self.available_plugins_model.set_sort_column_id( + 2, Gtk.SortType.ASCENDING) - if GObject.signal_lookup('error_signal', self.window) is 0: - GObject.signal_new('error_signal', self.window, - GObject.SignalFlags.RUN_LAST, GObject.TYPE_STRING, - (GObject.TYPE_STRING,)) - GObject.signal_new('plugin_downloaded', self.window, - GObject.SignalFlags.RUN_LAST, GObject.TYPE_STRING, - (GObject.TYPE_PYOBJECT,)) - - id_ = self.window.connect('error_signal', self.on_some_ftp_error) - self.connected_ids[id_] = self.window - id_ = self.window.connect('plugin_downloaded', - self.on_plugin_downloaded) - self.connected_ids[id_] = self.window - - selection = self.available_treeview.get_selection() - selection.connect('changed', - self.available_plugins_treeview_selection_changed) - selection.set_mode(Gtk.SelectionMode.SINGLE) - - self._clear_available_plugin_info() - - self.plugin_description_textview = HtmlTextView() - self.plugin_description_textview.set_wrap_mode(Gtk.WrapMode.WORD) - sw = self.xml.get_object('scrolledwindow1') - sw.add(self.plugin_description_textview) + self.description_textview = HtmlTextView() + self.description_textview.set_wrap_mode(Gtk.WrapMode.WORD) + self.scrolled_description_window.add(self.description_textview) self.xml.connect_signals(self) self.window.show_all() def on_win_destroy(self, widget): - if hasattr(self, 'ftp'): - del self.ftp - if hasattr(self, 'page_num'): - del self.page_num + if hasattr(self, 'thread'): + del self.thread + if hasattr(self, 'available_page'): + del self.available_page def available_plugins_toggled_cb(self, cell, path): - is_active = self.available_plugins_model[path][C_UPGRADE] - self.available_plugins_model[path][C_UPGRADE] = not is_active + is_active = self.available_plugins_model[path][Column.UPGRADE] + self.available_plugins_model[path][Column.UPGRADE] = not is_active dir_list = [] for i in range(len(self.available_plugins_model)): - if self.available_plugins_model[i][C_UPGRADE]: - dir_list.append(self.available_plugins_model[i][C_DIR]) - if not dir_list: - self.inslall_upgrade_button.set_property('sensitive', False) - else: - self.inslall_upgrade_button.set_property('sensitive', True) + if self.available_plugins_model[i][Column.UPGRADE]: + dir_list.append(self.available_plugins_model[i][Column.DIR]) + self.install_button.set_property('sensitive', bool(dir_list)) def on_notebook_switch_page(self, widget, page, page_num): - tab_label_text = self.notebook.get_tab_label_text(self.hpaned) + tab_label_text = self.notebook.get_tab_label_text(page) if tab_label_text != (_('Available')): return - if not hasattr(self, 'ftp'): + if not hasattr(self, 'thread'): self.available_plugins_model.clear() - self.progressbar.show() - self.ftp = Ftp(self) - self.ftp.remote_dirs = None - self.ftp.upgrading = True - self.ftp.start() + self.start_download(upgrading=True) - def on_inslall_upgrade_clicked(self, widget): - self.inslall_upgrade_button.set_property('sensitive', False) + def on_install_upgrade_clicked(self, widget): + self.install_button.set_property('sensitive', False) dir_list = [] for i in range(len(self.available_plugins_model)): - if self.available_plugins_model[i][C_UPGRADE]: - dir_list.append(self.available_plugins_model[i][C_DIR]) + if self.available_plugins_model[i][Column.UPGRADE]: + dir_list.append(self.available_plugins_model[i][Column.DIR]) - ftp = Ftp(self) - ftp.remote_dirs = dir_list - ftp.start() + self.start_download(remote_dirs=dir_list) - def on_some_ftp_error(self, widget, error_text): - for i in range(len(self.available_plugins_model)): - self.available_plugins_model[i][C_UPGRADE] = False - self.progressbar.hide() - def warn(): - WarningDialog(_('Ftp error'), error_text, self.window) - GLib.idle_add(warn) + def on_error(self, reason): + if reason == 'CERTIFICATE_VERIFY_FAILED': + YesNoDialog( + _('Security error during download'), + _('A security error occurred when ' + 'downloading. The certificate of the ' + 'plugin archive could not be verified. ' + 'this might be a security attack. ' + '\n\nYou can continue at your risk. ' + 'Do you want to do so? ' + '(not recommended)' + ), + on_response_yes=lambda dlg: + self.start_download(secure=False, upgrading=True)) + else: + if self.available_plugins_model: + for i in range(len(self.available_plugins_model)): + self.available_plugins_model[i][Column.UPGRADE] = False + self.progressbar.hide() + text = GLib.markup_escape_text(reason) + WarningDialog(_('Error in download'), + _('An error occurred when downloading\n\n' + '[%s]' % (str(text))), self.window) - def on_plugin_downloaded(self, widget, plugin_dirs): - dialog = HigDialog(None, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, - '', _('All selected plugins downloaded')) - dialog.set_modal(False) - dialog.set_transient_for(self.window) + def start_download(self, secure=True, remote_dirs=False, + upgrading=False, check_update=False): + log.info('Start Download...') + log.debug( + 'secure: %s, remote_dirs: %s, upgrading: %s, check_update: %s', + secure, remote_dirs, upgrading, check_update) + self.thread = DownloadAsync( + self, secure=secure, remote_dirs=remote_dirs, + upgrading=upgrading, check_update=check_update) + self.thread.start() + def on_plugin_downloaded(self, plugin_dirs): for _dir in plugin_dirs: is_active = False plugins = None @@ -336,8 +253,8 @@ class PluginInstaller(GajimPlugin): if plugin: if plugin.active: is_active = True - GLib.idle_add(gajim.plugin_manager.deactivate_plugin, - plugin) + log.info('Deactivate Plugin: %s', plugin) + gajim.plugin_manager.deactivate_plugin(plugin) gajim.plugin_manager.plugins.remove(plugin) model = self.installed_plugins_model @@ -346,315 +263,257 @@ class PluginInstaller(GajimPlugin): model.remove(model.get_iter((row, 0))) break - plugins = self.scan_dir_for_plugin(plugin_dir) + log.info('Load Plugin from: %s', plugin_dir) + plugins = gajim.plugin_manager.scan_dir_for_plugins( + plugin_dir, package=True) if not plugins: + log.warn('Loading Plugin failed') continue gajim.plugin_manager.add_plugin(plugins[0]) plugin = gajim.plugin_manager.plugins[-1] + log.info('Loading successful') for row in range(len(self.available_plugins_model)): - if plugin.name == self.available_plugins_model[row][C_NAME]: - self.available_plugins_model[row][C_LOCAL_VERSION] = \ - plugin.version - self.available_plugins_model[row][C_UPGRADE] = False + model_row = self.available_plugins_model[row] + if plugin.name == model_row[Column.NAME]: + model_row[Column.LOCAL_VERSION] = plugin.version + model_row[Column.UPGRADE] = False if is_active: - GLib.idle_add(gajim.plugin_manager.activate_plugin, plugin) + log.info('Activate Plugin: %s', plugin) + gajim.plugin_manager.activate_plugin(plugin) # get plugin icon icon_file = os.path.join(plugin.__path__, os.path.split( plugin.__path__)[1]) + '.png' icon = self.def_icon if os.path.isfile(icon_file): icon = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_file, 16, 16) - if not hasattr(plugin, 'activatable'): - # version 0.15 - plugin.activatable = False row = [plugin, plugin.name, is_active, plugin.activatable, icon] self.installed_plugins_model.append(row) + dialog = HigDialog( + self.window, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, + '', _('All selected plugins downloaded')) + dialog.set_modal(False) dialog.popup() def available_plugins_treeview_selection_changed(self, treeview_selection): model, iter = treeview_selection.get_selected() - self.xml.get_object('scrolledwindow1').get_children()[0].destroy() - self.plugin_description_textview = HtmlTextView() - self.plugin_description_textview.set_wrap_mode(Gtk.WrapMode.WORD) - sw = self.xml.get_object('scrolledwindow1') - sw.add(self.plugin_description_textview) - sw.show_all() - if iter: - self.plugin_name_label.set_text(model.get_value(iter, C_NAME)) - self.plugin_version_label.set_text(model.get_value(iter, C_VERSION)) - self.plugin_authors_label.set_text(model.get_value(iter, C_AUTHORS)) - self.plugin_homepage_linkbutton.set_uri(model.get_value(iter, - C_HOMEPAGE)) - self.plugin_homepage_linkbutton.set_label(model.get_value(iter, - C_HOMEPAGE)) - label = self.plugin_homepage_linkbutton.get_children()[0] - label.set_ellipsize(Pango.EllipsizeMode.END) - self.plugin_homepage_linkbutton.set_property('sensitive', True) - desc = _(model.get_value(iter, C_DESCRIPTION)) - if not desc.startswith('' + \ - desc + ' ' - desc = desc.replace('\n', '
') - self.plugin_description_textview.display_html( - desc, self.plugin_description_textview, None) - self.plugin_description_textview.set_property('sensitive', True) - else: - self._clear_available_plugin_info() - - def _clear_available_plugin_info(self): - self.plugin_name_label.set_text('') - self.plugin_version_label.set_text('') - self.plugin_authors_label.set_text('') - self.plugin_homepage_linkbutton.set_uri('') - self.plugin_homepage_linkbutton.set_label('') - self.plugin_homepage_linkbutton.set_property('sensitive', False) - - def scan_dir_for_plugin(self, path): - plugins_found = [] - conf = configparser.ConfigParser() - fields = ('name', 'short_name', 'version', 'description', 'authors', - 'homepage') - if not os.path.isdir(path): - return plugins_found - - dir_list = os.listdir(path) - dir_, mod = os.path.split(path) - sys.path.insert(0, dir_) - - manifest_path = os.path.join(path, 'manifest.ini') - if not os.path.isfile(manifest_path): - return plugins_found - - for elem_name in dir_list: - file_path = os.path.join(path, elem_name) - module = None - - if os.path.isfile(file_path) and fnmatch.fnmatch(file_path, '*.py'): - module_name = os.path.splitext(elem_name)[0] - if module_name == '__init__': - continue - try: - full_module_name = '%s.%s' % (mod, module_name) - if full_module_name in sys.modules: - from imp import reload - module = reload(sys.modules[full_module_name]) - else: - module = __import__(full_module_name) - except ValueError as value_error: - pass - except ImportError as import_error: - pass - except AttributeError as attribute_error: - pass - if module is None: - continue - - 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) - try: - if not issubclass(module_attr, GajimPlugin) or \ - module_attr is GajimPlugin: - continue - module_attr.__path__ = os.path.abspath(os.path.dirname( - file_path)) - - # read metadata from manifest.ini - with open(manifest_path) as _file: - conf.read_file(_file) - for option in fields: - if conf.get('info', option) is '': - raise configparser.NoOptionError('field empty') - setattr(module_attr, option, conf.get('info', option)) - conf.remove_section('info') - plugins_found.append(module_attr) - - except TypeError as type_error: - pass - except configparser.NoOptionError as type_error: - # all fields are required - pass - return plugins_found + self.description_textview.get_buffer().set_text('') + self.name_label.set_text(model.get_value(iter, Column.NAME)) + self.version_label.set_text(model.get_value(iter, Column.VERSION)) + self.authors_label.set_text(model.get_value(iter, Column.AUTHORS)) + self.homepage_linkbutton.set_uri( + model.get_value(iter, Column.HOMEPAGE)) + self.homepage_linkbutton.set_label( + model.get_value(iter, Column.HOMEPAGE)) + link_label = self.homepage_linkbutton.get_children()[0] + link_label.set_ellipsize(Pango.EllipsizeMode.END) + desc = _(model.get_value(iter, Column.DESCRIPTION)) + if not desc.startswith('' + '%s') % desc + desc = desc.replace('\n', '
') + self.description_textview.display_html( + desc, self.description_textview, None) def select_root_iter(self): - if hasattr(self, 'page_num'): - selection = self.available_treeview.get_selection() - if selection.count_selected_rows() == 0: - root_iter = self.available_plugins_model.get_iter_first() - selection.select_iter(root_iter) - scr_win = self.xml.get_object('scrolledwindow2') - vadjustment = scr_win.get_vadjustment() - if vadjustment: - vadjustment.set_value(0) + selection = self.available_treeview.get_selection() + if selection.count_selected_rows() == 0: + root_iter = self.available_plugins_model.get_iter_first() + path = self.available_plugins_model.get_path(root_iter) + selection.select_iter(root_iter) + self.name_label.show() + self.homepage_linkbutton.show() + self.available_treeview.scroll_to_cell(path) -class Ftp(threading.Thread): - def __init__(self, plugin): - super(Ftp, self).__init__() +class DownloadAsync(threading.Thread): + def __init__(self, plugin, secure, remote_dirs, upgrading, check_update): + threading.Thread.__init__(self) self.plugin = plugin self.window = plugin.window self.progressbar = plugin.progressbar self.model = plugin.available_plugins_model - self.buffer_ = io.BytesIO() - self.remote_dirs = None - self.append_to_model = True - self.upgrading = False + self.remote_dirs = remote_dirs + self.upgrading = upgrading + self.secure = secure + self.check_update = check_update + self.pulse = None icon = Gtk.Image() - self.def_icon = icon.render_icon(Gtk.STOCK_PREFERENCES, - Gtk.IconSize.MENU) + self.def_icon = icon.render_icon( + Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU) def model_append(self, row): - self.model.append(row) + row_data = [ + row['icon'], row['remote_dir'], row['name'], row['local_version'], + row['version'], row['upgrade'], row['description'], row['authors'], + row['homepage'] + ] + self.model.append(row_data) return False def progressbar_pulse(self): self.progressbar.pulse() return True - def get_plugin_version(self, plugin_name): - for plugin in gajim.plugin_manager.plugins: - if plugin.name == plugin_name: - return plugin.version - def run(self): try: - GLib.idle_add(self.progressbar.set_text, - _('Connecting to server')) - self.ftp = self.plugin.ftp_connect() - self.ftp.cwd(self.plugin.server_folder) - self.progressbar.set_show_text(True) - if not self.remote_dirs: - GLib.idle_add(self.progressbar.set_text, - _('Scan files on the server')) - self.ftp.retrbinary('RETR manifests_images.zip', self.handleDownload) - zip_file = zipfile.ZipFile(self.buffer_) - manifest_list = zip_file.namelist() - progress_step = 1.0 / len(manifest_list) - for filename in manifest_list: - if not filename.endswith('manifest.ini'): - continue - dir_ = filename.split('/')[0] - fract = self.progressbar.get_fraction() + progress_step - GLib.idle_add(self.progressbar.set_fraction, fract) - GLib.idle_add(self.progressbar.set_text, - _('Reading "%s"') % dir_) + if self.check_update: + self.run_check_update() + else: + GLib.idle_add(self.progressbar.show) + self.pulse = GLib.timeout_add(150, self.progressbar_pulse) + self.run_download_plugin_list() + except urllib.error.URLError as exc: + if isinstance(exc.reason, ssl.SSLError): + ssl_reason = exc.reason.reason + if ssl_reason == 'CERTIFICATE_VERIFY_FAILED': + log.exception('Certificate verify failed') + GLib.idle_add(self.plugin.on_error, ssl_reason) + except Exception as exc: + GLib.idle_add(self.plugin.on_error, str(exc)) + log.exception('Error fetching plugin list') + finally: + if self.pulse: + GLib.source_remove(self.pulse) + GLib.idle_add(self.progressbar.hide) + self.pulse = None - config = configparser.ConfigParser() - conf_file = zip_file.open(filename) - config.read_file(io.TextIOWrapper(conf_file, encoding='utf-8')) - conf_file.close() - if not config.has_section('info'): - continue - opts = config.options('info') - if 'name' not in opts or 'version' not in opts or \ - 'description' not in opts or 'authors' not in opts or \ - 'homepage' not in opts: - continue + def parse_manifest(self, buf): + ''' + given the buffer of the zipfile, returns the list of plugin manifests + ''' + zip_file = ZipFile(buf) + manifest_list = zip_file.namelist() + plugins = [] + for filename in manifest_list: + # Parse manifest + if not filename.endswith('manifest.ini'): + continue + config = configparser.ConfigParser() + conf_file = zip_file.open(filename) + config.read_file(io.TextIOWrapper(conf_file, encoding='utf-8')) + conf_file.close() + if not config.has_section('info'): + log.warn('Plugin is missing INFO section in manifest.ini. ' + 'Plugin not loaded.') + continue + opts = config.options('info') + if not set(MANDATORY_FIELDS).issubset(opts): + log.warn('Plugin is missing mandatory fields in manifest.ini. ' + 'Plugin not loaded.') + continue + # Add icon and remote dir + icon = None + remote_dir = filename.split('/')[0] + png_filename = '{0}/{0}.png'.format(remote_dir) + icon = self.def_icon + if png_filename in manifest_list: + data = zip_file.open(png_filename).read() + pix = GdkPixbuf.PixbufLoader() + pix.set_size(16, 16) + pix.write(data) + pix.close() + icon = pix.get_pixbuf() - local_version = self.get_plugin_version( - config.get('info', 'name')) - upgrade = False - if self.upgrading and local_version: - local = convert_version_to_list(local_version) - remote = convert_version_to_list(config.get('info', - 'version')) - if remote > local: - upgrade = True - GLib.idle_add( - self.plugin.inslall_upgrade_button.set_property, - 'sensitive', True) - png_filename = dir_ + '/' + dir_ + '.png' - if png_filename in manifest_list: - data = zip_file.open(png_filename).read() - pbl = GdkPixbuf.PixbufLoader() - pbl.set_size(16, 16) - pbl.write(data) - pbl.close() - def_icon = pbl.get_pixbuf() - else: - def_icon = self.def_icon - if local_version: - base_dir, user_dir = gajim.PLUGINS_DIRS - local_dir = os.path.join(user_dir, dir_) + # transform to dictonary + config_dict = {} + for key, value in config.items('info'): + config_dict[key] = value + config_dict['icon'] = icon + config_dict['remote_dir'] = remote_dir + config_dict['upgrade'] = False - GLib.idle_add(self.model_append, [def_icon, dir_, - config.get('info', 'name'), local_version, - config.get('info', 'version'), upgrade, - config.get('info', 'description'), - config.get('info', 'authors'), - config.get('info', 'homepage'), ]) - self.ftp.quit() - GLib.idle_add(self.progressbar.set_fraction, 0) - if self.remote_dirs: - self.download_plugin() - GLib.idle_add(self.progressbar.hide) + plugins.append(config_dict) + return plugins + + def download_url(self, url): + log.info('Fetching %s', url) + ssl_args = {} + if self.secure: + ssl_args['context'] = ssl.create_default_context( + cafile=self.plugin.local_file_path('DST_Root_CA_X3.pem')) + else: + ssl_args['context'] = ssl.create_default_context() + ssl_args['context'].check_hostname = False + ssl_args['context'].verify_mode = ssl.CERT_NONE + + for flag in ('OP_NO_SSLv2', 'OP_NO_SSLv3', + 'OP_NO_TLSv1', 'OP_NO_TLSv1_1', + 'OP_NO_COMPRESSION', + ): + log.debug('SSL Options: +%s' % flag) + ssl_args['context'].options |= getattr(ssl, flag) + request = urlopen(url, **ssl_args) + + return io.BytesIO(request.read()) + + def run_check_update(self): + to_update = [] + zipbuf = self.download_url(MANIFEST_URL) + plugin_list = self.parse_manifest(zipbuf) + for plugin in plugin_list: + local_version = get_local_version(plugin['name']) + if local_version: + if V(plugin['version']) > V(local_version): + to_update.append(plugin['name']) + GLib.idle_add(self.plugin.warn_update, to_update) + + def run_download_plugin_list(self): + if not self.remote_dirs: + log.info('Downloading Pluginlist...') + zipbuf = self.download_url(MANIFEST_IMAGE_URL) + plugin_list = self.parse_manifest(zipbuf) + for plugin in plugin_list: + plugin['local_version'] = get_local_version(plugin['name']) + if self.upgrading and plugin['local_version']: + if V(plugin['version']) > V(plugin['local_version']): + plugin['upgrade'] = True + GLib.idle_add( + self.plugin.install_button.set_property, + 'sensitive', True) + GLib.idle_add(self.model_append, plugin) GLib.idle_add(self.plugin.select_root_iter) - except Exception as e: - self.window.emit('error_signal', str(e)) - - def handleDownload(self, block): - self.buffer_.write(block) + else: + self.download_plugin() def download_plugin(self): - GLib.idle_add(self.progressbar.show) - self.pulse = GLib.timeout_add(150, self.progressbar_pulse) - GLib.idle_add(self.progressbar.set_text, _('Creating a list of files')) for remote_dir in self.remote_dirs: filename = remote_dir + '.zip' + log.info('Download: %s', filename) base_dir, user_dir = gajim.PLUGINS_DIRS if not os.path.isdir(user_dir): os.mkdir(user_dir) - local_dir = ld = os.path.join(user_dir, remote_dir) + local_dir = os.path.join(user_dir, remote_dir) if not os.path.isdir(local_dir): os.mkdir(local_dir) local_dir = os.path.split(user_dir)[0] # downloading zip file - GLib.idle_add(self.progressbar.set_text, - _('Downloading "%s"') % filename) - full_filename = os.path.join(user_dir, filename) - self.buffer_ = io.BytesIO() try: - self.ftp.retrbinary('RETR %s' % filename, self.handleDownload) - except ftplib.all_errors as e: - print (str(e)) - - with zipfile.ZipFile(self.buffer_) as zip_file: - zip_file.extractall(os.path.join(user_dir)) - - self.ftp.quit() - GLib.idle_add(self.window.emit, 'plugin_downloaded', self.remote_dirs) - GLib.source_remove(self.pulse) + plugin = posixpath.join(PLUGINS_URL, filename) + buf = self.download_url(plugin) + except: + log.exception("Error downloading plugin %s" % filename) + continue + with ZipFile(buf) as zip_file: + zip_file.extractall(os.path.join(local_dir, 'plugins')) + GLib.idle_add(self.plugin.on_plugin_downloaded, self.remote_dirs) class PluginInstallerPluginConfigDialog(GajimPluginConfigDialog): def init(self): - self.Gtk_BUILDER_FILE_PATH = self.plugin.local_file_path( - 'config_dialog.ui') + glade_file_path = self.plugin.local_file_path('config_dialog.ui') self.xml = Gtk.Builder() self.xml.set_translation_domain('gajim_plugins') - self.xml.add_objects_from_file(self.Gtk_BUILDER_FILE_PATH, ['hbox111']) - hbox = self.xml.get_object('hbox111') - self.get_child().pack_start(hbox, True, True, 0) + self.xml.add_objects_from_file(glade_file_path, ['config_grid']) + grid = self.xml.get_object('config_grid') + self.get_child().pack_start(grid, True, True, 0) self.xml.connect_signals(self) - self.connect('hide', self.on_hide) def on_run(self): - widget = self.xml.get_object('ftp_server') - widget.set_text(str(self.plugin.config['ftp_server'])) self.xml.get_object('check_update').set_active( self.plugin.config['check_update']) - self.xml.get_object('check_update_periodically').set_active( - self.plugin.config['check_update_periodically']) - - def on_hide(self, widget): - widget = self.xml.get_object('ftp_server') - self.plugin.config['ftp_server'] = widget.get_text() def on_check_update_toggled(self, widget): self.plugin.config['check_update'] = widget.get_active() - - def on_check_update_periodically_toggled(self, widget): - self.plugin.config['check_update_periodically'] = widget.get_active()