Provide org.freedesktop.impl.portal.access implementation
If a sandboxed app requests access to some system resource (camera, microphone, location), the portal frontend needs to ask the user for permission. In GNOME, we want this to be a system modal dialog, so provide an org.freedesktop.impl.portal.access implementation that exposes a generic system modal permission dialog on the bus. https://bugzilla.gnome.org/show_bug.cgi?id=768669
This commit is contained in:
parent
f5c058a036
commit
4fc0c513af
@ -7,6 +7,9 @@ desktop_DATA = org.gnome.Shell.desktop gnome-shell-extension-prefs.desktop
|
|||||||
if HAVE_NETWORKMANAGER
|
if HAVE_NETWORKMANAGER
|
||||||
desktop_DATA += org.gnome.Shell.PortalHelper.desktop
|
desktop_DATA += org.gnome.Shell.PortalHelper.desktop
|
||||||
|
|
||||||
|
portaldir = $(datadir)/xdg-desktop-portal/portals
|
||||||
|
portal_DATA = gnome-shell.portal
|
||||||
|
|
||||||
servicedir = $(datadir)/dbus-1/services
|
servicedir = $(datadir)/dbus-1/services
|
||||||
service_DATA = org.gnome.Shell.PortalHelper.service
|
service_DATA = org.gnome.Shell.PortalHelper.service
|
||||||
|
|
||||||
|
4
data/gnome-shell.portal
Normal file
4
data/gnome-shell.portal
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[portal]
|
||||||
|
DBusName=org.freedesktop.impl.portal.desktop.gnome
|
||||||
|
Interfaces=org.freedesktop.impl.portal.Access
|
||||||
|
UseIn=gnome
|
@ -427,6 +427,29 @@ StScrollBar {
|
|||||||
.audio-selection-device-icon {
|
.audio-selection-device-icon {
|
||||||
icon-size: 64px; }
|
icon-size: 64px; }
|
||||||
|
|
||||||
|
/* Access Dialog */
|
||||||
|
.access-dialog {
|
||||||
|
spacing: 30px; }
|
||||||
|
|
||||||
|
.access-dialog-main-layout {
|
||||||
|
padding: 12px 20px 0;
|
||||||
|
spacing: 12px; }
|
||||||
|
|
||||||
|
.access-dialog-content {
|
||||||
|
max-width: 28em;
|
||||||
|
spacing: 20px; }
|
||||||
|
|
||||||
|
.access-dialog-icon {
|
||||||
|
min-width: 48px;
|
||||||
|
icon-size: 48px; }
|
||||||
|
|
||||||
|
.access-dialog-title {
|
||||||
|
font-weight: bold; }
|
||||||
|
|
||||||
|
.access-dialog-subtitle {
|
||||||
|
color: #999999;
|
||||||
|
font-weight: bold; }
|
||||||
|
|
||||||
/* Geolocation Dialog */
|
/* Geolocation Dialog */
|
||||||
.geolocation-dialog {
|
.geolocation-dialog {
|
||||||
spacing: 30px; }
|
spacing: 30px; }
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit d184e5c44ed824ab62fc35a4a8d75cda1f387206
|
Subproject commit 7ab2789464b2e917b56aec9df6b78975db1a8eb6
|
@ -427,6 +427,29 @@ StScrollBar {
|
|||||||
.audio-selection-device-icon {
|
.audio-selection-device-icon {
|
||||||
icon-size: 64px; }
|
icon-size: 64px; }
|
||||||
|
|
||||||
|
/* Access Dialog */
|
||||||
|
.access-dialog {
|
||||||
|
spacing: 30px; }
|
||||||
|
|
||||||
|
.access-dialog-main-layout {
|
||||||
|
padding: 12px 20px 0;
|
||||||
|
spacing: 12px; }
|
||||||
|
|
||||||
|
.access-dialog-content {
|
||||||
|
max-width: 28em;
|
||||||
|
spacing: 20px; }
|
||||||
|
|
||||||
|
.access-dialog-icon {
|
||||||
|
min-width: 48px;
|
||||||
|
icon-size: 48px; }
|
||||||
|
|
||||||
|
.access-dialog-title {
|
||||||
|
font-weight: bold; }
|
||||||
|
|
||||||
|
.access-dialog-subtitle {
|
||||||
|
color: #8e8e80;
|
||||||
|
font-weight: bold; }
|
||||||
|
|
||||||
/* Geolocation Dialog */
|
/* Geolocation Dialog */
|
||||||
.geolocation-dialog {
|
.geolocation-dialog {
|
||||||
spacing: 30px; }
|
spacing: 30px; }
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
<file>portalHelper/main.js</file>
|
<file>portalHelper/main.js</file>
|
||||||
|
|
||||||
|
<file>ui/accessDialog.js</file>
|
||||||
<file>ui/altTab.js</file>
|
<file>ui/altTab.js</file>
|
||||||
<file>ui/animation.js</file>
|
<file>ui/animation.js</file>
|
||||||
<file>ui/appDisplay.js</file>
|
<file>ui/appDisplay.js</file>
|
||||||
|
202
js/ui/accessDialog.js
Normal file
202
js/ui/accessDialog.js
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
const Clutter = imports.gi.Clutter;
|
||||||
|
const Gio = imports.gi.Gio;
|
||||||
|
const GLib = imports.gi.GLib;
|
||||||
|
const Lang = imports.lang;
|
||||||
|
const Pango = imports.gi.Pango;
|
||||||
|
const Shell = imports.gi.Shell;
|
||||||
|
const St = imports.gi.St;
|
||||||
|
|
||||||
|
const CheckBox = imports.ui.checkBox;
|
||||||
|
const ModalDialog = imports.ui.modalDialog;
|
||||||
|
|
||||||
|
const RequestIface = '<node> \
|
||||||
|
<interface name="org.freedesktop.impl.portal.Request"> \
|
||||||
|
<method name="Close"/> \
|
||||||
|
</interface> \
|
||||||
|
</node>';
|
||||||
|
|
||||||
|
const AccessIface = '<node> \
|
||||||
|
<interface name="org.freedesktop.impl.portal.Access"> \
|
||||||
|
<method name="AccessDialog"> \
|
||||||
|
<arg type="o" name="handle" direction="in"/> \
|
||||||
|
<arg type="s" name="app_id" direction="in"/> \
|
||||||
|
<arg type="s" name="parent_window" direction="in"/> \
|
||||||
|
<arg type="s" name="title" direction="in"/> \
|
||||||
|
<arg type="s" name="subtitle" direction="in"/> \
|
||||||
|
<arg type="s" name="body" direction="in"/> \
|
||||||
|
<arg type="a{sv}" name="options" direction="in"/> \
|
||||||
|
<arg type="u" name="response" direction="out"/> \
|
||||||
|
<arg type="a{sv}" name="results" direction="out"/> \
|
||||||
|
</method> \
|
||||||
|
</interface> \
|
||||||
|
</node>';
|
||||||
|
|
||||||
|
const DialogResponse = {
|
||||||
|
OK: 0,
|
||||||
|
CANCEL: 1,
|
||||||
|
CLOSED: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccessDialog = new Lang.Class({
|
||||||
|
Name: 'AccessDialog',
|
||||||
|
Extends: ModalDialog.ModalDialog,
|
||||||
|
|
||||||
|
_init: function(invocation, handle, title, subtitle, body, options) {
|
||||||
|
this.parent({ styleClass: 'access-dialog' });
|
||||||
|
|
||||||
|
this._invocation = invocation;
|
||||||
|
this._handle = handle;
|
||||||
|
|
||||||
|
this._requestExported = false;
|
||||||
|
this._request = Gio.DBusExportedObject.wrapJSObject(RequestIface, this);
|
||||||
|
|
||||||
|
for (let option in options)
|
||||||
|
options[option] = options[option].deep_unpack();
|
||||||
|
|
||||||
|
this._buildLayout(title, subtitle, body, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
_buildLayout: function(title, subtitle, body, options) {
|
||||||
|
// No support for non-modal system dialogs, so ignore the option
|
||||||
|
//let modal = options['modal'] || true;
|
||||||
|
let denyLabel = options['deny_label'] || _("Deny Access");
|
||||||
|
let grantLabel = options['grant_label'] || _("Grant Access");
|
||||||
|
let iconName = options['icon'] || null;
|
||||||
|
let choices = options['choices'] || [];
|
||||||
|
|
||||||
|
let mainContentBox = new St.BoxLayout();
|
||||||
|
mainContentBox.style_class = 'access-dialog-main-layout';
|
||||||
|
this.contentLayout.add_actor(mainContentBox);
|
||||||
|
|
||||||
|
let icon = new St.Icon({ style_class: 'access-dialog-icon',
|
||||||
|
icon_name: iconName,
|
||||||
|
y_align: Clutter.ActorAlign.START });
|
||||||
|
mainContentBox.add_actor(icon);
|
||||||
|
|
||||||
|
let messageBox = new St.BoxLayout({ vertical: true });
|
||||||
|
messageBox.style_class = 'access-dialog-content',
|
||||||
|
mainContentBox.add_actor(messageBox);
|
||||||
|
|
||||||
|
let label;
|
||||||
|
label = new St.Label({ style_class: 'access-dialog-title headline',
|
||||||
|
text: title });
|
||||||
|
messageBox.add_actor(label);
|
||||||
|
|
||||||
|
label = new St.Label({ style_class: 'access-dialog-subtitle',
|
||||||
|
text: subtitle });
|
||||||
|
label.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
||||||
|
label.clutter_text.line_wrap = true;
|
||||||
|
messageBox.add_actor(label);
|
||||||
|
|
||||||
|
this._choices = new Map();
|
||||||
|
|
||||||
|
for (let i = 0; i < choices.length; i++) {
|
||||||
|
let [id, name, opts, selected] = choices[i];
|
||||||
|
if (opts.length > 0)
|
||||||
|
continue; // radio buttons, not implemented
|
||||||
|
|
||||||
|
let check = new CheckBox.CheckBox();
|
||||||
|
check.getLabelActor().text = name;
|
||||||
|
check.actor.checked = selected == "true";
|
||||||
|
messageBox.add_actor(check.actor);
|
||||||
|
|
||||||
|
this._choices.set(id, check);
|
||||||
|
}
|
||||||
|
|
||||||
|
label = new St.Label({ text: body });
|
||||||
|
label.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
||||||
|
label.clutter_text.line_wrap = true;
|
||||||
|
messageBox.add_actor(label);
|
||||||
|
|
||||||
|
this.addButton({ label: denyLabel,
|
||||||
|
action: () => {
|
||||||
|
this._sendResponse(DialogResponse.CANCEL);
|
||||||
|
},
|
||||||
|
key: Clutter.KEY_Escape });
|
||||||
|
this.addButton({ label: grantLabel,
|
||||||
|
action: () => {
|
||||||
|
this._sendResponse(DialogResponse.OK);
|
||||||
|
}});
|
||||||
|
},
|
||||||
|
|
||||||
|
open: function() {
|
||||||
|
this.parent();
|
||||||
|
|
||||||
|
let connection = this._invocation.get_connection();
|
||||||
|
this._requestExported = this._request.export(connection, this._handle);
|
||||||
|
},
|
||||||
|
|
||||||
|
CloseAsync: function(invocation, params) {
|
||||||
|
if (this._invocation.get_sender() != invocation.get_sender()) {
|
||||||
|
invocation.return_error_literal(Gio.DBusError,
|
||||||
|
Gio.DBusError.ACCESS_DENIED,
|
||||||
|
'');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sendResponse(DialogResponse.CLOSED);
|
||||||
|
},
|
||||||
|
|
||||||
|
_sendResponse: function(response) {
|
||||||
|
if (this._requestExported)
|
||||||
|
this._request.unexport();
|
||||||
|
this._requestExported = false;
|
||||||
|
|
||||||
|
let results = {};
|
||||||
|
if (response == DialogResponse.OK) {
|
||||||
|
for (let [id, check] of this._choices) {
|
||||||
|
let checked = check.actor.checked ? 'true' : 'false';
|
||||||
|
results[id] = new GLib.Variant('s', checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay actual response until the end of the close animation (if any)
|
||||||
|
this.connect('closed', () => {
|
||||||
|
this._invocation.return_value(new GLib.Variant('(ua{sv})',
|
||||||
|
[response, results]));
|
||||||
|
});
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const AccessDialogDBus = new Lang.Class({
|
||||||
|
Name: 'AccessDialogDBus',
|
||||||
|
|
||||||
|
_init: function() {
|
||||||
|
this._accessDialog = null;
|
||||||
|
|
||||||
|
this._windowTracker = Shell.WindowTracker.get_default();
|
||||||
|
|
||||||
|
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(AccessIface, this);
|
||||||
|
this._dbusImpl.export(Gio.DBus.session, '/org/freedesktop/portal/desktop');
|
||||||
|
|
||||||
|
Gio.DBus.session.own_name('org.freedesktop.impl.portal.desktop.gnome', Gio.BusNameOwnerFlags.REPLACE, null, null);
|
||||||
|
},
|
||||||
|
|
||||||
|
AccessDialogAsync: function(params, invocation) {
|
||||||
|
if (this._accessDialog) {
|
||||||
|
invocation.return_error_literal(Gio.DBusError,
|
||||||
|
Gio.DBusError.LIMITS_EXCEEDED,
|
||||||
|
'Already showing a system access dialog');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [handle, appId, parentWindow, title, subtitle, body, options] = params;
|
||||||
|
// We probably want to use parentWindow and global.display.focus_window
|
||||||
|
// for this check in the future
|
||||||
|
if (appId && appId + '.desktop' != this._windowTracker.focus_app.id) {
|
||||||
|
invocation.return_error_literal(Gio.DBusError,
|
||||||
|
Gio.DBusError.ACCESS_DENIED,
|
||||||
|
'Only the focused app is allowed to show a system access dialog');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dialog = new AccessDialog(invocation, handle, title,
|
||||||
|
subtitle, body, options);
|
||||||
|
dialog.open();
|
||||||
|
|
||||||
|
dialog.connect('closed', () => { this._accessDialog = null; });
|
||||||
|
|
||||||
|
this._accessDialog = dialog;
|
||||||
|
}
|
||||||
|
});
|
@ -11,6 +11,7 @@ const Meta = imports.gi.Meta;
|
|||||||
const Shell = imports.gi.Shell;
|
const Shell = imports.gi.Shell;
|
||||||
const St = imports.gi.St;
|
const St = imports.gi.St;
|
||||||
|
|
||||||
|
const AccessDialog = imports.ui.accessDialog;
|
||||||
const AudioDeviceSelection = imports.ui.audioDeviceSelection;
|
const AudioDeviceSelection = imports.ui.audioDeviceSelection;
|
||||||
const Components = imports.ui.components;
|
const Components = imports.ui.components;
|
||||||
const CtrlAltTab = imports.ui.ctrlAltTab;
|
const CtrlAltTab = imports.ui.ctrlAltTab;
|
||||||
@ -63,6 +64,7 @@ let ctrlAltTabManager = null;
|
|||||||
let osdWindowManager = null;
|
let osdWindowManager = null;
|
||||||
let osdMonitorLabeler = null;
|
let osdMonitorLabeler = null;
|
||||||
let sessionMode = null;
|
let sessionMode = null;
|
||||||
|
let shellAccessDialogDBusService = null;
|
||||||
let shellAudioSelectionDBusService = null;
|
let shellAudioSelectionDBusService = null;
|
||||||
let shellDBusService = null;
|
let shellDBusService = null;
|
||||||
let shellMountOpDBusService = null;
|
let shellMountOpDBusService = null;
|
||||||
@ -122,6 +124,7 @@ function start() {
|
|||||||
_loadDefaultStylesheet);
|
_loadDefaultStylesheet);
|
||||||
_initializeUI();
|
_initializeUI();
|
||||||
|
|
||||||
|
shellAccessDialogDBusService = new AccessDialog.AccessDialogDBus();
|
||||||
shellAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus();
|
shellAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus();
|
||||||
shellDBusService = new ShellDBus.GnomeShell();
|
shellDBusService = new ShellDBus.GnomeShell();
|
||||||
shellMountOpDBusService = new ShellMountOperation.GnomeShellMountOpHandler();
|
shellMountOpDBusService = new ShellMountOperation.GnomeShellMountOpHandler();
|
||||||
|
Loading…
Reference in New Issue
Block a user