[plugin_installer] Remove plugin
This is integrated into Gajim now
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
from .plugin_installer import PluginInstaller
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# 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.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
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!-- Generated with glade 3.38.2 -->
|
|
||||||
<interface>
|
|
||||||
<requires lib="gtk+" version="3.20"/>
|
|
||||||
<object class="GtkListStore" id="plugin_store">
|
|
||||||
<columns>
|
|
||||||
<!-- column-name icon -->
|
|
||||||
<column type="GdkPixbuf"/>
|
|
||||||
<!-- column-name name -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
<!-- column-name localversion -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
<!-- column-name version -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
<!-- column-name upgrade -->
|
|
||||||
<column type="gboolean"/>
|
|
||||||
<!-- column-name plugin -->
|
|
||||||
<column type="PyObject"/>
|
|
||||||
</columns>
|
|
||||||
</object>
|
|
||||||
<object class="GtkBox" id="available_plugins_box">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="border-width">18</property>
|
|
||||||
<property name="spacing">18</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkOverlay">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox" id="plugins_box">
|
|
||||||
<property name="width-request">180</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="hexpand">True</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkScrolledWindow" id="scrolledwindow">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="hscrollbar-policy">never</property>
|
|
||||||
<property name="shadow-type">out</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeView" id="available_plugins_treeview">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="model">plugin_store</property>
|
|
||||||
<property name="search-column">2</property>
|
|
||||||
<child internal-child="selection">
|
|
||||||
<object class="GtkTreeSelection" id="treeview-selection">
|
|
||||||
<signal name="changed" handler="_on_plugin_selection_changed" swapped="no"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeViewColumn">
|
|
||||||
<property name="resizable">True</property>
|
|
||||||
<property name="sizing">fixed</property>
|
|
||||||
<property name="fixed-width">180</property>
|
|
||||||
<property name="min-width">150</property>
|
|
||||||
<property name="max-width">300</property>
|
|
||||||
<property name="title" translatable="yes">Plugin</property>
|
|
||||||
<property name="clickable">True</property>
|
|
||||||
<property name="sort-indicator">True</property>
|
|
||||||
<property name="sort-column-id">1</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererPixbuf" id="pixbufrenderer"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="pixbuf">0</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="plugintextrenderer">
|
|
||||||
<property name="ellipsize">end</property>
|
|
||||||
</object>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">1</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeViewColumn">
|
|
||||||
<property name="title" translatable="yes">Installed</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="versiontextrenderer"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">2</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeViewColumn">
|
|
||||||
<property name="title" translatable="yes">Available</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="availabletextrenderer"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">3</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeViewColumn">
|
|
||||||
<property name="title" translatable="yes">Install</property>
|
|
||||||
<property name="clickable">True</property>
|
|
||||||
<property name="alignment">0.5</property>
|
|
||||||
<property name="sort-indicator">True</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">4</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<style>
|
|
||||||
<class name="space"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkToolbar">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="toolbar-style">text</property>
|
|
||||||
<property name="show-arrow">False</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkToolButton" id="install_plugin_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="label" translatable="yes">Install / Upda_te</property>
|
|
||||||
<property name="use-underline">True</property>
|
|
||||||
<signal name="clicked" handler="_on_install_update_clicked" swapped="no"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="homogeneous">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<style>
|
|
||||||
<class name="inline-toolbar"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="index">-1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child type="overlay">
|
|
||||||
<object class="GtkSpinner" id="spinner">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="active">True</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="width-request">400</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="hexpand">True</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">18</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="name_label">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="no-show-all">True</property>
|
|
||||||
<property name="halign">start</property>
|
|
||||||
<property name="label"><Plugin Name></property>
|
|
||||||
<property name="selectable">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<style>
|
|
||||||
<class name="large-header"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="description_label">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="halign">start</property>
|
|
||||||
<property name="label"><Description></property>
|
|
||||||
<property name="wrap">True</property>
|
|
||||||
<property name="wrap-mode">word-char</property>
|
|
||||||
<property name="selectable">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<!-- n-columns=3 n-rows=3 -->
|
|
||||||
<object class="GtkGrid">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="row-spacing">6</property>
|
|
||||||
<property name="column-spacing">12</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="halign">end</property>
|
|
||||||
<property name="valign">start</property>
|
|
||||||
<property name="label" translatable="yes">Version</property>
|
|
||||||
<style>
|
|
||||||
<class name="dim-label"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left-attach">0</property>
|
|
||||||
<property name="top-attach">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="halign">end</property>
|
|
||||||
<property name="valign">start</property>
|
|
||||||
<property name="label" translatable="yes">Authors</property>
|
|
||||||
<style>
|
|
||||||
<class name="dim-label"/>
|
|
||||||
</style>
|
|
||||||
</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="halign">end</property>
|
|
||||||
<property name="valign">start</property>
|
|
||||||
<property name="label" translatable="yes">Homepage</property>
|
|
||||||
<style>
|
|
||||||
<class name="dim-label"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left-attach">0</property>
|
|
||||||
<property name="top-attach">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="version_label">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="halign">start</property>
|
|
||||||
<property name="label"><empty></property>
|
|
||||||
<property name="wrap">True</property>
|
|
||||||
<property name="wrap-mode">word-char</property>
|
|
||||||
<property name="selectable">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left-attach">1</property>
|
|
||||||
<property name="top-attach">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="authors_label">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="halign">start</property>
|
|
||||||
<property name="label"><empty></property>
|
|
||||||
<property name="wrap">True</property>
|
|
||||||
<property name="wrap-mode">word-char</property>
|
|
||||||
<property name="selectable">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left-attach">1</property>
|
|
||||||
<property name="top-attach">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="homepage_linkbutton">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="halign">start</property>
|
|
||||||
<property name="label"><empty></property>
|
|
||||||
<property name="wrap">True</property>
|
|
||||||
<property name="wrap-mode">word-char</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left-attach">1</property>
|
|
||||||
<property name="top-attach">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</interface>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[info]
|
|
||||||
name: Plugin Installer
|
|
||||||
short_name: plugin_installer
|
|
||||||
version: 1.4.0
|
|
||||||
description: Install and upgrade plugins for Gajim
|
|
||||||
authors: Denis Fomin <fominde@gmail.com>
|
|
||||||
Yann Leboulanger <asterix@lagaule.org>
|
|
||||||
Thilo Molitor <thilo@eightysoft.de>
|
|
||||||
Philipp Hörist <philipp@hoerist.com>
|
|
||||||
homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/PluginInstallerPlugin
|
|
||||||
min_gajim_version: 1.4.0-dev1
|
|
||||||
max_gajim_version: 1.4.90
|
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 541 B |
@@ -1,260 +0,0 @@
|
|||||||
# Copyright (C) 2010-2012 Denis Fomin <fominde AT gmail.com>
|
|
||||||
# Copyright (C) 2011-2012 Yann Leboulanger <asterix AT lagaule.org>
|
|
||||||
# Copyright (C) 2017-2019 Philipp Hörist <philipp AT hoerist.com>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
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'
|
|
||||||
'<b>%s</b>') % 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
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
import 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()
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
# 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, 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 = '<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()
|
|
||||||
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)
|
|
||||||
Reference in New Issue
Block a user