[gnome-shell] js: Add support for parental controls filtering to the desktop



commit 3e5b90dbba11c9782e870c2081d5037e01f4adad
Author: Philip Withnall <withnall endlessm com>
Date:   Wed Apr 24 10:37:10 2019 +0100

    js: Add support for parental controls filtering to the desktop
    
    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 <withnall endlessm com>
    
    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(-)
---
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 @@
     <file>misc/modemManager.js</file>
     <file>misc/objectManager.js</file>
     <file>misc/params.js</file>
+    <file>misc/parentalControlsManager.js</file>
     <file>misc/permissionStore.js</file>
     <file>misc/smartcardManager.js</file>
     <file>misc/systemActions.js</file>
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();


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]