Bug 1225715: Part 5 - Add schema for extension manifests. r?billm draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 22 Jan 2016 17:20:27 -0800
changeset 324470 a0978eeeac09d88a0e6505d71f68896eaefd0fb9
parent 324469 bda062baae5de13fd0fb6cd608580c95ddc2c991
child 324471 20603a1a400e7c94c156660984d1824488ec5a67
push id9922
push usermaglione.k@gmail.com
push dateSat, 23 Jan 2016 01:23:37 +0000
reviewersbillm
bugs1225715
milestone46.0a1
Bug 1225715: Part 5 - Add schema for extension manifests. r?billm This currently forbids unknown top-level schema properties, and unknown permissions. In the future, I'd like to make those warnings rather than errors, for compatibility purposes, but I think errors are fine for now.
browser/components/extensions/schemas/bookmarks.json
browser/components/extensions/schemas/browser_action.json
browser/components/extensions/schemas/context_menus.json
browser/components/extensions/schemas/page_action.json
browser/components/extensions/schemas/tabs.json
browser/components/extensions/schemas/windows.json
browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
testing/specialpowers/content/specialpowersAPI.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/schemas/cookies.json
toolkit/components/extensions/schemas/i18n.json
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/schemas/manifest.json
toolkit/components/extensions/schemas/web_navigation.json
toolkit/components/extensions/schemas/web_request.json
toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
--- a/browser/components/extensions/schemas/bookmarks.json
+++ b/browser/components/extensions/schemas/bookmarks.json
@@ -1,14 +1,28 @@
 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "bookmarks"
+          ]
+        }]
+      }
+    ]
+  },
+  {
     "namespace": "bookmarks",
     "description": "Use the <code>browser.bookmarks</code> API to create, organize, and otherwise manipulate bookmarks. Also see $(topic:override)[Override Pages], which you can use to create a custom Bookmark Manager page.",
     "types": [
       {
         "id": "BookmarkTreeNodeUnmodifiable",
         "type": "string",
         "enum": ["managed"],
         "description": "Indicates the reason why this node is unmodifiable. The <var>managed</var> value indicates that this node was configured by the system administrator or by the custodian of a supervised user. Omitted if the node can be modified by the user and the extension (default)."
--- a/browser/components/extensions/schemas/browser_action.json
+++ b/browser/components/extensions/schemas/browser_action.json
@@ -1,14 +1,33 @@
 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "browser_action": {
+            "type": "object",
+            "properties": {
+              "default_title": { "type": "string", "optional": true },
+              "default_icon": { "$ref": "IconPath", "optional": true },
+              "default_popup": { "type": "string", "format": "relativeUrl", "optional": true }
+            },
+            "optional": true
+          }
+        }
+      }
+    ]
+  },
+  {
     "namespace": "browserAction",
     "description": "Use browser actions to put icons in the main browser toolbar, to the right of the address bar. In addition to its icon, a browser action can also have a tooltip, a badge, and a popup.",
     "types": [
       {
         "id": "ColorArray",
         "type": "array",
         "items": {
           "type": "integer",
--- a/browser/components/extensions/schemas/context_menus.json
+++ b/browser/components/extensions/schemas/context_menus.json
@@ -1,14 +1,28 @@
 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "contextMenus"
+          ]
+        }]
+      }
+    ]
+  },
+  {
     "namespace": "contextMenus",
     "description": "Use the <code>browser.contextMenus</code> API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
     "properties": {
       "ACTION_MENU_TOP_LEVEL_LIMIT": {
         "value": 6,
         "description": "The maximum number of top level extension items that can be added to an extension action context menu. Any items beyond this limit will be ignored."
       }
     },
--- a/browser/components/extensions/schemas/page_action.json
+++ b/browser/components/extensions/schemas/page_action.json
@@ -1,14 +1,33 @@
 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "page_action": {
+            "type": "object",
+            "properties": {
+              "default_title": { "type": "string", "optional": true },
+              "default_icon": { "$ref": "IconPath", "optional": true },
+              "default_popup": { "type": "string", "format": "relativeUrl", "optional": true }
+            },
+            "optional": true
+          }
+        }
+      }
+    ]
+  },
+  {
     "namespace": "pageAction",
     "description": "Use the <code>browser.pageAction</code> API to put icons inside the address bar. Page actions represent actions that can be taken on the current page, but that aren't applicable to all pages.",
     "types": [
       {
         "id": "ImageDataType",
         "type": "object",
         "isInstanceOf": "ImageData",
         "additionalProperties": { "type": "any" },
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -1,14 +1,29 @@
 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "activeTab",
+            "tabs"
+          ]
+        }]
+      }
+    ]
+  },
+  {
     "namespace": "tabs",
     "description": "Use the <code>browser.tabs</code> API to interact with the browser's tab system. You can use this API to create, modify, and rearrange tabs in the browser.",
     "types": [
       { "id": "MutedInfoReason",
         "type": "string",
         "description": "An event that caused a muted state change.",
         "enum": [
           {"name": "user", "description": "A user input action has set/overridden the muted state."},
--- a/browser/components/extensions/schemas/windows.json
+++ b/browser/components/extensions/schemas/windows.json
@@ -1,14 +1,28 @@
 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "windows"
+          ]
+        }]
+      }
+    ]
+  },
+  {
     "namespace": "windows",
     "description": "Use the <code>browser.windows</code> API to interact with browser windows. You can use this API to create, modify, and rearrange windows in the browser.",
     "types": [
       {
         "id": "WindowType",
         "type": "string",
         "description": "The type of browser window this is. Under some circumstances a Window may not be assigned type property, for example when querying closed windows from the $(ref:sessions) API.",
         "enum": ["normal", "popup", "panel", "app", "devtools"]
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -369,55 +369,54 @@ add_task(function* testSecureURLsDenied(
       });
     },
   });
 
   yield extension.startup();
 
   yield extension.awaitFinish("setIcon security tests");
   yield extension.unload();
+});
 
 
+add_task(function* testSecureManifestURLsDenied() {
   // Test URLs included in the manifest.
 
   let urls = ["chrome://browser/content/browser.xul",
               "javascript:true"];
 
-  let matchURLForbidden = url => ({
-    message: new RegExp(`Loading extension.*Invalid icon data: NS_ERROR_DOM_BAD_URI`),
-  });
+  let apis = ["browser_action", "page_action"];
+
+  for (let url of urls) {
+    for (let api of apis) {
+      info(`TEST ${api} icon url: ${url}`);
 
-  // Because the underlying method throws an error on invalid data,
-  // only the first invalid URL of each component will be logged.
-  let messages = [matchURLForbidden(urls[0]),
-                  matchURLForbidden(urls[1])];
+      let matchURLForbidden = url => ({
+        message: new RegExp(`String "${url}" must be a relative URL`),
+      });
 
-  let waitForConsole = new Promise(resolve => {
-    // Not necessary in browser-chrome tests, but monitorConsole gripes
-    // if we don't call it.
-    SimpleTest.waitForExplicitFinish();
+      let messages = [matchURLForbidden(url)];
 
-    SimpleTest.monitorConsole(resolve, messages);
-  });
+      let waitForConsole = new Promise(resolve => {
+        // Not necessary in browser-chrome tests, but monitorConsole gripes
+        // if we don't call it.
+        SimpleTest.waitForExplicitFinish();
 
-  extension = ExtensionTestUtils.loadExtension({
-    manifest: {
-      "browser_action": {
-        "default_icon": {
-          "19": urls[0],
-          "38": urls[1],
+        SimpleTest.monitorConsole(resolve, messages);
+      });
+
+      let extension = ExtensionTestUtils.loadExtension({
+        manifest: {
+          [api]: {
+            "default_icon": url,
+          },
         },
-      },
-      "page_action": {
-        "default_icon": {
-          "19": urls[1],
-          "38": urls[0],
-        },
-      },
-    },
-  });
+      });
+
+      yield Assert.rejects(extension.startup(),
+                           null,
+                           "Manifest rejected");
 
-  yield extension.startup();
-  yield extension.unload();
-
-  SimpleTest.endMonitorConsole();
-  yield waitForConsole;
+      SimpleTest.endMonitorConsole();
+      yield waitForConsole;
+    }
+  }
 });
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -143,52 +143,39 @@ add_task(function* testPageActionPopup()
   let panel = document.getElementById(panelId);
   is(panel, null, "pageAction panel removed from document");
 });
 
 
 add_task(function* testPageActionSecurity() {
   const URL = "chrome://browser/content/browser.xul";
 
-  let messages = [/Access to restricted URI denied/,
-                  /Access to restricted URI denied/];
+  let apis = ["browser_action", "page_action"];
 
-  let waitForConsole = new Promise(resolve => {
-    // Not necessary in browser-chrome tests, but monitorConsole gripes
-    // if we don't call it.
-    SimpleTest.waitForExplicitFinish();
+  for (let api of apis) {
+    info(`TEST ${api} icon url: ${URL}`);
 
-    SimpleTest.monitorConsole(resolve, messages);
-  });
+    let messages = [/Access to restricted URI denied/];
 
-  let extension = ExtensionTestUtils.loadExtension({
-    manifest: {
-      "browser_action": { "default_popup": URL },
-      "page_action": { "default_popup": URL },
-    },
-
-    background: function() {
-      browser.tabs.query({ active: true, currentWindow: true }, tabs => {
-        let tabId = tabs[0].id;
+    let waitForConsole = new Promise(resolve => {
+      // Not necessary in browser-chrome tests, but monitorConsole gripes
+      // if we don't call it.
+      SimpleTest.waitForExplicitFinish();
 
-        browser.pageAction.show(tabId);
-        browser.test.sendMessage("ready");
-      });
-    },
-  });
-
-  yield extension.startup();
-  yield extension.awaitMessage("ready");
+      SimpleTest.monitorConsole(resolve, messages);
+    });
 
-  yield clickBrowserAction(extension);
-  yield clickPageAction(extension);
-
-  yield extension.unload();
+    let extension = ExtensionTestUtils.loadExtension({
+      manifest: {
+        [api]: { "default_popup": URL },
+      },
+    });
 
-  let pageActionId = makeWidgetId(extension.id) + "-page-action";
-  let node = document.getElementById(pageActionId);
-  is(node, null, "pageAction image removed from document");
+    yield Assert.rejects(extension.startup(),
+                         null,
+                         "Manifest rejected");
 
-  SimpleTest.endMonitorConsole();
-  yield waitForConsole;
+    SimpleTest.endMonitorConsole();
+    yield waitForConsole;
+  }
 });
 
 add_task(forceGC);
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -1963,16 +1963,20 @@ SpecialPowersAPI.prototype = {
 
     let resolveStartup, resolveUnload, rejectStartup;
     let startupPromise = new Promise((resolve, reject) => {
       resolveStartup = resolve;
       rejectStartup = reject;
     });
     let unloadPromise = new Promise(resolve => { resolveUnload = resolve; });
 
+    startupPromise.catch(() => {
+      this._removeMessageListener("SPExtensionMessage", listener);
+    });
+
     handler = Cu.waiveXrays(handler);
     ext = Cu.waiveXrays(ext);
 
     let sp = this;
     let extension = {
       id,
 
       startup() {
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -63,16 +63,18 @@ ExtensionManagement.registerScript("chro
 ExtensionManagement.registerScript("chrome://extensions/content/ext-idle.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-runtime.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-extension.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-webNavigation.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-webRequest.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-storage.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-test.js");
 
+const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
+
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/cookies.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/extension.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/extension_types.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/i18n.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/idle.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/runtime.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/web_navigation.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/web_request.json");
@@ -103,33 +105,38 @@ var Management = {
   emitter: new EventEmitter(),
 
   // Loads all the ext-*.js scripts currently registered.
   lazyInit() {
     if (this.initialized) {
       return this.initialized;
     }
 
-    let promises = [];
-    for (let schema of ExtensionManagement.getSchemas()) {
-      promises.push(Schemas.load(schema));
-    }
+    // Load order maders here. The base manifest defines types which are
+    // extended by other schemas, so needs to be loaded first.
+    let promise = Schemas.load(BASE_SCHEMA).then(() => {
+      let promises = [];
+      for (let schema of ExtensionManagement.getSchemas()) {
+        promises.push(Schemas.load(schema));
+      }
+      return Promise.all(promises);
+    });
 
     for (let script of ExtensionManagement.getScripts()) {
       let scope = {extensions: this,
                    global: scriptScope,
                    ExtensionPage: ExtensionPage,
                    GlobalManager: GlobalManager};
       Services.scriptloader.loadSubScript(script, scope, "UTF-8");
 
       // Save the scope to avoid it being garbage collected.
       this.scopes.push(scope);
     }
 
-    this.initialized = Promise.all(promises);
+    this.initialized = promise;
     return this.initialized;
   },
 
   // Called by an ext-*.js script to register an API. The |api|
   // parameter should be an object of the form:
   // {
   //   tabs: {
   //     create: ...,
@@ -533,45 +540,56 @@ ExtensionData.prototype = {
         }
       });
     });
   },
 
   // Reads the extension's |manifest.json| file, and stores its
   // parsed contents in |this.manifest|.
   readManifest() {
-    return this.readJSON("manifest.json").then(manifest => {
-      this.manifest = manifest;
+    return Promise.all([
+      this.readJSON("manifest.json"),
+      Management.lazyInit(),
+    ]).then(([manifest]) => {
+      let context = {
+        url: (this.baseURI || this.rootURI).spec,
+
+        principal: this.principal,
+      };
+
+      let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context);
+      if (normalized.error) {
+        this.manifestError(normalized.error);
+        this.manifest = manifest;
+      } else {
+        this.manifest = normalized.value;
+      }
 
       try {
         this.id = this.manifest.applications.gecko.id;
       } catch (e) {
-        // Errors are handled by the type check below.
+        // Errors are handled by the type checks above.
       }
 
-      if (typeof this.id != "string") {
-        this.manifestError("Missing required `applications.gecko.id` property");
-      }
-
-      return manifest;
+      return this.manifest;
     });
   },
 
   localizeMessage(...args) {
     return this.localeData.localizeMessage(...args);
   },
 
   localize(...args) {
     return this.localeData.localize(...args);
   },
 
   // If a "default_locale" is specified in that manifest, returns it
   // as a Gecko-compatible locale string. Otherwise, returns null.
   get defaultLocale() {
-    if ("default_locale" in this.manifest) {
+    if (this.manifest.default_locale != null) {
       return this.normalizeLocaleCode(this.manifest.default_locale);
     }
 
     return null;
   },
 
   // Normalizes a Chrome-compatible locale code to the appropriate
   // Gecko-compatible variant. Currently, this means simply
@@ -957,17 +975,19 @@ Extension.prototype = extend(Object.crea
 
     let resources = new Set();
     for (let url of webAccessibleResources) {
       resources.add(url);
     }
     this.webAccessibleResources = resources;
 
     for (let directive in manifest) {
-      Management.emit("manifest_" + directive, directive, this, manifest);
+      if (manifest[directive] !== null) {
+        Management.emit("manifest_" + directive, directive, this, manifest);
+      }
     }
 
     let data = Services.ppmm.initialProcessData;
     if (!data["Extension:Extensions"]) {
       data["Extension:Extensions"] = [];
     }
     let serial = this.serialize();
     data["Extension:Extensions"].push(serial);
@@ -1009,22 +1029,26 @@ Extension.prototype = extend(Object.crea
 
   startup() {
     try {
       ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
     } catch (e) {
       return Promise.reject(e);
     }
 
-    let lazyInit = Management.lazyInit();
-
-    return lazyInit.then(() => {
-      return this.readManifest();
+    return this.readManifest().then(() => {
+      return this.initLocale();
     }).then(() => {
-      return this.initLocale();
+      if (this.errors.length) {
+        // b2g add-ons generate manifest errors that we've silently
+        // ignoring prior to adding this check.
+        if (!this.rootURI.schemeIs("app")) {
+          return Promise.reject({errors: this.errors});
+        }
+      }
     }).then(() => {
       if (this.hasShutdown) {
         return;
       }
 
       GlobalManager.init(this);
 
       Management.emit("startup", this);
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -4,16 +4,18 @@
 
 "use strict";
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+Cu.import("resource://gre/modules/Services.jsm");
+
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   instanceOf,
 } = ExtensionUtils;
 
 this.EXPORTED_SYMBOLS = ["Schemas"];
 
 /* globals Schemas, URL */
@@ -77,27 +79,46 @@ function getValueBaseType(value) {
 }
 
 class Context {
   constructor(params) {
     this.params = params;
 
     this.path = [];
 
-    let props = ["addListener", "callFunction", "checkLoadURL",
+    let props = ["addListener", "callFunction",
                  "hasListener", "removeListener"];
     for (let prop of props) {
       this[prop] = params[prop];
     }
+
+    if ("checkLoadURL" in params) {
+      this.checkLoadURL = params.checkLoadURL;
+    }
   }
 
   get url() {
     return this.params.url;
   }
 
+  get principal() {
+    return this.params.principal || Services.scriptSecurityManager.createNullPrincipal({});
+  }
+
+  checkLoadURL(url) {
+    let ssm = Services.scriptSecurityManager;
+    try {
+      ssm.checkLoadURIStrWithPrincipal(this.principal, url,
+                                       ssm.DISALLOW_INHERIT_PRINCIPAL);
+    } catch (e) {
+      return false;
+    }
+    return true;
+  }
+
   get cloneScope() {
     return this.params.cloneScope;
   }
 
   error(message) {
     if (this.currentTarget) {
       return {error: `Error processing ${this.currentTarget}: ${message}`};
     }
@@ -117,24 +138,28 @@ class Context {
     }
   }
 }
 
 const FORMATS = {
   url(string, context) {
     let url = new URL(string).href;
 
-    context.checkLoadURL(url);
+    if (!context.checkLoadURL(url)) {
+      throw new Error(`Access denied for URL ${url}`);
+    }
     return url;
   },
 
   relativeUrl(string, context) {
     let url = new URL(string, context.url).href;
 
-    context.checkLoadURL(url);
+    if (!context.checkLoadURL(url)) {
+      throw new Error(`Access denied for URL ${url}`);
+    }
     return url;
   },
 
   strictRelativeUrl(string, context) {
     // Do not accept a string which resolves as an absolute URL, or any
     // protocol-relative URL.
     if (!string.startsWith("//")) {
       try {
@@ -875,17 +900,17 @@ this.Schemas = {
 
         patternProperties.push({
           pattern,
           type: parseProperty(type.patternProperties[propName]),
         });
       }
 
       let additionalProperties = null;
-      if ("additionalProperties" in type) {
+      if (type.additionalProperties) {
         additionalProperties = this.parseType(namespaceName, type.additionalProperties);
       }
 
       if ("$extend" in type) {
         // Only allow extending "properties" and "patternProperties".
         checkTypeProperties("properties", "patternProperties");
       } else {
         checkTypeProperties("properties", "additionalProperties", "patternProperties", "isInstanceOf");
@@ -1037,9 +1062,17 @@ this.Schemas = {
   inject(dest, wrapperFuncs) {
     for (let [namespace, ns] of this.namespaces) {
       let obj = Cu.createObjectIn(dest, {defineAs: namespace});
       for (let [name, entry] of ns) {
         entry.inject(name, obj, new Context(wrapperFuncs));
       }
     }
   },
+
+  normalize(obj, typeName, context) {
+    let [namespaceName, prop] = typeName.split(".");
+    let ns = this.namespaces.get(namespaceName);
+    let type = ns.get(prop);
+
+    return type.normalize(obj, new Context(context));
+  },
 };
--- a/toolkit/components/extensions/schemas/cookies.json
+++ b/toolkit/components/extensions/schemas/cookies.json
@@ -1,14 +1,28 @@
 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "cookies"
+          ]
+        }]
+      }
+    ]
+  },
+  {
     "namespace": "cookies",
     "description": "Use the <code>browser.cookies</code> API to query and modify cookies, and to be notified when they change.",
     "types": [
       {
         "id": "Cookie",
         "type": "object",
         "description": "Represents information about an HTTP cookie.",
         "properties": {
--- a/toolkit/components/extensions/schemas/i18n.json
+++ b/toolkit/components/extensions/schemas/i18n.json
@@ -1,14 +1,28 @@
 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "default_locale": {
+            "type": "string",
+            "optional": "true"
+          }
+        }
+      }
+    ]
+  },
+  {
     "namespace": "i18n",
     "description": "Use the <code>browser.i18n</code> infrastructure to implement internationalization across your whole app or extension.",
     "types": [
       {
         "id": "LanguageCode",
         "type": "string",
         "description": "An ISO language code such as <code>en</code> or <code>fr</code>. For a complete list of languages supported by this method, see <a href='http://src.chromium.org/viewvc/chrome/trunk/src/third_party/cld/languages/internal/languages.cc'>kLanguageInfoTable</a>. For an unknown language, <code>und</code> will be returned, which means that [percentage] of the text is unknown to CLD"
       }
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -4,11 +4,12 @@
 
 toolkit.jar:
 % content extensions %content/extensions/
     content/extensions/schemas/cookies.json
     content/extensions/schemas/extension.json
     content/extensions/schemas/extension_types.json
     content/extensions/schemas/i18n.json
     content/extensions/schemas/idle.json
+    content/extensions/schemas/manifest.json
     content/extensions/schemas/runtime.json
     content/extensions/schemas/web_navigation.json
     content/extensions/schemas/web_request.json
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -0,0 +1,229 @@
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "id": "WebExtensionManifest",
+        "type": "object",
+        "description": "Represents a WebExtension manifest.json file",
+        "properties": {
+          "manifest_version": {
+            "type": "integer",
+            "minimum": 2,
+            "maximum": 2
+          },
+
+          "applications": {
+            "type": "object",
+            "properties": {
+              "gecko": {
+                "type": "object",
+                "properties": {
+                  "id": { "$ref": "ExtensionID" },
+
+                  "update_url": {
+                    "type": "string",
+                    "format": "url",
+                    "optional": true
+                  },
+
+                  "strict_min_version": {
+                    "type": "string",
+                    "optional": true
+                  },
+
+                  "strict_max_version": {
+                    "type": "string",
+                    "optional": true
+                  }
+                }
+              }
+            }
+          },
+
+          "name": {
+            "type": "string",
+            "optional": false
+          },
+
+          "description": {
+            "type": "string",
+            "optional": true
+          },
+
+          "version": {
+            "type": "string",
+            "optional": false
+          },
+
+          "icons": {
+            "type": "object",
+            "optional": true,
+            "patternProperties": {
+              "^[1-9]\\d*$": { "type": "string" }
+            }
+          },
+
+          "background": {
+            "choices": [
+              {
+                "type": "object",
+                "properties": {
+                  "page": { "$ref": "ExtensionURL" }
+                }
+              },
+              {
+                "type": "object",
+                "properties": {
+                  "scripts": {
+                    "type": "array",
+                    "items": { "$ref": "ExtensionURL" }
+                  }
+                }
+              }
+            ],
+            "optional": true
+          },
+
+          "content_scripts": {
+            "type": "array",
+            "optional": true,
+            "items": { "$ref": "ContentScript" }
+          },
+
+          "permissions": {
+            "type": "array",
+            "items": { "$ref": "Permission" },
+            "optional": true
+          },
+
+          "web_accessible_resources": {
+            "type": "array",
+            "items": { "type": "string" },
+            "optional": true
+          }
+        }
+      },
+      {
+        "id": "Permission",
+        "choices": [
+          {
+            "type": "string",
+            "enum": [
+              "alarms",
+              "idle",
+              "notifications",
+              "storage"
+            ]
+          },
+          { "$ref": "MatchPattern" }
+        ]
+      },
+      {
+        "id": "ExtensionURL",
+        "type": "string",
+        "format": "strictRelativeUrl"
+      },
+      {
+        "id": "ExtensionID",
+        "choices": [
+          {
+            "type": "string",
+            "pattern": "(?i)^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}$"
+          },
+          {
+            "type": "string",
+            "pattern": "(?i)^[a-z0-9-._]*@[a-z0-9-._]+$"
+          }
+        ]
+      },
+      {
+        "id": "MatchPattern",
+        "choices": [
+          {
+            "type": "string",
+            "enum": ["<all_urls>"]
+          },
+          {
+            "type": "string",
+            "pattern": "^(https?|file|ftp|app|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+)/.*$"
+          },
+          {
+            "type": "string",
+            "pattern": "^file:///.*$"
+          }
+        ]
+      },
+      {
+        "id": "ContentScript",
+        "type": "object",
+        "description": "Details of the script or CSS to inject. Either the code or the file property must be set, but both may not be set at the same time. Based on InjectDetails, but using underscore rather than camel case naming conventions.",
+        "properties": {
+          "matches": {
+            "type": "array",
+            "optional": false,
+            "minItems": 1,
+            "items": { "$ref": "MatchPattern" }
+          },
+          "exclude_matches": {
+            "type": "array",
+            "optional": true,
+            "minItems": 1,
+            "items": { "$ref": "MatchPattern" }
+          },
+          "css": {
+            "type": "array",
+            "optional": true,
+            "description": "The list of CSS files to inject",
+            "items": { "$ref": "ExtensionURL" }
+          },
+          "js": {
+            "type": "array",
+            "optional": true,
+            "description": "The list of CSS files to inject",
+            "items": { "$ref": "ExtensionURL" }
+          },
+          "all_frames": {"type": "boolean", "optional": true, "description": "If allFrames is <code>true</code>, implies that the JavaScript or CSS should be injected into all frames of current page. By default, it's <code>false</code> and is only injected into the top frame."},
+          "match_about_blank": {"type": "boolean", "optional": true, "description": "If matchAboutBlank is true, then the code is also injected in about:blank and about:srcdoc frames if your extension has access to its parent document. Code cannot be inserted in top-level about:-frames. By default it is <code>false</code>."},
+          "run_at": {
+            "$ref": "extensionTypes.RunAt",
+            "optional": true,
+            "description": "The soonest that the JavaScript or CSS will be injected into the tab. Defaults to \"document_idle\"."
+          }
+        }
+      },
+      {
+        "id": "IconPath",
+        "choices": [
+          {
+            "type": "object",
+            "patternProperties": {
+              "^[1-9]\\d*$": { "$ref": "ExtensionURL" }
+            },
+            "additionalProperties": false
+          },
+          { "$ref": "ExtensionURL" }
+        ]
+      },
+      {
+        "id": "IconImageData",
+        "choices": [
+          {
+            "type": "object",
+            "patternProperties": {
+              "^[1-9]\\d*$": {
+                "type": "object",
+                "isInstanceOf": "ImageData"
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "isInstanceOf": "ImageData"
+          }
+        ]
+      }
+    ]
+  }
+]
--- a/toolkit/components/extensions/schemas/web_navigation.json
+++ b/toolkit/components/extensions/schemas/web_navigation.json
@@ -1,14 +1,28 @@
 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "webNavigation"
+          ]
+        }]
+      }
+    ]
+  },
+  {
     "namespace": "webNavigation",
     "description": "Use the <code>browser.webNavigation</code> API to receive notifications about the status of navigation requests in-flight.",
     "types": [
       {
         "id": "TransitionType",
         "type": "string",
         "enum": ["link", "typed", "auto_bookmark", "auto_subframe", "manual_subframe", "generated", "start_page", "form_submit", "reload", "keyword", "keyword_generated"],
         "description": "Cause of the navigation. The same transition types as defined in the history API are used. These are the same transition types as defined in the $(topic:transition_types)[history API] except with <code>\"start_page\"</code> in place of <code>\"auto_toplevel\"</code> (for backwards compatibility)."
--- a/toolkit/components/extensions/schemas/web_request.json
+++ b/toolkit/components/extensions/schemas/web_request.json
@@ -1,14 +1,29 @@
 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "webRequest",
+            "webRequestBlocking"
+          ]
+        }]
+      }
+    ]
+  },
+  {
     "namespace": "webRequest",
     "description": "Use the <code>browser.webRequest</code> API to observe and analyze traffic and to intercept, block, or modify requests in-flight.",
     "properties": {
       "MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES": {
         "value": 20,
         "description": "The maximum number of times that <code>handlerBehaviorChanged</code> can be called per 10 minute sustained interval. <code>handlerBehaviorChanged</code> is an expensive function call that shouldn't be called often."
       }
     },
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -273,20 +273,17 @@ function verify(...args) {
   do_check_eq(JSON.stringify(tallied), JSON.stringify(args));
   tallied = null;
 }
 
 let wrapper = {
   url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
 
   checkLoadURL(url) {
-    if (url.startsWith("chrome:")) {
-      throw new Error("Access denied");
-    }
-    return url;
+    return !url.startsWith("chrome:");
   },
 
   callFunction(ns, name, args) {
     tally("call", ns, name, args);
   },
 
   addListener(ns, name, listener, args) {
     tally("addListener", ns, name, [listener, args]);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
@@ -130,17 +130,17 @@ function run_test() {
 
 
 // Check that compatibility updates are applied.
 add_task(function* checkUpdateMetadata() {
   let update = yield checkUpdates({
     addon: {
       manifest: {
         version: "1.0",
-        application: { gecko: { strict_max_version: "45" } },
+        applications: { gecko: { strict_max_version: "45" } },
       }
     },
     updates: {
       "1.0": {
         applications: { gecko: { strict_min_version: "40",
                                  strict_max_version: "48" } },
       }
     }
@@ -235,12 +235,12 @@ add_task(function* checkIllegalUpdateURL
 
           if (install && install.state == AddonManager.STATE_DOWNLOAD_FAILED)
             resolve();
           reject(new Error("Unexpected state: " + (install && install.state)))
         });
       });
     });
 
-    ok(messages.some(msg => /nsIScriptSecurityManager.checkLoadURIStrWithPrincipal/.test(msg)),
+    ok(messages.some(msg => /Access denied for URL|may not load or link to|is not a valid URL/.test(msg)),
        "Got checkLoadURI error");
   }
 });
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
@@ -68,57 +68,16 @@ add_task(function*() {
 
   check_icons(addon);
 
   addon.uninstall();
 
   yield promiseRestartManager();
 });
 
-// Test filtering invalid icon sizes
-add_task(function*() {
-  writeWebManifestForExtension({
-    name: "Web Extension Name",
-    version: "1.0",
-    manifest_version: 2,
-    applications: {
-      gecko: {
-        id: ID
-      }
-    },
-    icons: {
-      32: "icon32.png",
-      banana: "bananana.png",
-      "20.5": "icon20.5.png",
-      "20.0": "also invalid",
-      "123banana": "123banana.png",
-      64: "icon64.png"
-    }
-  }, profileDir);
-
-  yield promiseRestartManager();
-
-  let addon = yield promiseAddonByID(ID);
-  do_check_neq(addon, null);
-
-  let uri = do_get_addon_root_uri(profileDir, ID);
-
-  deepEqual(addon.icons, {
-      32: uri + "icon32.png",
-      64: uri + "icon64.png"
-  });
-
-  equal(addon.iconURL, uri + "icon64.png");
-  equal(addon.icon64URL, uri + "icon64.png");
-
-  addon.uninstall();
-
-  yield promiseRestartManager();
-});
-
 // Test AddonManager.getPreferredIconURL for retina screen sizes
 add_task(function*() {
   writeWebManifestForExtension({
     name: "Web Extension Name",
     version: "1.0",
     manifest_version: 2,
     applications: {
       gecko: {