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