Bug 1287010, 1286712 - Use schema-generated runtime API, split ext-runtime.js r=billm
authorRob Wu <rob@robwu.nl>
Thu, 18 Aug 2016 18:15:37 -0700 (2016-08-19)
changeset 311150 2427f8eb4e83add47679215a19cabbb81dcc12a1
parent 311149 e4ce08beaf7474321a89ee4f45cf88e943f32618
child 311151 598895fae31dc86756be5481478d32bb177a764e
push id30602
push userkwierso@gmail.com
push dateThu, 25 Aug 2016 23:53:05 +0000 (2016-08-25)
treeherdermozilla-central@cd4ed9909dc9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1287010, 1286712
milestone51.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 1287010, 1286712 - Use schema-generated runtime API, split ext-runtime.js r=billm - Use schema-generated runtime API for content scripts instead of untyped API. - Move logic that cannot be run in the main process to a new file. Together with the previous patch that migrated the i18n API, this concludes the fix for bug 1286712. MozReview-Commit-ID: A3yG0x1kjwx
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ext-c-runtime.js
toolkit/components/extensions/ext-runtime.js
toolkit/components/extensions/extensions-toolkit.manifest
toolkit/components/extensions/jar.mn
toolkit/components/extensions/schemas/runtime.json
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -89,64 +89,16 @@ var apiManager = new class extends Schem
     }
   }
 };
 
 // This is the fairly simple API that we inject into content
 // scripts.
 var api = context => {
   return {
-    runtime: {
-      connect: function(extensionId, connectInfo) {
-        if (!connectInfo) {
-          connectInfo = extensionId;
-          extensionId = null;
-        }
-        extensionId = extensionId || context.extension.id;
-        let name = connectInfo && connectInfo.name || "";
-        let recipient = {extensionId};
-        return context.messenger.connect(context.messageManager, name, recipient);
-      },
-
-      get id() {
-        return context.extension.id;
-      },
-
-      get lastError() {
-        return context.lastError;
-      },
-
-      getManifest: function() {
-        return Cu.cloneInto(context.extension.manifest, context.cloneScope);
-      },
-
-      getURL: function(url) {
-        return context.extension.baseURI.resolve(url);
-      },
-
-      onConnect: context.messenger.onConnect("runtime.onConnect"),
-
-      onMessage: context.messenger.onMessage("runtime.onMessage"),
-
-      sendMessage: function(...args) {
-        let options; // eslint-disable-line no-unused-vars
-        let extensionId, message, responseCallback;
-        if (args.length == 1) {
-          message = args[0];
-        } else if (args.length == 2) {
-          [message, responseCallback] = args;
-        } else {
-          [extensionId, message, options, responseCallback] = args;
-        }
-        extensionId = extensionId || context.extension.id;
-
-        let recipient = {extensionId};
-        return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
-      },
-    },
 
     extension: {
       getURL: function(url) {
         return context.extension.baseURI.resolve(url);
       },
 
       get lastError() {
         return context.lastError;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-runtime.js
@@ -0,0 +1,82 @@
+"use strict";
+
+function runtimeApiFactory(context) {
+  let {extension} = context;
+
+  // TODO(robwu): Investigate which message-manager to use once we start with
+  // reworking Messenger and ExtensionContext to be usable in a child process
+  // instead of the parent process.
+  // For now use exactly the original behavior out of caution.
+  let mm = context.envType == "content_child" ? context.messageManager : Services.cpmm;
+
+  return {
+    runtime: {
+      onConnect: context.messenger.onConnect("runtime.onConnect"),
+
+      onMessage: context.messenger.onMessage("runtime.onMessage"),
+
+      connect: function(extensionId, connectInfo) {
+        let name = connectInfo !== null && connectInfo.name || "";
+        extensionId = extensionId || extension.id;
+        let recipient = {extensionId};
+
+        return context.messenger.connect(mm, name, recipient);
+      },
+
+      sendMessage: function(...args) {
+        let options; // eslint-disable-line no-unused-vars
+        let extensionId, message, responseCallback;
+        if (typeof args[args.length - 1] == "function") {
+          responseCallback = args.pop();
+        }
+        if (!args.length) {
+          return Promise.reject({message: "runtime.sendMessage's message argument is missing"});
+        } else if (args.length == 1) {
+          message = args[0];
+        } else if (args.length == 2) {
+          if (typeof args[0] == "string" && args[0]) {
+            [extensionId, message] = args;
+          } else {
+            [message, options] = args;
+          }
+        } else if (args.length == 3) {
+          [extensionId, message, options] = args;
+        } else if (args.length == 4 && !responseCallback) {
+          return Promise.reject({message: "runtime.sendMessage's last argument is not a function"});
+        } else {
+          return Promise.reject({message: "runtime.sendMessage received too many arguments"});
+        }
+
+        if (extensionId != null && typeof extensionId != "string") {
+          return Promise.reject({message: "runtime.sendMessage's extensionId argument is invalid"});
+        }
+        if (options != null && typeof options != "object") {
+          return Promise.reject({message: "runtime.sendMessage's options argument is invalid"});
+        }
+        // TODO(robwu): Validate option keys and values when we support it.
+
+        extensionId = extensionId || extension.id;
+        let recipient = {extensionId};
+
+        return context.messenger.sendMessage(mm, message, recipient, responseCallback);
+      },
+
+      get lastError() {
+        return context.lastError;
+      },
+
+      getManifest() {
+        return Cu.cloneInto(extension.manifest, context.cloneScope);
+      },
+
+      id: extension.id,
+
+      getURL: function(url) {
+        return extension.baseURI.resolve(url);
+      },
+    },
+  };
+}
+
+extensions.registerSchemaAPI("runtime", "addon_child", runtimeApiFactory);
+extensions.registerSchemaAPI("runtime", "content_child", runtimeApiFactory);
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -28,20 +28,16 @@ extensions.registerSchemaAPI("runtime", 
         extension.onStartup = fire;
         return () => {
           extension.onStartup = null;
         };
       }).api(),
 
       onInstalled: ignoreEvent(context, "runtime.onInstalled"),
 
-      onMessage: context.messenger.onMessage("runtime.onMessage"),
-
-      onConnect: context.messenger.onConnect("runtime.onConnect"),
-
       onUpdateAvailable: new SingletonEventManager(context, "runtime.onUpdateAvailable", fire => {
         let instanceID = extension.addonData.instanceID;
         AddonManager.addUpgradeListener(instanceID, upgrade => {
           extension.upgrade = upgrade;
           let details = {
             version: upgrade.version,
           };
           context.runSafe(fire, details);
@@ -58,86 +54,33 @@ extensions.registerSchemaAPI("runtime", 
         } else {
           // Otherwise, reload the current extension.
           AddonManager.getAddonByID(extension.id, addon => {
             addon.reload();
           });
         }
       },
 
-      connect: function(extensionId, connectInfo) {
-        let name = connectInfo !== null && connectInfo.name || "";
-        extensionId = extensionId || extension.id;
-        let recipient = {extensionId};
-
-        return context.messenger.connect(Services.cpmm, name, recipient);
-      },
-
-      sendMessage: function(...args) {
-        let options; // eslint-disable-line no-unused-vars
-        let extensionId, message, responseCallback;
-        if (typeof args[args.length - 1] == "function") {
-          responseCallback = args.pop();
-        }
-        if (!args.length) {
-          return Promise.reject({message: "runtime.sendMessage's message argument is missing"});
-        } else if (args.length == 1) {
-          message = args[0];
-        } else if (args.length == 2) {
-          if (typeof args[0] == "string" && args[0]) {
-            [extensionId, message] = args;
-          } else {
-            [message, options] = args;
-          }
-        } else if (args.length == 3) {
-          [extensionId, message, options] = args;
-        } else if (args.length == 4 && !responseCallback) {
-          return Promise.reject({message: "runtime.sendMessage's last argument is not a function"});
-        } else {
-          return Promise.reject({message: "runtime.sendMessage received too many arguments"});
-        }
-
-        if (extensionId != null && typeof extensionId != "string") {
-          return Promise.reject({message: "runtime.sendMessage's extensionId argument is invalid"});
-        }
-        if (options != null && typeof options != "object") {
-          return Promise.reject({message: "runtime.sendMessage's options argument is invalid"});
-        }
-        // TODO(robwu): Validate option keys and values when we support it.
-
-        extensionId = extensionId || extension.id;
-        let recipient = {extensionId};
-
-        return context.messenger.sendMessage(Services.cpmm, message, recipient, responseCallback);
-      },
-
       connectNative(application) {
         let app = new NativeApp(extension, context, application);
         return app.portAPI();
       },
 
       sendNativeMessage(application, message) {
         let app = new NativeApp(extension, context, application);
         return app.sendMessage(message);
       },
 
       get lastError() {
+        // TODO(robwu): Figure out how to make sure that errors in the parent
+        // process are propagated to the child process.
+        // lastError should not be accessed from the parent.
         return context.lastError;
       },
 
-      getManifest() {
-        return Cu.cloneInto(extension.manifest, context.cloneScope);
-      },
-
-      id: extension.id,
-
-      getURL: function(url) {
-        return extension.baseURI.resolve(url);
-      },
-
       getPlatformInfo: function() {
         return Promise.resolve(ExtensionUtils.PlatformInfo);
       },
 
       openOptionsPage: function() {
         if (!extension.manifest.options_ui) {
           return Promise.reject({message: "No `options_ui` declared"});
         }
--- a/toolkit/components/extensions/extensions-toolkit.manifest
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -11,16 +11,17 @@ category webextension-scripts webRequest
 category webextension-scripts webNavigation chrome://extensions/content/ext-webNavigation.js
 category webextension-scripts runtime chrome://extensions/content/ext-runtime.js
 category webextension-scripts extension chrome://extensions/content/ext-extension.js
 category webextension-scripts storage chrome://extensions/content/ext-storage.js
 category webextension-scripts test chrome://extensions/content/ext-test.js
 
 # scripts specific for content process.
 category webextension-scripts-content i18n chrome://extensions/content/ext-i18n.js
+category webextension-scripts-content runtime chrome://extensions/content/ext-c-runtime.js
 
 # schemas
 category webextension-schemas alarms chrome://extensions/content/schemas/alarms.json
 category webextension-schemas cookies chrome://extensions/content/schemas/cookies.json
 category webextension-schemas downloads chrome://extensions/content/schemas/downloads.json
 category webextension-schemas events chrome://extensions/content/schemas/events.json
 category webextension-schemas extension chrome://extensions/content/schemas/extension.json
 category webextension-schemas extension_types chrome://extensions/content/schemas/extension_types.json
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -13,8 +13,9 @@ toolkit.jar:
     content/extensions/ext-i18n.js
     content/extensions/ext-idle.js
     content/extensions/ext-webRequest.js
     content/extensions/ext-webNavigation.js
     content/extensions/ext-runtime.js
     content/extensions/ext-extension.js
     content/extensions/ext-storage.js
     content/extensions/ext-test.js
+    content/extensions/ext-c-runtime.js
--- a/toolkit/components/extensions/schemas/runtime.json
+++ b/toolkit/components/extensions/schemas/runtime.json
@@ -14,21 +14,23 @@
             "nativeMessaging"
           ]
         }]
       }
     ]
   },
   {
     "namespace": "runtime",
+    "restrictions": ["content"],
     "description": "Use the <code>browser.runtime</code> API to retrieve the background page, return details about the manifest, and listen for and respond to events in the app or extension lifecycle. You can also use this API to convert the relative path of URLs to fully-qualified URLs.",
     "types": [
       {
         "id": "Port",
         "type": "object",
+        "restrictions": ["content"],
         "description": "An object which allows two way communication with other pages.",
         "properties": {
           "name": {"type": "string"},
           "disconnect": { "type": "function" },
           "onDisconnect": { "$ref": "events.Event" },
           "onMessage": { "$ref": "events.Event" },
           "postMessage": {"type": "function"},
           "sender": {
@@ -37,40 +39,44 @@
             "description": "This property will <b>only</b> be present on ports passed to onConnect/onConnectExternal listeners."
           }
         },
         "additionalProperties": { "type": "any"}
       },
       {
         "id": "MessageSender",
         "type": "object",
+        "restrictions": ["content"],
         "description": "An object containing information about the script context that sent a message or request.",
         "properties": {
           "tab": {"$ref": "tabs.Tab", "optional": true, "description": "The $(ref:tabs.Tab) which opened the connection, if any. This property will <strong>only</strong> be present when the connection was opened from a tab (including content scripts), and <strong>only</strong> if the receiver is an extension, not an app."},
           "frameId": {"type": "integer", "optional": true, "description": "The $(topic:frame_ids)[frame] that opened the connection. 0 for top-level frames, positive for child frames. This will only be set when <code>tab</code> is set."},
           "id": {"type": "string", "optional": true, "description": "The ID of the extension or app that opened the connection, if any."},
           "url": {"type": "string", "optional": true, "description": "The URL of the page or frame that opened the connection. If the sender is in an iframe, it will be iframe's URL not the URL of the page which hosts it."},
           "tlsChannelId": {"unsupported": true, "type": "string", "optional": true, "description": "The TLS channel ID of the page or frame that opened the connection, if requested by the extension or app, and if available."}
         }
       },
       {
         "id": "PlatformOs",
         "type": "string",
+        "restrictions": ["content"],
         "description": "The operating system the browser is running on.",
         "enum": ["mac", "win", "android", "cros", "linux", "openbsd"]
       },
       {
         "id": "PlatformArch",
         "type": "string",
         "enum": ["arm", "x86-32", "x86-64"],
+        "restrictions": ["content"],
         "description": "The machine's processor architecture."
       },
       {
         "id": "PlatformInfo",
         "type": "object",
+        "restrictions": ["content"],
         "description": "An object containing information about the current platform.",
         "properties": {
           "os": {
             "$ref": "PlatformOs",
             "description": "The operating system the browser is running on."
           },
           "arch": {
             "$ref": "PlatformArch",
@@ -82,49 +88,54 @@
             "$ref": "PlatformNaclArch"
           }
         }
       },
       {
         "id": "RequestUpdateCheckStatus",
         "type": "string",
         "enum": ["throttled", "no_update", "update_available"],
+        "restrictions": ["content"],
         "description": "Result of the update check."
       },
       {
         "id": "OnInstalledReason",
         "type": "string",
         "enum": ["install", "update", "chrome_update", "shared_module_update"],
+        "restrictions": ["content"],
         "description": "The reason that this event is being dispatched."
       },
       {
         "id": "OnRestartRequiredReason",
         "type": "string",
+        "restrictions": ["content"],
         "description": "The reason that the event is being dispatched. 'app_update' is used when the restart is needed because the application is updated to a newer version. 'os_update' is used when the restart is needed because the browser/OS is updated to a newer version. 'periodic' is used when the system runs for more than the permitted uptime set in the enterprise policy.",
         "enum": ["app_update", "os_update", "periodic"]
       }
     ],
     "properties": {
       "lastError": {
         "type": "object",
         "optional": true,
+        "restrictions": ["content"],
         "description": "This will be defined during an API method callback if there was an error",
         "properties": {
           "message": {
             "optional": true,
             "type": "string",
             "description": "Details about the error which occurred."
           }
         },
         "additionalProperties": {
           "type": "any"
         }
       },
       "id": {
         "type": "string",
+        "restrictions": ["content"],
         "description": "The ID of the extension/app."
       }
     },
     "functions": [
       {
         "name": "getBackgroundPage",
         "type": "function",
         "description": "Retrieves the JavaScript 'window' object for the background page running inside the current extension/app. If the background page is an event page, the system will ensure it is loaded before calling the callback. If there is no background page, an error is set.",
@@ -155,29 +166,31 @@
           "type": "function",
           "name": "callback",
           "parameters": [],
           "optional": true
         }]
       },
       {
         "name": "getManifest",
+        "restrictions": ["content"],
         "description": "Returns details about the app or extension from the manifest. The object returned is a serialization of the full $(topic:manifest)[manifest file].",
         "type": "function",
         "parameters": [],
         "returns": {
           "type": "object",
           "properties": {},
           "additionalProperties": { "type": "any" },
           "description": "The manifest details."
         }
       },
       {
         "name": "getURL",
         "type": "function",
+        "restrictions": ["content"],
         "description": "Converts a relative path within an app/extension install directory to a fully-qualified URL.",
         "parameters": [
           {
             "type": "string",
             "name": "path",
             "description": "A path to a resource within an app/extension expressed relative to its install directory."
           }
         ],
@@ -250,16 +263,17 @@
         "unsupported": true,
         "description": "Restart the device when the app runs in kiosk mode. Otherwise, it's no-op.",
         "type": "function",
         "parameters": []
       },
       {
         "name": "connect",
         "type": "function",
+        "restrictions": ["content"],
         "description": "Attempts to connect to connect listeners within an extension/app (such as the background page), or other extensions/apps. This is useful for content scripts connecting to their extension processes, inter-app/extension communication, and $(topic:manifest/externally_connectable)[web messaging]. Note that this does not connect to any listeners in a content script. Extensions may connect to content scripts embedded in tabs via $(ref:tabs.connect).",
         "parameters": [
           {"type": "string", "name": "extensionId", "optional": true, "description": "The ID of the extension or app to connect to. If omitted, a connection will be attempted with your own extension. Required if sending messages from a web page for $(topic:manifest/externally_connectable)[web messaging]."},
           {
             "type": "object",
             "name": "connectInfo",
             "properties": {
               "name": { "type": "string", "optional": true, "description": "Will be passed into onConnect for processes that are listening for the connection event." },
@@ -289,16 +303,17 @@
           "$ref": "Port",
           "description": "Port through which messages can be sent and received with the application"
         }
       },
       {
         "name": "sendMessage",
         "type": "function",
         "allowAmbiguousOptionalArguments": true,
+        "restrictions": ["content"],
         "description": "Sends a single message to event listeners within your extension/app or a different extension/app. Similar to $(ref:runtime.connect) but only sends a single message, with an optional response. If sending to your extension, the $(ref:runtime.onMessage) event will be fired in each page, or $(ref:runtime.onMessageExternal), if a different extension. Note that extensions cannot send messages to content scripts using this method. To send messages to content scripts, use $(ref:tabs.sendMessage).",
         "async": "responseCallback",
         "parameters": [
           {"type": "string", "name": "extensionId", "optional": true, "description": "The ID of the extension/app to send the message to. If omitted, the message will be sent to your own extension/app. Required if sending messages from a web page for $(topic:manifest/externally_connectable)[web messaging]."},
           { "type": "any", "name": "message" },
           {
             "type": "object",
             "name": "options",
@@ -465,16 +480,17 @@
         "type": "function",
         "description": "Fired when an update for the browser is available, but isn't installed immediately because a browser restart is required.",
         "deprecated": "Please use $(ref:runtime.onRestartRequired).",
         "parameters": []
       },
       {
         "name": "onConnect",
         "type": "function",
+        "restrictions": ["content"],
         "description": "Fired when a connection is made from either an extension process or a content script.",
         "parameters": [
           {"$ref": "Port", "name": "port"}
         ]
       },
       {
         "name": "onConnectExternal",
         "unsupported": true,
@@ -482,16 +498,17 @@
         "description": "Fired when a connection is made from another extension.",
         "parameters": [
           {"$ref": "Port", "name": "port"}
         ]
       },
       {
         "name": "onMessage",
         "type": "function",
+        "restrictions": ["content"],
         "description": "Fired when a message is sent from either an extension process or a content script.",
         "parameters": [
           {"name": "message", "type": "any", "optional": true, "description": "The message sent by the calling script."},
           {"name": "sender", "$ref": "MessageSender" },
           {"name": "sendResponse", "type": "function", "description": "Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one <code>onMessage</code> listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until <code>sendResponse</code> is called)." }
         ],
         "returns": {
           "type": "boolean",