[plugin_installer] Rewrite plugin
- Use libsoup for HTTP operations - Add new config dialog - Move code into smaller modules
This commit is contained in:
@@ -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-----
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<?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>
|
|
||||||
51
plugin_installer/config_dialog.py
Normal file
51
plugin_installer/config_dialog.py
Normal file
@@ -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 <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>
|
<columns>
|
||||||
<!-- column-name icon -->
|
<!-- column-name icon -->
|
||||||
<column type="GdkPixbuf"/>
|
<column type="GdkPixbuf"/>
|
||||||
<!-- column-name dir -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
<!-- column-name name -->
|
<!-- column-name name -->
|
||||||
<column type="gchararray"/>
|
<column type="gchararray"/>
|
||||||
<!-- column-name localversion -->
|
<!-- column-name localversion -->
|
||||||
@@ -16,12 +14,8 @@
|
|||||||
<column type="gchararray"/>
|
<column type="gchararray"/>
|
||||||
<!-- column-name upgrade -->
|
<!-- column-name upgrade -->
|
||||||
<column type="gboolean"/>
|
<column type="gboolean"/>
|
||||||
<!-- column-name description -->
|
<!-- column-name plugin -->
|
||||||
<column type="gchararray"/>
|
<column type="PyObject"/>
|
||||||
<!-- column-name authors -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
<!-- column-name homepage -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
</columns>
|
</columns>
|
||||||
</object>
|
</object>
|
||||||
<object class="GtkBox" id="available_plugins_box">
|
<object class="GtkBox" id="available_plugins_box">
|
||||||
@@ -54,7 +48,7 @@
|
|||||||
<property name="search_column">2</property>
|
<property name="search_column">2</property>
|
||||||
<child internal-child="selection">
|
<child internal-child="selection">
|
||||||
<object class="GtkTreeSelection" id="treeview-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>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
@@ -79,7 +73,7 @@
|
|||||||
<property name="ellipsize">end</property>
|
<property name="ellipsize">end</property>
|
||||||
</object>
|
</object>
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="text">2</attribute>
|
<attribute name="text">1</attribute>
|
||||||
</attributes>
|
</attributes>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
@@ -90,7 +84,7 @@
|
|||||||
<child>
|
<child>
|
||||||
<object class="GtkCellRendererText" id="versiontextrenderer"/>
|
<object class="GtkCellRendererText" id="versiontextrenderer"/>
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="text">3</attribute>
|
<attribute name="text">2</attribute>
|
||||||
</attributes>
|
</attributes>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
@@ -101,7 +95,7 @@
|
|||||||
<child>
|
<child>
|
||||||
<object class="GtkCellRendererText" id="availabletextrenderer"/>
|
<object class="GtkCellRendererText" id="availabletextrenderer"/>
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="text">4</attribute>
|
<attribute name="text">3</attribute>
|
||||||
</attributes>
|
</attributes>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
@@ -112,13 +106,13 @@
|
|||||||
<property name="clickable">True</property>
|
<property name="clickable">True</property>
|
||||||
<property name="alignment">0.5</property>
|
<property name="alignment">0.5</property>
|
||||||
<property name="sort_indicator">True</property>
|
<property name="sort_indicator">True</property>
|
||||||
<property name="sort_column_id">5</property>
|
<property name="sort_column_id">4</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkCellRendererToggle" id="togglerenderer">
|
<object class="GtkCellRendererToggle" id="togglerenderer">
|
||||||
<signal name="toggled" handler="_available_plugin_toggled" swapped="no"/>
|
<signal name="toggled" handler="_available_plugin_toggled" swapped="no"/>
|
||||||
</object>
|
</object>
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="active">5</attribute>
|
<attribute name="active">4</attribute>
|
||||||
</attributes>
|
</attributes>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
@@ -148,7 +142,7 @@
|
|||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="tooltip_text" translatable="yes">Install/Update Plugin</property>
|
<property name="tooltip_text" translatable="yes">Install/Update Plugin</property>
|
||||||
<property name="icon_name">software-update-available-symbolic</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>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
@@ -193,7 +187,7 @@
|
|||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="spacing">18</property>
|
<property name="spacing">18</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="plugin_name_label">
|
<object class="GtkLabel" id="name_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</property>
|
<property name="no_show_all">True</property>
|
||||||
@@ -212,7 +206,7 @@
|
|||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="plugin_description_label">
|
<object class="GtkLabel" id="description_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
@@ -283,7 +277,7 @@
|
|||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="plugin_version_label">
|
<object class="GtkLabel" id="version_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
@@ -299,7 +293,7 @@
|
|||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="plugin_authors_label">
|
<object class="GtkLabel" id="authors_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
@@ -315,7 +309,7 @@
|
|||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="plugin_homepage_linkbutton">
|
<object class="GtkLabel" id="homepage_linkbutton">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# plugins/plugin_installer/plugin_installer.py
|
|
||||||
#
|
|
||||||
# Copyright (C) 2010-2012 Denis Fomin <fominde AT gmail.com>
|
# Copyright (C) 2010-2012 Denis Fomin <fominde AT gmail.com>
|
||||||
# Copyright (C) 2011-2012 Yann Leboulanger <asterix AT lagaule.org>
|
# Copyright (C) 2011-2012 Yann Leboulanger <asterix AT lagaule.org>
|
||||||
# Copyright (C) 2017 Philipp Hörist <philipp AT hoerist.com>
|
# Copyright (C) 2017-2019 Philipp Hörist <philipp AT hoerist.com>
|
||||||
#
|
#
|
||||||
# This file is part of Gajim.
|
# This file is part of Gajim.
|
||||||
#
|
#
|
||||||
@@ -19,118 +15,116 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
|
||||||
|
|
||||||
import io
|
|
||||||
import threading
|
|
||||||
import configparser
|
|
||||||
import os
|
|
||||||
import ssl
|
|
||||||
import logging
|
import logging
|
||||||
import posixpath
|
from functools import partial
|
||||||
from enum import IntEnum
|
from io import BytesIO
|
||||||
from zipfile import ZipFile
|
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 GLib
|
||||||
|
from gi.repository import Soup
|
||||||
|
|
||||||
from gajim.common import app
|
from gajim.common import app
|
||||||
from gajim.common import configpaths
|
|
||||||
|
|
||||||
from gajim.plugins import GajimPlugin
|
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.plugins.plugins_i18n import _
|
||||||
|
|
||||||
from gajim.gtk.dialogs import DialogButton
|
from gajim.gtk.dialogs import DialogButton
|
||||||
from gajim.gtk.dialogs import InformationDialog
|
from gajim.gtk.dialogs import InformationDialog
|
||||||
from gajim.gtk.dialogs import NewConfirmationDialog
|
|
||||||
from gajim.gtk.dialogs import NewConfirmationCheckDialog
|
from gajim.gtk.dialogs import NewConfirmationCheckDialog
|
||||||
from gajim.gtk.dialogs import WarningDialog
|
|
||||||
from gajim.gtkgui_helpers import get_action
|
from gajim.gtkgui_helpers import get_action
|
||||||
|
|
||||||
log = logging.getLogger('gajim.p.plugin_installer')
|
from plugin_installer.config_dialog import PluginInstallerConfigDialog
|
||||||
|
from plugin_installer.widget import AvailablePage
|
||||||
PLUGINS_URL = 'https://ftp.gajim.org/plugins_master_zip/'
|
from plugin_installer.utils import parse_manifests_zip
|
||||||
MANIFEST_URL = 'https://ftp.gajim.org/plugins_master_zip/manifests.zip'
|
from plugin_installer.remote import MANIFEST_URL
|
||||||
MANIFEST_IMAGE_URL = \
|
from plugin_installer.remote import 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)
|
|
||||||
|
|
||||||
|
|
||||||
class Column(IntEnum):
|
log = logging.getLogger('gajim.p.installer')
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class PluginInstaller(GajimPlugin):
|
class PluginInstaller(GajimPlugin):
|
||||||
def init(self):
|
def init(self):
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
self.description = _('Install and upgrade plugins for Gajim')
|
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, ''),
|
self.config_default_values = {'check_update': (True, ''),
|
||||||
'auto_update': (False, ''),
|
'auto_update': (False, ''),
|
||||||
'auto_update_feedback': (True, '')}
|
'auto_update_feedback': (True, '')}
|
||||||
self.gui_extension_points = {'plugin_window': (self.on_activate, None)}
|
self.gui_extension_points = {
|
||||||
self.window = None
|
'plugin_window': (self._on_connect_plugin_window,
|
||||||
self.spinner = None
|
self._on_disconnect_plugin_window)}
|
||||||
self.available_plugins_model = None
|
|
||||||
self.timeout_id = 0
|
self._check_update_id = None
|
||||||
self.connected_ids = {}
|
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):
|
def activate(self):
|
||||||
if self.config['check_update']:
|
if self.config['check_update']:
|
||||||
# Check for updates 30 seconds after Gajim was started
|
# Check for updates 30 seconds after Gajim was started
|
||||||
self.timeout_id = GLib.timeout_add_seconds(30, self.check_update)
|
self._check_update_id = GLib.timeout_add_seconds(
|
||||||
if 'plugins' in app.interface.instances:
|
10, self._check_for_updates)
|
||||||
self.on_activate(app.interface.instances['plugins'])
|
|
||||||
|
|
||||||
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):
|
def _open_update(is_checked):
|
||||||
if is_checked:
|
if is_checked:
|
||||||
self.config['auto_update'] = True
|
self.config['auto_update'] = True
|
||||||
@@ -138,189 +132,105 @@ class PluginInstaller(GajimPlugin):
|
|||||||
page = self.notebook.page_num(self._ui.available_plugins_box)
|
page = self.notebook.page_num(self._ui.available_plugins_box)
|
||||||
self.notebook.set_current_page(page)
|
self.notebook.set_current_page(page)
|
||||||
|
|
||||||
if plugins:
|
plugins_str = '\n' + '\n'.join([plugin.name for plugin in plugins])
|
||||||
plugins_str = '\n' + '\n'.join(plugins)
|
NewConfirmationCheckDialog(
|
||||||
NewConfirmationCheckDialog(
|
_('Plugin Updates'),
|
||||||
_('Plugin Updates'),
|
_('Plugin Updates Available'),
|
||||||
_('Plugin Updates Available'),
|
_('There are updates for your plugins:\n'
|
||||||
_('There are updates for your plugins:\n'
|
'<b>%s</b>') % plugins_str,
|
||||||
'<b>%s</b>') % plugins_str,
|
_('Update plugins automatically next time'),
|
||||||
_('Update plugins automatically next time'),
|
[DialogButton.make('Cancel'),
|
||||||
[DialogButton.make('Cancel'),
|
DialogButton.make('Accept',
|
||||||
DialogButton.make('Accept',
|
text=_('_Update'),
|
||||||
text=_('_Update'),
|
is_default=True,
|
||||||
is_default=True,
|
callback=_open_update)]).show()
|
||||||
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:
|
else:
|
||||||
log.info('No updates found')
|
self._needs_restart = True
|
||||||
if hasattr(self, 'thread'):
|
log.info('Plugin %s needs restart', plugin.name)
|
||||||
del self.thread
|
|
||||||
|
|
||||||
def check_update(self):
|
if self._download_queue == 0:
|
||||||
if hasattr(self, 'thread'):
|
self._set_download_in_progress(False)
|
||||||
return
|
self._notify_about_download_finished()
|
||||||
log.info('Checking for Updates...')
|
self._update_in_progress = False
|
||||||
auto_update = self.config['auto_update']
|
self._needs_restart = False
|
||||||
self.start_download(check_update=True, auto_update=auto_update)
|
|
||||||
self.timeout_id = 0
|
|
||||||
|
|
||||||
def deactivate(self):
|
def _notify_about_download_finished(self):
|
||||||
if hasattr(self, 'available_page'):
|
if not self._update_in_progress:
|
||||||
self.notebook.remove_page(
|
if self._needs_restart:
|
||||||
self.notebook.page_num(self._ui.available_plugins_box))
|
InformationDialog(
|
||||||
self.notebook.set_current_page(0)
|
_('Plugins Downloaded'),
|
||||||
for id_, widget in list(self.connected_ids.items()):
|
_('Updates will be installed next time Gajim is '
|
||||||
widget.disconnect(id_)
|
'started.'))
|
||||||
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'
|
|
||||||
'<tt>[%s]</tt>' % (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)
|
|
||||||
else:
|
else:
|
||||||
need_restart = True
|
InformationDialog(_('Plugins Downloaded'))
|
||||||
|
|
||||||
if not auto_update:
|
elif self.config['auto_update_feedback']:
|
||||||
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']:
|
|
||||||
def _on_ok(is_checked):
|
def _on_ok(is_checked):
|
||||||
if is_checked:
|
if is_checked:
|
||||||
self.config['auto_update_feedback'] = False
|
self.config['auto_update_feedback'] = False
|
||||||
@@ -333,288 +243,16 @@ class PluginInstaller(GajimPlugin):
|
|||||||
[DialogButton.make('OK',
|
[DialogButton.make('OK',
|
||||||
callback=_on_ok)]).show()
|
callback=_on_ok)]).show()
|
||||||
|
|
||||||
if auto_update and not self.config['auto_update_feedback']:
|
def _on_connect_plugin_window(self, plugin_window):
|
||||||
log.info('Updates downloaded, will install on next restart')
|
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):
|
def _on_disconnect_plugin_window(self, _plugin_window):
|
||||||
model, iter_ = treeview_selection.get_selected()
|
self._session.abort()
|
||||||
if not iter_:
|
self._available_page.destroy()
|
||||||
self._ui.plugin_name_label.set_text('')
|
self._available_page = None
|
||||||
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 = '<a href="%s">%s</a>' % (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()
|
|
||||||
|
|||||||
8
plugin_installer/remote.py
Normal file
8
plugin_installer/remote.py
Normal file
@@ -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
|
||||||
188
plugin_installer/utils.py
Normal file
188
plugin_installer/utils.py
Normal file
@@ -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()
|
||||||
131
plugin_installer/widget.py
Normal file
131
plugin_installer/widget.py
Normal file
@@ -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 <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)
|
||||||
Reference in New Issue
Block a user