[gnome-shell/T27795: 52/138] Add Internet Search provider



commit 9e552b94af7915ff0ae0f380cdeaa6f6c6e4a79e
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).
    
    https://phabricator.endlessm.com/T20051

 js/js-resources.gresource.xml |   1 +
 js/misc/util.js               | 144 +++++++++++++++++++++++++++++++++++++++++-
 js/ui/internetSearch.js       | 133 ++++++++++++++++++++++++++++++++++++++
 js/ui/search.js               |   6 ++
 po/POTFILES.in                |   1 +
 5 files changed, 284 insertions(+), 1 deletion(-)
---
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index e0f5ee6bcd..2867080346 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/monitor.js</file>
     <file>ui/sideComponent.js</file>
     <file>ui/status/orientation.js</file>
diff --git a/js/misc/util.js b/js/misc/util.js
index db3742eb3d..05dcb4f2fa 100644
--- a/js/misc/util.js
+++ b/js/misc/util.js
@@ -1,16 +1,21 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported findUrls, spawn, spawnCommandLine, spawnApp, trySpawnCommandLine,
             formatTime, formatTimeSpan, createTimeLabel, insertSorted,
-            makeCloseButton, ensureActorVisibleInScrollView */
+            makeCloseButton, ensureActorVisibleInScrollView,
+            getSearchEngineName */
 
 const { Clutter, Gio, GLib, GObject, Shell, St } = imports.gi;
 const Gettext = imports.gettext;
 
+const Json = imports.gi.Json;
 const Main = imports.ui.main;
 const Params = imports.misc.params;
 
 var SCROLL_TIME = 100;
 
+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]';
@@ -55,6 +60,67 @@ function findUrls(str) {
     return res;
 }
 
+// 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(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;
+}
+
 // spawn:
 // @argv: an argv array
 //
@@ -429,3 +495,79 @@ function ensureActorVisibleInScrollView(scrollView, actor) {
         duration: SCROLL_TIME
     });
 }
+
+function getBrowserId() {
+    let id = FALLBACK_BROWSER_ID;
+    let app = Gio.app_info_get_default_for_type('x-scheme-handler/http', true);
+    if (app != null)
+        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 ' + 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..9e8911f13c
--- /dev/null
+++ b/js/ui/internetSearch.js
@@ -0,0 +1,133 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const { GLib, Gio } = imports.gi;
+
+const Main = imports.ui.main;
+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://' + 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: ' + 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: 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:' + query);
+            else
+                results.push('search:' + 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 8d70d35abb..b806ce5d09 100644
--- a/js/ui/search.js
+++ b/js/ui/search.js
@@ -5,6 +5,7 @@ const Signals = imports.signals;
 
 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;
@@ -464,6 +465,11 @@ var SearchResults = class {
 
         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 eb16422faa..02b385aa10 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -98,5 +98,6 @@ js/ui/endlessButton.js
 js/ui/forceAppExitDialog.js
 js/ui/hotCorner.js
 js/ui/iconGridLayout.js
+js/ui/internetSearch.js
 js/ui/status/orientation.js
 js/ui/userMenu.js


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