Bug 1190685 - [webext] Implements webNavigation.getFrame/getAllFrames API methods. r=kmag
authorLuca Greco <luca.greco@alcacoop.it>
Mon, 08 Feb 2016 18:30:48 +0100
changeset 329848 e941c359f7920becb71dc111d48d45ddf4be71c6
parent 329847 1e3b60dd103a84f3fb895de0498f11fd494384c0
child 329849 377d0dc81a91d808d791e65143dfa68d928eb844
push id10617
push userdtownsend@mozilla.com
push dateTue, 09 Feb 2016 16:30:19 +0000
reviewerskmag
bugs1190685
milestone47.0a1
Bug 1190685 - [webext] Implements webNavigation.getFrame/getAllFrames API methods. r=kmag
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ext-webNavigation.js
toolkit/components/extensions/schemas/web_navigation.json
toolkit/modules/addons/WebNavigationContent.js
toolkit/modules/addons/WebNavigationFrames.jsm
toolkit/modules/moz.build
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -29,16 +29,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
+                                  "resource://gre/modules/WebNavigationFrames.jsm");
+
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   runSafeSyncWithoutClone,
   BaseContext,
   LocaleData,
   MessageBroker,
   Messenger,
   injectAPI,
@@ -663,16 +666,18 @@ ExtensionManager = {
 };
 
 class ExtensionGlobal {
   constructor(global) {
     this.global = global;
 
     MessageChannel.addListener(global, "Extension:Capture", this);
     MessageChannel.addListener(global, "Extension:Execute", this);
+    MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
+    MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
 
     this.broker = new MessageBroker([global]);
 
     this.windowId = global.content
                           .QueryInterface(Ci.nsIInterfaceRequestor)
                           .getInterface(Ci.nsIDOMWindowUtils)
                           .outerWindowID;
 
@@ -690,46 +695,61 @@ class ExtensionGlobal {
                          .getInterface(Ci.nsIDOMWindowUtils)
                          .currentInnerWindowID,
     };
   }
 
   receiveMessage({ target, messageName, recipient, data }) {
     switch (messageName) {
       case "Extension:Capture":
-        let win = this.global.content;
+        return this.handleExtensionCapture(data.width, data.height, data.options);
+      case "Extension:Execute":
+        return this.handleExtensionExecute(target, recipient, data.options);
+      case "WebNavigation:GetFrame":
+        return this.handleWebNavigationGetFrame(data.options);
+      case "WebNavigation:GetAllFrames":
+        return this.handleWebNavigationGetAllFrames();
+    }
+  }
 
-        const XHTML_NS = "http://www.w3.org/1999/xhtml";
-        let canvas = win.document.createElementNS(XHTML_NS, "canvas");
-        canvas.width = data.width;
-        canvas.height = data.height;
-        canvas.mozOpaque = true;
+  handleExtensionCapture(width, height, options) {
+    let win = this.global.content;
 
-        let ctx = canvas.getContext("2d");
+    const XHTML_NS = "http://www.w3.org/1999/xhtml";
+    let canvas = win.document.createElementNS(XHTML_NS, "canvas");
+    canvas.width = width;
+    canvas.height = height;
+    canvas.mozOpaque = true;
+
+    let ctx = canvas.getContext("2d");
 
-        // We need to scale the image to the visible size of the browser,
-        // in order for the result to appear as the user sees it when
-        // settings like full zoom come into play.
-        ctx.scale(canvas.width / win.innerWidth,
-                  canvas.height / win.innerHeight);
+    // We need to scale the image to the visible size of the browser,
+    // in order for the result to appear as the user sees it when
+    // settings like full zoom come into play.
+    ctx.scale(canvas.width / win.innerWidth, canvas.height / win.innerHeight);
 
-        ctx.drawWindow(win, win.scrollX, win.scrollY, win.innerWidth, win.innerHeight, "#fff");
+    ctx.drawWindow(win, win.scrollX, win.scrollY, win.innerWidth, win.innerHeight, "#fff");
+
+    return canvas.toDataURL(`image/${options.format}`, options.quality / 100);
+  }
 
-        return canvas.toDataURL(`image/${data.options.format}`,
-                                data.options.quality / 100);
-
-      case "Extension:Execute":
-        let deferred = PromiseUtils.defer();
+  handleExtensionExecute(target, recipient, options) {
+    let deferred = PromiseUtils.defer();
+    let script = new Script(options, deferred);
+    let { extensionId } = recipient;
+    DocumentManager.executeScript(target, extensionId, script);
+    return deferred.promise;
+  }
 
-        let script = new Script(data.options, deferred);
-        let { extensionId } = recipient;
-        DocumentManager.executeScript(target, extensionId, script);
+  handleWebNavigationGetFrame({ frameId }) {
+    return WebNavigationFrames.getFrame(this.global.docShell, frameId);
+  }
 
-        return deferred.promise;
-    }
+  handleWebNavigationGetAllFrames() {
+    return WebNavigationFrames.getAllFrames(this.global.docShell);
   }
 }
 
 this.ExtensionContent = {
   globals: new Map(),
 
   init(global) {
     this.globals.set(global, new ExtensionGlobal(global));
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -55,21 +55,61 @@ function WebNavigationEventManager(conte
     };
   };
 
   return SingletonEventManager.call(this, context, name, register);
 }
 
 WebNavigationEventManager.prototype = Object.create(SingletonEventManager.prototype);
 
+function convertGetFrameResult(tabId, data) {
+  return {
+    errorOccurred: data.errorOccurred,
+    url: data.url,
+    tabId,
+    frameId: ExtensionManagement.getFrameId(data.windowId),
+    parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
+  };
+}
+
 extensions.registerSchemaAPI("webNavigation", "webNavigation", (extension, context) => {
   return {
     webNavigation: {
       onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
       onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
       onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
       onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
       onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
       onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
       onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
+      getAllFrames(details) {
+        let tab = TabManager.getTab(details.tabId);
+        if (!tab) {
+          return Promise.reject({ message: `No tab found with tabId: ${details.tabId}`});
+        }
+
+        let { innerWindowID, messageManager } = tab.linkedBrowser;
+        let recipient = { innerWindowID };
+
+        return context.sendMessage(messageManager, "WebNavigation:GetAllFrames", {}, recipient)
+                      .then((results) => results.map(convertGetFrameResult.bind(null, details.tabId)));
+      },
+      getFrame(details) {
+        let tab = TabManager.getTab(details.tabId);
+        if (!tab) {
+          return Promise.reject({ message: `No tab found with tabId: ${details.tabId}`});
+        }
+
+        let recipient = {
+          innerWindowID: tab.linkedBrowser.innerWindowID,
+        };
+
+        let mm = tab.linkedBrowser.messageManager;
+        return context.sendMessage(mm, "WebNavigation:GetFrame", { options: details }, recipient)
+                      .then((result) => {
+                        return result ?
+                          convertGetFrameResult(details.tabId, result) :
+                          Promise.reject({ message: `No frame found with frameId: ${details.frameId}`});
+                      });
+      },
     },
   };
 });
--- a/toolkit/components/extensions/schemas/web_navigation.json
+++ b/toolkit/components/extensions/schemas/web_navigation.json
@@ -31,42 +31,42 @@
         "id": "TransitionQualifier",
         "type": "string",
         "enum": ["client_redirect", "server_redirect", "forward_back", "from_address_bar"]
       }
     ],
     "functions": [
       {
         "name": "getFrame",
-        "unsupported": true,
         "type": "function",
         "description": "Retrieves information about the given frame. A frame refers to an &lt;iframe&gt; or a &lt;frame&gt; of a web page and is identified by a tab ID and a frame ID.",
         "async": "callback",
         "parameters": [
           {
             "type": "object",
             "name": "details",
             "description": "Information about the frame to retrieve information about.",
             "properties": {
               "tabId": { "type": "integer", "minimum": 0, "description": "The ID of the tab in which the frame is." },
-              "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."},
+              "processId": {"optional": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."},
               "frameId": { "type": "integer", "minimum": 0, "description": "The ID of the frame in the given tab." }
             }
           },
           {
             "type": "function",
             "name": "callback",
             "parameters": [
               {
                 "type": "object",
                 "name": "details",
                 "optional": true,
                 "description": "Information about the requested frame, null if the specified frame ID and/or tab ID are invalid.",
                 "properties": {
                   "errorOccurred": {
+                    "unsupported": true,
                     "type": "boolean",
                     "description": "True if the last navigation in this frame was interrupted by an error, i.e. the onErrorOccurred event fired."
                   },
                   "url": {
                     "type": "string",
                     "description": "The URL currently associated with this frame, if the frame identified by the frameId existed at one point in the given tab. The fact that an URL is associated with a given frameId does not imply that the corresponding frame still exists."
                   },
                   "parentFrameId": {
@@ -76,17 +76,16 @@
                 }
               }
             ]
           }
         ]
       },
       {
         "name": "getAllFrames",
-        "unsupported": true,
         "type": "function",
         "description": "Retrieves information about all frames of a given tab.",
         "async": "callback",
         "parameters": [
           {
             "type": "object",
             "name": "details",
             "description": "Information about the tab to retrieve all frames from.",
@@ -102,16 +101,17 @@
                 "name": "details",
                 "type": "array",
                 "description": "A list of frames in the given tab, null if the specified tab ID is invalid.",
                 "optional": true,
                 "items": {
                   "type": "object",
                   "properties": {
                     "errorOccurred": {
+                      "unsupported": true,
                       "type": "boolean",
                       "description": "True if the last navigation in this frame was interrupted by an error, i.e. the onErrorOccurred event fired."
                     },
                     "processId": {
                       "unsupported": true,
                       "type": "integer",
                       "description": "The ID of the process runs the renderer for this tab."
                     },
--- a/toolkit/modules/addons/WebNavigationContent.js
+++ b/toolkit/modules/addons/WebNavigationContent.js
@@ -1,32 +1,25 @@
 "use strict";
 
 /* globals docShell */
 
 var Ci = Components.interfaces;
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-function getWindowId(window) {
-  return window.QueryInterface(Ci.nsIInterfaceRequestor)
-               .getInterface(Ci.nsIDOMWindowUtils)
-               .outerWindowID;
-}
-
-function getParentWindowId(window) {
-  return getWindowId(window.parent);
-}
+XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
+                                  "resource://gre/modules/WebNavigationFrames.jsm");
 
 function loadListener(event) {
   let document = event.target;
   let window = document.defaultView;
   let url = document.documentURI;
-  let windowId = getWindowId(window);
-  let parentWindowId = getParentWindowId(window);
+  let windowId = WebNavigationFrames.getWindowId(window);
+  let parentWindowId = WebNavigationFrames.getParentWindowId(window);
   sendAsyncMessage("Extension:DOMContentLoaded", {windowId, parentWindowId, url});
 }
 
 addEventListener("DOMContentLoaded", loadListener);
 addMessageListener("Extension:DisableWebNavigation", () => {
   removeEventListener("DOMContentLoaded", loadListener);
 });
 
@@ -46,55 +39,58 @@ var WebProgressListener = {
                               .getInterface(Ci.nsIWebProgress);
     webProgress.removeProgressListener(this);
   },
 
   onStateChange: function onStateChange(webProgress, request, stateFlags, status) {
     let data = {
       requestURL: request.QueryInterface(Ci.nsIChannel).URI.spec,
       windowId: webProgress.DOMWindowID,
-      parentWindowId: getParentWindowId(webProgress.DOMWindow),
+      parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
       status,
       stateFlags,
     };
     sendAsyncMessage("Extension:StateChange", data);
 
     if (webProgress.DOMWindow.top != webProgress.DOMWindow) {
       let webNav = webProgress.QueryInterface(Ci.nsIWebNavigation);
       if (!webNav.canGoBack) {
         // For some reason we don't fire onLocationChange for the
         // initial navigation of a sub-frame. So we need to simulate
         // it here.
         let data = {
           location: request.QueryInterface(Ci.nsIChannel).URI.spec,
           windowId: webProgress.DOMWindowID,
-          parentWindowId: getParentWindowId(webProgress.DOMWindow),
+          parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
           flags: 0,
         };
         sendAsyncMessage("Extension:LocationChange", data);
       }
     }
   },
 
   onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
     let data = {
       location: locationURI ? locationURI.spec : "",
       windowId: webProgress.DOMWindowID,
-      parentWindowId: getParentWindowId(webProgress.DOMWindow),
+      parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
       flags,
     };
     sendAsyncMessage("Extension:LocationChange", data);
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
 };
 
 var disabled = false;
 WebProgressListener.init();
 addEventListener("unload", () => {
   if (!disabled) {
+    disabled = true;
     WebProgressListener.uninit();
   }
 });
 addMessageListener("Extension:DisableWebNavigation", () => {
-  disabled = true;
-  WebProgressListener.uninit();
+  if (!disabled) {
+    disabled = true;
+    WebProgressListener.uninit();
+  }
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/addons/WebNavigationFrames.jsm
@@ -0,0 +1,114 @@
+/* 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/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["WebNavigationFrames"];
+
+var Ci = Components.interfaces;
+
+/* exported WebNavigationFrames */
+
+function getWindowId(window) {
+  return window.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindowUtils)
+               .outerWindowID;
+}
+
+function getParentWindowId(window) {
+  return getWindowId(window.parent);
+}
+
+/**
+ * Retrieve the DOMWindow associated to the docShell passed as parameter.
+ *
+ * @param    {nsIDocShell}  docShell - the docShell that we want to get the DOMWindow from.
+ * @return   {nsIDOMWindow}          - the DOMWindow associated to the docShell.
+ */
+function docShellToWindow(docShell) {
+  return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                 .getInterface(Ci.nsIDOMWindow);
+}
+
+/**
+ * The FrameDetail object which represents a frame in WebExtensions APIs.
+ *
+ * @typedef  {Object}  FrameDetail
+ * @inner
+ * @property {number}  windowId       - Represents the numeric id which identify the frame in its tab.
+ * @property {number}  parentWindowId - Represents the numeric id which identify the parent frame.
+ * @property {string}  url            - Represents the current location URL loaded in the frame.
+ * @property {boolean} errorOccurred  - Indicates whether an error is occurred during the last load
+ *                                      happened on this frame (NOT YET SUPPORTED).
+ */
+
+/**
+ * Convert a docShell object into its internal FrameDetail representation.
+ *
+ * @param    {nsIDocShell} docShell - the docShell object to be converted into a FrameDetail JSON object.
+ * @return   {FrameDetail} the FrameDetail JSON object which represents the docShell.
+ */
+function convertDocShellToFrameDetail(docShell) {
+  let window = docShellToWindow(docShell);
+
+  return {
+    windowId: getWindowId(window),
+    parentWindowId: getParentWindowId(window),
+    url: window.location.href,
+  };
+}
+
+/**
+ * A generator function which iterates over a docShell tree, given a root docShell.
+ *
+ * @param  {nsIDocShell} docShell - the root docShell object
+ * @return {Iterator<DocShell>} the FrameDetail JSON object which represents the docShell.
+ */
+function* iterateDocShellTree(docShell) {
+  let docShellsEnum = docShell.getDocShellEnumerator(
+    Ci.nsIDocShellTreeItem.typeContent,
+    Ci.nsIDocShell.ENUMERATE_FORWARDS
+  );
+
+  while (docShellsEnum.hasMoreElements()) {
+    yield docShellsEnum.getNext();
+  }
+
+  return null;
+}
+
+/**
+ * Search for a frame starting from the passed root docShell and
+ * convert it to its related frame detail representation.
+ *
+ * @param  {number}      windowId - the windowId of the frame to retrieve
+ * @param  {nsIDocShell} docShell - the root docShell object
+ * @return {FrameDetail} the FrameDetail JSON object which represents the docShell.
+ */
+function findFrame(windowId, rootDocShell) {
+  for (let docShell of iterateDocShellTree(rootDocShell)) {
+    if (windowId == getWindowId(docShellToWindow(docShell))) {
+      return convertDocShellToFrameDetail(docShell);
+    }
+  }
+
+  return null;
+}
+
+var WebNavigationFrames = {
+  getFrame(docShell, frameId) {
+    if (frameId == 0) {
+      return convertDocShellToFrameDetail(docShell);
+    }
+
+    return findFrame(frameId, docShell);
+  },
+
+  getAllFrames(docShell) {
+    return Array.from(iterateDocShellTree(docShell), convertDocShellToFrameDetail);
+  },
+
+  getWindowId,
+  getParentWindowId,
+};
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -14,16 +14,17 @@ TESTING_JS_MODULES += [
 ]
 
 SPHINX_TREES['toolkit_modules'] = 'docs'
 
 EXTRA_JS_MODULES += [
     'addons/MatchPattern.jsm',
     'addons/WebNavigation.jsm',
     'addons/WebNavigationContent.js',
+    'addons/WebNavigationFrames.jsm',
     'addons/WebRequest.jsm',
     'addons/WebRequestCommon.jsm',
     'addons/WebRequestContent.js',
     'Battery.jsm',
     'BinarySearch.jsm',
     'BrowserUtils.jsm',
     'CertUtils.jsm',
     'CharsetMenu.jsm',