mirror of
https://github.com/classilla/tenfourfox.git
synced 2025-01-09 13:30:34 +00:00
289 lines
8.7 KiB
JavaScript
289 lines
8.7 KiB
JavaScript
/* 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/. */
|
|
|
|
/**
|
|
*
|
|
* `deprecated/sync-worker` was previously `content/worker`, that was
|
|
* incompatible with e10s. we are in the process of switching to the new
|
|
* asynchronous `Worker`, which behaves slightly differently in some edge
|
|
* cases, so we are keeping this one around for a short period.
|
|
* try to switch to the new one as soon as possible..
|
|
*
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
module.metadata = {
|
|
"stability": "unstable"
|
|
};
|
|
|
|
const { Class } = require('../core/heritage');
|
|
const { EventTarget } = require('../event/target');
|
|
const { on, off, emit, setListeners } = require('../event/core');
|
|
const {
|
|
attach, detach, destroy
|
|
} = require('../content/utils');
|
|
const { method } = require('../lang/functional');
|
|
const { Ci, Cu, Cc } = require('chrome');
|
|
const unload = require('../system/unload');
|
|
const events = require('../system/events');
|
|
const { getInnerId } = require("../window/utils");
|
|
const { WorkerSandbox } = require('../content/sandbox');
|
|
const { isPrivate } = require('../private-browsing/utils');
|
|
|
|
// A weak map of workers to hold private attributes that
|
|
// should not be exposed
|
|
const workers = new WeakMap();
|
|
|
|
var modelFor = (worker) => workers.get(worker);
|
|
|
|
const ERR_DESTROYED =
|
|
"Couldn't find the worker to receive this message. " +
|
|
"The script may not be initialized yet, or may already have been unloaded.";
|
|
|
|
const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
|
|
"until it is visible again.";
|
|
|
|
/**
|
|
* Message-passing facility for communication between code running
|
|
* in the content and add-on process.
|
|
* @see https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/content_worker
|
|
*/
|
|
const Worker = Class({
|
|
implements: [EventTarget],
|
|
initialize: function WorkerConstructor (options) {
|
|
// Save model in weak map to not expose properties
|
|
let model = createModel();
|
|
workers.set(this, model);
|
|
|
|
options = options || {};
|
|
|
|
if ('contentScriptFile' in options)
|
|
this.contentScriptFile = options.contentScriptFile;
|
|
if ('contentScriptOptions' in options)
|
|
this.contentScriptOptions = options.contentScriptOptions;
|
|
if ('contentScript' in options)
|
|
this.contentScript = options.contentScript;
|
|
if ('injectInDocument' in options)
|
|
this.injectInDocument = !!options.injectInDocument;
|
|
|
|
setListeners(this, options);
|
|
|
|
unload.ensure(this, "destroy");
|
|
|
|
// Ensure that worker.port is initialized for contentWorker to be able
|
|
// to send events during worker initialization.
|
|
this.port = createPort(this);
|
|
|
|
model.documentUnload = documentUnload.bind(this);
|
|
model.pageShow = pageShow.bind(this);
|
|
model.pageHide = pageHide.bind(this);
|
|
|
|
if ('window' in options)
|
|
attach(this, options.window);
|
|
},
|
|
|
|
/**
|
|
* Sends a message to the worker's global scope. Method takes single
|
|
* argument, which represents data to be sent to the worker. The data may
|
|
* be any primitive type value or `JSON`. Call of this method asynchronously
|
|
* emits `message` event with data value in the global scope of this
|
|
* worker.
|
|
*
|
|
* `message` event listeners can be set either by calling
|
|
* `self.on` with a first argument string `"message"` or by
|
|
* implementing `onMessage` function in the global scope of this worker.
|
|
* @param {Number|String|JSON} data
|
|
*/
|
|
postMessage: function (...data) {
|
|
let model = modelFor(this);
|
|
let args = ['message'].concat(data);
|
|
if (!model.inited) {
|
|
model.earlyEvents.push(args);
|
|
return;
|
|
}
|
|
processMessage.apply(null, [this].concat(args));
|
|
},
|
|
|
|
get url () {
|
|
let model = modelFor(this);
|
|
// model.window will be null after detach
|
|
return model.window ? model.window.document.location.href : null;
|
|
},
|
|
|
|
get contentURL () {
|
|
let model = modelFor(this);
|
|
return model.window ? model.window.document.URL : null;
|
|
},
|
|
|
|
// Implemented to provide some of the previous features of exposing sandbox
|
|
// so that Worker can be extended
|
|
getSandbox: function () {
|
|
return modelFor(this).contentWorker;
|
|
},
|
|
|
|
toString: function () { return '[object Worker]'; },
|
|
attach: method(attach),
|
|
detach: method(detach),
|
|
destroy: method(destroy)
|
|
});
|
|
exports.Worker = Worker;
|
|
|
|
attach.define(Worker, function (worker, window) {
|
|
let model = modelFor(worker);
|
|
model.window = window;
|
|
// Track document unload to destroy this worker.
|
|
// We can't watch for unload event on page's window object as it
|
|
// prevents bfcache from working:
|
|
// https://developer.mozilla.org/En/Working_with_BFCache
|
|
model.windowID = getInnerId(model.window);
|
|
events.on("inner-window-destroyed", model.documentUnload);
|
|
|
|
// will set model.contentWorker pointing to the private API:
|
|
model.contentWorker = WorkerSandbox(worker, model.window);
|
|
|
|
// Listen to pagehide event in order to freeze the content script
|
|
// while the document is frozen in bfcache:
|
|
model.window.addEventListener("pageshow", model.pageShow, true);
|
|
model.window.addEventListener("pagehide", model.pageHide, true);
|
|
|
|
// Mainly enable worker.port.emit to send event to the content worker
|
|
model.inited = true;
|
|
model.frozen = false;
|
|
|
|
// Fire off `attach` event
|
|
emit(worker, 'attach', window);
|
|
|
|
// Process all events and messages that were fired before the
|
|
// worker was initialized.
|
|
model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args)));
|
|
});
|
|
|
|
/**
|
|
* Remove all internal references to the attached document
|
|
* Tells _port to unload itself and removes all the references from itself.
|
|
*/
|
|
detach.define(Worker, function (worker, reason) {
|
|
let model = modelFor(worker);
|
|
|
|
// maybe unloaded before content side is created
|
|
if (model.contentWorker) {
|
|
model.contentWorker.destroy(reason);
|
|
}
|
|
|
|
model.contentWorker = null;
|
|
if (model.window) {
|
|
model.window.removeEventListener("pageshow", model.pageShow, true);
|
|
model.window.removeEventListener("pagehide", model.pageHide, true);
|
|
}
|
|
model.window = null;
|
|
// This method may be called multiple times,
|
|
// avoid dispatching `detach` event more than once
|
|
if (model.windowID) {
|
|
model.windowID = null;
|
|
events.off("inner-window-destroyed", model.documentUnload);
|
|
model.earlyEvents.length = 0;
|
|
emit(worker, 'detach');
|
|
}
|
|
model.inited = false;
|
|
});
|
|
|
|
isPrivate.define(Worker, ({ tab }) => isPrivate(tab));
|
|
|
|
/**
|
|
* Tells content worker to unload itself and
|
|
* removes all the references from itself.
|
|
*/
|
|
destroy.define(Worker, function (worker, reason) {
|
|
detach(worker, reason);
|
|
modelFor(worker).inited = true;
|
|
// Specifying no type or listener removes all listeners
|
|
// from target
|
|
off(worker);
|
|
off(worker.port);
|
|
});
|
|
|
|
/**
|
|
* Events fired by workers
|
|
*/
|
|
function documentUnload ({ subject, data }) {
|
|
let model = modelFor(this);
|
|
let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
|
if (innerWinID != model.windowID) return false;
|
|
detach(this);
|
|
return true;
|
|
}
|
|
|
|
function pageShow () {
|
|
let model = modelFor(this);
|
|
model.contentWorker.emitSync('pageshow');
|
|
emit(this, 'pageshow');
|
|
model.frozen = false;
|
|
}
|
|
|
|
function pageHide () {
|
|
let model = modelFor(this);
|
|
model.contentWorker.emitSync('pagehide');
|
|
emit(this, 'pagehide');
|
|
model.frozen = true;
|
|
}
|
|
|
|
/**
|
|
* Fired from postMessage and emitEventToContent, or from the earlyMessage
|
|
* queue when fired before the content is loaded. Sends arguments to
|
|
* contentWorker if able
|
|
*/
|
|
|
|
function processMessage (worker, ...args) {
|
|
let model = modelFor(worker) || {};
|
|
if (!model.contentWorker)
|
|
throw new Error(ERR_DESTROYED);
|
|
if (model.frozen)
|
|
throw new Error(ERR_FROZEN);
|
|
model.contentWorker.emit.apply(null, args);
|
|
}
|
|
|
|
function createModel () {
|
|
return {
|
|
// List of messages fired before worker is initialized
|
|
earlyEvents: [],
|
|
// Is worker connected to the content worker sandbox ?
|
|
inited: false,
|
|
// Is worker being frozen? i.e related document is frozen in bfcache.
|
|
// Content script should not be reachable if frozen.
|
|
frozen: true,
|
|
/**
|
|
* Reference to the content side of the worker.
|
|
* @type {WorkerGlobalScope}
|
|
*/
|
|
contentWorker: null,
|
|
/**
|
|
* Reference to the window that is accessible from
|
|
* the content scripts.
|
|
* @type {Object}
|
|
*/
|
|
window: null
|
|
};
|
|
}
|
|
|
|
function createPort (worker) {
|
|
let port = EventTarget();
|
|
port.emit = emitEventToContent.bind(null, worker);
|
|
return port;
|
|
}
|
|
|
|
/**
|
|
* Emit a custom event to the content script,
|
|
* i.e. emit this event on `self.port`
|
|
*/
|
|
function emitEventToContent (worker, ...eventArgs) {
|
|
let model = modelFor(worker);
|
|
let args = ['event'].concat(eventArgs);
|
|
if (!model.inited) {
|
|
model.earlyEvents.push(args);
|
|
return;
|
|
}
|
|
processMessage.apply(null, [worker].concat(args));
|
|
}
|