/* 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 {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); this.EXPORTED_SYMBOLS = ["frame"]; this.frame = {}; const FRAME_SCRIPT = "chrome://marionette/content/listener.js"; // list of OOP frames that has the frame script loaded var remoteFrames = []; /** * An object representing a frame that Marionette has loaded a * frame script in. */ frame.RemoteFrame = function (windowId, frameId) { // outerWindowId relative to main process this.windowId = windowId; // actual frame relative to the windowId's frames list this.frameId = frameId; // assigned frame ID, used for messaging this.targetFrameId = this.frameId; // list of OOP frames that has the frame script loaded this.remoteFrames = []; }; /** * The FrameManager will maintain the list of Out Of Process (OOP) * frames and will handle frame switching between them. * * It handles explicit frame switching (switchToFrame), and implicit * frame switching, which occurs when a modal dialog is triggered in B2G. * * @param {GeckoDriver} driver * Reference to the driver instance. */ frame.Manager = class { constructor(driver) { // messageManager maintains the messageManager // for the current process' chrome frame or the global message manager // holds a member of the remoteFrames (for an OOP frame) // or null (for the main process) this.currentRemoteFrame = null; // frame we'll need to restore once interrupt is gone this.previousRemoteFrame = null; // set to true when we have been interrupted by a modal this.handledModal = false; this.driver = driver; } /** * Receives all messages from content messageManager. */ receiveMessage(message) { switch (message.name) { case "MarionetteFrame:getInterruptedState": // this will return true if the calling frame was interrupted by a modal dialog if (this.previousRemoteFrame) { // get the frame window of the interrupted frame let interruptedFrame = Services.wm.getOuterWindowWithId( this.previousRemoteFrame.windowId); if (this.previousRemoteFrame.frameId !== null) { // find OOP frame let iframes = interruptedFrame.document.getElementsByTagName("iframe"); interruptedFrame = iframes[this.previousRemoteFrame.frameId]; } // check if the interrupted frame is the same as the calling frame if (interruptedFrame.src == message.target.src) { return {value: this.handledModal}; } // we get here if previousRemoteFrame and currentRemoteFrame are null, // i.e. if we're in a non-OOP process, or we haven't switched into an OOP frame, // in which case, handledModal can't be set to true } else if (this.currentRemoteFrame === null) { return {value: this.handledModal}; } return {value: false}; // handleModal is called when we need to switch frames to the main // process due to a modal dialog interrupt case "MarionetteFrame:handleModal": // If previousRemoteFrame was set, that means we switched into a // remote frame. If this is the case, then we want to switch back // into the system frame. If it isn't the case, then we're in a // non-OOP environment, so we don't need to handle remote frames. let isLocal = true; if (this.currentRemoteFrame !== null) { isLocal = false; this.removeMessageManagerListeners( this.currentRemoteFrame.messageManager.get()); // store the previous frame so we can switch back to it when // the modal is dismissed this.previousRemoteFrame = this.currentRemoteFrame; // by setting currentRemoteFrame to null, // it signifies we're in the main process this.currentRemoteFrame = null; this.driver.messageManager = Cc["@mozilla.org/globalmessagemanager;1"] .getService(Ci.nsIMessageBroadcaster); } this.handledModal = true; this.driver.sendOk(this.driver.command_id); return {value: isLocal}; case "MarionetteFrame:getCurrentFrameId": if (this.currentRemoteFrame !== null) { return this.currentRemoteFrame.frameId; } } } getOopFrame(winId, frameId) { // get original frame window let outerWin = Services.wm.getOuterWindowWithId(winId); // find the OOP frame let f = outerWin.document.getElementsByTagName("iframe")[frameId]; return f; } getFrameMM(winId, frameId) { let oopFrame = this.getOopFrame(winId, frameId); let mm = oopFrame.QueryInterface(Ci.nsIFrameLoaderOwner) .frameLoader.messageManager; return mm; } /** * Switch to OOP frame. We're handling this here so we can maintain * a list of remote frames. */ switchToFrame(winId, frameId) { let oopFrame = this.getOopFrame(winId, frameId); let mm = this.getFrameMM(winId, frameId); // see if this frame already has our frame script loaded in it; // if so, just wake it up for (let i = 0; i < remoteFrames.length; i++) { let f = remoteFrames[i]; let fmm = f.messageManager.get(); try { fmm.sendAsyncMessage("aliveCheck", {}); } catch (e) { if (e.result == Cr.NS_ERROR_NOT_INITIALIZED) { remoteFrames.splice(i--, 1); continue; } } if (fmm == mm) { this.currentRemoteFrame = f; this.addMessageManagerListeners(mm); mm.sendAsyncMessage("Marionette:restart"); return oopFrame.id; } } // if we get here, then we need to load the frame script in this frame, // and set the frame's ChromeMessageSender as the active message manager // the driver will listen to. this.addMessageManagerListeners(mm); let f = new frame.RemoteFrame(winId, frameId); f.messageManager = Cu.getWeakReference(mm); remoteFrames.push(f); this.currentRemoteFrame = f; mm.loadFrameScript(FRAME_SCRIPT, true, true); return oopFrame.id; } /* * This function handles switching back to the frame that was * interrupted by the modal dialog. It gets called by the interrupted * frame once the dialog is dismissed and the frame resumes its process. */ switchToModalOrigin() { // only handle this if we indeed switched out of the modal's // originating frame if (this.previousRemoteFrame !== null) { this.currentRemoteFrame = this.previousRemoteFrame; let mm = this.currentRemoteFrame.messageManager.get(); this.addMessageManagerListeners(mm); } this.handledModal = false; } /** * Adds message listeners to the driver, listening for * messages from content frame scripts. It also adds a * MarionetteFrame:getInterruptedState message listener to the * FrameManager, so the frame manager's state can be checked by the frame. * * @param {nsIMessageListenerManager} mm * The message manager object, typically * ChromeMessageBroadcaster or ChromeMessageSender. */ addMessageManagerListeners(mm) { mm.addWeakMessageListener("Marionette:ok", this.driver); mm.addWeakMessageListener("Marionette:done", this.driver); mm.addWeakMessageListener("Marionette:error", this.driver); mm.addWeakMessageListener("Marionette:emitTouchEvent", this.driver); mm.addWeakMessageListener("Marionette:log", this.driver); mm.addWeakMessageListener("Marionette:shareData", this.driver); mm.addWeakMessageListener("Marionette:switchToModalOrigin", this.driver); mm.addWeakMessageListener("Marionette:switchedToFrame", this.driver); mm.addWeakMessageListener("Marionette:getVisibleCookies", this.driver); mm.addWeakMessageListener("Marionette:getImportedScripts", this.driver.importedScripts); mm.addWeakMessageListener("Marionette:register", this.driver); mm.addWeakMessageListener("Marionette:listenersAttached", this.driver); mm.addWeakMessageListener("MarionetteFrame:handleModal", this); mm.addWeakMessageListener("MarionetteFrame:getCurrentFrameId", this); mm.addWeakMessageListener("MarionetteFrame:getInterruptedState", this); } /** * Removes listeners for messages from content frame scripts. * We do not remove the MarionetteFrame:getInterruptedState or * the Marionette:switchToModalOrigin message listener, because we * want to allow all known frames to contact the frame manager so * that it can check if it was interrupted, and if so, it will call * switchToModalOrigin when its process gets resumed. * * @param {nsIMessageListenerManager} mm * The message manager object, typically * ChromeMessageBroadcaster or ChromeMessageSender. */ removeMessageManagerListeners(mm) { mm.removeWeakMessageListener("Marionette:ok", this.driver); mm.removeWeakMessageListener("Marionette:done", this.driver); mm.removeWeakMessageListener("Marionette:error", this.driver); mm.removeWeakMessageListener("Marionette:log", this.driver); mm.removeWeakMessageListener("Marionette:shareData", this.driver); mm.removeWeakMessageListener("Marionette:switchedToFrame", this.driver); mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.driver); mm.removeWeakMessageListener("Marionette:getImportedScripts", this.driver.importedScripts); mm.removeWeakMessageListener("Marionette:listenersAttached", this.driver); mm.removeWeakMessageListener("Marionette:register", this.driver); mm.removeWeakMessageListener("MarionetteFrame:handleModal", this); mm.removeWeakMessageListener("MarionetteFrame:getCurrentFrameId", this); } }; frame.Manager.prototype.QueryInterface = XPCOMUtils.generateQI( [Ci.nsIMessageListener, Ci.nsISupportsWeakReference]);