From b82039e324a28e2fe9cf5379b73e6f2e3dc7c86d Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Thu, 22 Nov 2018 15:03:04 +0000 Subject: [PATCH 1/4] util: Add a wrapper around getuid() So we can use it from JS in an upcoming commit. Signed-off-by: Philip Withnall https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/465 --- src/shell-util.c | 14 ++++++++++++++ src/shell-util.h | 2 ++ 2 files changed, 16 insertions(+) diff --git a/src/shell-util.c b/src/shell-util.c index d1a75bc6b4..bb2329536b 100644 --- a/src/shell-util.c +++ b/src/shell-util.c @@ -638,6 +638,20 @@ shell_util_check_cloexec_fds (void) g_info ("Open fd CLOEXEC check complete"); } +/** + * shell_util_get_uid: + * + * A wrapper around getuid() so that it can be used from JavaScript. This + * function will always succeed. + * + * Returns: the real user ID of the calling process + */ +gint +shell_util_get_uid (void) +{ + return getuid (); +} + static void on_systemd_call_cb (GObject *source, GAsyncResult *res, diff --git a/src/shell-util.h b/src/shell-util.h index 055e881905..aa79f49736 100644 --- a/src/shell-util.h +++ b/src/shell-util.h @@ -78,6 +78,8 @@ gboolean shell_util_has_x11_display_extension (MetaDisplay *display, char *shell_util_get_translated_folder_name (const char *name); +gint shell_util_get_uid (void); + G_END_DECLS #endif /* __SHELL_UTIL_H__ */ -- GitLab From 3e5b90dbba11c9782e870c2081d5037e01f4adad Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 24 Apr 2019 10:37:10 +0100 Subject: [PATCH 2/4] 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 aec3427e0b..2cf86a08af 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 0000000000..3c69efe305 --- /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 ed616e855a..7dc9634394 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 bb579c3474..3fcc8b2852 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(); -- GitLab From 143ab6ac7ffbcc8c47cda7720dfbabc3674aa443 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 30 Mar 2020 12:40:11 +0100 Subject: [PATCH 3/4] search: Hide search providers which are blacklisted by parental controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a search provider is installed by an app which is blacklisted for the current user by their parental controls, don’t show it or results for it. Currently, this only filters ‘remote’ (not built-in to the shell) search providers. This seems fine for now; in future it could be expanded to also filter built-in search providers, if any of them end up needing to be filtered. No corresponding changes need to be made `remoteSearch.js`, because the results of `loadRemoteSearchProviders()` are filtered in `search.js`. Signed-off-by: Philip Withnall https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/465 --- js/ui/search.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/js/ui/search.js b/js/ui/search.js index 88f06211c4..f9125123e6 100644 --- a/js/ui/search.js +++ b/js/ui/search.js @@ -6,6 +6,7 @@ const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi; const AppDisplay = imports.ui.appDisplay; const IconGrid = imports.ui.iconGrid; const Main = imports.ui.main; +const ParentalControlsManager = imports.misc.parentalControlsManager; const RemoteSearch = imports.ui.remoteSearch; const Util = imports.misc.util; @@ -431,6 +432,9 @@ var SearchResultsView = GObject.registerClass({ _init() { super._init({ name: 'searchResults', vertical: true }); + this._parentalControlsManager = ParentalControlsManager.getDefault(); + this._parentalControlsManager.connect('app-filter-changed', this._reloadRemoteProviders.bind(this)); + this._content = new MaxWidthBox({ name: 'searchResultsContent', vertical: true, @@ -505,6 +509,11 @@ var SearchResultsView = GObject.registerClass({ _registerProvider(provider) { provider.searchInProgress = false; + + // Filter out unwanted providers. + if (provider.appInfo && !this._parentalControlsManager.shouldShowApp(provider.appInfo)) + return; + this._providers.push(provider); this._ensureProviderDisplay(provider); } -- GitLab From 91b13effc8b417bc12c3d91d40a3e5b713149308 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 31 Mar 2020 10:33:18 +0100 Subject: [PATCH 4/4] appFavorites: Hide favourites which are blacklisted by parental controls If a favourite is set for an app which is blocked by parental controls, that favourite should be hidden. Signed-off-by: Philip Withnall https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/465 --- js/ui/appFavorites.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/js/ui/appFavorites.js b/js/ui/appFavorites.js index 653f0cfba1..c65040d8a2 100644 --- a/js/ui/appFavorites.js +++ b/js/ui/appFavorites.js @@ -2,6 +2,7 @@ /* exported getAppFavorites */ const Shell = imports.gi.Shell; +const ParentalControlsManager = imports.misc.parentalControlsManager; const Signals = imports.signals; const Main = imports.ui.main; @@ -64,6 +65,13 @@ const RENAMED_DESKTOP_IDS = { class AppFavorites { constructor() { + // Filter the apps through the user’s parental controls. + this._parentalControlsManager = ParentalControlsManager.getDefault(); + this._parentalControlsManager.connect('app-filter-changed', () => { + this.reload(); + this.emit('changed'); + }); + this.FAVORITE_APPS_KEY = 'favorite-apps'; this._favorites = {}; global.settings.connect('changed::%s'.format(this.FAVORITE_APPS_KEY), this._onFavsChanged.bind(this)); @@ -95,7 +103,7 @@ class AppFavorites { global.settings.set_strv(this.FAVORITE_APPS_KEY, ids); let apps = ids.map(id => appSys.lookup_app(id)) - .filter(app => app != null); + .filter(app => app !== null && this._parentalControlsManager.shouldShowApp(app.app_info)); this._favorites = {}; for (let i = 0; i < apps.length; i++) { let app = apps[i]; @@ -134,6 +142,9 @@ class AppFavorites { if (!app) return false; + if (!this._parentalControlsManager.shouldShowApp(app.app_info)) + return false; + let ids = this._getIds(); if (pos == -1) ids.push(appId); -- GitLab