1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/content/worker.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,282 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 +"use strict"; 1.8 + 1.9 +module.metadata = { 1.10 + "stability": "unstable" 1.11 +}; 1.12 + 1.13 +const { Class } = require('../core/heritage'); 1.14 +const { EventTarget } = require('../event/target'); 1.15 +const { on, off, emit, setListeners } = require('../event/core'); 1.16 +const { 1.17 + attach, detach, destroy 1.18 +} = require('./utils'); 1.19 +const { method } = require('../lang/functional'); 1.20 +const { Ci, Cu, Cc } = require('chrome'); 1.21 +const unload = require('../system/unload'); 1.22 +const events = require('../system/events'); 1.23 +const { getInnerId } = require("../window/utils"); 1.24 +const { WorkerSandbox } = require('./sandbox'); 1.25 +const { getTabForWindow } = require('../tabs/helpers'); 1.26 + 1.27 +// A weak map of workers to hold private attributes that 1.28 +// should not be exposed 1.29 +const workers = new WeakMap(); 1.30 + 1.31 +let modelFor = (worker) => workers.get(worker); 1.32 + 1.33 +const ERR_DESTROYED = 1.34 + "Couldn't find the worker to receive this message. " + 1.35 + "The script may not be initialized yet, or may already have been unloaded."; 1.36 + 1.37 +const ERR_FROZEN = "The page is currently hidden and can no longer be used " + 1.38 + "until it is visible again."; 1.39 + 1.40 +/** 1.41 + * Message-passing facility for communication between code running 1.42 + * in the content and add-on process. 1.43 + * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html 1.44 + */ 1.45 +const Worker = Class({ 1.46 + implements: [EventTarget], 1.47 + initialize: function WorkerConstructor (options) { 1.48 + // Save model in weak map to not expose properties 1.49 + let model = createModel(); 1.50 + workers.set(this, model); 1.51 + 1.52 + options = options || {}; 1.53 + 1.54 + if ('contentScriptFile' in options) 1.55 + this.contentScriptFile = options.contentScriptFile; 1.56 + if ('contentScriptOptions' in options) 1.57 + this.contentScriptOptions = options.contentScriptOptions; 1.58 + if ('contentScript' in options) 1.59 + this.contentScript = options.contentScript; 1.60 + if ('injectInDocument' in options) 1.61 + this.injectInDocument = !!options.injectInDocument; 1.62 + 1.63 + setListeners(this, options); 1.64 + 1.65 + unload.ensure(this, "destroy"); 1.66 + 1.67 + // Ensure that worker.port is initialized for contentWorker to be able 1.68 + // to send events during worker initialization. 1.69 + this.port = createPort(this); 1.70 + 1.71 + model.documentUnload = documentUnload.bind(this); 1.72 + model.pageShow = pageShow.bind(this); 1.73 + model.pageHide = pageHide.bind(this); 1.74 + 1.75 + if ('window' in options) 1.76 + attach(this, options.window); 1.77 + }, 1.78 + 1.79 + /** 1.80 + * Sends a message to the worker's global scope. Method takes single 1.81 + * argument, which represents data to be sent to the worker. The data may 1.82 + * be any primitive type value or `JSON`. Call of this method asynchronously 1.83 + * emits `message` event with data value in the global scope of this 1.84 + * symbiont. 1.85 + * 1.86 + * `message` event listeners can be set either by calling 1.87 + * `self.on` with a first argument string `"message"` or by 1.88 + * implementing `onMessage` function in the global scope of this worker. 1.89 + * @param {Number|String|JSON} data 1.90 + */ 1.91 + postMessage: function (...data) { 1.92 + let model = modelFor(this); 1.93 + let args = ['message'].concat(data); 1.94 + if (!model.inited) { 1.95 + model.earlyEvents.push(args); 1.96 + return; 1.97 + } 1.98 + processMessage.apply(null, [this].concat(args)); 1.99 + }, 1.100 + 1.101 + get url () { 1.102 + let model = modelFor(this); 1.103 + // model.window will be null after detach 1.104 + return model.window ? model.window.document.location.href : null; 1.105 + }, 1.106 + 1.107 + get contentURL () { 1.108 + let model = modelFor(this); 1.109 + return model.window ? model.window.document.URL : null; 1.110 + }, 1.111 + 1.112 + get tab () { 1.113 + let model = modelFor(this); 1.114 + // model.window will be null after detach 1.115 + if (model.window) 1.116 + return getTabForWindow(model.window); 1.117 + return null; 1.118 + }, 1.119 + 1.120 + // Implemented to provide some of the previous features of exposing sandbox 1.121 + // so that Worker can be extended 1.122 + getSandbox: function () { 1.123 + return modelFor(this).contentWorker; 1.124 + }, 1.125 + 1.126 + toString: function () { return '[object Worker]'; }, 1.127 + attach: method(attach), 1.128 + detach: method(detach), 1.129 + destroy: method(destroy) 1.130 +}); 1.131 +exports.Worker = Worker; 1.132 + 1.133 +attach.define(Worker, function (worker, window) { 1.134 + let model = modelFor(worker); 1.135 + model.window = window; 1.136 + // Track document unload to destroy this worker. 1.137 + // We can't watch for unload event on page's window object as it 1.138 + // prevents bfcache from working: 1.139 + // https://developer.mozilla.org/En/Working_with_BFCache 1.140 + model.windowID = getInnerId(model.window); 1.141 + events.on("inner-window-destroyed", model.documentUnload); 1.142 + 1.143 + // Listen to pagehide event in order to freeze the content script 1.144 + // while the document is frozen in bfcache: 1.145 + model.window.addEventListener("pageshow", model.pageShow, true); 1.146 + model.window.addEventListener("pagehide", model.pageHide, true); 1.147 + 1.148 + // will set model.contentWorker pointing to the private API: 1.149 + model.contentWorker = WorkerSandbox(worker, model.window); 1.150 + 1.151 + // Mainly enable worker.port.emit to send event to the content worker 1.152 + model.inited = true; 1.153 + model.frozen = false; 1.154 + 1.155 + // Fire off `attach` event 1.156 + emit(worker, 'attach', window); 1.157 + 1.158 + // Process all events and messages that were fired before the 1.159 + // worker was initialized. 1.160 + model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args))); 1.161 +}); 1.162 + 1.163 +/** 1.164 + * Remove all internal references to the attached document 1.165 + * Tells _port to unload itself and removes all the references from itself. 1.166 + */ 1.167 +detach.define(Worker, function (worker, reason) { 1.168 + let model = modelFor(worker); 1.169 + 1.170 + // maybe unloaded before content side is created 1.171 + if (model.contentWorker) { 1.172 + model.contentWorker.destroy(reason); 1.173 + } 1.174 + 1.175 + model.contentWorker = null; 1.176 + if (model.window) { 1.177 + model.window.removeEventListener("pageshow", model.pageShow, true); 1.178 + model.window.removeEventListener("pagehide", model.pageHide, true); 1.179 + } 1.180 + model.window = null; 1.181 + // This method may be called multiple times, 1.182 + // avoid dispatching `detach` event more than once 1.183 + if (model.windowID) { 1.184 + model.windowID = null; 1.185 + events.off("inner-window-destroyed", model.documentUnload); 1.186 + model.earlyEvents.length = 0; 1.187 + emit(worker, 'detach'); 1.188 + } 1.189 + model.inited = false; 1.190 +}); 1.191 + 1.192 +/** 1.193 + * Tells content worker to unload itself and 1.194 + * removes all the references from itself. 1.195 + */ 1.196 +destroy.define(Worker, function (worker, reason) { 1.197 + detach(worker, reason); 1.198 + modelFor(worker).inited = true; 1.199 + // Specifying no type or listener removes all listeners 1.200 + // from target 1.201 + off(worker); 1.202 +}); 1.203 + 1.204 +/** 1.205 + * Events fired by workers 1.206 + */ 1.207 +function documentUnload ({ subject, data }) { 1.208 + let model = modelFor(this); 1.209 + let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; 1.210 + if (innerWinID != model.windowID) return false; 1.211 + detach(this); 1.212 + return true; 1.213 +} 1.214 + 1.215 +function pageShow () { 1.216 + let model = modelFor(this); 1.217 + model.contentWorker.emitSync('pageshow'); 1.218 + emit(this, 'pageshow'); 1.219 + model.frozen = false; 1.220 +} 1.221 + 1.222 +function pageHide () { 1.223 + let model = modelFor(this); 1.224 + model.contentWorker.emitSync('pagehide'); 1.225 + emit(this, 'pagehide'); 1.226 + model.frozen = true; 1.227 +} 1.228 + 1.229 +/** 1.230 + * Fired from postMessage and emitEventToContent, or from the earlyMessage 1.231 + * queue when fired before the content is loaded. Sends arguments to 1.232 + * contentWorker if able 1.233 + */ 1.234 + 1.235 +function processMessage (worker, ...args) { 1.236 + let model = modelFor(worker) || {}; 1.237 + if (!model.contentWorker) 1.238 + throw new Error(ERR_DESTROYED); 1.239 + if (model.frozen) 1.240 + throw new Error(ERR_FROZEN); 1.241 + model.contentWorker.emit.apply(null, args); 1.242 +} 1.243 + 1.244 +function createModel () { 1.245 + return { 1.246 + // List of messages fired before worker is initialized 1.247 + earlyEvents: [], 1.248 + // Is worker connected to the content worker sandbox ? 1.249 + inited: false, 1.250 + // Is worker being frozen? i.e related document is frozen in bfcache. 1.251 + // Content script should not be reachable if frozen. 1.252 + frozen: true, 1.253 + /** 1.254 + * Reference to the content side of the worker. 1.255 + * @type {WorkerGlobalScope} 1.256 + */ 1.257 + contentWorker: null, 1.258 + /** 1.259 + * Reference to the window that is accessible from 1.260 + * the content scripts. 1.261 + * @type {Object} 1.262 + */ 1.263 + window: null 1.264 + }; 1.265 +} 1.266 + 1.267 +function createPort (worker) { 1.268 + let port = EventTarget(); 1.269 + port.emit = emitEventToContent.bind(null, worker); 1.270 + return port; 1.271 +} 1.272 + 1.273 +/** 1.274 + * Emit a custom event to the content script, 1.275 + * i.e. emit this event on `self.port` 1.276 + */ 1.277 +function emitEventToContent (worker, ...eventArgs) { 1.278 + let model = modelFor(worker); 1.279 + let args = ['event'].concat(eventArgs); 1.280 + if (!model.inited) { 1.281 + model.earlyEvents.push(args); 1.282 + return; 1.283 + } 1.284 + processMessage.apply(null, [worker].concat(args)); 1.285 +}