196 lines
6.0 KiB
Python
196 lines
6.0 KiB
Python
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()
|