From 3e5b90dbba11c9782e870c2081d5037e01f4adad Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 24 Apr 2019 10:37:10 +0100 Subject: [PATCH] js: Add support for parental controls filtering to the desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter the apps shown on the desktop and in search results according to whether they are blacklisted by the user’s parental controls. This supports dynamically updating the filter during the user’s session. This adds an optional dependency on libmalcontent. If that’s unavailable, no parental controls filtering will occur. Signed-off-by: Philip Withnall https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/465 --- js/js-resources.gresource.xml | 1 + js/misc/parentalControlsManager.js | 146 +++++++++++++++++++++++++++++ js/ui/appDisplay.js | 30 +++++- js/ui/main.js | 5 + 4 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 js/misc/parentalControlsManager.js diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index aec3427e0..2cf86a08a 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -23,6 +23,7 @@ misc/modemManager.js misc/objectManager.js misc/params.js + misc/parentalControlsManager.js misc/permissionStore.js misc/smartcardManager.js misc/systemActions.js diff --git a/js/misc/parentalControlsManager.js b/js/misc/parentalControlsManager.js new file mode 100644 index 000000000..3c69efe30 --- /dev/null +++ b/js/misc/parentalControlsManager.js @@ -0,0 +1,146 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +// +// Copyright (C) 2018, 2019, 2020 Endless Mobile, Inc. +// +// This is a GNOME Shell component to wrap the interactions over +// D-Bus with the malcontent library. +// +// Licensed under the GNU General Public License Version 2 +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +/* exported getDefault */ + +const { Gio, GObject, Shell } = imports.gi; + +// We require libmalcontent ≥ 0.6.0 +const HAVE_MALCONTENT = imports.package.checkSymbol( + 'Malcontent', '0', 'ManagerGetValueFlags'); + +var Malcontent = null; +if (HAVE_MALCONTENT) { + Malcontent = imports.gi.Malcontent; + Gio._promisify(Malcontent.Manager.prototype, 'get_app_filter_async', 'get_app_filter_finish'); +} + +let _singleton = null; + +function getDefault() { + if (_singleton === null) + _singleton = new ParentalControlsManager(); + + return _singleton; +} + +// A manager class which provides cached access to the constructing user’s +// parental controls settings. It’s possible for the user’s parental controls +// to change at runtime if the Parental Controls application is used by an +// administrator from within the user’s session. +var ParentalControlsManager = GObject.registerClass({ + Signals: { + 'app-filter-changed': {}, + }, +}, class ParentalControlsManager extends GObject.Object { + _init() { + super._init(); + + this._initialized = false; + this._disabled = false; + this._appFilter = null; + + this._initializeManager(); + } + + async _initializeManager() { + if (!HAVE_MALCONTENT) { + log('Skipping parental controls support as it’s disabled'); + this._initialized = true; + this.emit('app-filter-changed'); + return; + } + + log(`Getting parental controls for user ${Shell.util_get_uid()}`); + try { + const connection = await Gio.DBus.get(Gio.BusType.SYSTEM, null); + this._manager = new Malcontent.Manager({ connection }); + this._appFilter = await this._manager.get_app_filter_async( + Shell.util_get_uid(), + Malcontent.ManagerGetValueFlags.NONE, + null); + } catch (e) { + if (e.matches(Malcontent.ManagerError, Malcontent.ManagerError.DISABLED)) { + log('Parental controls globally disabled'); + this._disabled = true; + } else { + logError(e, 'Failed to get parental controls settings'); + return; + } + } + + this._manager.connect('app-filter-changed', this._onAppFilterChanged.bind(this)); + + // Signal initialisation is complete. + this._initialized = true; + this.emit('app-filter-changed'); + } + + async _onAppFilterChanged(manager, uid) { + // Emit 'changed' signal only if app-filter is changed for currently logged-in user. + let currentUid = Shell.util_get_uid(); + if (currentUid !== uid) + return; + + try { + this._appFilter = await this._manager.get_app_filter_async( + currentUid, + Malcontent.ManagerGetValueFlags.NONE, + null); + this.emit('app-filter-changed'); + } catch (e) { + // Log an error and keep the old app filter. + logError(e, `Failed to get new MctAppFilter for uid ${Shell.util_get_uid()} on app-filter-changed`); + } + } + + get initialized() { + return this._initialized; + } + + // Calculate whether the given app (a Gio.DesktopAppInfo) should be shown + // on the desktop, in search results, etc. The app should be shown if: + // - The .desktop file doesn’t say it should be hidden. + // - The executable from the .desktop file’s Exec line isn’t blacklisted in + // the user’s parental controls. + // - None of the flatpak app IDs from the X-Flatpak and the + // X-Flatpak-RenamedFrom lines are blacklisted in the user’s parental + // controls. + shouldShowApp(appInfo) { + // Quick decision? + if (!appInfo.should_show()) + return false; + + // Are parental controls enabled (at configure time or runtime)? + if (!HAVE_MALCONTENT || this._disabled) + return true; + + // Have we finished initialising yet? + if (!this.initialized) { + log(`Warning: Hiding app because parental controls not yet initialised: ${appInfo.get_id()}`); + return false; + } + + return this._appFilter.is_appinfo_allowed(appInfo); + } +}); diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index ed616e855..7dc963439 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -10,6 +10,7 @@ const GrabHelper = imports.ui.grabHelper; const IconGrid = imports.ui.iconGrid; const Main = imports.ui.main; const PageIndicators = imports.ui.pageIndicators; +const ParentalControlsManager = imports.misc.parentalControlsManager; const PopupMenu = imports.ui.popupMenu; const Search = imports.ui.search; const SwipeTracker = imports.ui.swipeTracker; @@ -161,6 +162,12 @@ var BaseAppView = GObject.registerClass({ this._animateLaterId = 0; this._viewLoadedHandlerId = 0; this._viewIsReady = false; + + // Filter the apps through the user’s parental controls. + this._parentalControlsManager = ParentalControlsManager.getDefault(); + this._parentalControlsManager.connect('app-filter-changed', () => { + this._redisplay(); + }); } _childFocused(_actor) { @@ -514,7 +521,7 @@ var AllView = GObject.registerClass({ } catch (e) { return false; } - return appInfo.should_show(); + return this._parentalControlsManager.shouldShowApp(appInfo); }); let apps = this._appInfoList.map(app => app.get_id()); @@ -1004,7 +1011,7 @@ class FrequentView extends BaseAppView { let favoritesWritable = global.settings.is_writable('favorite-apps'); for (let i = 0; i < mostUsed.length; i++) { - if (!mostUsed[i].get_app_info().should_show()) + if (!this._parentalControlsManager.shouldShowApp(mostUsed[i].get_app_info())) continue; let appIcon = this._items.get(mostUsed[i].get_id()); if (!appIcon) { @@ -1250,6 +1257,8 @@ var AppSearchProvider = class AppSearchProvider { this.canLaunchSearch = false; this._systemActions = new SystemActions.getDefault(); + + this._parentalControlsManager = ParentalControlsManager.getDefault(); } getResultMetas(apps, callback) { @@ -1284,14 +1293,27 @@ var AppSearchProvider = class AppSearchProvider { } getInitialResultSet(terms, callback, _cancellable) { + // Defer until the parental controls manager is initialised, so the + // results can be filtered correctly. + if (!this._parentalControlsManager.initialized) { + let initializedId = this._parentalControlsManager.connect('app-filter-changed', () => { + if (this._parentalControlsManager.initialized) { + this._parentalControlsManager.disconnect(initializedId); + this.getInitialResultSet(terms, callback, _cancellable); + } + }); + return; + } + let query = terms.join(' '); let groups = Shell.AppSystem.search(query); let usage = Shell.AppUsage.get_default(); let results = []; + groups.forEach(group => { group = group.filter(appID => { const app = this._appSys.lookup_app(appID); - return app && app.app_info.should_show(); + return app && this._parentalControlsManager.shouldShowApp(app.app_info); }); results = results.concat(group.sort( (a, b) => usage.compare(a, b) @@ -1430,7 +1452,7 @@ class FolderView extends BaseAppView { if (!app) return; - if (!app.get_app_info().should_show()) + if (!this._parentalControlsManager.shouldShowApp(app.get_app_info())) return; if (apps.some(appIcon => appIcon.id == appId)) diff --git a/js/ui/main.js b/js/ui/main.js index bb579c347..3fcc8b285 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -46,6 +46,7 @@ const XdndHandler = imports.ui.xdndHandler; const KbdA11yDialog = imports.ui.kbdA11yDialog; const LocatePointer = imports.ui.locatePointer; const PointerA11yTimeout = imports.ui.pointerA11yTimeout; +const ParentalControlsManager = imports.misc.parentalControlsManager; const A11Y_SCHEMA = 'org.gnome.desktop.a11y.keyboard'; const STICKY_KEYS_ENABLE = 'stickykeys-enable'; @@ -140,6 +141,10 @@ function start() { sessionMode.connect('updated', _sessionUpdated); St.Settings.get().connect('notify::gtk-theme', _loadDefaultStylesheet); + + // Initialize ParentalControlsManager before the UI + ParentalControlsManager.getDefault(); + _initializeUI(); shellAccessDialogDBusService = new AccessDialog.AccessDialogDBus();