31
const _httpSession = new Soup.SessionAsync();
33
// The unfortunate state of gjs, gobject-introspection and libsoup
34
// means that I have to do a hack to add a feature.
35
// See: https://bugzilla.gnome.org/show_bug.cgi?id=655189 for context.
37
if (Soup.Session.prototype.add_feature != null)
38
Soup.Session.prototype.add_feature.call(_httpSession, new Soup.ProxyResolverDefault());
22
40
// Maps uuid -> metadata object
23
41
const extensionMeta = {};
24
42
// Maps uuid -> importer object (extension directory tree)
25
43
const extensions = {};
44
// Maps uuid -> extension state object (returned from init())
45
const extensionStateObjs = {};
27
var disabledExtensions;
28
47
var enabledExtensions;
29
48
// GFile for user extensions
30
49
var userExtensionsDir = null;
51
// We don't really have a class to add signals on. So, create
52
// a simple dummy object, add the signal methods, and export those
55
Signals.addSignalMethods(_signals);
57
const connect = Lang.bind(_signals, _signals.connect);
58
const disconnect = Lang.bind(_signals, _signals.disconnect);
60
// UUID => Array of error messages
63
const ENABLED_EXTENSIONS_KEY = 'enabled-extensions';
34
67
* @required: an array of versions we're compatible with
95
function installExtensionFromManifestURL(uuid, url) {
96
_httpSession.queue_message(
97
Soup.Message.new('GET', url),
98
function(session, message) {
99
if (message.status_code != Soup.KnownStatusCode.OK) {
100
logExtensionError(uuid, 'downloading manifest: ' + message.status_code.toString());
106
manifest = JSON.parse(message.response_body.data);
108
logExtensionError(uuid, 'parsing: ' + e.toString());
112
if (uuid != manifest['uuid']) {
113
logExtensionError(uuid, 'manifest: manifest uuids do not match');
117
let meta = extensionMeta[uuid] = { uuid: uuid,
118
state: ExtensionState.DOWNLOADING,
121
_signals.emit('extension-state-changed', meta);
123
installExtensionFromManifest(manifest, meta);
127
function installExtensionFromManifest(manifest, meta) {
128
let uuid = manifest['uuid'];
129
let name = manifest['name'];
131
if (!versionCheck(manifest['shell-version'], Config.PACKAGE_VERSION)) {
132
meta.state = ExtensionState.OUT_OF_DATE;
133
logExtensionError(uuid, 'version: ' + name + ' is not compatible with current GNOME Shell version', meta.state);
137
let url = manifest['__installer'];
138
_httpSession.queue_message(Soup.Message.new('GET', url),
139
function(session, message) {
140
gotExtensionZipFile(session, message, uuid);
144
function gotExtensionZipFile(session, message, uuid) {
145
if (message.status_code != Soup.KnownStatusCode.OK) {
146
logExtensionError(uuid, 'downloading extension: ' + message.status_code);
150
// FIXME: use a GFile mkstemp-type method once one exists
153
[fd, tmpzip] = GLib.file_open_tmp('XXXXXX.shell-extension.zip');
155
logExtensionError(uuid, 'tempfile: ' + e.toString());
159
let stream = new Gio.UnixOutputStream({ fd: fd });
160
let dir = userExtensionsDir.get_child(uuid);
161
Shell.write_soup_message_to_stream(stream, message);
163
let [success, pid] = GLib.spawn_async(null,
164
['unzip', '-uod', dir.get_path(), '--', tmpzip],
166
GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
170
logExtensionError(uuid, 'extract: could not extract');
174
GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, function(pid, status) {
175
GLib.spawn_close_pid(pid);
176
loadExtension(dir, true, ExtensionType.PER_USER);
180
function disableExtension(uuid) {
181
let meta = extensionMeta[uuid];
185
if (meta.state != ExtensionState.ENABLED)
188
let extensionState = extensionStateObjs[uuid];
191
extensionState.disable();
193
logExtensionError(uuid, e.toString());
197
meta.state = ExtensionState.DISABLED;
198
_signals.emit('extension-state-changed', meta);
201
function enableExtension(uuid) {
202
let meta = extensionMeta[uuid];
206
if (meta.state != ExtensionState.DISABLED)
209
let extensionState = extensionStateObjs[uuid];
212
extensionState.enable();
214
logExtensionError(uuid, e.toString());
218
meta.state = ExtensionState.ENABLED;
219
_signals.emit('extension-state-changed', meta);
222
function logExtensionError(uuid, message, state) {
223
if (!errors[uuid]) errors[uuid] = [];
224
errors[uuid].push(message);
225
global.logError('Extension "%s" had error: %s'.format(uuid, message));
226
state = state || ExtensionState.ERROR;
227
_signals.emit('extension-state-changed', { uuid: uuid,
62
232
function loadExtension(dir, enabled, type) {
64
let baseErrorString = 'While loading extension from "' + dir.get_parse_name() + '": ';
234
let uuid = dir.get_basename();
66
236
let metadataFile = dir.get_child('metadata.json');
67
237
if (!metadataFile.query_exists(null)) {
68
global.logError(baseErrorString + 'Missing metadata.json');
238
logExtensionError(uuid, 'Missing metadata.json');
74
244
metadataContents = Shell.get_file_contents_utf8_sync(metadataFile.get_path());
76
global.logError(baseErrorString + 'Failed to load metadata.json: ' + e);
246
logExtensionError(uuid, 'Failed to load metadata.json: ' + e);
81
251
meta = JSON.parse(metadataContents);
83
global.logError(baseErrorString + 'Failed to parse metadata.json: ' + e);
253
logExtensionError(uuid, 'Failed to parse metadata.json: ' + e);
86
257
let requiredProperties = ['uuid', 'name', 'description', 'shell-version'];
87
258
for (let i = 0; i < requiredProperties.length; i++) {
88
259
let prop = requiredProperties[i];
89
260
if (!meta[prop]) {
90
global.logError(baseErrorString + 'missing "' + prop + '" property in metadata.json');
261
logExtensionError(uuid, 'missing "' + prop + '" property in metadata.json');
95
if (extensions[meta.uuid] != undefined) {
96
global.logError(baseErrorString + "extension already loaded");
266
if (extensions[uuid] != undefined) {
267
logExtensionError(uuid, "extension already loaded");
100
271
// Encourage people to add this
101
272
if (!meta['url']) {
102
global.log(baseErrorString + 'Warning: Missing "url" property in metadata.json');
273
global.log('Warning: Missing "url" property in metadata.json');
105
let base = dir.get_basename();
106
if (base != meta.uuid) {
107
global.logError(baseErrorString + 'uuid "' + meta.uuid + '" from metadata.json does not match directory name "' + base + '"');
276
if (uuid != meta.uuid) {
277
logExtensionError(uuid, 'uuid "' + meta.uuid + '" from metadata.json does not match directory name "' + uuid + '"');
111
281
if (!versionCheck(meta['shell-version'], Config.PACKAGE_VERSION) ||
112
282
(meta['js-version'] && !versionCheck(meta['js-version'], Config.GJS_VERSION))) {
113
global.logError(baseErrorString + 'extension is not compatible with current GNOME Shell and/or GJS version');
283
logExtensionError(uuid, 'extension is not compatible with current GNOME Shell and/or GJS version');
117
extensionMeta[meta.uuid] = meta;
118
extensionMeta[meta.uuid].type = type;
119
extensionMeta[meta.uuid].path = dir.get_path();
121
extensionMeta[meta.uuid].state = ExtensionState.DISABLED;
287
extensionMeta[uuid] = meta;
289
meta.path = dir.get_path();
125
292
// Default to error, we set success as the last step
126
extensionMeta[meta.uuid].state = ExtensionState.ERROR;
293
meta.state = ExtensionState.ERROR;
295
if (!versionCheck(meta['shell-version'], Config.PACKAGE_VERSION) ||
296
(meta['js-version'] && !versionCheck(meta['js-version'], Config.GJS_VERSION))) {
297
logExtensionError(uuid, 'extension is not compatible with current GNOME Shell and/or GJS version', ExtensionState.OUT_OF_DATE);
298
meta.state = ExtensionState.OUT_OF_DATE;
128
302
let extensionJs = dir.get_child('extension.js');
129
303
if (!extensionJs.query_exists(null)) {
130
global.logError(baseErrorString + 'Missing extension.js');
304
logExtensionError(uuid, 'Missing extension.js');
133
307
let stylesheetPath = null;
139
313
theme.load_stylesheet(stylesheetFile.get_path());
141
global.logError(baseErrorString + 'Stylesheet parse error: ' + e);
315
logExtensionError(uuid, 'Stylesheet parse error: ' + e);
146
320
let extensionModule;
321
let extensionState = null;
148
323
global.add_extension_importer('imports.ui.extensionSystem.extensions', meta.uuid, dir.get_path());
149
324
extensionModule = extensions[meta.uuid].extension;
151
326
if (stylesheetPath != null)
152
327
theme.unload_stylesheet(stylesheetPath);
153
global.logError(baseErrorString + e);
156
if (!extensionModule.main) {
157
global.logError(baseErrorString + 'missing \'main\' function');
328
logExtensionError(uuid, e);
332
if (!extensionModule.init) {
333
logExtensionError(uuid, 'missing \'init\' function');
161
extensionModule.main(meta);
338
extensionState = extensionModule.init(meta);
163
340
if (stylesheetPath != null)
164
341
theme.unload_stylesheet(stylesheetPath);
165
global.logError(baseErrorString + 'Failed to evaluate main function:' + e);
168
extensionMeta[meta.uuid].state = ExtensionState.ENABLED;
342
logExtensionError(uuid, 'Failed to evaluate init function:' + e);
347
extensionState = extensionModule;
348
extensionStateObjs[uuid] = extensionState;
350
if (!extensionState.enable) {
351
logExtensionError(uuid, 'missing \'enable\' function');
354
if (!extensionState.disable) {
355
logExtensionError(uuid, 'missing \'disable\' function');
359
meta.state = ExtensionState.DISABLED;
362
enableExtension(uuid);
364
_signals.emit('extension-loaded', meta.uuid);
365
_signals.emit('extension-state-changed', meta);
169
366
global.log('Loaded extension ' + meta.uuid);
369
function onEnabledExtensionsChanged() {
370
let newEnabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
372
// Find and enable all the newly enabled extensions: UUIDs found in the
373
// new setting, but not in the old one.
374
newEnabledExtensions.filter(function(uuid) {
375
return enabledExtensions.indexOf(uuid) == -1;
376
}).forEach(function(uuid) {
377
enableExtension(uuid);
380
// Find and disable all the newly disabled extensions: UUIDs found in the
381
// old setting, but not in the new one.
382
enabledExtensions.filter(function(item) {
383
return newEnabledExtensions.indexOf(item) == -1;
384
}).forEach(function(uuid) {
385
disableExtension(uuid);
388
enabledExtensions = newEnabledExtensions;
172
391
function init() {
173
392
let userExtensionsPath = GLib.build_filenamev([global.userdatadir, 'extensions']);
174
393
userExtensionsDir = Gio.file_new_for_path(userExtensionsPath);
176
userExtensionsDir.make_directory_with_parents(null);
395
if (!userExtensionsDir.query_exists(null))
396
userExtensionsDir.make_directory_with_parents(null);
178
398
global.logError('' + e);
181
disabledExtensions = global.settings.get_strv('disabled-extensions', -1);
182
enabledExtensions = global.settings.get_strv('enabled-extensions', -1);
401
global.settings.connect('changed::' + ENABLED_EXTENSIONS_KEY, onEnabledExtensionsChanged);
402
enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
185
405
function _loadExtensionsIn(dir, type) {