Skip to content
Snippets Groups Projects
Commit b6457f0e authored by Philipp Hörist's avatar Philipp Hörist
Browse files

[plugin_installer] Rewrite plugin

- Use libsoup for HTTP operations
- Add new config dialog
- Move code into smaller modules
parent 09dcd6ba
No related branches found
No related tags found
No related merge requests found
-----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
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkGrid" id="config_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">18</property>
<property name="row_spacing">6</property>
<child>
<object class="GtkCheckButton" id="auto_update_feedback">
<property name="label" translatable="yes">_Show message when automatic update was successful</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="_on_auto_update_feedback_toggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="auto_update">
<property name="label" translatable="yes">_Update plugins automatically</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="_on_auto_update_toggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="check_update">
<property name="label" translatable="yes">_Check for updates after start</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="_on_check_update_toggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">6</property>
<property name="label" translatable="yes">Plugin updates</property>
<style>
<class name="bold16"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
</interface>
# 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 <http://www.gnu.org/licenses/>.
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
......@@ -6,8 +6,6 @@
<columns>
<!-- column-name icon -->
<column type="GdkPixbuf"/>
<!-- column-name dir -->
<column type="gchararray"/>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name localversion -->
......@@ -16,12 +14,8 @@
<column type="gchararray"/>
<!-- column-name upgrade -->
<column type="gboolean"/>
<!-- column-name description -->
<column type="gchararray"/>
<!-- column-name authors -->
<column type="gchararray"/>
<!-- column-name homepage -->
<column type="gchararray"/>
<!-- column-name plugin -->
<column type="PyObject"/>
</columns>
</object>
<object class="GtkBox" id="available_plugins_box">
......@@ -54,7 +48,7 @@
<property name="search_column">2</property>
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeview-selection">
<signal name="changed" handler="_available_plugins_treeview_selection_changed" swapped="no"/>
<signal name="changed" handler="_on_plugin_selection_changed" swapped="no"/>
</object>
</child>
<child>
......@@ -79,7 +73,7 @@
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">2</attribute>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
......@@ -90,7 +84,7 @@
<child>
<object class="GtkCellRendererText" id="versiontextrenderer"/>
<attributes>
<attribute name="text">3</attribute>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
......@@ -101,7 +95,7 @@
<child>
<object class="GtkCellRendererText" id="availabletextrenderer"/>
<attributes>
<attribute name="text">4</attribute>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
......@@ -112,13 +106,13 @@
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">5</property>
<property name="sort_column_id">4</property>
<child>
<object class="GtkCellRendererToggle" id="togglerenderer">
<signal name="toggled" handler="_available_plugin_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">5</attribute>
<attribute name="active">4</attribute>
</attributes>
</child>
</object>
......@@ -148,7 +142,7 @@
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Install/Update Plugin</property>
<property name="icon_name">software-update-available-symbolic</property>
<signal name="clicked" handler="_on_install_upgrade_clicked" swapped="no"/>
<signal name="clicked" handler="_on_install_update_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
......@@ -193,7 +187,7 @@
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkLabel" id="plugin_name_label">
<object class="GtkLabel" id="name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
......@@ -212,7 +206,7 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="plugin_description_label">
<object class="GtkLabel" id="description_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
......@@ -283,7 +277,7 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="plugin_version_label">
<object class="GtkLabel" id="version_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
......@@ -299,7 +293,7 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="plugin_authors_label">
<object class="GtkLabel" id="authors_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
......@@ -315,7 +309,7 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="plugin_homepage_linkbutton">
<object class="GtkLabel" id="homepage_linkbutton">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
......
This diff is collapsed.
# 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
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()
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
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 = '<a href="%s">%s</a>' % (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)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment