plugin_installer/0000755000175500017550000000000013773402760014174 5ustar debacledebacleplugin_installer/installer.ui0000644000175500017550000003554613773402760016545 0ustar debacledebacle True False 18 18 True False 180 True False True vertical True True never out True True plugin_store 2 True fixed 180 150 300 Plugin True True 1 0 end 1 Installed 2 Available 3 Install True 0.5 True 4 4 True True 0 True False text False True False False Install / Upda_te True False False False True 1 -1 True False True False True 0 400 True False True vertical 18 True False True start <Plugin Name> True 0 False True 0 True False start <Description> True word-char True 0 False True 1 True False 6 12 True False end start Version 0 0 True False end start Authors 0 1 True False end start Homepage 0 2 True False start <empty> True word-char True 0 1 0 True False start <empty> True word-char True 0 1 1 True False start <empty> True word-char 0 1 2 False True 2 False True 1 plugin_installer/plugin_installer.png0000644000175500017550000000103513773402760020254 0ustar debacledebaclePNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxb?%BҸ<E; vP6X>ͽ,/)o zmP`E_ W \@&##Հo?4d ;YGEf3mg7af`dd`x חQ 8v4rX0&521n\;pܶ,?~@8~0|g<;Lˈh1˙珟p/5on߿`myf?X PfIL(-XXyE'>f 0Bezh;\WO߁_yE.H/D@gb(>֟Y͓/H.$iV$#YHav0eذx6IENDB`plugin_installer/utils.py0000644000175500017550000001404113773402760015706 0ustar debacledebacleimport logging from io import BytesIO from pathlib import Path from zipfile import ZipFile import configparser from configparser import ConfigParser from packaging.version import Version as V from gi.repository import Gtk from gi.repository import GdkPixbuf import gajim 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() # ZipFile can only handle posix paths with zip_file.open(manifest_path.as_posix()) 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(gajim.__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 V(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 plugin_settings = app.settings.get_plugins() if self.short_name not in plugin_settings: return None active = app.settings.get_plugin_setting(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): # ZipFile can only handle posix paths try: zip_file.getinfo(icon_path.as_posix()) except KeyError: return None with zip_file.open(icon_path.as_posix()) 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() plugin_installer/config_dialog.py0000644000175500017550000000410613773402760017333 0ustar debacledebacle# 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.gui.settings import SettingsDialog from gajim.gui.const import Setting from gajim.gui.const import SettingKind from gajim.gui.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 plugin_installer/__init__.py0000644000175500017550000000005613773402760016306 0ustar debacledebaclefrom .plugin_installer import PluginInstaller plugin_installer/manifest.ini0000644000175500017550000000063713773402760016511 0ustar debacledebacle[info] name: Plugin Installer short_name: plugin_installer version: 1.3.8 description: Install and upgrade plugins for Gajim authors: Denis Fomin Yann Leboulanger Thilo Molitor Philipp Hörist homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/PluginInstallerPlugin min_gajim_version: 1.2.91 max_gajim_version: 1.3.90 plugin_installer/widget.py0000644000175500017550000001111713773402760016032 0ustar debacledebacle# 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, notebook): Observable.__init__(self) self._ui = get_builder(builder_path) self._notebook = 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 show_page(self): self._notebook.set_current_page(self._page_num) def append_plugins(self, plugins): for plugin in plugins: self._ui.plugin_store.append(plugin.fields) if plugins: 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() if iter_ is not None: selection.select_iter(iter_) path = self._ui.plugin_store.get_path(iter_) self._ui.available_plugins_treeview.scroll_to_cell(path) plugin_installer/plugin_installer.py0000644000175500017550000002325213773402760020125 0ustar debacledebacle# Copyright (C) 2010-2012 Denis Fomin # Copyright (C) 2011-2012 Yann Leboulanger # Copyright (C) 2017-2019 Philipp Hörist # # 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 . import logging from functools import partial from io import BytesIO from zipfile import ZipFile from gi.repository import GLib from gi.repository import Soup from gajim.common import app from gajim.plugins import GajimPlugin from gajim.plugins.plugins_i18n import _ from gajim.gui.dialogs import DialogButton from gajim.gui.dialogs import InformationDialog from gajim.gui.dialogs import ConfirmationCheckDialog 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 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 = 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_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 X seconds after Gajim was started self._check_update_id = GLib.timeout_add_seconds( 10, self._check_for_updates) 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 self._download_plugins(plugins) plugins_str = '\n' + '\n'.join([plugin.name for plugin in plugins]) ConfirmationCheckDialog( _('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 not plugin_list: log.warning('No plugins found in zip') 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: if self._available_page is not None: self._available_page.update_plugin(plugin) else: self._needs_restart = True log.info('Plugin %s needs restart', plugin.name) 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 _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: InformationDialog(_('Plugins Downloaded')) elif self.config['auto_update_feedback']: def _on_ok(is_checked): if is_checked: self.config['auto_update_feedback'] = False ConfirmationCheckDialog( _('Plugins Updated'), _('Plugins Updated'), _('Plugin updates have successfully been downloaded.\n' 'Updates will be installed next time Gajim is started.'), _('Do not show this message again'), [DialogButton.make('OK', callback=_on_ok)]).show() def _on_connect_plugin_window(self, plugin_window): self._available_page = AvailablePage( self.local_file_path('installer.ui'), plugin_window.get_notebook()) 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 _on_disconnect_plugin_window(self, _plugin_window): self._session.abort() self._available_page.destroy() self._available_page = None plugin_installer/remote.py0000644000175500017550000000041313773402760016037 0ustar debacledebacle# File which defines all remote URLs server = 'https://ftp.gajim.org' directory = 'plugins_1.3_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