Bug 1547285 - Add the registerProvider function API. r=adw,mixedpuppy
authorMarco Bonardo <mbonardo@mozilla.com>
Fri, 31 May 2019 09:30:15 +0000
changeset 476347 0efaeeb612920be2733bac63ce4374995da0a91a
parent 476346 8f6b95ca75d779cbc094955fce11200c51537331
child 476348 4f62dfaee27f3d0620a44d092a85ee652bf0a0ca
push id36092
push userarchaeopteryx@coole-files.de
push dateFri, 31 May 2019 17:03:46 +0000
treeherdermozilla-central@8384972e1f6a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw, mixedpuppy
bugs1547285, 1547279, 1547666
milestone69.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 1547285 - Add the registerProvider function API. r=adw,mixedpuppy This implements the first part of the new urlbar API to be used by many future mozilla-signed experiments. This part sets the foundation for the "urlbar" permission and API, and introduces a first API to register an extension provider. Further API calls will be added in dependencies of Bug 1547279, and in particular Bug 1547666 will add a way for these extension providers to return results for the urlbar. Differential Revision: https://phabricator.services.mozilla.com/D32332
browser/components/extensions/ext-browser.json
browser/components/extensions/jar.mn
browser/components/extensions/parent/ext-urlbar.js
browser/components/extensions/schemas/jar.mn
browser/components/extensions/schemas/urlbar.json
browser/components/extensions/test/xpcshell/test_ext_urlbar.js
browser/components/extensions/test/xpcshell/xpcshell-common.ini
browser/components/urlbar/UrlbarProvidersManager.jsm
browser/components/urlbar/UrlbarUtils.jsm
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
--- a/browser/components/extensions/ext-browser.json
+++ b/browser/components/extensions/ext-browser.json
@@ -185,16 +185,24 @@
     "url": "chrome://browser/content/parent/ext-tabs.js",
     "schema": "chrome://browser/content/schemas/tabs.json",
     "scopes": ["addon_parent"],
     "events": ["update", "disable"],
     "paths": [
       ["tabs"]
     ]
   },
+  "urlbar": {
+    "url": "chrome://browser/content/parent/ext-urlbar.js",
+    "schema": "chrome://browser/content/schemas/urlbar.json",
+    "scopes": ["addon_parent"],
+    "paths": [
+      ["urlbar"]
+    ]
+  },
   "urlOverrides": {
     "url": "chrome://browser/content/parent/ext-url-overrides.js",
     "schema": "chrome://browser/content/schemas/url_overrides.json",
     "scopes": ["addon_parent"],
     "events": ["update", "uninstall", "disable"],
     "manifest": ["chrome_url_overrides"],
     "paths": [
       ["urlOverrides"]
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -29,16 +29,17 @@ browser.jar:
     content/browser/parent/ext-omnibox.js (parent/ext-omnibox.js)
     content/browser/parent/ext-pageAction.js (parent/ext-pageAction.js)
     content/browser/parent/ext-pkcs11.js (parent/ext-pkcs11.js)
     content/browser/parent/ext-search.js (parent/ext-search.js)
     content/browser/parent/ext-sessions.js (parent/ext-sessions.js)
     content/browser/parent/ext-sidebarAction.js (parent/ext-sidebarAction.js)
     content/browser/parent/ext-tabs.js (parent/ext-tabs.js)
     content/browser/parent/ext-url-overrides.js (parent/ext-url-overrides.js)
+    content/browser/parent/ext-urlbar.js (parent/ext-urlbar.js)
     content/browser/parent/ext-windows.js (parent/ext-windows.js)
     content/browser/child/ext-browser.js (child/ext-browser.js)
     content/browser/child/ext-browser-content-only.js (child/ext-browser-content-only.js)
     content/browser/child/ext-devtools-inspectedWindow.js (child/ext-devtools-inspectedWindow.js)
     content/browser/child/ext-devtools-network.js (child/ext-devtools-network.js)
     content/browser/child/ext-devtools-panels.js (child/ext-devtools-panels.js)
     content/browser/child/ext-devtools.js (child/ext-devtools.js)
     content/browser/child/ext-menus.js (child/ext-menus.js)
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/parent/ext-urlbar.js
@@ -0,0 +1,29 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+});
+
+this.urlbar = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      urlbar: {
+        /**
+         * Event fired before a search starts, to get the provider behavior.
+         */
+        onQueryReady: new EventManager(context, "urlbar.onQueryReady", (fire, name) => {
+          UrlbarProvidersManager.addExtensionListener(
+            name, "queryready", queryContext => {
+              if (queryContext.isPrivate && !context.privateBrowsingAllowed) {
+                return Promise.resolve("inactive");
+              }
+              return fire.async(queryContext);
+            });
+          return () => {
+            UrlbarProvidersManager.removeExtensionListener(name, "queryready");
+          };
+        }).api(),
+      },
+    };
+  }
+};
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -19,9 +19,10 @@ browser.jar:
     content/browser/schemas/omnibox.json
     content/browser/schemas/page_action.json
     content/browser/schemas/pkcs11.json
     content/browser/schemas/search.json
     content/browser/schemas/sessions.json
     content/browser/schemas/sidebar_action.json
     content/browser/schemas/tabs.json
     content/browser/schemas/url_overrides.json
+    content/browser/schemas/urlbar.json
     content/browser/schemas/windows.json
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/urlbar.json
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+[
+  {
+    "namespace": "manifest",
+    "types": [{
+      "$extend": "Permission",
+      "choices": [{
+        "type": "string",
+        "enum": [
+          "urlbar"
+        ]
+      }]
+    }]
+  },
+  {
+    "namespace": "urlbar",
+    "description": "Use the <code>browser.urlbar</code> API to experiment with new features in the URLBar. Restricted to Mozilla privileged WebExtensions.",
+    "permissions": [
+      "urlbar"
+    ],
+    "types": [
+      {
+        "id": "SourceType",
+        "type": "string",
+        "description": "The source of a result.",
+        "enum": ["bookmarks", "history", "search", "tabs", "local", "network"]
+      },
+      {
+        "id": "QueryContext",
+        "type": "object",
+        "description": "Context of the current query request.",
+        "properties": {
+          "isPrivate": {
+            "type": "boolean",
+            "description": "Whether the browser context is private."
+          },
+          "maxResults": {
+            "type": "integer",
+            "description": "The maximum number of results shown to the user."
+          },
+          "searchString": {
+            "type": "string",
+            "description": "The current search string."
+          },
+          "acceptableSources": {
+            "type": "array",
+            "description": "List of acceptable SourceType to return.",
+            "items": {
+              "$ref": "SourceType"
+            }
+          }
+        }
+      }
+    ],
+    "events": [
+      {
+        "name": "onQueryReady",
+        "type": "function",
+        "description": "Fired before starting a search to get the provider's behavior.",
+        "parameters": [
+          {
+            "name": "context",
+            "$ref": "QueryContext"
+          }
+        ],
+        "extraParameters": [
+          {
+            "name": "name",
+            "type": "string",
+            "description": "Name of the provider.",
+            "pattern": "^[a-zA-Z0-9_-]+$"
+          }
+        ],
+        "returns": {
+          "type": "string",
+          "description": "Whether this provider should be queried, and if it wants to restrict results",
+          "enum": ["active", "inactive", "restricting"]
+        }
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
@@ -0,0 +1,181 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  UrlbarController: "resource:///modules/UrlbarController.jsm",
+  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+  UrlbarQueryContext: "resource:///modules/UrlbarUtils.jsm",
+});
+
+add_task(async function test_urlbar_without_urlbar_permission() {
+  let ext = ExtensionTestUtils.loadExtension({
+    isPrivileged: true,
+    background() {
+      browser.test.assertEq(browser.urlbar, undefined,
+                            "'urlbar' permission is required");
+    },
+  });
+  await ext.startup();
+  await ext.unload();
+});
+
+add_task(async function test_urlbar_no_privilege() {
+  let ext = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["urlbar"],
+    },
+    background() {
+      browser.test.assertEq(browser.urlbar, undefined,
+                            "'urlbar' permission is privileged");
+    },
+  });
+  await ext.startup();
+  await ext.unload();
+});
+
+add_task(async function test_privateBrowsing_not_allowed() {
+  let ext = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["urlbar"],
+      incognito: "not_allowed",
+    },
+    isPrivileged: true,
+    background() {
+      browser.urlbar.onQueryReady.addListener(queryContext => {
+        browser.test.notifyFail("urlbar called in private browsing");
+        return "active";
+      }, "Test-private");
+    },
+  });
+  await ext.startup();
+
+  // Run a query, this should execute the above listeners and checks, plus it
+  // will set the provider's isActive and isRestricting.
+  let queryContext = new UrlbarQueryContext({
+    allowAutofill: false,
+    isPrivate: true,
+    maxResults: 10,
+    searchString: "*",
+  });
+  let controller = new UrlbarController({
+    browserWindow: {
+      location: {
+        href: AppConstants.BROWSER_CHROME_URL,
+      },
+    },
+  });
+  await UrlbarProvidersManager.startQuery(queryContext, controller);
+  // Check the providers behavior has been setup properly.
+  let provider = UrlbarProvidersManager.providers
+                                       .find(p => p.name == "Test-private");
+  Assert.ok(!provider.isActive({}), "Check provider is inactive");
+
+  await ext.unload();
+});
+
+add_task(async function test_privateBrowsing_allowed() {
+  let ext = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["urlbar"],
+    },
+    isPrivileged: true,
+    incognitoOverride: "spanning",
+    background() {
+      browser.urlbar.onQueryReady.addListener(queryContext => {
+        return "active";
+      }, "Test-private");
+    },
+  });
+  await ext.startup();
+
+  // Run a query, this should execute the above listeners and checks, plus it
+  // will set the provider's isActive and isRestricting.
+  let queryContext = new UrlbarQueryContext({
+    allowAutofill: false,
+    isPrivate: true,
+    maxResults: 10,
+    searchString: "*",
+  });
+  let controller = new UrlbarController({
+    browserWindow: {
+      location: {
+        href: AppConstants.BROWSER_CHROME_URL,
+      },
+    },
+  });
+  await UrlbarProvidersManager.startQuery(queryContext, controller);
+  // Check the providers behavior has been setup properly.
+  let provider = UrlbarProvidersManager.providers
+                                       .find(p => p.name == "Test-private");
+  Assert.ok(provider.isActive({}), "Check provider is active");
+
+  await ext.unload();
+});
+
+add_task(async function test_registerProvider() {
+  // A copy of the default providers.
+  let providers = UrlbarProvidersManager.providers.slice();
+
+  let ext = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["urlbar"],
+    },
+    isPrivileged: true,
+    incognitoOverride: "spanning",
+    background() {
+      for (let state of ["active", "inactive", "restricting"]) {
+        let name = `Test-${state}`;
+        browser.urlbar.onQueryReady.addListener(queryContext => {
+          browser.test.assertFalse(queryContext.isPrivate,
+                                   "Context is non private");
+          browser.test.assertEq(queryContext.maxResults, 10,
+                                "Check maxResults");
+          browser.test.assertTrue(queryContext.searchString,
+                                  "SearchString is non empty");
+          browser.test.assertTrue(Array.isArray(queryContext.acceptableSources),
+                                  "acceptableSources is an array");
+          return state;
+        }, name);
+      }
+    },
+  });
+  await ext.startup();
+
+  Assert.greater(UrlbarProvidersManager.providers.length, providers.length,
+                 "Providers have been added");
+
+  // Run a query, this should execute the above listeners and checks, plus it
+  // will set the provider's isActive and isRestricting.
+  let queryContext = new UrlbarQueryContext({
+    allowAutofill: false,
+    isPrivate: false,
+    maxResults: 10,
+    searchString: "*",
+  });
+  let controller = new UrlbarController({
+    browserWindow: {
+      location: {
+        href: AppConstants.BROWSER_CHROME_URL,
+      },
+    },
+  });
+  await UrlbarProvidersManager.startQuery(queryContext, controller);
+  // Check the providers behavior has been setup properly.
+  for (let provider of UrlbarProvidersManager.providers) {
+    if (!provider.name.startsWith("Test")) {
+      continue;
+    }
+    let [, state] = provider.name.split("-");
+    let isActive = state != "inactive";
+    let restricting = state == "restricting";
+    Assert.equal(isActive, provider.isActive(queryContext),
+                 "Check active callback");
+    Assert.equal(restricting, provider.isRestricting(queryContext),
+                 "Check restrict callback");
+  }
+
+  await ext.unload();
+
+  // Sanity check the providers.
+  Assert.deepEqual(UrlbarProvidersManager.providers, providers,
+                   "Should return to the default providers");
+});
--- a/browser/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/browser/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -7,10 +7,12 @@
 [test_ext_chrome_settings_overrides_update.js]
 [test_ext_distribution_popup.js]
 [test_ext_history.js]
 [test_ext_settings_overrides_search.js]
 [test_ext_settings_overrides_search_mozParam.js]
 [test_ext_settings_overrides_shutdown.js]
 [test_ext_url_overrides_newtab.js]
 [test_ext_url_overrides_newtab_update.js]
+[test_ext_urlbar.js]
+skip-if = os == "android"
 [test_ext_homepage_overrides_private.js]
 
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -13,16 +13,17 @@ var EXPORTED_SYMBOLS = ["UrlbarProviders
 
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetters(this, {
   Log: "resource://gre/modules/Log.jsm",
   PlacesUtils: "resource://modules/PlacesUtils.jsm",
   UrlbarMuxer: "resource:///modules/UrlbarUtils.jsm",
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
   UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+  UrlbarProviderExtension: "resource:///modules/UrlbarUtils.jsm",
   UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
   UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(this, "logger", () =>
   Log.repository.getLogger("Urlbar.ProvidersManager"));
 
 // List of available local providers, each is implemented in its own jsm module
@@ -66,16 +67,61 @@ class ProvidersManager {
     this.interruptLevel = 0;
 
     // This maps muxer names to muxers.
     this.muxers = new Map();
     for (let [symbol, module] of Object.entries(localMuxerModules)) {
       let {[symbol]: muxer} = ChromeUtils.import(module, {});
       this.registerMuxer(muxer);
     }
+
+    // Extension listeners.
+    this._extensionListeners = new Map([
+      ["queryready", new Map()],
+    ]);
+  }
+
+  /**
+   * Registers an extension listener for a specific event.
+   * When queries are executed the extension gets queries through these
+   * listeners. There can only be one listener per [provider, event] tuple.
+   * For the special and mandatory "queryready" event, this also registers a
+   * new extension provider.
+   * @param {string} providerName
+   *   The name of the provider to add.
+   * @param {string} eventName
+   *   The name of the event to register.
+   * @param {function} callback
+   *   The callback to be invoked. Gets the UrlbarQueryContext as argument and
+   *   returns a string having a value of "active", "inactive" or "restricting".
+   */
+  addExtensionListener(providerName, eventName, callback) {
+    // The "queryready" event is the first one to be registered, and it's
+    // mandatory, thus we register and unregister the provider on it.
+    if (eventName == "queryready") {
+      let provider = new UrlbarProviderExtension(providerName);
+      this.registerProvider(provider);
+    }
+    this._extensionListeners.get(eventName).set(providerName, callback);
+  }
+
+  /**
+   * Removes a previously added extension listener.
+   * For the special and mandatory "queryready" event, this also unregisters a
+   * previously added extension provider.
+   * @param {string} providerName
+   *   The name of the provider to remove.
+   * @param {string} eventName
+   *   The name of the event to unregister.
+   */
+  removeExtensionListener(providerName, eventName) {
+    this._extensionListeners.get(eventName).delete(providerName);
+    if (eventName == "queryready") {
+      this.unregisterProvider({name: providerName}, true);
+    }
   }
 
   /**
    * Registers a provider object with the manager.
    * @param {object} provider
    */
   registerProvider(provider) {
     if (!provider || !(provider instanceof UrlbarProvider)) {
@@ -90,21 +136,26 @@ class ProvidersManager {
     } else {
       this.providers.push(provider);
     }
   }
 
   /**
    * Unregisters a previously registered provider object.
    * @param {object} provider
+   * @param {boolean} isExtensionRequest
+   *   Whether this request comes from an extension. Extensions can only remove
+   *   EXTENSION providers.
    */
-  unregisterProvider(provider) {
+  unregisterProvider(provider, isExtensionRequest = false) {
     logger.info(`Unregistering provider ${provider.name}`);
-    let index = this.providers.indexOf(provider);
-    if (index != -1) {
+    let index = this.providers.findIndex(p => p.name == provider.name);
+    if (index != -1 &&
+        (!isExtensionRequest ||
+         this.providers[index].type == UrlbarUtils.PROVIDER_TYPE.EXTENSION)) {
       this.providers.splice(index, 1);
     }
   }
 
   /**
    * Registers a muxer object with the manager.
    * @param {object} muxer a UrlbarMuxer object
    */
@@ -143,16 +194,47 @@ class ProvidersManager {
     }
 
     // If the queryContext specifies a list of providers to use, filter on it,
     // otherwise just pass the full list of providers.
     let providers = queryContext.providers ?
                       this.providers.filter(p => queryContext.providers.includes(p.name)) :
                       this.providers;
 
+    // Apply tokenization.
+    UrlbarTokenizer.tokenize(queryContext);
+
+    // Array of acceptable RESULT_SOURCE values for this query. Providers can
+    // use queryContext.acceptableSources to decide whether they want to be
+    // invoked or not.
+    queryContext.acceptableSources = getAcceptableMatchSources(queryContext);
+    logger.debug(`Acceptable sources ${queryContext.acceptableSources}`);
+
+    // Update behavior for extension providers.
+    for (let [name, listener] of this._extensionListeners.get("queryready")) {
+      let behavior = "inactive";
+      // Handle bogus case where the extension may not be responding or may
+      // throw.
+      let timeoutPromise = new Promise((resolve, reject) => new SkippableTimer(() => {
+        Cu.reportError("An extension didn't handle the queryready callback");
+        reject();
+      }, 50));
+      try {
+        behavior = await Promise.race([timeoutPromise, listener(queryContext)]);
+      } catch (ex) {}
+      // Look up the provider and set its properties accordingly.
+      let provider =
+        UrlbarProvidersManager.providers.find(p => p.name == name);
+      if (provider) {
+        provider.behavior = behavior;
+      } else {
+        Cu.reportError("Couldn't find expected urlbar provider " + name);
+      }
+    }
+
     let query = new Query(queryContext, controller, muxer, providers);
     this.queries.set(queryContext, query);
     await query.start();
   }
 
   /**
    * Cancels a running query.
    * @param {object} queryContext
@@ -201,50 +283,42 @@ class Query {
   /**
    * Initializes the query object.
    * @param {object} queryContext
    *        The query context
    * @param {object} controller
    *        The controller to be notified
    * @param {object} muxer
    *        The muxer to sort results
-   * @param {object} providers
-   *        Map of all the providers by type and name
+   * @param {Array} providers
+   *        Array of all the providers.
    */
   constructor(queryContext, controller, muxer, providers) {
     this.context = queryContext;
     this.context.results = [];
     this.muxer = muxer;
     this.controller = controller;
     this.providers = providers;
     this.started = false;
     this.canceled = false;
     this.complete = false;
 
-    // Array of acceptable RESULT_SOURCE values for this query. Providers can
-    // use queryContext.acceptableSources to decide whether they want to be
-    // invoked or not.
-    // This is also used to filter results in add().
-    this.acceptableSources = [];
+    // This is used as a last safety filter in add(), thus we keep an unmodified
+    // copy of it.
+    this.acceptableSources = queryContext.acceptableSources.slice();
   }
 
   /**
    * Starts querying.
    */
   async start() {
     if (this.started) {
       throw new Error("This Query has been started already");
     }
     this.started = true;
-    UrlbarTokenizer.tokenize(this.context);
-
-    this.acceptableSources = getAcceptableMatchSources(this.context);
-    logger.debug(`Acceptable sources ${this.acceptableSources}`);
-    // Pass a copy so the provider can't modify our local version.
-    this.context.acceptableSources = this.acceptableSources.slice();
 
     // Check which providers should be queried.
     let providers = this.providers.filter(p => p.isActive(this.context));
     // Check if any of the remaining providers wants to restrict the search.
     let restrictProviders = providers.filter(p => p.isRestricting(this.context));
     if (restrictProviders.length) {
       providers = restrictProviders;
     }
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -7,16 +7,17 @@
 /**
  * This module exports the UrlbarUtils singleton, which contains constants and
  * helper functions that are useful to all components of the urlbar.
  */
 
 var EXPORTED_SYMBOLS = [
   "UrlbarMuxer",
   "UrlbarProvider",
+  "UrlbarProviderExtension",
   "UrlbarQueryContext",
   "UrlbarUtils",
 ];
 
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetters(this, {
   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
@@ -524,8 +525,38 @@ class UrlbarProvider {
    * @param {UrlbarQueryContext} queryContext the query context object to cancel
    *        query for.
    * @abstract
    */
   cancelQuery(queryContext) {
     throw new Error("Trying to access the base class, must be overridden");
   }
 }
+
+/**
+ * Class for an Extension UrlbarProvider.
+ */
+class UrlbarProviderExtension extends UrlbarProvider {
+  constructor(name) {
+    super();
+    this._name = name;
+    this.behavior = "inactive";
+  }
+  get name() {
+    return this._name;
+  }
+  get type() {
+    return UrlbarUtils.PROVIDER_TYPE.EXTENSION;
+  }
+  isActive(queryContext) {
+    return this.behavior != "inactive";
+  }
+  isRestricting(queryContext) {
+    return this.behavior == "restricting";
+  }
+  startQuery(queryContext, addCallback) {
+    // TODO (Bug 1547666)
+    return Promise.resolve();
+  }
+  cancelQuery(queryContext) {
+    // TODO (Bug 1547666)
+  }
+}
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -117,17 +117,17 @@ const PRIVATE_ALLOWED_PERMISSION = "inte
 // storage used by the browser.storage.local API is not directly accessible from the extension code,
 // it is defined and reserved as "userContextIdInternal.webextStorageLocal" in ContextualIdentityService.jsm).
 const WEBEXT_STORAGE_USER_CONTEXT_ID = -1 >>> 0;
 
 // The maximum time to wait for extension child shutdown blockers to complete.
 const CHILD_SHUTDOWN_TIMEOUT_MS = 8000;
 
 // Permissions that are only available to privileged extensions.
-const PRIVILEGED_PERMS = new Set(["mozillaAddons", "geckoViewAddons", "telemetry"]);
+const PRIVILEGED_PERMS = new Set(["mozillaAddons", "geckoViewAddons", "telemetry", "urlbar"]);
 
 /**
  * Classify an individual permission from a webextension manifest
  * as a host/origin permission, an api permission, or a regular permission.
  *
  * @param {string} perm  The permission string to classify
  * @param {boolean} restrictSchemes
  * @param {boolean} isPrivileged whether or not the webextension is privileged
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -438,16 +438,17 @@ const GRANTED_WITHOUT_USER_PROMPT = [
   "idle",
   "menus",
   "menus.overrideContext",
   "mozillaAddons",
   "search",
   "storage",
   "telemetry",
   "theme",
+  "urlbar",
   "webRequest",
   "webRequestBlocking",
 ];
 
 add_task(function test_permissions_have_localization_strings() {
   const ns = Schemas.getNamespace("manifest");
 
   const permissions = ns.get("Permission").choices;