diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index aec3427e0bb5e93ff60c9b1fbd3d32ed9f1252d3..2cf86a08af3dba156f078005b30603337ee4209d 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 0000000000000000000000000000000000000000..3c69efe30591b7050a4b9b23901fca4622066890 --- /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 ed616e855ac00e0faaf5114d19b347c250050b8b..7dc963439416bdd08efabe27ab4acd12ab460685 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/appFavorites.js b/js/ui/appFavorites.js index 653f0cfba1e3debe8aa175d2a8f1ce04cae9902f..c65040d8a2af1b2c7d53b2f4aca05d2507a1e219 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); diff --git a/js/ui/main.js b/js/ui/main.js index bb579c34744135257afb1d579341d1c026c49390..3fcc8b2852c67624b5437db300ef2b375b488754 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(); diff --git a/js/ui/search.js b/js/ui/search.js index 88f06211c4f6ccb473ecee530b8030ef6b655085..f9125123e6599f6d8e4c6602851e4f3cf317b25d 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); } diff --git a/src/shell-util.c b/src/shell-util.c index d1a75bc6b42ca3cab058d9223fa9e9bb53d0650c..bb2329536bfbc5de02b77132b84b5d8b887d4fa5 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 055e881905ab7be0dd6cf4b97a22dfc3626f3640..aa79f49736283badd168374949eddfe8ca7cc0a2 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__ */