dbusServices/extensions: Proxy Extensions API
Similar to the previously added org.freedesktop.Notifications proxy,
this exposes the org.gnome.Shell.Extensions API and forwards any
request to the real implementation in gnome-shell.
The motivation differs though: We want to be able to package the
extension app as flatpak and distribute it separately, but the
extension prefs dialog is hard to impossible to sandbox:
- filenames need translating between host and sandbox, and we
can only do that in some cases (serializing/deserializing
extensions), but not others (extension settings that refer
to files)
- system extensions install their GSettings schemas in the system
path; the best we can do there is assume a host prefix of /usr
and set GSETTINGS_SCHEMA_DIR in the flatpak (eeks)
- extensions may rely on additional typelibs that are present on
the host (for example because gnome-shell itself depends on
them), but not inside the sandbox - unless we bundle all of
gnome-shell's dependencies
- if gjs/mozjs differ between host and sandbox, extensions must
handle different runtimes for the extension and its prefs
And all those issues occur despite a very permissive sandbox (full
host filesystem access, full dconf access, full org.gnome.Shell
access (including Eval()!)).
This new service will give us an alternative place for handling
the preference dialog:
- it runs outside of gnome-shell process, so can open windows
- it runs on the host, so the extension's prefs get to run
in the same namespace as the extension itself
That is, the service will provide portal-like functionality (albeit
not using the org.freedesktop.portal.* namespace, as extension
management is an inherently privileged operation).
https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1106
2020-03-04 03:02:28 +00:00
|
|
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
/* exported ExtensionsService */
|
|
|
|
|
2020-03-05 16:19:38 +00:00
|
|
|
const { Gdk, Gio, GLib, GObject, Gtk, Shew } = imports.gi;
|
2020-03-04 04:05:08 +00:00
|
|
|
|
|
|
|
const ExtensionUtils = imports.misc.extensionUtils;
|
dbusServices/extensions: Proxy Extensions API
Similar to the previously added org.freedesktop.Notifications proxy,
this exposes the org.gnome.Shell.Extensions API and forwards any
request to the real implementation in gnome-shell.
The motivation differs though: We want to be able to package the
extension app as flatpak and distribute it separately, but the
extension prefs dialog is hard to impossible to sandbox:
- filenames need translating between host and sandbox, and we
can only do that in some cases (serializing/deserializing
extensions), but not others (extension settings that refer
to files)
- system extensions install their GSettings schemas in the system
path; the best we can do there is assume a host prefix of /usr
and set GSETTINGS_SCHEMA_DIR in the flatpak (eeks)
- extensions may rely on additional typelibs that are present on
the host (for example because gnome-shell itself depends on
them), but not inside the sandbox - unless we bundle all of
gnome-shell's dependencies
- if gjs/mozjs differ between host and sandbox, extensions must
handle different runtimes for the extension and its prefs
And all those issues occur despite a very permissive sandbox (full
host filesystem access, full dconf access, full org.gnome.Shell
access (including Eval()!)).
This new service will give us an alternative place for handling
the preference dialog:
- it runs outside of gnome-shell process, so can open windows
- it runs on the host, so the extension's prefs get to run
in the same namespace as the extension itself
That is, the service will provide portal-like functionality (albeit
not using the org.freedesktop.portal.* namespace, as extension
management is an inherently privileged operation).
https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1106
2020-03-04 03:02:28 +00:00
|
|
|
|
|
|
|
const { loadInterfaceXML } = imports.misc.fileUtils;
|
|
|
|
const { ServiceImplementation } = imports.dbusService;
|
|
|
|
|
|
|
|
const ExtensionsIface = loadInterfaceXML('org.gnome.Shell.Extensions');
|
|
|
|
const ExtensionsProxy = Gio.DBusProxy.makeProxyWrapper(ExtensionsIface);
|
|
|
|
|
|
|
|
var ExtensionsService = class extends ServiceImplementation {
|
|
|
|
constructor() {
|
|
|
|
super(ExtensionsIface, '/org/gnome/Shell/Extensions');
|
|
|
|
|
|
|
|
this._proxy = new ExtensionsProxy(Gio.DBus.session,
|
|
|
|
'org.gnome.Shell', '/org/gnome/Shell');
|
|
|
|
|
|
|
|
this._proxy.connectSignal('ExtensionStateChanged',
|
|
|
|
(proxy, sender, params) => {
|
|
|
|
this._dbusImpl.emit_signal('ExtensionStateChanged',
|
|
|
|
new GLib.Variant('(sa{sv})', params));
|
|
|
|
});
|
|
|
|
|
|
|
|
this._proxy.connect('g-properties-changed', () => {
|
|
|
|
this._dbusImpl.emit_property_changed('UserExtensionsEnabled',
|
|
|
|
new GLib.Variant('b', this._proxy.UserExtensionsEnabled));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
get ShellVersion() {
|
|
|
|
return this._proxy.ShellVersion;
|
|
|
|
}
|
|
|
|
|
|
|
|
get UserExtensionsEnabled() {
|
|
|
|
return this._proxy.UserExtensionsEnabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
set UserExtensionsEnabled(enable) {
|
|
|
|
this._proxy.UserExtensionsEnabled = enable;
|
|
|
|
}
|
|
|
|
|
|
|
|
ListExtensionsAsync(params, invocation) {
|
|
|
|
this._proxy.ListExtensionsRemote(...params, (res, error) => {
|
|
|
|
if (this._handleError(invocation, error))
|
|
|
|
return;
|
|
|
|
|
|
|
|
invocation.return_value(new GLib.Variant('(a{sa{sv}})', res));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
GetExtensionInfoAsync(params, invocation) {
|
|
|
|
this._proxy.GetExtensionInfoRemote(...params, (res, error) => {
|
|
|
|
if (this._handleError(invocation, error))
|
|
|
|
return;
|
|
|
|
|
|
|
|
invocation.return_value(new GLib.Variant('(a{sv})', res));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
GetExtensionErrorsAsync(params, invocation) {
|
|
|
|
this._proxy.GetExtensionErrorsRemote(...params, (res, error) => {
|
|
|
|
if (this._handleError(invocation, error))
|
|
|
|
return;
|
|
|
|
|
|
|
|
invocation.return_value(new GLib.Variant('(as)', res));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
InstallRemoteExtensionAsync(params, invocation) {
|
|
|
|
this._proxy.InstallRemoteExtensionRemote(...params, (res, error) => {
|
|
|
|
if (this._handleError(invocation, error))
|
|
|
|
return;
|
|
|
|
|
|
|
|
invocation.return_value(new GLib.Variant('(s)', res));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
UninstallExtensionAsync(params, invocation) {
|
|
|
|
this._proxy.UninstallExtensionRemote(...params, (res, error) => {
|
|
|
|
if (this._handleError(invocation, error))
|
|
|
|
return;
|
|
|
|
|
|
|
|
invocation.return_value(new GLib.Variant('(b)', res));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
EnableExtensionAsync(params, invocation) {
|
|
|
|
this._proxy.EnableExtensionRemote(...params, (res, error) => {
|
|
|
|
if (this._handleError(invocation, error))
|
|
|
|
return;
|
|
|
|
|
|
|
|
invocation.return_value(new GLib.Variant('(b)', res));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
DisableExtensionAsync(params, invocation) {
|
|
|
|
this._proxy.DisableExtensionRemote(...params, (res, error) => {
|
|
|
|
if (this._handleError(invocation, error))
|
|
|
|
return;
|
|
|
|
|
|
|
|
invocation.return_value(new GLib.Variant('(b)', res));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
LaunchExtensionPrefsAsync([uuid], invocation) {
|
|
|
|
this.OpenExtensionPrefsAsync([uuid, '', {}], invocation);
|
|
|
|
}
|
|
|
|
|
|
|
|
OpenExtensionPrefsAsync(params, invocation) {
|
2020-03-05 16:19:38 +00:00
|
|
|
const [uuid, parentWindow, options] = params;
|
2020-03-04 04:05:08 +00:00
|
|
|
|
|
|
|
this._proxy.GetExtensionInfoRemote(uuid, (res, error) => {
|
dbusServices/extensions: Proxy Extensions API
Similar to the previously added org.freedesktop.Notifications proxy,
this exposes the org.gnome.Shell.Extensions API and forwards any
request to the real implementation in gnome-shell.
The motivation differs though: We want to be able to package the
extension app as flatpak and distribute it separately, but the
extension prefs dialog is hard to impossible to sandbox:
- filenames need translating between host and sandbox, and we
can only do that in some cases (serializing/deserializing
extensions), but not others (extension settings that refer
to files)
- system extensions install their GSettings schemas in the system
path; the best we can do there is assume a host prefix of /usr
and set GSETTINGS_SCHEMA_DIR in the flatpak (eeks)
- extensions may rely on additional typelibs that are present on
the host (for example because gnome-shell itself depends on
them), but not inside the sandbox - unless we bundle all of
gnome-shell's dependencies
- if gjs/mozjs differ between host and sandbox, extensions must
handle different runtimes for the extension and its prefs
And all those issues occur despite a very permissive sandbox (full
host filesystem access, full dconf access, full org.gnome.Shell
access (including Eval()!)).
This new service will give us an alternative place for handling
the preference dialog:
- it runs outside of gnome-shell process, so can open windows
- it runs on the host, so the extension's prefs get to run
in the same namespace as the extension itself
That is, the service will provide portal-like functionality (albeit
not using the org.freedesktop.portal.* namespace, as extension
management is an inherently privileged operation).
https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1106
2020-03-04 03:02:28 +00:00
|
|
|
if (this._handleError(invocation, error))
|
|
|
|
return;
|
|
|
|
|
2020-03-04 04:05:08 +00:00
|
|
|
const [serialized] = res;
|
|
|
|
const extension = ExtensionUtils.deserializeExtension(serialized);
|
|
|
|
|
|
|
|
const window = new ExtensionPrefsDialog(extension);
|
2021-01-16 00:43:53 +00:00
|
|
|
window.connect('realize', () => {
|
|
|
|
let externalWindow = null;
|
2020-03-05 16:19:38 +00:00
|
|
|
|
2021-01-16 00:43:53 +00:00
|
|
|
if (parentWindow)
|
|
|
|
externalWindow = Shew.ExternalWindow.new_from_handle(parentWindow);
|
2020-03-05 16:19:38 +00:00
|
|
|
|
2021-01-16 00:43:53 +00:00
|
|
|
if (externalWindow)
|
|
|
|
externalWindow.set_parent_of(window.get_surface());
|
|
|
|
});
|
2020-03-04 04:05:08 +00:00
|
|
|
|
|
|
|
if (options.modal)
|
|
|
|
window.modal = options.modal.get_boolean();
|
|
|
|
|
|
|
|
window.connect('destroy', () => this.release());
|
|
|
|
this.hold();
|
|
|
|
|
|
|
|
window.show();
|
|
|
|
|
dbusServices/extensions: Proxy Extensions API
Similar to the previously added org.freedesktop.Notifications proxy,
this exposes the org.gnome.Shell.Extensions API and forwards any
request to the real implementation in gnome-shell.
The motivation differs though: We want to be able to package the
extension app as flatpak and distribute it separately, but the
extension prefs dialog is hard to impossible to sandbox:
- filenames need translating between host and sandbox, and we
can only do that in some cases (serializing/deserializing
extensions), but not others (extension settings that refer
to files)
- system extensions install their GSettings schemas in the system
path; the best we can do there is assume a host prefix of /usr
and set GSETTINGS_SCHEMA_DIR in the flatpak (eeks)
- extensions may rely on additional typelibs that are present on
the host (for example because gnome-shell itself depends on
them), but not inside the sandbox - unless we bundle all of
gnome-shell's dependencies
- if gjs/mozjs differ between host and sandbox, extensions must
handle different runtimes for the extension and its prefs
And all those issues occur despite a very permissive sandbox (full
host filesystem access, full dconf access, full org.gnome.Shell
access (including Eval()!)).
This new service will give us an alternative place for handling
the preference dialog:
- it runs outside of gnome-shell process, so can open windows
- it runs on the host, so the extension's prefs get to run
in the same namespace as the extension itself
That is, the service will provide portal-like functionality (albeit
not using the org.freedesktop.portal.* namespace, as extension
management is an inherently privileged operation).
https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1106
2020-03-04 03:02:28 +00:00
|
|
|
invocation.return_value(null);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
CheckForUpdatesAsync(params, invocation) {
|
|
|
|
this._proxy.CheckForUpdatesRemote(...params, (res, error) => {
|
|
|
|
if (this._handleError(invocation, error))
|
|
|
|
return;
|
|
|
|
|
|
|
|
invocation.return_value(null);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
2020-03-04 04:05:08 +00:00
|
|
|
|
|
|
|
var ExtensionPrefsDialog = GObject.registerClass({
|
|
|
|
GTypeName: 'ExtensionPrefsDialog',
|
|
|
|
Template: 'resource:///org/gnome/Shell/Extensions/ui/extension-prefs-dialog.ui',
|
|
|
|
InternalChildren: [
|
|
|
|
'stack',
|
|
|
|
'expander',
|
|
|
|
'expanderArrow',
|
|
|
|
'revealer',
|
|
|
|
'errorView',
|
|
|
|
],
|
|
|
|
}, class ExtensionPrefsDialog extends Gtk.Window {
|
|
|
|
_init(extension) {
|
2020-04-15 18:27:15 +00:00
|
|
|
super._init({
|
|
|
|
title: extension.metadata.name,
|
|
|
|
});
|
2020-03-04 04:05:08 +00:00
|
|
|
|
|
|
|
this._uuid = extension.uuid;
|
|
|
|
this._url = extension.metadata.url || '';
|
|
|
|
|
|
|
|
this._actionGroup = new Gio.SimpleActionGroup();
|
|
|
|
this.insert_action_group('win', this._actionGroup);
|
|
|
|
|
|
|
|
this._initActions();
|
|
|
|
this._addCustomStylesheet();
|
|
|
|
|
2020-04-15 18:27:15 +00:00
|
|
|
this._gesture = new Gtk.GestureClick({
|
2020-03-04 04:05:08 +00:00
|
|
|
button: 0,
|
|
|
|
exclusive: true,
|
|
|
|
});
|
2020-04-15 18:27:15 +00:00
|
|
|
this._expander.add_controller(this._gesture);
|
2020-03-04 04:05:08 +00:00
|
|
|
|
|
|
|
this._gesture.connect('released', (gesture, nPress) => {
|
|
|
|
if (nPress === 1)
|
|
|
|
this._revealer.reveal_child = !this._revealer.reveal_child;
|
|
|
|
});
|
|
|
|
|
|
|
|
this._revealer.connect('notify::reveal-child', () => {
|
|
|
|
this._expanderArrow.icon_name = this._revealer.reveal_child
|
|
|
|
? 'pan-down-symbolic'
|
|
|
|
: 'pan-end-symbolic';
|
2020-11-13 01:36:08 +00:00
|
|
|
this._syncExpandedStyle();
|
2020-03-04 04:05:08 +00:00
|
|
|
});
|
2020-11-13 01:36:08 +00:00
|
|
|
this._revealer.connect('notify::child-revealed',
|
|
|
|
() => this._syncExpandedStyle());
|
2020-03-04 04:05:08 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
ExtensionUtils.installImporter(extension);
|
|
|
|
|
|
|
|
// give extension prefs access to their own extension object
|
|
|
|
ExtensionUtils.getCurrentExtension = () => extension;
|
|
|
|
|
|
|
|
const prefsModule = extension.imports.prefs;
|
|
|
|
prefsModule.init(extension.metadata);
|
|
|
|
|
|
|
|
const widget = prefsModule.buildPrefsWidget();
|
2020-04-15 18:27:15 +00:00
|
|
|
this._stack.add_named(widget, 'prefs');
|
2020-03-04 04:05:08 +00:00
|
|
|
this._stack.visible_child = widget;
|
|
|
|
} catch (e) {
|
|
|
|
this._setError(e);
|
2021-05-01 18:15:50 +00:00
|
|
|
logError(e, 'Failed to open preferences');
|
2020-03-04 04:05:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-13 01:36:08 +00:00
|
|
|
_syncExpandedStyle() {
|
|
|
|
if (this._revealer.reveal_child)
|
2020-04-15 18:27:15 +00:00
|
|
|
this._expander.add_css_class('expanded');
|
2020-11-13 01:36:08 +00:00
|
|
|
else if (!this._revealer.child_revealed)
|
2020-04-15 18:27:15 +00:00
|
|
|
this._expander.remove_css_class('expanded');
|
2020-11-13 01:36:08 +00:00
|
|
|
}
|
|
|
|
|
2020-03-04 04:05:08 +00:00
|
|
|
_setError(exc) {
|
|
|
|
this._errorView.buffer.text = `${exc}\n\nStack trace:\n`;
|
|
|
|
// Indent stack trace.
|
|
|
|
this._errorView.buffer.text +=
|
|
|
|
exc.stack.split('\n').map(line => ` ${line}`).join('\n');
|
|
|
|
|
|
|
|
// markdown for pasting in gitlab issues
|
|
|
|
let lines = [
|
|
|
|
`The settings of extension ${this._uuid} had an error:`,
|
|
|
|
'```',
|
|
|
|
`${exc}`,
|
|
|
|
'```',
|
|
|
|
'',
|
|
|
|
'Stack trace:',
|
|
|
|
'```',
|
|
|
|
exc.stack.replace(/\n$/, ''), // stack without trailing newline
|
|
|
|
'```',
|
|
|
|
'',
|
|
|
|
];
|
|
|
|
this._errorMarkdown = lines.join('\n');
|
|
|
|
this._actionGroup.lookup('copy-error').enabled = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
_initActions() {
|
|
|
|
let action;
|
|
|
|
|
|
|
|
action = new Gio.SimpleAction({
|
|
|
|
name: 'copy-error',
|
|
|
|
enabled: false,
|
|
|
|
});
|
|
|
|
action.connect('activate', () => {
|
2021-04-08 04:25:28 +00:00
|
|
|
const clipboard = this.get_display().get_clipboard();
|
2021-04-08 04:25:28 +00:00
|
|
|
clipboard.set(this._errorMarkdown);
|
2020-03-04 04:05:08 +00:00
|
|
|
});
|
|
|
|
this._actionGroup.add_action(action);
|
|
|
|
|
|
|
|
action = new Gio.SimpleAction({
|
|
|
|
name: 'show-url',
|
|
|
|
enabled: this._url !== '',
|
|
|
|
});
|
|
|
|
action.connect('activate', () => {
|
|
|
|
Gio.AppInfo.launch_default_for_uri(this._url,
|
|
|
|
this.get_display().get_app_launch_context());
|
|
|
|
});
|
|
|
|
this._actionGroup.add_action(action);
|
|
|
|
}
|
|
|
|
|
|
|
|
_addCustomStylesheet() {
|
|
|
|
let provider = new Gtk.CssProvider();
|
|
|
|
let uri = 'resource:///org/gnome/Shell/Extensions/css/application.css';
|
|
|
|
try {
|
|
|
|
provider.load_from_file(Gio.File.new_for_uri(uri));
|
|
|
|
} catch (e) {
|
|
|
|
logError(e, 'Failed to add application style');
|
|
|
|
}
|
2020-04-15 18:27:15 +00:00
|
|
|
Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(),
|
2020-03-04 04:05:08 +00:00
|
|
|
provider,
|
|
|
|
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
|
|
|
|
}
|
|
|
|
});
|