123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616 |
- /* 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 events = require("sdk/event/core");
- const protocol = require("devtools/shared/protocol");
- const { Cu, Ci } = require("chrome");
- const { on, once, off, emit } = events;
- const { method, Arg, Option, RetVal, types } = protocol;
- const { sandbox, evaluate } = require("sdk/loader/sandbox");
- const { Class } = require("sdk/core/heritage");
- const { PlainTextConsole } = require("sdk/console/plain-text");
- const { DirectorRegistry } = require("./director-registry");
- const {
- messagePortSpec,
- directorManagerSpec,
- directorScriptSpec,
- } = require("devtools/shared/specs/director-manager");
- /**
- * Error Messages
- */
- const ERR_MESSAGEPORT_FINALIZED = "message port finalized";
- const ERR_DIRECTOR_UNKNOWN_SCRIPTID = "unkown director-script id";
- const ERR_DIRECTOR_UNINSTALLED_SCRIPTID = "uninstalled director-script id";
- /**
- * A MessagePort Actor allowing communication through messageport events
- * over the remote debugging protocol.
- */
- var MessagePortActor = exports.MessagePortActor = protocol.ActorClassWithSpec(messagePortSpec, {
- /**
- * Create a MessagePort actor.
- *
- * @param DebuggerServerConnection conn
- * The server connection.
- * @param MessagePort port
- * The wrapped MessagePort.
- */
- initialize: function (conn, port) {
- protocol.Actor.prototype.initialize.call(this, conn);
- // NOTE: can't get a weak reference because we need to subscribe events
- // using port.onmessage or addEventListener
- this.port = port;
- },
- destroy: function (conn) {
- protocol.Actor.prototype.destroy.call(this, conn);
- this.finalize();
- },
- /**
- * Sends a message on the wrapped message port.
- *
- * @param Object msg
- * The JSON serializable message event payload
- */
- postMessage: function (msg) {
- if (!this.port) {
- console.error(ERR_MESSAGEPORT_FINALIZED);
- return;
- }
- this.port.postMessage(msg);
- },
- /**
- * Starts to receive and send queued messages on this message port.
- */
- start: function () {
- if (!this.port) {
- console.error(ERR_MESSAGEPORT_FINALIZED);
- return;
- }
- // NOTE: set port.onmessage to a function is an implicit start
- // and starts to send queued messages.
- // On the client side we should set MessagePortClient.onmessage
- // to a setter which register an handler to the message event
- // and call the actor start method to start receiving messages
- // from the MessagePort's queue.
- this.port.onmessage = (evt) => {
- var ports;
- // TODO: test these wrapped ports
- if (Array.isArray(evt.ports)) {
- ports = evt.ports.map((port) => {
- let actor = new MessagePortActor(this.conn, port);
- this.manage(actor);
- return actor;
- });
- }
- emit(this, "message", {
- isTrusted: evt.isTrusted,
- data: evt.data,
- origin: evt.origin,
- lastEventId: evt.lastEventId,
- source: this,
- ports: ports
- });
- };
- },
- /**
- * Starts to receive and send queued messages on this message port, or
- * raise an exception if the port is null
- */
- close: function () {
- if (!this.port) {
- console.error(ERR_MESSAGEPORT_FINALIZED);
- return;
- }
- try {
- this.port.onmessage = null;
- this.port.close();
- } catch (e) {
- // The port might be a dead object
- console.error(e);
- }
- },
- finalize: function () {
- this.close();
- this.port = null;
- },
- });
- /**
- * The Director Script Actor manage javascript code running in a non-privileged sandbox with the same
- * privileges of the target global (browser tab or a firefox os app).
- *
- * After retrieving an instance of this actor (from the tab director actor), you'll need to set it up
- * by calling setup().
- *
- * After the setup, this actor will automatically attach/detach the content script (and optionally a
- * directly connect the debugger client and the content script using a MessageChannel) on tab
- * navigation.
- */
- var DirectorScriptActor = exports.DirectorScriptActor = protocol.ActorClassWithSpec(directorScriptSpec, {
- /**
- * Creates the director script actor
- *
- * @param DebuggerServerConnection conn
- * The server connection.
- * @param Actor tabActor
- * The tab (or root) actor.
- * @param String scriptId
- * The director-script id.
- * @param String scriptCode
- * The director-script javascript source.
- * @param Object scriptOptions
- * The director-script options object.
- */
- initialize: function (conn, tabActor, { scriptId, scriptCode, scriptOptions }) {
- protocol.Actor.prototype.initialize.call(this, conn, tabActor);
- this.tabActor = tabActor;
- this._scriptId = scriptId;
- this._scriptCode = scriptCode;
- this._scriptOptions = scriptOptions;
- this._setupCalled = false;
- this._onGlobalCreated = this._onGlobalCreated.bind(this);
- this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
- },
- destroy: function (conn) {
- protocol.Actor.prototype.destroy.call(this, conn);
- this.finalize();
- },
- /**
- * Starts listening to the tab global created, in order to create the director-script sandbox
- * using the configured scriptCode, attached/detached automatically to the tab
- * window on tab navigation.
- *
- * @param Boolean reload
- * attach the page immediately or reload it first.
- * @param Boolean skipAttach
- * skip the attach
- */
- setup: function ({ reload, skipAttach }) {
- if (this._setupCalled) {
- // do nothing
- return;
- }
- this._setupCalled = true;
- on(this.tabActor, "window-ready", this._onGlobalCreated);
- on(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
- // optional skip attach (needed by director-manager for director scripts bulk activation)
- if (skipAttach) {
- return;
- }
- if (reload) {
- this.window.location.reload();
- } else {
- // fake a global created event to attach without reload
- this._onGlobalCreated({ id: getWindowID(this.window), window: this.window, isTopLevel: true });
- }
- },
- /**
- * Get the attached MessagePort actor if any
- */
- getMessagePort: function () {
- return this._messagePortActor;
- },
- /**
- * Stop listening for document global changes, destroy the content worker and puts
- * this actor to hibernation.
- */
- finalize: function () {
- if (!this._setupCalled) {
- return;
- }
- off(this.tabActor, "window-ready", this._onGlobalCreated);
- off(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
- this._onGlobalDestroyed({ id: this._lastAttachedWinId });
- this._setupCalled = false;
- },
- // local helpers
- get window() {
- return this.tabActor.window;
- },
- /* event handlers */
- _onGlobalCreated: function ({ id, window, isTopLevel }) {
- if (!isTopLevel) {
- // filter iframes
- return;
- }
- try {
- if (this._lastAttachedWinId) {
- // if we have received a global created without a previous global destroyed,
- // it's time to cleanup the previous state
- this._onGlobalDestroyed(this._lastAttachedWinId);
- }
- // TODO: check if we want to share a single sandbox per global
- // for multiple debugger clients
- // create & attach the new sandbox
- this._scriptSandbox = new DirectorScriptSandbox({
- scriptId: this._scriptId,
- scriptCode: this._scriptCode,
- scriptOptions: this._scriptOptions
- });
- // attach the global window
- this._lastAttachedWinId = id;
- var port = this._scriptSandbox.attach(window, id);
- this._onDirectorScriptAttach(window, port);
- } catch (e) {
- this._onDirectorScriptError(e);
- }
- },
- _onGlobalDestroyed: function ({ id }) {
- if (id !== this._lastAttachedWinId) {
- // filter destroyed globals
- return;
- }
- // unmanage and cleanup the messageport actor
- if (this._messagePortActor) {
- this.unmanage(this._messagePortActor);
- this._messagePortActor = null;
- }
- // NOTE: destroy here the old worker
- if (this._scriptSandbox) {
- this._scriptSandbox.destroy(this._onDirectorScriptError.bind(this));
- // send a detach event to the debugger client
- emit(this, "detach", {
- directorScriptId: this._scriptId,
- innerId: this._lastAttachedWinId
- });
- this._lastAttachedWinId = null;
- this._scriptSandbox = null;
- }
- },
- _onDirectorScriptError: function (error) {
- // route the content script error to the debugger client
- if (error) {
- // prevents silent director-script-errors
- console.error("director-script-error", error);
- // route errors to debugger server clients
- emit(this, "error", {
- directorScriptId: this._scriptId,
- message: error.toString(),
- stack: error.stack,
- fileName: error.fileName,
- lineNumber: error.lineNumber,
- columnNumber: error.columnNumber
- });
- }
- },
- _onDirectorScriptAttach: function (window, port) {
- let portActor = new MessagePortActor(this.conn, port);
- this.manage(portActor);
- this._messagePortActor = portActor;
- emit(this, "attach", {
- directorScriptId: this._scriptId,
- url: (window && window.location) ? window.location.toString() : "",
- innerId: this._lastAttachedWinId,
- port: this._messagePortActor
- });
- }
- });
- /**
- * The DirectorManager Actor is a tab actor which manages enabling/disabling director scripts.
- */
- const DirectorManagerActor = exports.DirectorManagerActor = protocol.ActorClassWithSpec(directorManagerSpec, {
- /* init & destroy methods */
- initialize: function (conn, tabActor) {
- protocol.Actor.prototype.initialize.call(this, conn);
- this.tabActor = tabActor;
- this._directorScriptActorsMap = new Map();
- },
- destroy: function (conn) {
- protocol.Actor.prototype.destroy.call(this, conn);
- this.finalize();
- },
- /**
- * Retrieves the list of installed director-scripts.
- */
- list: function () {
- let enabledScriptIds = [...this._directorScriptActorsMap.keys()];
- return {
- installed: DirectorRegistry.list(),
- enabled: enabledScriptIds
- };
- },
- /**
- * Bulk enabling director-scripts.
- *
- * @param Array[String] selectedIds
- * The list of director-script ids to be enabled,
- * ["*"] will activate all the installed director-scripts
- * @param Boolean reload
- * optionally reload the target window
- */
- enableByScriptIds: function (selectedIds, { reload }) {
- if (selectedIds && selectedIds.length === 0) {
- // filtered all director scripts ids
- return;
- }
- for (let scriptId of DirectorRegistry.list()) {
- // filter director script ids
- if (selectedIds.indexOf("*") < 0 &&
- selectedIds.indexOf(scriptId) < 0) {
- continue;
- }
- let actor = this.getByScriptId(scriptId);
- // skip attach if reload is true (activated director scripts
- // will be automatically attached on the final reload)
- actor.setup({ reload: false, skipAttach: reload });
- }
- if (reload) {
- this.tabActor.window.location.reload();
- }
- },
- /**
- * Bulk disabling director-scripts.
- *
- * @param Array[String] selectedIds
- * The list of director-script ids to be disable,
- * ["*"] will de-activate all the enable director-scripts
- * @param Boolean reload
- * optionally reload the target window
- */
- disableByScriptIds: function (selectedIds, { reload }) {
- if (selectedIds && selectedIds.length === 0) {
- // filtered all director scripts ids
- return;
- }
- for (let scriptId of this._directorScriptActorsMap.keys()) {
- // filter director script ids
- if (selectedIds.indexOf("*") < 0 &&
- selectedIds.indexOf(scriptId) < 0) {
- continue;
- }
- let actor = this._directorScriptActorsMap.get(scriptId);
- this._directorScriptActorsMap.delete(scriptId);
- // finalize the actor (which will produce director-script-detach event)
- actor.finalize();
- // unsubscribe event handlers on the disabled actor
- off(actor);
- this.unmanage(actor);
- }
- if (reload) {
- this.tabActor.window.location.reload();
- }
- },
- /**
- * Retrieves the actor instance of an installed director-script
- * (and create the actor instance if it doesn't exists yet).
- */
- getByScriptId: function (scriptId) {
- var id = scriptId;
- // raise an unknown director-script id exception
- if (!DirectorRegistry.checkInstalled(id)) {
- console.error(ERR_DIRECTOR_UNKNOWN_SCRIPTID, id);
- throw Error(ERR_DIRECTOR_UNKNOWN_SCRIPTID);
- }
- // get a previous created actor instance
- let actor = this._directorScriptActorsMap.get(id);
- // create a new actor instance
- if (!actor) {
- let directorScriptDefinition = DirectorRegistry.get(id);
- // test lazy director-script (e.g. uninstalled in the parent process)
- if (!directorScriptDefinition) {
- console.error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID, id);
- throw Error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID);
- }
- actor = new DirectorScriptActor(this.conn, this.tabActor, directorScriptDefinition);
- this._directorScriptActorsMap.set(id, actor);
- on(actor, "error", emit.bind(null, this, "director-script-error"));
- on(actor, "attach", emit.bind(null, this, "director-script-attach"));
- on(actor, "detach", emit.bind(null, this, "director-script-detach"));
- this.manage(actor);
- }
- return actor;
- },
- finalize: function () {
- this.disableByScriptIds(["*"], false);
- }
- });
- /* private helpers */
- /**
- * DirectorScriptSandbox is a private utility class, which attach a non-priviliged sandbox
- * to a target window.
- */
- const DirectorScriptSandbox = Class({
- initialize: function ({scriptId, scriptCode, scriptOptions}) {
- this._scriptId = scriptId;
- this._scriptCode = scriptCode;
- this._scriptOptions = scriptOptions;
- },
- attach: function (window, innerId) {
- this._innerId = innerId,
- this._window = window;
- this._proto = Cu.createObjectIn(this._window);
- var id = this._scriptId;
- var uri = this._scriptCode;
- this._sandbox = sandbox(window, {
- sandboxName: uri,
- sandboxPrototype: this._proto,
- sameZoneAs: window,
- wantXrays: true,
- wantComponents: false,
- wantExportHelpers: false,
- metadata: {
- URI: uri,
- addonID: id,
- SDKDirectorScript: true,
- "inner-window-id": innerId
- }
- });
- // create a CommonJS module object which match the interface from addon-sdk
- // (addon-sdk/sources/lib/toolkit/loader.js#L678-L686)
- var module = Cu.cloneInto(Object.create(null, {
- id: { enumerable: true, value: id },
- uri: { enumerable: true, value: uri },
- exports: { enumerable: true, value: Cu.createObjectIn(this._sandbox) }
- }), this._sandbox);
- // create a console API object
- let directorScriptConsole = new PlainTextConsole(null, this._innerId);
- // inject CommonJS module globals into the sandbox prototype
- Object.defineProperties(this._proto, {
- module: { enumerable: true, value: module },
- exports: { enumerable: true, value: module.exports },
- console: {
- enumerable: true,
- value: Cu.cloneInto(directorScriptConsole, this._sandbox, { cloneFunctions: true })
- }
- });
- Object.defineProperties(this._sandbox, {
- require: {
- enumerable: true,
- value: Cu.cloneInto(function () {
- throw Error("NOT IMPLEMENTED");
- }, this._sandbox, { cloneFunctions: true })
- }
- });
- // TODO: if the debugger target is local, the debugger client could pass
- // to the director actor the resource url instead of the entire javascript source code.
- // evaluate the director script source in the sandbox
- evaluate(this._sandbox, this._scriptCode, "javascript:" + this._scriptCode);
- // prepare the messageport connected to the debugger client
- let { port1, port2 } = new this._window.MessageChannel();
- // prepare the unload callbacks queue
- var sandboxOnUnloadQueue = this._sandboxOnUnloadQueue = [];
- // create the attach options
- var attachOptions = this._attachOptions = Cu.createObjectIn(this._sandbox);
- Object.defineProperties(attachOptions, {
- port: { enumerable: true, value: port1 },
- window: { enumerable: true, value: window },
- scriptOptions: { enumerable: true, value: Cu.cloneInto(this._scriptOptions, this._sandbox) },
- onUnload: {
- enumerable: true,
- value: Cu.cloneInto(function (cb) {
- // collect unload callbacks
- if (typeof cb == "function") {
- sandboxOnUnloadQueue.push(cb);
- }
- }, this._sandbox, { cloneFunctions: true })
- }
- });
- // select the attach method
- var exports = this._proto.module.exports;
- if (this._scriptOptions && "attachMethod" in this._scriptOptions) {
- this._sandboxOnAttach = exports[this._scriptOptions.attachMethod];
- } else {
- this._sandboxOnAttach = exports;
- }
- if (typeof this._sandboxOnAttach !== "function") {
- throw Error("the configured attachMethod '" +
- (this._scriptOptions.attachMethod || "module.exports") +
- "' is not exported by the directorScript");
- }
- // call the attach method
- this._sandboxOnAttach.call(this._sandbox, attachOptions);
- return port2;
- },
- destroy: function (onError) {
- // evaluate queue unload methods if any
- while (this._sandboxOnUnloadQueue && this._sandboxOnUnloadQueue.length > 0) {
- let cb = this._sandboxOnUnloadQueue.pop();
- try {
- cb();
- } catch (e) {
- console.error("Exception on DirectorScript Sandbox destroy", e);
- onError(e);
- }
- }
- Cu.nukeSandbox(this._sandbox);
- }
- });
- function getWindowID(window) {
- return window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils)
- .currentInnerWindowID;
- }
|