diff --git a/plugin_installer/DST_Root_CA_X3.pem b/plugin_installer/DST_Root_CA_X3.pem
deleted file mode 100644
index 300cd7d..0000000
--- a/plugin_installer/DST_Root_CA_X3.pem
+++ /dev/null
@@ -1,20 +0,0 @@
------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.ui b/plugin_installer/config.ui
deleted file mode 100644
index 6b1c11d..0000000
--- a/plugin_installer/config.ui
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
diff --git a/plugin_installer/config_dialog.py b/plugin_installer/config_dialog.py
new file mode 100644
index 0000000..3ab6a6e
--- /dev/null
+++ b/plugin_installer/config_dialog.py
@@ -0,0 +1,51 @@
+# 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 .
+
+from gi.repository import Gtk
+
+from gajim.gtk.settings import SettingsDialog
+from gajim.gtk.const import Setting
+from gajim.gtk.const import SettingKind
+from gajim.gtk.const import SettingType
+
+from gajim.plugins.plugins_i18n import _
+
+
+class PluginInstallerConfigDialog(SettingsDialog):
+ def __init__(self, plugin, parent):
+
+ self.plugin = plugin
+ settings = [
+ Setting(SettingKind.SWITCH, _('Check for updates'),
+ SettingType.VALUE, self.plugin.config['check_update'],
+ desc=_('Check for updates after start'),
+ callback=self.on_setting, data='check_update'),
+
+ Setting(SettingKind.SWITCH, _('Update automatically'),
+ SettingType.VALUE, self.plugin.config['auto_update'],
+ desc=_('Update plugins automatically'),
+ callback=self.on_setting, data='auto_update'),
+
+ Setting(SettingKind.SWITCH, _('Notify after update'),
+ SettingType.VALUE, self.plugin.config['auto_update_feedback'],
+ desc=_('Show message when automatic update was successful'),
+ callback=self.on_setting, data='auto_update_feedback'),
+ ]
+
+ SettingsDialog.__init__(self, parent, _('Plugin Installer Configuration'),
+ Gtk.DialogFlags.MODAL, settings, None)
+
+ def on_setting(self, value, data):
+ self.plugin.config[data] = value
diff --git a/plugin_installer/installer.ui b/plugin_installer/installer.ui
index b37ca5a..ee5269f 100644
--- a/plugin_installer/installer.ui
+++ b/plugin_installer/installer.ui
@@ -6,8 +6,6 @@
-
-
@@ -16,12 +14,8 @@
-
-
-
-
-
-
+
+
@@ -54,7 +48,7 @@
2
-
+
@@ -79,7 +73,7 @@
end
- 2
+ 1
@@ -90,7 +84,7 @@
- 3
+ 2
@@ -101,7 +95,7 @@
- 4
+ 3
@@ -112,13 +106,13 @@
True
0.5
True
- 5
+ 4
- 5
+ 4
@@ -148,7 +142,7 @@
False
Install/Update Plugin
software-update-available-symbolic
-
+
False
@@ -193,7 +187,7 @@
vertical
18
-
+
True
False
True
@@ -212,7 +206,7 @@
-
+
True
False
start
@@ -283,7 +277,7 @@
-
+
True
False
start
@@ -299,7 +293,7 @@
-
+
True
False
start
@@ -315,7 +309,7 @@
-
+
True
False
start
diff --git a/plugin_installer/plugin_installer.py b/plugin_installer/plugin_installer.py
index 2d164b5..f353483 100644
--- a/plugin_installer/plugin_installer.py
+++ b/plugin_installer/plugin_installer.py
@@ -1,10 +1,6 @@
-# -*- coding: utf-8 -*-
-#
-# plugins/plugin_installer/plugin_installer.py
-#
# Copyright (C) 2010-2012 Denis Fomin
# Copyright (C) 2011-2012 Yann Leboulanger
-# Copyright (C) 2017 Philipp Hörist
+# Copyright (C) 2017-2019 Philipp Hörist
#
# This file is part of Gajim.
#
@@ -19,118 +15,116 @@
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see .
-#
-import io
-import threading
-import configparser
-import os
-import ssl
import logging
-import posixpath
-from enum import IntEnum
+from functools import partial
+from io import BytesIO
from zipfile import ZipFile
-from distutils.version import LooseVersion as V
-import urllib.error
-from urllib.request import urlopen
-from gi.repository import Gtk
-from gi.repository import GdkPixbuf
from gi.repository import GLib
+from gi.repository import Soup
from gajim.common import app
-from gajim.common import configpaths
from gajim.plugins import GajimPlugin
-from gajim.plugins.gui import GajimPluginConfigDialog
-from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import InformationDialog
-from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.gtk.dialogs import NewConfirmationCheckDialog
-from gajim.gtk.dialogs import WarningDialog
from gajim.gtkgui_helpers import get_action
-log = logging.getLogger('gajim.p.plugin_installer')
-
-PLUGINS_URL = 'https://ftp.gajim.org/plugins_master_zip/'
-MANIFEST_URL = 'https://ftp.gajim.org/plugins_master_zip/manifests.zip'
-MANIFEST_IMAGE_URL = \
- 'https://ftp.gajim.org/plugins_master_zip/manifests_images.zip'
-MANDATORY_FIELDS = ['name', 'version', 'description', 'authors', 'homepage']
-FALLBACK_ICON = Gtk.IconTheme.get_default().load_icon(
- 'preferences-system', Gtk.IconSize.MENU, 0)
+from plugin_installer.config_dialog import PluginInstallerConfigDialog
+from plugin_installer.widget import AvailablePage
+from plugin_installer.utils import parse_manifests_zip
+from plugin_installer.remote import MANIFEST_URL
+from plugin_installer.remote import MANIFEST_IMAGE_URL
-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_manifest):
- name = plugin_manifest['name']
- short_name = plugin_manifest['short_name']
-
- for plugin in app.plugin_manager.plugins:
- if plugin.name == name:
- return plugin.version
-
- # Fallback:
- # If the plugin has errors and is not loaded by the
- # PluginManager. Look in the Gajim config if the plugin is
- # known and active, if yes load the manifest from the Plugin
- # dir and parse the version
- active = app.config.get_per('plugins', short_name, 'active')
- if not active:
- return
- manifest_path = os.path.join(
- configpaths.get('PLUGINS_USER'), short_name, 'manifest.ini')
- if not os.path.exists(manifest_path):
- return
- conf = configparser.ConfigParser()
- with open(manifest_path, encoding='utf-8') as conf_file:
- try:
- conf.read_file(conf_file)
- except configparser.Error:
- log.warning('Cant parse version for %s from manifest',
- short_name)
- return
-
- version = conf.get('info', 'version', fallback=None)
- return version
+log = logging.getLogger('gajim.p.installer')
class PluginInstaller(GajimPlugin):
def init(self):
+ # pylint: disable=attribute-defined-outside-init
self.description = _('Install and upgrade plugins for Gajim')
- self.config_dialog = PluginInstallerPluginConfigDialog(self)
+ self.config_dialog = partial(PluginInstallerConfigDialog, self)
self.config_default_values = {'check_update': (True, ''),
'auto_update': (False, ''),
'auto_update_feedback': (True, '')}
- self.gui_extension_points = {'plugin_window': (self.on_activate, None)}
- self.window = None
- self.spinner = None
- self.available_plugins_model = None
- self.timeout_id = 0
- self.connected_ids = {}
+ self.gui_extension_points = {
+ 'plugin_window': (self._on_connect_plugin_window,
+ self._on_disconnect_plugin_window)}
+
+ self._check_update_id = None
+ self._available_page = None
+
+ self._update_in_progress = False
+ self._download_in_progress = False
+ self._download_queue = 0
+ self._needs_restart = False
+
+ self._session = Soup.Session()
+
+ @property
+ def download_in_progress(self):
+ return self._download_in_progress
def activate(self):
if self.config['check_update']:
# Check for updates 30 seconds after Gajim was started
- self.timeout_id = GLib.timeout_add_seconds(30, self.check_update)
- if 'plugins' in app.interface.instances:
- self.on_activate(app.interface.instances['plugins'])
+ self._check_update_id = GLib.timeout_add_seconds(
+ 10, self._check_for_updates)
- def warn_update(self, plugins):
+ def deactivate(self):
+ if self._check_update_id is not None:
+ GLib.source_remove(self._check_update_id)
+ self._check_update_id = None
+
+ def _set_download_in_progress(self, state):
+ self._download_in_progress = state
+ if self._available_page is not None:
+ self._available_page.set_download_in_progress(state)
+
+ def _check_for_updates(self):
+ if self._download_in_progress:
+ log.info('Abort checking for updates because '
+ 'other downloads are in progress')
+ return
+ log.info('Checking for Updates...')
+ message = Soup.Message.new('GET', MANIFEST_URL)
+ self._session.queue_message(message,
+ self._on_check_for_updates_finished)
+
+ def _on_check_for_updates_finished(self, _session, message):
+ if message.status_code != Soup.Status.OK:
+ log.warning('Download failed: %s', MANIFEST_URL)
+ log.warning(Soup.Status.get_phrase(message.status_code))
+ return
+
+ data = message.props.response_body_data.get_data()
+ if data is None:
+ return
+
+ plugin_list = parse_manifests_zip(data)
+ for plugin in list(plugin_list):
+ if plugin.needs_update():
+ log.info('Update available for: %s - %s',
+ plugin.name, plugin.version)
+ else:
+ plugin_list.remove(plugin)
+
+ if not plugin_list:
+ log.info('No updates available')
+ return
+
+ if self.config['auto_update']:
+ self._update_in_progress = True
+ self._download_plugins(plugin_list)
+ else:
+ self._notify_about_update(plugin_list)
+
+ def _notify_about_update(self, plugins):
def _open_update(is_checked):
if is_checked:
self.config['auto_update'] = True
@@ -138,189 +132,105 @@ class PluginInstaller(GajimPlugin):
page = self.notebook.page_num(self._ui.available_plugins_box)
self.notebook.set_current_page(page)
- if plugins:
- plugins_str = '\n' + '\n'.join(plugins)
- NewConfirmationCheckDialog(
- _('Plugin Updates'),
- _('Plugin Updates Available'),
- _('There are updates for your plugins:\n'
- '%s') % plugins_str,
- _('Update plugins automatically next time'),
- [DialogButton.make('Cancel'),
- DialogButton.make('Accept',
- text=_('_Update'),
- is_default=True,
- callback=_open_update)]).show()
+ plugins_str = '\n' + '\n'.join([plugin.name for plugin in plugins])
+ NewConfirmationCheckDialog(
+ _('Plugin Updates'),
+ _('Plugin Updates Available'),
+ _('There are updates for your plugins:\n'
+ '%s') % plugins_str,
+ _('Update plugins automatically next time'),
+ [DialogButton.make('Cancel'),
+ DialogButton.make('Accept',
+ text=_('_Update'),
+ is_default=True,
+ callback=_open_update)]).show()
+
+ def _download_plugin_list(self):
+ log.info('Download plugin list...')
+ message = Soup.Message.new('GET', MANIFEST_IMAGE_URL)
+ self._session.queue_message(message,
+ self._on_download_plugin_list_finished)
+
+ def _on_download_plugin_list_finished(self, _session, message):
+ if message.status_code != Soup.Status.OK:
+ log.warning('Download failed: %s', MANIFEST_IMAGE_URL)
+ log.warning(Soup.Status.get_phrase(message.status_code))
+ return
+
+ data = message.props.response_body_data.get_data()
+ if data is None:
+ return
+
+ plugin_list = parse_manifests_zip(data)
+ if self._available_page is None:
+ return
+ self._available_page.append_plugins(plugin_list)
+ log.info('Downloading plugin list finished')
+
+ def _on_download_plugins(self, _available_page, _signal_name, plugin_list):
+ self._download_plugins(plugin_list)
+
+ def _download_plugins(self, plugin_list):
+ if self._download_in_progress:
+ log.warning('Download started while other download in progress')
+ return
+
+ self._set_download_in_progress(True)
+ self._download_queue = len(plugin_list)
+ for plugin in plugin_list:
+ self._download_plugin(plugin)
+
+ def _download_plugin(self, plugin):
+ log.info('Download plugin %s', plugin.name)
+ message = Soup.Message.new('GET', plugin.remote_uri)
+ self._session.queue_message(message,
+ self._on_download_plugin_finished,
+ plugin)
+
+ def _on_download_plugin_finished(self, _session, message, plugin):
+ self._download_queue -= 1
+ if message.status_code != Soup.Status.OK:
+ log.warning('Download failed: %s', plugin.remote_uri)
+ log.warning(Soup.Status.get_phrase(message.status_code))
+ return
+
+ data = message.props.response_body_data.get_data()
+ if data is None:
+ return
+
+ log.info('Finished downloading %s', plugin.name)
+
+ if not plugin.download_path.exists():
+ plugin.download_path.mkdir(mode=0o700)
+
+ with ZipFile(BytesIO(data)) as zip_file:
+ zip_file.extractall(str(plugin.download_path))
+
+ activated = app.plugin_manager.update_plugins(
+ replace=False, activate=True, plugin_name=plugin.short_name)
+ if activated:
+ self._available_page.update_plugin(plugin)
else:
- log.info('No updates found')
- if hasattr(self, 'thread'):
- del self.thread
+ self._needs_restart = True
+ log.info('Plugin %s needs restart', plugin.name)
- def check_update(self):
- if hasattr(self, 'thread'):
- return
- log.info('Checking for Updates...')
- auto_update = self.config['auto_update']
- self.start_download(check_update=True, auto_update=auto_update)
- self.timeout_id = 0
+ if self._download_queue == 0:
+ self._set_download_in_progress(False)
+ self._notify_about_download_finished()
+ self._update_in_progress = False
+ self._needs_restart = False
- def deactivate(self):
- if hasattr(self, 'available_page'):
- self.notebook.remove_page(
- self.notebook.page_num(self._ui.available_plugins_box))
- self.notebook.set_current_page(0)
- for id_, widget in list(self.connected_ids.items()):
- widget.disconnect(id_)
- 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, plugin_win):
- if hasattr(self, 'available_page'):
- # 'Available' tab exists
- return
- 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 = plugin_win.window
- id_ = self.window.connect('destroy', self._on_destroy)
- self.connected_ids[id_] = self.window
-
- self._ui = get_builder(self.local_file_path('installer.ui'))
-
- self.spinner = self._ui.spinner
- self.install_plugin_button = self._ui.install_plugin_button
- self.available_plugins_model = self._ui.plugin_store
- self.available_plugins_model.set_sort_column_id(
- 2, Gtk.SortType.ASCENDING)
- self.available_page = self.notebook.append_page(
- self._ui.available_plugins_box, Gtk.Label.new(_('Available')))
-
- self._ui.connect_signals(self)
- self.window.show_all()
-
- def _on_destroy(self, widget):
- if hasattr(self, 'thread'):
- del self.thread
- if hasattr(self, 'available_page'):
- del self.available_page
-
- def _available_plugin_toggled(self, cell, path):
- 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][Column.UPGRADE]:
- dir_list.append(self.available_plugins_model[i][Column.DIR])
- self._ui.install_plugin_button.set_sensitive(bool(dir_list))
-
- def _on_notebook_switch_page(self, widget, page, page_num):
- tab_label_text = self.notebook.get_tab_label_text(page)
- if tab_label_text != (_('Available')):
- return
- if not hasattr(self, 'thread'):
- self.available_plugins_model.clear()
- self.start_download(upgrading=True)
-
- def _on_install_upgrade_clicked(self, widget):
- self._ui.install_plugin_button.set_sensitive(False)
- dir_list = []
- for i in range(len(self.available_plugins_model)):
- if self.available_plugins_model[i][Column.UPGRADE]:
- dir_list.append(self.available_plugins_model[i][Column.DIR])
-
- self.start_download(remote_dirs=dir_list, auto_update=False)
-
- def on_error(self, reason):
- if reason == 'CERTIFICATE_VERIFY_FAILED':
- NewConfirmationDialog(
- _('Security Error'),
- _('Security error while trying to download'),
- _('A security error occurred while trying to download. The '
- 'certificate of the plugin archive could not be verified. '
- 'This might be a security attack. \n\nYou can continue at '
- 'your own risk (not recommended).'),
- [DialogButton.make('Cancel'),
- DialogButton.make('Remove',
- text=_('_Continue'),
- callback=lambda dlg:
- self.start_download(
- secure=False, upgrading=True))]).show()
- else:
- if self.available_plugins_model:
- for i in range(len(self.available_plugins_model)):
- self.available_plugins_model[i][Column.UPGRADE] = False
- self._ui.spinner.hide()
- text = GLib.markup_escape_text(reason)
- WarningDialog(_('Error While Downloading'),
- _('An error occurred while downloading\n\n'
- '[%s]' % (str(text))), self.window)
-
- def start_download(self, secure=True, remote_dirs=False, upgrading=False,
- check_update=False, auto_update=False):
- log.info('Start Download...')
- log.debug(
- 'secure: %s, remote_dirs: %s, upgrading: %s, check_update: %s, '
- 'auto_update: %s', secure, remote_dirs, upgrading, check_update,
- auto_update)
- self.thread = DownloadAsync(
- self, secure=secure, remote_dirs=remote_dirs, upgrading=upgrading,
- check_update=check_update, auto_update=auto_update)
- self.thread.start()
-
- @staticmethod
- def _get_plugin(short_name):
- for plugin in app.plugin_manager.plugins:
- if plugin.short_name == short_name:
- return plugin
-
- def on_plugin_downloaded(self, plugin_dirs, auto_update):
- need_restart = False
- for _dir in plugin_dirs:
- updated = app.plugin_manager.update_plugins(
- replace=False, activate=True, plugin_name=_dir)
- if updated:
- if not auto_update:
- plugin = self._get_plugin(updated[0])
- if plugin is None:
- log.error('Plugin %s not found', updated[0])
- continue
- for row in range(len(self.available_plugins_model)):
- 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
- break
-
- # Get plugin icon
- icon_file = os.path.join(plugin.__path__, os.path.split(
- plugin.__path__)[1]) + '.png'
- icon = FALLBACK_ICON
- if os.path.isfile(icon_file):
- icon = GdkPixbuf.Pixbuf.new_from_file_at_size(
- icon_file, 16, 16)
- row = [plugin, plugin.name, plugin.active,
- plugin.activatable, icon]
- self.installed_plugins_model.append(row)
+ def _notify_about_download_finished(self):
+ if not self._update_in_progress:
+ if self._needs_restart:
+ InformationDialog(
+ _('Plugins Downloaded'),
+ _('Updates will be installed next time Gajim is '
+ 'started.'))
else:
- need_restart = True
+ InformationDialog(_('Plugins Downloaded'))
- if not auto_update:
- if need_restart:
- sectext = _('Updates will be installed next time Gajim is '
- 'started.')
- else:
- sectext = _('All selected plugins downloaded and activated.')
- InformationDialog(_('Plugin Updates Downloaded'), sectext)
-
- if auto_update and self.config['auto_update_feedback']:
+ elif self.config['auto_update_feedback']:
def _on_ok(is_checked):
if is_checked:
self.config['auto_update_feedback'] = False
@@ -333,288 +243,16 @@ class PluginInstaller(GajimPlugin):
[DialogButton.make('OK',
callback=_on_ok)]).show()
- if auto_update and not self.config['auto_update_feedback']:
- log.info('Updates downloaded, will install on next restart')
+ def _on_connect_plugin_window(self, plugin_window):
+ self._available_page = AvailablePage(
+ self.local_file_path('installer.ui'), plugin_window)
+ self._available_page.set_download_in_progress(
+ self._download_in_progress)
+ self._available_page.connect('download-plugins',
+ self._on_download_plugins)
+ self._download_plugin_list()
- def _available_plugins_treeview_selection_changed(self, treeview_selection):
- model, iter_ = treeview_selection.get_selected()
- if not iter_:
- self._ui.plugin_name_label.set_text('')
- self._ui.plugin_description_label.set_text('')
- self._ui.plugin_version_label.set_text('')
- self._ui.plugin_authors_label.set_text('')
- self._ui.plugin_homepage_linkbutton.set_text('')
- self._ui.install_plugin_button.set_sensitive(False)
- return
- self._ui.plugin_name_label.set_text(
- model.get_value(iter_, Column.NAME))
- self._ui.plugin_version_label.set_text(
- model.get_value(iter_, Column.VERSION))
- self._ui.plugin_authors_label.set_text(
- model.get_value(iter_, Column.AUTHORS))
- homepage = model.get_value(iter_, Column.HOMEPAGE)
- markup = '%s' % (homepage, homepage)
- self._ui.plugin_homepage_linkbutton.set_markup(markup)
- self._ui.plugin_description_label.set_text(
- model.get_value(iter_, Column.DESCRIPTION))
-
- def select_root_iter(self):
- selection = self._ui.available_plugins_treeview.get_selection()
- # Selection can ne None if there is no treeview (window closed)
- if not selection:
- return
- model, iter_ = selection.get_selected()
- if not iter_:
- iter_ = self.available_plugins_model.get_iter_first()
- # Try to get first plugin with update available
- for row in range(len(self.available_plugins_model)):
- model_row = self.available_plugins_model[row]
- if model_row[Column.UPGRADE]:
- iter_ = self.available_plugins_model.get_iter(row)
- break
- selection.select_iter(iter_)
- path = self.available_plugins_model.get_path(iter_)
- self._ui.available_plugins_treeview.scroll_to_cell(path)
- self._ui.spinner.hide()
- self.window.present()
-
-
-class DownloadAsync(threading.Thread):
- def __init__(self, plugin, secure, remote_dirs,
- upgrading, check_update, auto_update):
- threading.Thread.__init__(self)
- self.plugin = plugin
- self.spinner = plugin.spinner
- self.model = plugin.available_plugins_model
- self.secure = secure
- self.remote_dirs = remote_dirs
- self.upgrading = upgrading
- self.check_update = check_update
- self.auto_update = auto_update
- self.pulse = None
-
- def model_append(self, 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 run(self):
- try:
- if self.check_update:
- self.run_check_update()
- else:
- if not self.auto_update:
- GLib.idle_add(self._show_spinner, True)
- self.pulse = GLib.timeout_add(
- 150, self._show_spinner, False)
- 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._show_spinner, False)
-
- def _show_spinner(self, show):
- if show:
- self.spinner.show()
- else:
- self.spinner.hide()
- self.pulse = None
-
- def parse_manifest(self, buf):
- '''
- Input: buffer of zip file
- Returns 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.warning('Plugin is missing INFO section in manifest.ini. '
- 'Plugin not loaded.')
- continue
- opts = config.options('info')
- if not set(MANDATORY_FIELDS).issubset(opts):
- log.warning(
- '%s is missing mandatory fields %s. '
- 'Plugin not loaded.',
- filename,
- set(MANDATORY_FIELDS).difference(opts))
- continue
- # Add icon and remote dir
- icon = None
- remote_dir = filename.split('/')[0]
- png_filename = '{0}/{0}.png'.format(remote_dir)
- icon = FALLBACK_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()
-
- # 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
-
- 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 plugin_is_valid(self, plugin):
- gajim_v = V(app.config.get('version'))
- min_v = plugin.get('min_gajim_version', None)
- min_v = V(min_v) if min_v else gajim_v
- max_v = plugin.get('max_gajim_version', None)
- max_v = V(max_v) if max_v else gajim_v
- if (gajim_v >= min_v) and (gajim_v <= max_v):
- return True
- return False
-
- def run_check_update(self):
- to_update = []
- auto_update_list = []
- zipbuf = self.download_url(MANIFEST_URL)
- plugin_list = self.parse_manifest(zipbuf)
- for plugin in plugin_list:
- local_version = get_local_version(plugin)
- if local_version:
- if ((V(plugin['version']) > V(local_version)) and
- self.plugin_is_valid(plugin)):
- to_update.append(plugin['name'])
- auto_update_list.append(plugin['remote_dir'])
- if not self.auto_update:
- GLib.idle_add(self.plugin.warn_update, to_update)
- else:
- if auto_update_list:
- self.remote_dirs = auto_update_list
- GLib.idle_add(self.download_plugin)
- else:
- log.info('No updates found')
- if hasattr(self.plugin, 'thread'):
- del self.plugin.thread
-
- def run_download_plugin_list(self):
- if not self.remote_dirs:
- log.info('Downloading Plugin list...')
- zipbuf = self.download_url(MANIFEST_IMAGE_URL)
- plugin_list = self.parse_manifest(zipbuf)
- nb_plugins = 0
- for plugin in plugin_list:
- if not self.plugin_is_valid(plugin):
- continue
- nb_plugins += 1
- plugin['local_version'] = get_local_version(plugin)
- if self.upgrading and plugin['local_version']:
- if V(plugin['version']) > V(plugin['local_version']):
- plugin['upgrade'] = True
- GLib.idle_add(
- self.plugin.install_plugin_button.set_sensitive,
- True)
- GLib.idle_add(self.model_append, plugin)
- if nb_plugins:
- GLib.idle_add(self.plugin.select_root_iter)
- else:
- self.download_plugin()
-
- def download_plugin(self):
- for remote_dir in self.remote_dirs:
- filename = remote_dir + '.zip'
- log.info('Download: %s', filename)
-
- user_dir = configpaths.get('PLUGINS_DOWNLOAD')
- local_dir = os.path.join(user_dir, remote_dir)
- if not os.path.isdir(local_dir):
- os.mkdir(local_dir)
- local_dir = os.path.dirname(local_dir)
-
- # Downloading zip file
- try:
- plugin = posixpath.join(PLUGINS_URL, filename)
- buf = self.download_url(plugin)
- except Exception:
- log.exception('Error downloading plugin %s' % filename)
- continue
- with ZipFile(buf) as zip_file:
- zip_file.extractall(local_dir)
- GLib.idle_add(self.plugin.on_plugin_downloaded,
- self.remote_dirs, self.auto_update)
-
-
-class PluginInstallerPluginConfigDialog(GajimPluginConfigDialog):
- def init(self):
- self._ui = get_builder(self.plugin.local_file_path('config.ui'))
- self.get_child().add(self._ui.config_grid)
- self._ui.connect_signals(self)
-
- def on_run(self):
- self._ui.check_update.set_active(self.plugin.config['check_update'])
- self._ui.auto_update.set_sensitive(self.plugin.config['check_update'])
- self._ui.auto_update.set_active(self.plugin.config['auto_update'])
- self._ui.auto_update_feedback.set_sensitive(
- self.plugin.config['auto_update'])
- self._ui.auto_update_feedback.set_active(
- self.plugin.config['auto_update_feedback'])
-
- def _on_check_update_toggled(self, widget):
- self.plugin.config['check_update'] = widget.get_active()
- if not self.plugin.config['check_update']:
- self.plugin.config['auto_update'] = False
- self._ui.auto_update.set_sensitive(self.plugin.config['check_update'])
- self._ui.auto_update.set_active(self.plugin.config['auto_update'])
- self._ui.auto_update_feedback.set_sensitive(
- self.plugin.config['auto_update'])
- self._ui.auto_update_feedback.set_active(
- self.plugin.config['auto_update_feedback'])
-
- def _on_auto_update_toggled(self, widget):
- self.plugin.config['auto_update'] = widget.get_active()
- self._ui.auto_update_feedback.set_sensitive(
- self.plugin.config['auto_update'])
-
- def _on_auto_update_feedback_toggled(self, widget):
- self.plugin.config['auto_update_feedback'] = widget.get_active()
+ def _on_disconnect_plugin_window(self, _plugin_window):
+ self._session.abort()
+ self._available_page.destroy()
+ self._available_page = None
diff --git a/plugin_installer/remote.py b/plugin_installer/remote.py
new file mode 100644
index 0000000..9bebeb6
--- /dev/null
+++ b/plugin_installer/remote.py
@@ -0,0 +1,8 @@
+# File which defines all remote URLs
+
+server = 'https://ftp.gajim.org'
+directory = 'plugins_master_zip'
+
+PLUGINS_DIR_URL = '%s/%s' % (server, directory)
+MANIFEST_URL = '%s/manifests.zip' % PLUGINS_DIR_URL
+MANIFEST_IMAGE_URL = '%s/manifests_images.zip' % PLUGINS_DIR_URL
diff --git a/plugin_installer/utils.py b/plugin_installer/utils.py
new file mode 100644
index 0000000..afb25e4
--- /dev/null
+++ b/plugin_installer/utils.py
@@ -0,0 +1,188 @@
+import logging
+from io import BytesIO
+from pathlib import Path
+from zipfile import ZipFile
+import configparser
+from configparser import ConfigParser
+from distutils.version import LooseVersion as V
+
+from gi.repository import Gtk
+from gi.repository import GdkPixbuf
+
+from gajim.common import app
+from gajim.common import configpaths
+
+from plugin_installer.remote import PLUGINS_DIR_URL
+
+log = logging.getLogger('gajim.p.installer.utils')
+
+MANDATORY_FIELDS = {'name', 'short_name', 'version',
+ 'description', 'authors', 'homepage'}
+FALLBACK_ICON = Gtk.IconTheme.get_default().load_icon(
+ 'preferences-system', Gtk.IconSize.MENU, 0)
+
+
+class PluginInfo:
+ def __init__(self, config, icon):
+ self.icon = icon
+ self.name = config.get('info', 'name')
+ self.short_name = config.get('info', 'short_name')
+ self.version = V(config.get('info', 'version'))
+ self._installed_version = None
+ self.min_gajim_version = V(config.get('info', 'min_gajim_version'))
+ self.max_gajim_version = V(config.get('info', 'max_gajim_version'))
+ self.description = config.get('info', 'description')
+ self.authors = config.get('info', 'authors')
+ self.homepage = config.get('info', 'homepage')
+
+
+ @classmethod
+ def from_zip_file(cls, zip_file, manifest_path):
+ config = ConfigParser()
+ with zip_file.open(str(manifest_path)) as manifest_file:
+ try:
+ config.read_string(manifest_file.read().decode())
+ except configparser.Error as error:
+ log.warning(error)
+ raise ValueError('Invalid manifest: %s' % manifest_path)
+
+ if not is_manifest_valid(config):
+ raise ValueError('Invalid manifest: %s' % manifest_path)
+
+ short_name = config.get('info', 'short_name')
+ png_filename = '%s.png' % short_name
+ png_path = manifest_path.parent / png_filename
+ icon = load_icon_from_zip(zip_file, png_path) or FALLBACK_ICON
+
+ return cls(config, icon)
+
+ @classmethod
+ def from_path(cls, manifest_path):
+ config = ConfigParser()
+ with open(manifest_path, encoding='utf-8') as conf_file:
+ try:
+ config.read_file(conf_file)
+ except configparser.Error as error:
+ log.warning(error)
+ raise ValueError('Invalid manifest: %s' % manifest_path)
+
+ if not is_manifest_valid(config):
+ raise ValueError('Invalid manifest: %s' % manifest_path)
+
+ return cls(config, None)
+
+ @property
+ def remote_uri(self):
+ return '%s/%s.zip' % (PLUGINS_DIR_URL, self.short_name)
+
+ @property
+ def download_path(self):
+ return Path(configpaths.get('PLUGINS_DOWNLOAD'))
+
+ @property
+ def installed_version(self):
+ if self._installed_version is None:
+ self._installed_version = self._get_installed_version()
+ return self._installed_version
+
+ def has_valid_version(self):
+ gajim_version = V(app.config.get('version'))
+ return self.min_gajim_version <= gajim_version <= self.max_gajim_version
+
+ def _get_installed_version(self):
+ for plugin in app.plugin_manager.plugins:
+ if plugin.name == self.name:
+ return plugin.version
+
+ # Fallback:
+ # If the plugin has errors and is not loaded by the
+ # PluginManager. Look in the Gajim config if the plugin is
+ # known and active, if yes load the manifest from the Plugin
+ # dir and parse the version
+ active = app.config.get_per('plugins', self.short_name, 'active')
+ if not active:
+ return None
+
+ manifest_path = (Path(configpaths.get('PLUGINS_USER')) /
+ self.short_name /
+ 'manifest.ini')
+ if not manifest_path.exists():
+ return None
+ try:
+ return PluginInfo.from_path(manifest_path).version
+ except Exception as error:
+ log.warning(error)
+ return None
+
+ def needs_update(self):
+ if self.installed_version is None:
+ return False
+ return self.installed_version < self.version
+
+ @property
+ def fields(self):
+ return [self.icon,
+ self.name,
+ str(self.installed_version or ''),
+ str(self.version),
+ self.needs_update(),
+ self]
+
+
+def parse_manifests_zip(bytes_):
+ plugins = []
+ with ZipFile(BytesIO(bytes_)) as zip_file:
+ files = list(map(Path, zip_file.namelist()))
+ for manifest_path in filter(is_manifest, files):
+ try:
+ plugin = PluginInfo.from_zip_file(zip_file, manifest_path)
+ except Exception as error:
+ log.warning(error)
+ continue
+
+ if not plugin.has_valid_version():
+ continue
+ plugins.append(plugin)
+
+ return plugins
+
+
+def is_manifest(path):
+ if path.name == 'manifest.ini':
+ return True
+ return False
+
+
+def is_manifest_valid(config):
+ if not config.has_section('info'):
+ log.warning('Manifest is missing INFO section')
+ return False
+
+ opts = config.options('info')
+ if not MANDATORY_FIELDS.issubset(opts):
+ log.warning('Manifest is missing mandatory fields %s.',
+ MANDATORY_FIELDS.difference(opts))
+ return False
+ return True
+
+
+def load_icon_from_zip(zip_file, icon_path):
+ try:
+ zip_file.getinfo(str(icon_path))
+ except KeyError:
+ return None
+
+ with zip_file.open(str(icon_path)) as png_file:
+ data = png_file.read()
+
+ pixbuf = GdkPixbuf.PixbufLoader()
+ pixbuf.set_size(16, 16)
+ try:
+ pixbuf.write(data)
+ except Exception:
+ log.exception('Can\'t load icon: %s', icon_path)
+ pixbuf.close()
+ return None
+
+ pixbuf.close()
+ return pixbuf.get_pixbuf()
diff --git a/plugin_installer/widget.py b/plugin_installer/widget.py
new file mode 100644
index 0000000..1ce15fd
--- /dev/null
+++ b/plugin_installer/widget.py
@@ -0,0 +1,131 @@
+# 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 .
+
+from enum import IntEnum
+
+from gi.repository import Gtk
+
+from gajim.common.helpers import Observable
+
+from gajim.plugins.plugins_i18n import _
+from gajim.plugins.helpers import get_builder
+
+class Column(IntEnum):
+ PIXBUF = 0
+ NAME = 1
+ INSTALLED_VERSION = 2
+ VERSION = 3
+ INSTALL = 4
+ PLUGIN = 5
+
+
+class AvailablePage(Observable):
+ def __init__(self, builder_path, plugin_window):
+ Observable.__init__(self)
+ self._ui = get_builder(builder_path)
+
+ self._notebook = plugin_window.plugins_notebook
+ self._page_num = self._notebook.append_page(
+ self._ui.available_plugins_box,
+ Gtk.Label.new(_('Available')))
+
+ self._ui.plugin_store.set_sort_column_id(1, Gtk.SortType.ASCENDING)
+ self._ui.connect_signals(self)
+
+ def destroy(self):
+ self._notebook.remove_page(self._page_num)
+ self._notebook = None
+ self._ui.plugin_store.clear()
+ self._ui.available_plugins_box.destroy()
+ self._ui = None
+ self._plugin = None
+ self.disconnect_signals()
+
+ def append_plugins(self, plugins):
+ for plugin in plugins:
+ self._ui.plugin_store.append(plugin.fields)
+ self._select_first_plugin()
+ self._update_install_button()
+ self._ui.spinner.stop()
+ self._ui.spinner.hide()
+
+ def update_plugin(self, plugin):
+ for row in self._ui.plugin_store:
+ if row[Column.NAME] == plugin.name:
+ row[Column.INSTALLED_VERSION] = str(plugin.version)
+ row[Column.INSTALL] = False
+ break
+
+ def set_download_in_progress(self, state):
+ self._download_in_progress = state
+ self._update_install_button()
+
+ def _available_plugin_toggled(self, _cell, path):
+ is_active = self._ui.plugin_store[path][Column.INSTALL]
+ self._ui.plugin_store[path][Column.INSTALL] = not is_active
+ self._update_install_button()
+
+ def _update_install_button(self):
+ if self._download_in_progress:
+ self._ui.install_plugin_button.set_sensitive(False)
+ return
+
+ sensitive = False
+ for row in self._ui.plugin_store:
+ if row[Column.INSTALL]:
+ sensitive = True
+ break
+ self._ui.install_plugin_button.set_sensitive(sensitive)
+
+ def _on_install_update_clicked(self, _button):
+ self._ui.install_plugin_button.set_sensitive(False)
+
+ plugins = []
+ for row in self._ui.plugin_store:
+ if row[Column.INSTALL]:
+ plugins.append(row[Column.PLUGIN])
+
+ self.notify('download-plugins', plugins)
+
+ def _on_plugin_selection_changed(self, selection):
+ model, iter_ = selection.get_selected()
+ if not iter_:
+ self._clear_plugin_info()
+ else:
+ self._set_plugin_info(model, iter_)
+
+ def _clear_plugin_info(self):
+ self._ui.name_label.set_text('')
+ self._ui.description_label.set_text('')
+ self._ui.version_label.set_text('')
+ self._ui.authors_label.set_text('')
+ self._ui.homepage_linkbutton.set_text('')
+ self._ui.install_plugin_button.set_sensitive(False)
+
+ def _set_plugin_info(self, model, iter_):
+ plugin = model[iter_][Column.PLUGIN]
+ self._ui.name_label.set_text(plugin.name)
+ self._ui.version_label.set_text(str(plugin.version))
+ self._ui.authors_label.set_text(plugin.authors)
+ homepage = '%s' % (plugin.homepage, plugin.homepage)
+ self._ui.homepage_linkbutton.set_markup(homepage)
+ self._ui.description_label.set_text(plugin.description)
+
+ def _select_first_plugin(self):
+ selection = self._ui.available_plugins_treeview.get_selection()
+ iter_ = self._ui.plugin_store.get_iter_first()
+ selection.select_iter(iter_)
+
+ path = self._ui.plugin_store.get_path(iter_)
+ self._ui.available_plugins_treeview.scroll_to_cell(path)