Bug 1428948 - Add policies to modify the available search engines r=Felipe,florian
authorKirk Steuber <ksteuber@mozilla.com>
Fri, 02 Mar 2018 12:11:16 -0800
changeset 409973 ebad80a762663d07a29434ec612d194e133e09ea
parent 409972 51394984ff2b3338e0f1ba759c6b64a91ab39200
child 409974 80a1cfc4f0698c4703c58df7aa868fea523a3604
push id33716
push userbtara@mozilla.com
push dateTue, 27 Mar 2018 09:11:40 +0000
treeherdermozilla-central@6580b15bcd33 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersFelipe, florian
bugs1428948
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1428948 - Add policies to modify the available search engines r=Felipe,florian This adds a policy with the capability of adding search engines, choosing the default search engine, and blocking the installation of new search engines. Additionally, fixes the messages for errors reported by MainProcessSingleton.addSearchEngine so that the offending URL is printed rather than "[xpconnect wrapped nsIURI]". MozReview-Commit-ID: HuLT15Rnq0r
browser/components/enterprisepolicies/Policies.jsm
browser/components/enterprisepolicies/schemas/policies-schema.json
browser/components/enterprisepolicies/tests/browser/browser.ini
browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
browser/components/enterprisepolicies/tests/browser/opensearch.html
browser/components/enterprisepolicies/tests/browser/opensearchEngine.xml
browser/components/preferences/in-content/search.js
browser/modules/ContentLinkHandler.jsm
toolkit/components/processsingleton/MainProcessSingleton.js
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -342,16 +342,73 @@ var Policies = {
     }
   },
 
   "RememberPasswords": {
     onBeforeUIStartup(manager, param) {
       setAndLockPref("signon.rememberSignons", param);
     }
   },
+
+  "SearchEngines": {
+    onAllWindowsRestored(manager, param) {
+      Services.search.init(() => {
+        if (param.Add) {
+          // Only rerun if the list of engine names has changed.
+          let engineNameList = param.Add.map(engine => engine.Name);
+          runOncePerModification("addSearchEngines",
+                                 JSON.stringify(engineNameList),
+                                 () => {
+            for (let newEngine of param.Add) {
+              let newEngineParameters = {
+                template:    newEngine.URLTemplate,
+                iconURL:     newEngine.IconURL,
+                alias:       newEngine.Alias,
+                description: newEngine.Description,
+                method:      newEngine.Method,
+                suggestURL:  newEngine.SuggestURLTemplate,
+                extensionID: "set-via-policy"
+              };
+              try {
+                Services.search.addEngineWithDetails(newEngine.Name,
+                                                     newEngineParameters);
+              } catch (ex) {
+                log.error("Unable to add search engine", ex);
+              }
+            }
+          });
+        }
+        if (param.Default) {
+          runOnce("setDefaultSearchEngine", () => {
+            let defaultEngine;
+            try {
+              defaultEngine = Services.search.getEngineByName(param.Default);
+              if (!defaultEngine) {
+                throw "No engine by that name could be found";
+              }
+            } catch (ex) {
+              log.error(`Search engine lookup failed when attempting to set ` +
+                        `the default engine. Requested engine was ` +
+                        `"${param.Default}".`, ex);
+            }
+            if (defaultEngine) {
+              try {
+                Services.search.currentEngine = defaultEngine;
+              } catch (ex) {
+                log.error("Unable to set the default search engine", ex);
+              }
+            }
+          });
+        }
+        if (param.PreventInstalls) {
+          manager.disallowFeature("installSearchEngine");
+        }
+      });
+    }
+  }
 };
 
 /*
  * ====================
  * = HELPER FUNCTIONS =
  * ====================
  *
  * The functions below are helpers to be used by several policies.
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -362,11 +362,59 @@
       }
     },
 
     "RememberPasswords": {
       "description": "Enforces the setting to allow Firefox to remember saved logins and passwords. Both true and false values are accepted.",
       "first_available": "60.0",
 
       "type": "boolean"
+    },
+
+    "SearchEngines": {
+      "description": "Modifies the list of search engines built into Firefox",
+      "first_available": "60.0",
+      "enterprise_only": true,
+
+      "type": "object",
+      "properties": {
+        "Add": {
+          "type": "array",
+          "items": {
+            "type": "object",
+            "required": ["Name", "URLTemplate"],
+
+            "properties": {
+              "Name": {
+                "type": "string"
+              },
+              "IconURL": {
+                "type": "URLorEmpty"
+              },
+              "Alias": {
+                "type": "string"
+              },
+              "Description": {
+                "type": "string"
+              },
+              "Method": {
+                "type": "string",
+                "enum": ["GET", "POST"]
+              },
+              "URLTemplate": {
+                "type": "string"
+              },
+              "SuggestURLTemplate": {
+                "type": "string"
+              }
+            }
+          }
+        },
+        "Default": {
+          "type": "string"
+        },
+        "PreventInstalls": {
+          "type": "boolean"
+        }
+      }
     }
   }
 }
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -1,13 +1,15 @@
 [DEFAULT]
 support-files =
   head.js
   config_popups_cookies_addons_flash.json
   config_broken_json.json
+  opensearch.html
+  opensearchEngine.xml
 
 [browser_policies_basic_tests.js]
 [browser_policies_broken_json.js]
 [browser_policies_enterprise_only.js]
 [browser_policies_notice_in_aboutpreferences.js]
 [browser_policies_popups_cookies_addons_flash.js]
 [browser_policies_runOnce_helper.js]
 [browser_policies_setAndLockPref_API.js]
@@ -29,9 +31,10 @@ support-files =
 [browser_policy_disable_pdfjs.js]
 [browser_policy_disable_pocket.js]
 [browser_policy_disable_privatebrowsing.js]
 [browser_policy_disable_safemode.js]
 [browser_policy_disable_shield.js]
 [browser_policy_display_bookmarks.js]
 [browser_policy_display_menu.js]
 [browser_policy_proxy.js]
+[browser_policy_search_engine.js]
 [browser_policy_set_homepage.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("browser.policies.runonce.setDefaultSearchEngine");
+  Services.prefs.clearUserPref("browser.policies.runOncePerModification.addSearchEngines");
+});
+
+// |shouldWork| should be true if opensearch is expected to work and false if
+// it is not.
+async function test_opensearch(shouldWork) {
+  await SpecialPowers.pushPrefEnv({ set: [
+    ["browser.search.widget.inNavBar", true],
+  ]});
+  let rootDir = getRootDirectory(gTestPath);
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, rootDir + "opensearch.html");
+  let searchPopup = document.getElementById("PopupSearchAutoComplete");
+  let searchBar = document.getElementById("searchbar");
+  let promiseSearchPopupShown = BrowserTestUtils.waitForEvent(searchPopup, "popupshown");
+  let searchBarButton = document.getAnonymousElementByAttribute(searchBar,
+                                                                "anonid",
+                                                                "searchbar-search-button");
+  searchBarButton.click();
+  await promiseSearchPopupShown;
+  let oneOffsContainer = document.getAnonymousElementByAttribute(searchPopup,
+                                                                 "anonid",
+                                                                 "search-one-off-buttons");
+  let engineListElement = document.getAnonymousElementByAttribute(oneOffsContainer,
+                                                                  "anonid",
+                                                                  "add-engines");
+  if (shouldWork) {
+    ok(engineListElement.firstChild,
+       "There should be search engines available to add");
+    ok(searchBar.getAttribute("addengines"),
+       "Search bar should have addengines attribute");
+  } else {
+    is(engineListElement.firstChild, null,
+       "There should be no search engines available to add");
+    ok(!searchBar.getAttribute("addengines"),
+       "Search bar should not have addengines attribute");
+  }
+  await BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_install_and_set_default() {
+  // Make sure we are starting in an expected state to avoid false positive
+  // test results.
+  isnot(Services.search.currentEngine.name, "MozSearch",
+        "Default search engine should not be MozSearch when test starts");
+  is(Services.search.getEngineByName("Foo"), null,
+     "Engine \"Foo\" should not be present when test starts");
+
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "SearchEngines": {
+        "Add": [
+          {
+            "Name": "MozSearch",
+            "URLTemplate": "http://example.com/?q={searchTerms}"
+          }
+        ],
+        "Default": "MozSearch"
+      }
+    }
+  });
+
+  // If this passes, it means that the new search engine was properly installed
+  // *and* was properly set as the default.
+  is(Services.search.currentEngine.name, "MozSearch",
+     "Specified search engine should be the default");
+
+  // Clean up
+  Services.search.removeEngine(Services.search.currentEngine);
+});
+
+add_task(async function test_opensearch_works() {
+  // Ensure that opensearch works before we make sure that it can be properly
+  // disabled
+  await test_opensearch(true);
+});
+
+add_task(async function setup_prevent_installs() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "SearchEngines": {
+        "PreventInstalls": true
+      }
+    }
+  });
+});
+
+add_task(async function test_prevent_install_ui() {
+  // Check that about:preferences does not prompt user to install search engines
+  // if that feature is disabled
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences");
+  await ContentTask.spawn(tab.linkedBrowser, null, async function() {
+    let linkContainer = content.document.getElementById("addEnginesBox");
+    if (!linkContainer.hidden) {
+      await new Promise(resolve => {
+        let mut = new linkContainer.ownerGlobal.MutationObserver(mutations => {
+          mut.disconnect();
+          resolve();
+        });
+        mut.observe(linkContainer, {attributeFilter: ["hidden"]});
+      });
+    }
+    is(linkContainer.hidden, true,
+       "\"Find more search engines\" link should be hidden");
+  });
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_opensearch_disabled() {
+  // Check that search engines cannot be added via opensearch
+  await test_opensearch(false);
+});
+
+add_task(async function test_AddSearchProvider() {
+  // Mock the modal error dialog
+  let mockPrompter = {
+    promptCount: 0,
+    alert() {
+      this.promptCount++;
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
+  };
+  let windowWatcher = {
+    getNewPrompter: () => mockPrompter,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher]),
+  };
+  let origWindowWatcher = Services.ww;
+  Services.ww = windowWatcher;
+  registerCleanupFunction(() => {
+    Services.ww = origWindowWatcher;
+  });
+
+  let engineURL = getRootDirectory(gTestPath) + "opensearchEngine.xml";
+  // AddSearchProvider will refuse to take URLs with a "chrome:" scheme
+  engineURL = engineURL.replace("chrome://mochitests/content", "http://example.com");
+  await ContentTask.spawn(gBrowser.selectedBrowser, {engineURL}, async function(args) {
+    content.window.external.AddSearchProvider(args.engineURL);
+  });
+
+  is(Services.search.getEngineByName("Foo"), null,
+     "Engine should not have been added successfully.");
+  is(mockPrompter.promptCount, 1,
+     "Should have alerted the user of an error when installing new search engine");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/opensearch.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="newEngine" href="http://mochi.test:8888/browser/browser/components/enterprisepolicies/tests/browser/opensearchEngine.xml">
+</head>
+<body></body>
+</html>
copy from browser/components/search/test/testEngine.xml
copy to browser/components/enterprisepolicies/tests/browser/opensearchEngine.xml
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -35,20 +35,25 @@ var gSearchPane = {
       .getService(Ci.mozIPlacesAutoComplete);
   },
 
   init() {
     gEngineView = new EngineView(new EngineStore());
     document.getElementById("engineList").view = gEngineView;
     this.buildDefaultEngineDropDown();
 
-    let addEnginesLink = document.getElementById("addEngines");
-    let searchEnginesURL = Services.wm.getMostRecentWindow("navigator:browser")
-                                      .BrowserSearch.searchEnginesURL;
-    addEnginesLink.setAttribute("href", searchEnginesURL);
+    if (Services.policies &&
+        !Services.policies.isAllowed("installSearchEngine")) {
+      document.getElementById("addEnginesBox").hidden = true;
+    } else {
+      let addEnginesLink = document.getElementById("addEngines");
+      let searchEnginesURL = Services.wm.getMostRecentWindow("navigator:browser")
+                                        .BrowserSearch.searchEnginesURL;
+      addEnginesLink.setAttribute("href", searchEnginesURL);
+    }
 
     window.addEventListener("click", this);
     window.addEventListener("command", this);
     window.addEventListener("dragstart", this);
     window.addEventListener("keypress", this);
     window.addEventListener("select", this);
     window.addEventListener("blur", this, true);
 
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/ContentLinkHandler.jsm
@@ -331,16 +331,20 @@ var ContentLinkHandler = {
               iconAdded ||
               !Services.prefs.getBoolPref("browser.chrome.site_icons")) {
             break;
           }
 
           iconAdded = handleFaviconLink(link, isRichIcon, chromeGlobal, faviconLoads);
           break;
         case "search":
+          if (Services.policies &&
+              !Services.policies.isAllowed("installSearchEngine")) {
+            break;
+          }
           if (!searchAdded && event.type == "DOMLinkAdded") {
             var type = link.type && link.type.toLowerCase();
             type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
 
             let re = /^(?:https?|ftp):/i;
             if (type == "application/opensearchdescription+xml" && link.title &&
                 re.test(link.href)) {
               let engine = { title: link.title, href: link.href };
--- a/toolkit/components/processsingleton/MainProcessSingleton.js
+++ b/toolkit/components/processsingleton/MainProcessSingleton.js
@@ -26,20 +26,25 @@ MainProcessSingleton.prototype = {
     if (browser.mIconURL && (!tabbrowser || tabbrowser.shouldLoadFavIcon(pageURL)))
       iconURL = NetUtil.newURI(browser.mIconURL);
 
     try {
       // Make sure the URLs are HTTP, HTTPS, or FTP.
       let isWeb = ["https", "http", "ftp"];
 
       if (!isWeb.includes(engineURL.scheme))
-        throw "Unsupported search engine URL: " + engineURL;
+        throw "Unsupported search engine URL: " + engineURL.spec;
 
       if (iconURL && !isWeb.includes(iconURL.scheme))
-        throw "Unsupported search icon URL: " + iconURL;
+        throw "Unsupported search icon URL: " + iconURL.spec;
+
+      if (Services.policies &&
+          !Services.policies.isAllowed("installSearchEngine")) {
+        throw "Search Engine installation blocked by the Enterprise Policy Manager.";
+      }
     } catch (ex) {
       Cu.reportError("Invalid argument passed to window.external.AddSearchProvider: " + ex);
 
       var searchBundle = Services.strings.createBundle("chrome://global/locale/search/search.properties");
       var brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
       var brandName = brandBundle.GetStringFromName("brandShortName");
       var title = searchBundle.GetStringFromName("error_invalid_format_title");
       var msg = searchBundle.formatStringFromName("error_invalid_engine_msg2",