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

196 lines
6 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()