1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
|
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
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(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):
# 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()
|