Bug 1428948 - Add policies to modify the available search engines draft
authorKirk Steuber <ksteuber@mozilla.com>
Fri, 02 Mar 2018 12:11:16 -0800
changeset 772650 53dcec859629116373e3b734552f084a7e278ec2
parent 772574 7b9da7139d94951431a148dcaf8a388640c91b27
push id104010
push userksteuber@mozilla.com
push dateMon, 26 Mar 2018 18:44:23 +0000
bugs1428948
milestone61.0a1
Bug 1428948 - Add policies to modify the available search engines 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
@@ -344,16 +344,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,15 +1,17 @@
 [DEFAULT]
 prefs =
   browser.policies.enabled=true
 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]
@@ -31,9 +33,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",