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__ */