c170d6c956
Currently extensions can only be locked down completely by restricting the `enabled-extensions` key via dconf. This is too restrictive for environments that want to allow users to customize their system with extensions, while still limiting the set of possible extensions. To fill that gap, add a new `allow-extension-installation` setting, which restricts extensions to system extensions when disabled. As the setting is mainly intended for locking down by system administrators, there is no attempt to load/unload extensions on settings changes. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3180>
852 lines
29 KiB
JavaScript
852 lines
29 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
import GLib from 'gi://GLib';
|
|
import Gio from 'gi://Gio';
|
|
import GObject from 'gi://GObject';
|
|
import St from 'gi://St';
|
|
import Shell from 'gi://Shell';
|
|
import * as Signals from '../misc/signals.js';
|
|
|
|
import * as Config from '../misc/config.js';
|
|
import * as ExtensionDownloader from './extensionDownloader.js';
|
|
import {formatError} from '../misc/errorUtils.js';
|
|
import {ExtensionState, ExtensionType} from '../misc/extensionUtils.js';
|
|
import * as FileUtils from '../misc/fileUtils.js';
|
|
import * as Main from './main.js';
|
|
import * as MessageTray from './messageTray.js';
|
|
|
|
const ENABLED_EXTENSIONS_KEY = 'enabled-extensions';
|
|
const DISABLED_EXTENSIONS_KEY = 'disabled-extensions';
|
|
const DISABLE_USER_EXTENSIONS_KEY = 'disable-user-extensions';
|
|
const EXTENSION_DISABLE_VERSION_CHECK_KEY = 'disable-extension-version-validation';
|
|
|
|
const UPDATE_CHECK_TIMEOUT = 24 * 60 * 60; // 1 day in seconds
|
|
|
|
function stateToString(state) {
|
|
return Object.keys(ExtensionState).find(k => ExtensionState[k] === state);
|
|
}
|
|
|
|
export class ExtensionManager extends Signals.EventEmitter {
|
|
constructor() {
|
|
super();
|
|
|
|
this._initializationPromise = null;
|
|
this._updateNotified = false;
|
|
this._updateInProgress = false;
|
|
this._updatedUUIDS = [];
|
|
|
|
this._extensions = new Map();
|
|
this._unloadedExtensions = new Map();
|
|
this._enabledExtensions = [];
|
|
this._extensionOrder = [];
|
|
this._checkVersion = false;
|
|
|
|
St.Settings.get().connect('notify::color-scheme',
|
|
() => this._reloadExtensionStylesheets());
|
|
|
|
Main.sessionMode.connect('updated', () => {
|
|
this._sessionUpdated();
|
|
});
|
|
}
|
|
|
|
init() {
|
|
// The following file should exist for a period of time when extensions
|
|
// are enabled after start. If it exists, then the systemd unit will
|
|
// disable extensions should gnome-shell crash.
|
|
// Should the file already exist from a previous login, then this is OK.
|
|
let disableFilename = GLib.build_filenamev([GLib.get_user_runtime_dir(), 'gnome-shell-disable-extensions']);
|
|
let disableFile = Gio.File.new_for_path(disableFilename);
|
|
try {
|
|
disableFile.create(Gio.FileCreateFlags.REPLACE_DESTINATION, null);
|
|
} catch (e) {
|
|
log(`Failed to create file ${disableFilename}: ${e.message}`);
|
|
}
|
|
|
|
const shutdownId = global.connect('shutdown',
|
|
() => disableFile.delete(null));
|
|
|
|
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 60, () => {
|
|
global.disconnect(shutdownId);
|
|
|
|
disableFile.delete(null);
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
|
|
this._installExtensionUpdates();
|
|
this._sessionUpdated().then(() => {
|
|
ExtensionDownloader.checkForUpdates();
|
|
});
|
|
|
|
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, UPDATE_CHECK_TIMEOUT, () => {
|
|
ExtensionDownloader.checkForUpdates();
|
|
|
|
return GLib.SOURCE_CONTINUE;
|
|
});
|
|
}
|
|
|
|
get updatesSupported() {
|
|
const appSys = Shell.AppSystem.get_default();
|
|
const hasUpdatesApp =
|
|
appSys.lookup_app('org.gnome.Extensions.desktop') !== null ||
|
|
appSys.lookup_app('com.mattjakeman.ExtensionManager.desktop') !== null;
|
|
const allowed = global.settings.get_boolean('allow-extension-installation');
|
|
return allowed && hasUpdatesApp;
|
|
}
|
|
|
|
lookup(uuid) {
|
|
return this._extensions.get(uuid);
|
|
}
|
|
|
|
getUuids() {
|
|
return [...this._extensions.keys()];
|
|
}
|
|
|
|
_reloadExtensionStylesheets() {
|
|
for (const ext of this._extensions.values()) {
|
|
// No stylesheet, nothing to reload
|
|
if (!ext.stylesheet)
|
|
continue;
|
|
|
|
// No variants, so skip reloading
|
|
const path = ext.stylesheet.get_path();
|
|
if (!path.endsWith('-dark.css') && !path.endsWith('-light.css'))
|
|
continue;
|
|
|
|
try {
|
|
this._unloadExtensionStylesheet(ext);
|
|
this._loadExtensionStylesheet(ext);
|
|
} catch (e) {
|
|
this._callExtensionDisable(ext.uuid);
|
|
this.logExtensionError(ext.uuid, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
_loadExtensionStylesheet(extension) {
|
|
if (extension.state !== ExtensionState.ACTIVE &&
|
|
extension.state !== ExtensionState.ACTIVATING)
|
|
return;
|
|
|
|
const variant = Main.getStyleVariant();
|
|
const stylesheetNames = [
|
|
`${global.sessionMode}-${variant}.css`,
|
|
`stylesheet-${variant}.css`,
|
|
`${global.sessionMode}.css`,
|
|
'stylesheet.css',
|
|
];
|
|
const theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
|
|
for (const name of stylesheetNames) {
|
|
try {
|
|
const stylesheetFile = extension.dir.get_child(name);
|
|
theme.load_stylesheet(stylesheetFile);
|
|
extension.stylesheet = stylesheetFile;
|
|
break;
|
|
} catch (e) {
|
|
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
|
|
continue; // not an error
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
_unloadExtensionStylesheet(extension) {
|
|
if (!extension.stylesheet)
|
|
return;
|
|
|
|
const theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
|
|
theme.unload_stylesheet(extension.stylesheet);
|
|
delete extension.stylesheet;
|
|
}
|
|
|
|
_changeExtensionState(extension, newState) {
|
|
const strState = stateToString(newState);
|
|
console.debug(`Changing state of extension ${extension.uuid} to ${strState}`);
|
|
|
|
extension.state = newState;
|
|
this.emit('extension-state-changed', extension);
|
|
}
|
|
|
|
_extensionSupportsSessionMode(uuid) {
|
|
const extension = this.lookup(uuid);
|
|
|
|
if (!extension)
|
|
return false;
|
|
|
|
if (extension.sessionModes.includes(Main.sessionMode.currentMode))
|
|
return true;
|
|
|
|
if (extension.sessionModes.includes(Main.sessionMode.parentMode))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
async _callExtensionDisable(uuid) {
|
|
let extension = this.lookup(uuid);
|
|
if (!extension)
|
|
return;
|
|
|
|
if (extension.state !== ExtensionState.ACTIVE)
|
|
return;
|
|
|
|
this._changeExtensionState(extension, ExtensionState.DEACTIVATING);
|
|
|
|
// "Rebase" the extension order by disabling and then enabling extensions
|
|
// in order to help prevent conflicts.
|
|
|
|
// Example:
|
|
// order = [A, B, C, D, E]
|
|
// user disables C
|
|
// this should: disable E, disable D, disable C, enable D, enable E
|
|
|
|
let orderIdx = this._extensionOrder.indexOf(uuid);
|
|
let order = this._extensionOrder.slice(orderIdx + 1);
|
|
let orderReversed = order.slice().reverse();
|
|
|
|
for (let i = 0; i < orderReversed.length; i++) {
|
|
let otherUuid = orderReversed[i];
|
|
try {
|
|
console.debug(`Temporarily disable extension ${otherUuid}`);
|
|
this.lookup(otherUuid).stateObj.disable();
|
|
} catch (e) {
|
|
this.logExtensionError(otherUuid, e);
|
|
}
|
|
}
|
|
|
|
try {
|
|
extension.stateObj.disable();
|
|
} catch (e) {
|
|
this.logExtensionError(uuid, e);
|
|
}
|
|
|
|
this._unloadExtensionStylesheet(extension);
|
|
|
|
for (let i = 0; i < order.length; i++) {
|
|
let otherUuid = order[i];
|
|
try {
|
|
console.debug(`Re-enable extension ${otherUuid}`);
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.lookup(otherUuid).stateObj.enable();
|
|
} catch (e) {
|
|
this.logExtensionError(otherUuid, e);
|
|
}
|
|
}
|
|
|
|
this._extensionOrder.splice(orderIdx, 1);
|
|
|
|
if (extension.state !== ExtensionState.ERROR)
|
|
this._changeExtensionState(extension, ExtensionState.INACTIVE);
|
|
}
|
|
|
|
async _callExtensionEnable(uuid) {
|
|
if (!this._extensionSupportsSessionMode(uuid))
|
|
return;
|
|
|
|
let extension = this.lookup(uuid);
|
|
if (!extension)
|
|
return;
|
|
|
|
if (extension.state === ExtensionState.INITIALIZED)
|
|
await this._callExtensionInit(uuid);
|
|
|
|
|
|
if (extension.state !== ExtensionState.INACTIVE)
|
|
return;
|
|
|
|
this._changeExtensionState(extension, ExtensionState.ACTIVATING);
|
|
|
|
try {
|
|
this._loadExtensionStylesheet(extension);
|
|
} catch (e) {
|
|
this.logExtensionError(uuid, e);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await extension.stateObj.enable();
|
|
this._changeExtensionState(extension, ExtensionState.ACTIVE);
|
|
this._extensionOrder.push(uuid);
|
|
} catch (e) {
|
|
this._unloadExtensionStylesheet(extension);
|
|
this.logExtensionError(uuid, e);
|
|
}
|
|
}
|
|
|
|
enableExtension(uuid) {
|
|
if (!this._extensions.has(uuid))
|
|
return false;
|
|
|
|
let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
|
|
let disabledExtensions = global.settings.get_strv(DISABLED_EXTENSIONS_KEY);
|
|
|
|
if (disabledExtensions.includes(uuid)) {
|
|
disabledExtensions = disabledExtensions.filter(item => item !== uuid);
|
|
global.settings.set_strv(DISABLED_EXTENSIONS_KEY, disabledExtensions);
|
|
}
|
|
|
|
if (!enabledExtensions.includes(uuid)) {
|
|
enabledExtensions.push(uuid);
|
|
global.settings.set_strv(ENABLED_EXTENSIONS_KEY, enabledExtensions);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
disableExtension(uuid) {
|
|
if (!this._extensions.has(uuid))
|
|
return false;
|
|
|
|
let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
|
|
let disabledExtensions = global.settings.get_strv(DISABLED_EXTENSIONS_KEY);
|
|
|
|
if (enabledExtensions.includes(uuid)) {
|
|
enabledExtensions = enabledExtensions.filter(item => item !== uuid);
|
|
global.settings.set_strv(ENABLED_EXTENSIONS_KEY, enabledExtensions);
|
|
}
|
|
|
|
if (!disabledExtensions.includes(uuid)) {
|
|
disabledExtensions.push(uuid);
|
|
global.settings.set_strv(DISABLED_EXTENSIONS_KEY, disabledExtensions);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
openExtensionPrefs(uuid, parentWindow, options) {
|
|
const extension = this.lookup(uuid);
|
|
if (!extension || !extension.hasPrefs)
|
|
return false;
|
|
|
|
Gio.DBus.session.call(
|
|
'org.gnome.Shell.Extensions',
|
|
'/org/gnome/Shell/Extensions',
|
|
'org.gnome.Shell.Extensions',
|
|
'OpenExtensionPrefs',
|
|
new GLib.Variant('(ssa{sv})', [uuid, parentWindow, options]),
|
|
null,
|
|
Gio.DBusCallFlags.NONE,
|
|
-1,
|
|
null);
|
|
return true;
|
|
}
|
|
|
|
notifyExtensionUpdate(uuid) {
|
|
if (this._updateInProgress) {
|
|
this._updatedUUIDS.push(uuid);
|
|
return;
|
|
}
|
|
|
|
let extension = this.lookup(uuid);
|
|
if (!extension)
|
|
return;
|
|
|
|
extension.hasUpdate = true;
|
|
this.emit('extension-state-changed', extension);
|
|
|
|
if (!this._updateNotified) {
|
|
this._updateNotified = true;
|
|
|
|
let source = new ExtensionUpdateSource();
|
|
Main.messageTray.add(source);
|
|
|
|
let notification = new MessageTray.Notification(source,
|
|
_('Extension Updates Available'),
|
|
_('Extension updates are ready to be installed.'));
|
|
notification.connect('activated',
|
|
() => source.open());
|
|
source.showNotification(notification);
|
|
}
|
|
}
|
|
|
|
logExtensionError(uuid, error) {
|
|
let extension = this.lookup(uuid);
|
|
if (!extension)
|
|
return;
|
|
|
|
const message = formatError(error, {showStack: false});
|
|
|
|
console.debug(`Changing state of extension ${uuid} to ERROR`);
|
|
extension.error = message;
|
|
extension.state = ExtensionState.ERROR;
|
|
if (!extension.errors)
|
|
extension.errors = [];
|
|
extension.errors.push(message);
|
|
|
|
logError(error, `Extension ${uuid}`);
|
|
this.emit('extension-state-changed', extension);
|
|
}
|
|
|
|
createExtensionObject(uuid, dir, type) {
|
|
let metadataFile = dir.get_child('metadata.json');
|
|
if (!metadataFile.query_exists(null))
|
|
throw new Error('Missing metadata.json');
|
|
|
|
let metadataContents, success_;
|
|
try {
|
|
[success_, metadataContents] = metadataFile.load_contents(null);
|
|
metadataContents = new TextDecoder().decode(metadataContents);
|
|
} catch (e) {
|
|
throw new Error(`Failed to load metadata.json: ${e}`);
|
|
}
|
|
let meta;
|
|
try {
|
|
meta = JSON.parse(metadataContents);
|
|
} catch (e) {
|
|
throw new Error(`Failed to parse metadata.json: ${e}`);
|
|
}
|
|
|
|
const requiredProperties = [{
|
|
prop: 'uuid',
|
|
typeName: 'string',
|
|
}, {
|
|
prop: 'name',
|
|
typeName: 'string',
|
|
}, {
|
|
prop: 'description',
|
|
typeName: 'string',
|
|
}, {
|
|
prop: 'shell-version',
|
|
typeName: 'string array',
|
|
typeCheck: v => Array.isArray(v) && v.length > 0 && v.every(e => typeof e === 'string'),
|
|
}];
|
|
for (let i = 0; i < requiredProperties.length; i++) {
|
|
const {
|
|
prop, typeName, typeCheck = v => typeof v === typeName,
|
|
} = requiredProperties[i];
|
|
|
|
if (!meta[prop])
|
|
throw new Error(`missing "${prop}" property in metadata.json`);
|
|
if (!typeCheck(meta[prop]))
|
|
throw new Error(`property "${prop}" is not of type ${typeName}`);
|
|
}
|
|
|
|
if (uuid !== meta.uuid)
|
|
throw new Error(`uuid "${meta.uuid}" from metadata.json does not match directory name "${uuid}"`);
|
|
|
|
let extension = {
|
|
metadata: meta,
|
|
uuid: meta.uuid,
|
|
type,
|
|
dir,
|
|
path: dir.get_path(),
|
|
error: '',
|
|
hasPrefs: dir.get_child('prefs.js').query_exists(null),
|
|
enabled: this._enabledExtensions.includes(uuid),
|
|
hasUpdate: false,
|
|
canChange: false,
|
|
sessionModes: meta['session-modes'] ? meta['session-modes'] : ['user'],
|
|
};
|
|
this._extensions.set(uuid, extension);
|
|
|
|
return extension;
|
|
}
|
|
|
|
_canLoad(extension) {
|
|
if (!this._unloadedExtensions.has(extension.uuid))
|
|
return true;
|
|
|
|
const version = this._unloadedExtensions.get(extension.uuid);
|
|
return extension.metadata.version === version;
|
|
}
|
|
|
|
_isOutOfDate(extension) {
|
|
const [major] = Config.PACKAGE_VERSION.split('.');
|
|
return !extension.metadata['shell-version'].some(v => v.startsWith(major));
|
|
}
|
|
|
|
async loadExtension(extension) {
|
|
const {uuid} = extension;
|
|
console.debug(`Loading extension ${uuid}`);
|
|
// Default to error, we set success as the last step
|
|
extension.state = ExtensionState.ERROR;
|
|
|
|
if (this._checkVersion && this._isOutOfDate(extension)) {
|
|
extension.state = ExtensionState.OUT_OF_DATE;
|
|
} else if (!this._canLoad(extension)) {
|
|
this.logExtensionError(uuid, new Error(
|
|
'A different version was loaded previously. You need to log out for changes to take effect.'));
|
|
} else {
|
|
const enabled = this._enabledExtensions.includes(uuid) &&
|
|
this._extensionSupportsSessionMode(uuid);
|
|
if (enabled) {
|
|
if (!await this._callExtensionInit(uuid))
|
|
return;
|
|
|
|
if (extension.state === ExtensionState.INACTIVE)
|
|
await this._callExtensionEnable(uuid);
|
|
} else {
|
|
extension.state = ExtensionState.INITIALIZED;
|
|
}
|
|
|
|
this._unloadedExtensions.delete(uuid);
|
|
}
|
|
|
|
console.debug(`Extension ${uuid} in state ${stateToString(extension.state)} after loading`);
|
|
this._updateCanChange(extension);
|
|
this.emit('extension-state-changed', extension);
|
|
}
|
|
|
|
async unloadExtension(extension) {
|
|
const {uuid, type} = extension;
|
|
|
|
// Try to disable it -- if it's ERROR'd, we can't guarantee that,
|
|
// but it will be removed on next reboot, and hopefully nothing
|
|
// broke too much.
|
|
await this._callExtensionDisable(uuid);
|
|
|
|
this._changeExtensionState(extension, ExtensionState.UNINSTALLED);
|
|
|
|
// The extension is now cached and it's impossible to load a different version
|
|
if (type === ExtensionType.PER_USER && extension.isImported)
|
|
this._unloadedExtensions.set(uuid, extension.metadata.version);
|
|
|
|
this._extensions.delete(uuid);
|
|
return true;
|
|
}
|
|
|
|
async reloadExtension(oldExtension) {
|
|
// Grab the things we'll need to pass to createExtensionObject
|
|
// to reload it.
|
|
let {uuid, dir, type} = oldExtension;
|
|
|
|
// Then unload the old extension.
|
|
await this.unloadExtension(oldExtension);
|
|
|
|
// Now, recreate the extension and load it.
|
|
let newExtension;
|
|
try {
|
|
newExtension = this.createExtensionObject(uuid, dir, type);
|
|
} catch (e) {
|
|
this.logExtensionError(uuid, e);
|
|
return;
|
|
}
|
|
|
|
await this.loadExtension(newExtension);
|
|
}
|
|
|
|
async _callExtensionInit(uuid) {
|
|
if (!this._extensionSupportsSessionMode(uuid))
|
|
return false;
|
|
|
|
let extension = this.lookup(uuid);
|
|
if (!extension)
|
|
throw new Error('Extension was not properly created. Call createExtensionObject first');
|
|
|
|
let dir = extension.dir;
|
|
let extensionJs = dir.get_child('extension.js');
|
|
if (!extensionJs.query_exists(null)) {
|
|
this.logExtensionError(uuid, new Error('Missing extension.js'));
|
|
return false;
|
|
}
|
|
|
|
let extensionModule;
|
|
let extensionState = null;
|
|
|
|
try {
|
|
extensionModule = await import(extensionJs.get_uri());
|
|
|
|
// Extensions can only be imported once, so add a property to avoid
|
|
// attempting to re-import an extension.
|
|
extension.isImported = true;
|
|
} catch (e) {
|
|
this.logExtensionError(uuid, e);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const {metadata, path} = extension;
|
|
extensionState =
|
|
new extensionModule.default({...metadata, dir, path});
|
|
} catch (e) {
|
|
this.logExtensionError(uuid, e);
|
|
return false;
|
|
}
|
|
|
|
extension.stateObj = extensionState;
|
|
this._changeExtensionState(extension, ExtensionState.INACTIVE);
|
|
return true;
|
|
}
|
|
|
|
_getModeExtensions() {
|
|
if (Array.isArray(Main.sessionMode.enabledExtensions))
|
|
return Main.sessionMode.enabledExtensions;
|
|
return [];
|
|
}
|
|
|
|
_updateCanChange(extension) {
|
|
let isMode = this._getModeExtensions().includes(extension.uuid);
|
|
let modeOnly = global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY);
|
|
|
|
let changeKey = isMode
|
|
? DISABLE_USER_EXTENSIONS_KEY
|
|
: ENABLED_EXTENSIONS_KEY;
|
|
|
|
extension.canChange =
|
|
global.settings.is_writable(changeKey) &&
|
|
(isMode || !modeOnly);
|
|
}
|
|
|
|
_getEnabledExtensions() {
|
|
let extensions = this._getModeExtensions();
|
|
|
|
if (!global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY))
|
|
extensions = extensions.concat(global.settings.get_strv(ENABLED_EXTENSIONS_KEY));
|
|
|
|
extensions.sort((a, b) => this._compareExtensions(this.lookup(a), this.lookup(b)));
|
|
|
|
// filter out 'disabled-extensions' which takes precedence
|
|
let disabledExtensions = global.settings.get_strv(DISABLED_EXTENSIONS_KEY);
|
|
return extensions.filter(item => !disabledExtensions.includes(item));
|
|
}
|
|
|
|
async _onUserExtensionsEnabledChanged() {
|
|
await this._onEnabledExtensionsChanged();
|
|
this._onSettingsWritableChanged();
|
|
}
|
|
|
|
async _onEnabledExtensionsChanged() {
|
|
let newEnabledExtensions = this._getEnabledExtensions();
|
|
|
|
for (const extension of this._extensions.values()) {
|
|
const wasEnabled = extension.enabled;
|
|
extension.enabled = newEnabledExtensions.includes(extension.uuid);
|
|
if (wasEnabled !== extension.enabled)
|
|
this.emit('extension-state-changed', extension);
|
|
}
|
|
|
|
// Find and enable all the newly enabled extensions: UUIDs found in the
|
|
// new setting, but not in the old one.
|
|
const extensionsToEnable = newEnabledExtensions
|
|
.filter(uuid => !this._enabledExtensions.includes(uuid) &&
|
|
this._extensionSupportsSessionMode(uuid));
|
|
for (const uuid of extensionsToEnable) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this._callExtensionEnable(uuid);
|
|
}
|
|
|
|
// Find and disable all the newly disabled extensions: UUIDs found in the
|
|
// old setting, but not in the new one.
|
|
const extensionsToDisable = this._extensionOrder
|
|
.filter(uuid => !newEnabledExtensions.includes(uuid) ||
|
|
!this._extensionSupportsSessionMode(uuid));
|
|
// Reverse mutates the original array, but .filter() creates a new array.
|
|
extensionsToDisable.reverse();
|
|
|
|
for (const uuid of extensionsToDisable) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this._callExtensionDisable(uuid);
|
|
}
|
|
|
|
this._enabledExtensions = newEnabledExtensions;
|
|
}
|
|
|
|
_onSettingsWritableChanged() {
|
|
for (let extension of this._extensions.values()) {
|
|
this._updateCanChange(extension);
|
|
this.emit('extension-state-changed', extension);
|
|
}
|
|
}
|
|
|
|
async _onVersionValidationChanged() {
|
|
const checkVersion = !global.settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY);
|
|
if (checkVersion === this._checkVersion)
|
|
return;
|
|
|
|
this._checkVersion = checkVersion;
|
|
|
|
// Disabling extensions modifies the order array, so use a copy
|
|
let extensionOrder = this._extensionOrder.slice();
|
|
|
|
// Disable enabled extensions first to avoid
|
|
// the "rebasing" done in _callExtensionDisable...
|
|
this._disableAllExtensions();
|
|
|
|
// ...and then reload and enable extensions in the correct order again.
|
|
const extensionsToReload = [...this._extensions.values()].sort((a, b) => {
|
|
return extensionOrder.indexOf(a.uuid) - extensionOrder.indexOf(b.uuid);
|
|
});
|
|
for (const extension of extensionsToReload) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.reloadExtension(extension);
|
|
}
|
|
}
|
|
|
|
async _handleMajorUpdate() {
|
|
const [majorVersion] = Config.PACKAGE_VERSION.split('.');
|
|
const path = `${global.userdatadir}/update-check-${majorVersion}`;
|
|
const file = Gio.File.new_for_path(path);
|
|
|
|
try {
|
|
if (!await file.touch_async())
|
|
return;
|
|
} catch (e) {
|
|
logError(e);
|
|
}
|
|
|
|
this._updateInProgress = true;
|
|
|
|
await ExtensionDownloader.checkForUpdates();
|
|
this._installExtensionUpdates();
|
|
|
|
this._updatedUUIDS.map(uuid => this.lookup(uuid)).forEach(
|
|
ext => this.reloadExtension(ext));
|
|
this._updatedUUIDS = [];
|
|
|
|
this._updateInProgress = false;
|
|
}
|
|
|
|
_installExtensionUpdates() {
|
|
if (!this.updatesSupported)
|
|
return;
|
|
|
|
for (const {dir, info} of FileUtils.collectFromDatadirs('extension-updates', true)) {
|
|
let fileType = info.get_file_type();
|
|
if (fileType !== Gio.FileType.DIRECTORY)
|
|
continue;
|
|
let uuid = info.get_name();
|
|
let extensionDir = Gio.File.new_for_path(
|
|
GLib.build_filenamev([global.userdatadir, 'extensions', uuid]));
|
|
|
|
try {
|
|
FileUtils.recursivelyDeleteDir(extensionDir, false);
|
|
FileUtils.recursivelyMoveDir(dir, extensionDir);
|
|
} catch (e) {
|
|
log(`Failed to install extension updates for ${uuid}`);
|
|
} finally {
|
|
FileUtils.recursivelyDeleteDir(dir, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
_compareExtensions(a, b) {
|
|
const modesA = a?.sessionModes ?? [];
|
|
const modesB = b?.sessionModes ?? [];
|
|
return modesB.length - modesA.length;
|
|
}
|
|
|
|
async _loadExtensions() {
|
|
global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, () => {
|
|
this._onEnabledExtensionsChanged();
|
|
});
|
|
global.settings.connect(`changed::${DISABLED_EXTENSIONS_KEY}`, () => {
|
|
this._onEnabledExtensionsChanged();
|
|
});
|
|
global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`, () => {
|
|
this._onUserExtensionsEnabledChanged();
|
|
});
|
|
global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`, () => {
|
|
this._onVersionValidationChanged();
|
|
});
|
|
global.settings.connect(`writable-changed::${ENABLED_EXTENSIONS_KEY}`, () =>
|
|
this._onSettingsWritableChanged());
|
|
global.settings.connect(`writable-changed::${DISABLED_EXTENSIONS_KEY}`, () =>
|
|
this._onSettingsWritableChanged());
|
|
|
|
await this._onVersionValidationChanged();
|
|
|
|
this._enabledExtensions = this._getEnabledExtensions();
|
|
|
|
let perUserDir = Gio.File.new_for_path(global.userdatadir);
|
|
|
|
const includeUserDir = global.settings.get_boolean('allow-extension-installation');
|
|
const extensionFiles = [...FileUtils.collectFromDatadirs('extensions', includeUserDir)];
|
|
const extensionObjects = extensionFiles.map(({dir, info}) => {
|
|
let fileType = info.get_file_type();
|
|
if (fileType !== Gio.FileType.DIRECTORY)
|
|
return null;
|
|
let uuid = info.get_name();
|
|
let existing = this.lookup(uuid);
|
|
if (existing) {
|
|
log(`Extension ${uuid} already installed in ${existing.path}. ${dir.get_path()} will not be loaded`);
|
|
return null;
|
|
}
|
|
|
|
let extension;
|
|
let type = dir.has_prefix(perUserDir)
|
|
? ExtensionType.PER_USER
|
|
: ExtensionType.SYSTEM;
|
|
try {
|
|
extension = this.createExtensionObject(uuid, dir, type);
|
|
} catch (error) {
|
|
logError(error, `Could not load extension ${uuid}`);
|
|
return null;
|
|
}
|
|
|
|
return extension;
|
|
}).filter(extension => extension !== null).sort(this._compareExtensions.bind(this));
|
|
|
|
// after updating to a new major version,
|
|
// update extensions before loading them
|
|
await this._handleMajorUpdate();
|
|
|
|
for (const extension of extensionObjects) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.loadExtension(extension);
|
|
}
|
|
}
|
|
|
|
async _enableAllExtensions() {
|
|
if (!this._initializationPromise)
|
|
this._initializationPromise = this._loadExtensions();
|
|
|
|
await this._initializationPromise;
|
|
|
|
for (const uuid of this._enabledExtensions) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this._callExtensionEnable(uuid);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Disables all currently enabled extensions.
|
|
*/
|
|
async _disableAllExtensions() {
|
|
// Wait for extensions to finish loading before starting
|
|
// to disable, otherwise some extensions may enable after
|
|
// this function.
|
|
if (this._initializationPromise)
|
|
await this._initializationPromise;
|
|
|
|
const extensionsToDisable = this._extensionOrder.slice();
|
|
// Extensions are disabled in the reverse order
|
|
// from when they were enabled.
|
|
extensionsToDisable.reverse();
|
|
|
|
for (const uuid of extensionsToDisable) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this._callExtensionDisable(uuid);
|
|
}
|
|
}
|
|
|
|
async _sessionUpdated() {
|
|
// Take care of added or removed sessionMode extensions
|
|
await this._onEnabledExtensionsChanged();
|
|
await this._enableAllExtensions();
|
|
}
|
|
}
|
|
|
|
const ExtensionUpdateSource = GObject.registerClass(
|
|
class ExtensionUpdateSource extends MessageTray.Source {
|
|
constructor() {
|
|
const appSys = Shell.AppSystem.get_default();
|
|
const app =
|
|
appSys.lookup_app('org.gnome.Extensions.desktop') ||
|
|
appSys.lookup_app('com.mattjakeman.ExtensionManager.desktop');
|
|
|
|
super({
|
|
title: app.get_name(),
|
|
icon: app.get_icon(),
|
|
policy: MessageTray.NotificationPolicy.newForApp(app),
|
|
});
|
|
|
|
this._app = app;
|
|
}
|
|
|
|
open() {
|
|
this._app.activate();
|
|
Main.overview.hide();
|
|
Main.panel.closeCalendar();
|
|
}
|
|
});
|