[gnome-shell/eos3.8: 40/255] Add Internet Search provider



commit 235433d948ff82fe9ab3bb472c6fe42793e7d1d3
Author: Mario Sanchez Prada <mario endlessm com>
Date:   Mon Sep 11 14:07:23 2017 +0100

    Add Internet Search provider
    
    Also, avoid executing RegExps to find URLs in the search bar whenever
    possible, to prevent the desktop from locking up with long queries due
    to the way regular expressions are being matched (see T20051).
    
     * 2020-03-14: Code style cleanups
    
    https://phabricator.endlessm.com/T20051

 js/js-resources.gresource.xml |   1 +
 js/misc/util.js               | 144 +++++++++++++++++++++++++++++++++++++++++-
 js/ui/internetSearch.js       | 138 ++++++++++++++++++++++++++++++++++++++++
 js/ui/search.js               |   6 ++
 po/POTFILES.in                |   1 +
 5 files changed, 289 insertions(+), 1 deletion(-)
---
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 9e8914a4a6..84516fd863 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -147,6 +147,7 @@
     <file>ui/forceAppExitDialog.js</file>
     <file>ui/hotCorner.js</file>
     <file>ui/iconGridLayout.js</file>
+    <file>ui/internetSearch.js</file>
     <file>ui/sideComponent.js</file>
     <file>ui/userMenu.js</file>
     <file>ui/workspaceMonitor.js</file>
diff --git a/js/misc/util.js b/js/misc/util.js
index c68604e87b..1e92ccf0da 100644
--- a/js/misc/util.js
+++ b/js/misc/util.js
@@ -1,11 +1,13 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported findUrls, spawn, spawnCommandLine, spawnApp, trySpawnCommandLine,
             formatTime, formatTimeSpan, createTimeLabel, insertSorted,
-            makeCloseButton, ensureActorVisibleInScrollView, wiggle */
+            makeCloseButton, ensureActorVisibleInScrollView, wiggle,
+            getSearchEngineName, findSearchUrls, getBrowserApp */
 
 const { Clutter, Gio, GLib, GObject, Shell, St, GnomeDesktop } = imports.gi;
 const Gettext = imports.gettext;
 
+const Json = imports.gi.Json;
 const Main = imports.ui.main;
 const Params = imports.misc.params;
 
@@ -15,6 +17,9 @@ const WIGGLE_OFFSET = 6;
 const WIGGLE_DURATION = 65;
 const N_WIGGLES = 3;
 
+const FALLBACK_BROWSER_ID = 'chromium-browser.desktop';
+const GOOGLE_CHROME_ID = 'google-chrome.desktop';
+
 // http://daringfireball.net/2010/07/improved_regex_for_matching_urls
 const _balancedParens = '\\([^\\s()<>]+\\)';
 const _leadingJunk = '[\\s`(\\[{\'\\"<\u00AB\u201C\u2018]';
@@ -480,3 +485,140 @@ function wiggle(actor, params) {
         },
     });
 }
+
+// http://stackoverflow.com/questions/4691070/validate-url-without-www-or-http
+const _searchUrlRegexp = new RegExp(
+    '^([a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+.*)\\.+[A-Za-z0-9\.\/%&=\?\-_]+$',
+    'gi');
+
+const supportedSearchSchemes = ['http', 'https', 'ftp'];
+
+// findSearchUrls:
+// @terms: list of searchbar terms to find URLs in
+// @maxLength: maximum number of characters in each non-URI term to match against, defaults
+//             to 32 characters to prevent hogging the CPU with too long generic strings.
+//
+// Similar to "findUrls", but adapted for use only with terms from the searchbar.
+//
+// In order not to be too CPU-expensive, this function is implemented in the following way:
+//   1. If the term is a valid URI in that it's possible to parse at least
+//      its scheme and host fields, it's considered a valid URL "as-is".
+//   2. Else, if the term is a generic string exceeding the maximum length
+//      specified then we simply ignore it and move onto the next term.
+//   3. In any other case (non-URI term, valid length) we match the term
+//      passed against the regular expression to determine if it's a URL.
+//
+// Note that the regex for these URLs matches strings such as "google.com" (no need to the
+// specify a preceding scheme), which is why we have to limit its execution to a certain
+// maximum length, as the term can be pretty free-form. By default, this maximum length
+// is 32 characters, which should be a good compromise considering that "many of the world's
+// most visited web sites have domain names of between 6 - 10 characters" (see [1][2]).
+//
+// [1] https://www.domainregistration.com.au/news/2013/1301-domain-length.php
+// [2] https://www.domainregistration.com.au/infocentre/info-domain-length.php
+//
+// Return value: the list of URLs found in the string
+function findSearchUrls(terms, maxLength = 32) {
+    let res = [], match;
+    for (let term of terms) {
+        if (GLib.uri_parse_scheme(term)) {
+            let supportedScheme = false;
+            for (let scheme of supportedSearchSchemes) {
+                if (term.startsWith('%s://'.format(scheme))) {
+                    supportedScheme = true;
+                    break;
+                }
+            }
+
+            // Check that there's a valid host after the scheme part.
+            if (supportedScheme && term.split('://')[1]) {
+                res.push(term);
+                continue;
+            }
+        }
+
+        // Try to save CPU cycles from regexp-matching too long strings.
+        if (term.length > maxLength)
+            continue;
+
+        while ((match = _searchUrlRegexp.exec(term)))
+            res.push(match[0]);
+    }
+    return res;
+}
+
+function getBrowserId() {
+    let id = FALLBACK_BROWSER_ID;
+    let app = Gio.app_info_get_default_for_type('x-scheme-handler/http', true);
+    if (app)
+        id = app.get_id();
+    return id;
+}
+
+function getBrowserApp() {
+    let id = getBrowserId();
+    let appSystem = Shell.AppSystem.get_default();
+    let browserApp = appSystem.lookup_app(id);
+    return browserApp;
+}
+
+function _getJsonSearchEngine(folder) {
+    let path = GLib.build_filenamev([GLib.get_user_config_dir(), folder, 'Default', 'Preferences']);
+    let parser = new Json.Parser();
+
+    /*
+     * Translators: this is the name of the search engine that shows in the
+     * Shell's desktop search entry.
+     */
+    let defaultString = _('Google');
+
+    try {
+        parser.load_from_file(path);
+    } catch (e) {
+        if (e.matches(GLib.FileError, GLib.FileError.NOENT))
+            return defaultString;
+
+        logError(e, 'error while parsing %s'.format(path));
+        return null;
+    }
+
+    let root = parser.get_root().get_object();
+
+    let searchProviderDataNode = root.get_member('default_search_provider_data');
+    if (!searchProviderDataNode || searchProviderDataNode.get_node_type() !== Json.NodeType.OBJECT)
+        return defaultString;
+
+    let searchProviderData = searchProviderDataNode.get_object();
+    if (!searchProviderData)
+        return defaultString;
+
+    let templateUrlDataNode = searchProviderData.get_member('template_url_data');
+    if (!templateUrlDataNode || templateUrlDataNode.get_node_type() !== Json.NodeType.OBJECT)
+        return defaultString;
+
+    let templateUrlData = templateUrlDataNode.get_object();
+    if (!templateUrlData)
+        return defaultString;
+
+    let shortNameNode = templateUrlData.get_member('short_name');
+    if (!shortNameNode || shortNameNode.get_node_type() !== Json.NodeType.VALUE)
+        return defaultString;
+
+    return shortNameNode.get_string();
+}
+
+// getSearchEngineName:
+//
+// Retrieves the current search engine from
+// the default browser.
+function getSearchEngineName() {
+    let browser = getBrowserId();
+
+    if (browser === FALLBACK_BROWSER_ID)
+        return _getJsonSearchEngine('chromium');
+
+    if (browser === GOOGLE_CHROME_ID)
+        return _getJsonSearchEngine('google-chrome');
+
+    return null;
+}
diff --git a/js/ui/internetSearch.js b/js/ui/internetSearch.js
new file mode 100644
index 0000000000..ef8ab1d36d
--- /dev/null
+++ b/js/ui/internetSearch.js
@@ -0,0 +1,138 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+/* exported getInternetSearchProvider */
+
+const { GLib, Gio } = imports.gi;
+
+const Util = imports.misc.util;
+
+// Returns a plain URI if the user types in
+// something like "facebook.com"
+function getURIForSearch(terms) {
+    let searchedUris = Util.findSearchUrls(terms);
+    // Make sure search contains only a uri
+    // Avoid cases like "what is github.com"
+    if (searchedUris.length === 1 && terms.length === 1) {
+        let uri = searchedUris[0];
+        // Ensure all uri has a scheme name
+        if (!GLib.uri_parse_scheme(uri))
+            uri = 'http://'.format(uri);
+
+        return uri;
+    } else {
+        return null;
+    }
+}
+
+function getInternetSearchProvider() {
+    let browserApp = Util.getBrowserApp();
+    if (browserApp)
+        return new InternetSearchProvider(browserApp);
+
+    return null;
+}
+
+var InternetSearchProvider = class {
+    constructor(browserApp) {
+        this.id = 'internet';
+        this.appInfo = browserApp.get_app_info();
+        this.canLaunchSearch = true;
+        this.isRemoteProvider = false;
+
+        this._engineNameParsed = false;
+        this._engineName = null;
+
+        this._networkMonitor = Gio.NetworkMonitor.get_default();
+    }
+
+    _getEngineName() {
+        if (!this._engineNameParsed) {
+            this._engineNameParsed = true;
+            this._engineName = Util.getSearchEngineName();
+        }
+
+        return this._engineName;
+    }
+
+    _launchURI(uri) {
+        try {
+            this.appInfo.launch_uris([uri], null);
+        } catch (e) {
+            logError(e, 'error while launching browser for uri: %s'.format(uri));
+        }
+    }
+
+    getResultMetas(results, callback) {
+        let metas = results.map(resultId => {
+            let name;
+            if (resultId.startsWith('uri:')) {
+                let uri = resultId.slice('uri:'.length);
+                name = _('Open "%s" in browser').format(uri);
+            } else if (resultId.startsWith('search:')) {
+                let query = resultId.slice('search:'.length);
+                let engineName = this._getEngineName();
+
+                if (engineName) {
+                    /* Translators: the first %s is the search engine name, and the second
+                     * is the search string. For instance, 'Search Google for "hello"'.
+                     */
+                    name = _('Search %s for "%s"').format(engineName, query);
+                } else {
+                    name = _('Search the internet for "%s"').format(query);
+                }
+            }
+
+            return {
+                id: resultId,
+                name,
+                // We will already have an app icon next to our result,
+                // so we don't need an individual result icon.
+                createIcon() {
+                    return null;
+                },
+            };
+        });
+        callback(metas);
+    }
+
+    filterResults(results, maxNumber) {
+        return results.slice(0, maxNumber);
+    }
+
+    getInitialResultSet(terms, callback, _cancellable) {
+        let results = [];
+
+        if (this._networkMonitor.network_available) {
+            let uri = getURIForSearch(terms);
+            let query = terms.join(' ');
+            if (uri)
+                results.push('uri:%s'.format(query));
+            else
+                results.push('search:'.format(query));
+        }
+
+        callback(results);
+    }
+
+    getSubsearchResultSet(previousResults, terms, callback, cancellable) {
+        this.getInitialResultSet(terms, callback, cancellable);
+    }
+
+    activateResult(metaId) {
+        if (metaId.startsWith('uri:')) {
+            let uri = metaId.slice('uri:'.length);
+            uri = getURIForSearch([uri]);
+            this._launchURI(uri);
+        } else if (metaId.startsWith('search:')) {
+            let query = metaId.slice('search:'.length);
+            this._launchURI('? '.concat(query));
+        }
+    }
+
+    launchSearch(terms) {
+        this.getInitialResultSet(terms, results => {
+            if (results)
+                this.activateResult(results[0]);
+        });
+    }
+};
diff --git a/js/ui/search.js b/js/ui/search.js
index 88f06211c4..cce72fae3e 100644
--- a/js/ui/search.js
+++ b/js/ui/search.js
@@ -5,6 +5,7 @@ const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
 
 const AppDisplay = imports.ui.appDisplay;
 const IconGrid = imports.ui.iconGrid;
+const InternetSearch = imports.ui.internetSearch;
 const Main = imports.ui.main;
 const RemoteSearch = imports.ui.remoteSearch;
 const Util = imports.misc.util;
@@ -485,6 +486,11 @@ var SearchResultsView = GObject.registerClass({
 
         let appSystem = Shell.AppSystem.get_default();
         appSystem.connect('installed-changed', this._reloadRemoteProviders.bind(this));
+
+        this._internetProvider = InternetSearch.getInternetSearchProvider();
+        if (this._internetProvider)
+            this._registerProvider(this._internetProvider);
+
         this._reloadRemoteProviders();
     }
 
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 7824ba5b2d..872eb98eb5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -102,4 +102,5 @@ js/ui/endlessButton.js
 js/ui/forceAppExitDialog.js
 js/ui/hotCorner.js
 js/ui/iconGridLayout.js
+js/ui/internetSearch.js
 js/ui/userMenu.js


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