gajim3/gajim/data/plugins/plugin_installer/utils.py

196 lines
6 KiB
Python
Raw Normal View History

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()