/* 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 { frames } = require("../remote/child"); const { Class } = require("../core/heritage"); const { Disposable } = require('../core/disposable'); const { data } = require("../self"); const { once } = require("../dom/events"); const { getAttachEventType } = require("./utils"); const { Rules } = require('../util/rules'); const { uuid } = require('../util/uuid'); const { WorkerChild } = require("./worker-child"); const { Cc, Ci, Cu } = require("chrome"); const { observe } = require("../event/chrome"); const { on } = require("../event/core"); const appShell = Cc["@mozilla.org/appshell/appShellService;1"].getService(Ci.nsIAppShellService); const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); const pages = new Map(); const DOC_INSERTED = "document-element-inserted"; function isValidURL(page, url) { return !page.rules || page.rules.matchesAny(url); } const ChildPage = Class({ implements: [ Disposable ], setup: function(frame, id, options) { this.id = id; this.frame = frame; this.options = options; this.webNav = appShell.createWindowlessBrowser(false); this.docShell.allowJavascript = this.options.allow.script; // Accessing the browser's window forces the initial about:blank document to // be created before we start listening for notifications this.contentWindow; this.webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); pages.set(this.id, this); this.contentURL = options.contentURL; if (options.include) { this.rules = Rules(); this.rules.add.apply(this.rules, [].concat(options.include)); } }, dispose: function() { pages.delete(this.id); this.webProgress.removeProgressListener(this); this.webNav.close(); this.webNav = null; }, attachWorker: function() { if (!isValidURL(this, this.contentWindow.location.href)) return; this.options.id = uuid().toString(); this.options.window = this.contentWindow; this.frame.port.emit("sdk/frame/connect", this.id, { id: this.options.id, url: this.contentWindow.document.documentURIObject.spec }); new WorkerChild(this.options); }, get docShell() { return this.webNav.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDocShell); }, get webProgress() { return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebProgress); }, get contentWindow() { return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindow); }, get contentURL() { return this.options.contentURL; }, set contentURL(url) { this.options.contentURL = url; url = this.options.contentURL ? data.url(this.options.contentURL) : "about:blank"; this.webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null); }, onLocationChange: function(progress, request, location, flags) { // Ignore inner-frame events if (progress != this.webProgress) return; // Ignore events that don't change the document if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) return; let event = getAttachEventType(this.options); // Attaching at the start of the load is handled by the // document-element-inserted listener. if (event == DOC_INSERTED) return; once(this.contentWindow, event, () => { this.attachWorker(); }, false); }, QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"]) }); on(observe(DOC_INSERTED), "data", ({ target }) => { let page = Array.from(pages.values()).find(p => p.contentWindow.document === target); if (!page) return; if (getAttachEventType(page.options) == DOC_INSERTED) page.attachWorker(); }); frames.port.on("sdk/frame/create", (frame, id, options) => { new ChildPage(frame, id, options); }); frames.port.on("sdk/frame/set", (frame, id, params) => { let page = pages.get(id); if (!page) return; if ("allowScript" in params) page.docShell.allowJavascript = params.allowScript; if ("contentURL" in params) page.contentURL = params.contentURL; }); frames.port.on("sdk/frame/destroy", (frame, id) => { let page = pages.get(id); if (!page) return; page.destroy(); });