author | Erik Vold <evold@mozilla.com> |
Thu, 16 Jan 2014 17:29:40 -0800 | |
changeset 163990 | cf3f073c8b4bc54f4c0ecd94f4b9cecc0290199b |
parent 163989 | 356ef7c535f236a5c29d6911d69ec945d0a9c45f |
child 163991 | 14a4f50a46811dfa1f70298407cac23ae1012c14 |
push id | 26022 |
push user | ryanvm@gmail.com |
push date | Fri, 17 Jan 2014 19:56:22 +0000 |
treeherder | mozilla-central@fad7172d4542 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
bugs | 960771 |
milestone | 29.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/addon-sdk/source/lib/sdk/content/content.js +++ b/addon-sdk/source/lib/sdk/content/content.js @@ -3,11 +3,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; module.metadata = { "stability": "unstable" }; exports.Loader = require('./loader').Loader; -exports.Symbiont = require('./symbiont').Symbiont; +exports.Symbiont = require('../deprecated/symbiont').Symbiont; exports.Worker = require('./worker').Worker;
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/sandbox.js @@ -0,0 +1,404 @@ +/* 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'; + +module.metadata = { + 'stability': 'unstable' +}; + +const { Class } = require('../core/heritage'); +const { EventTarget } = require('../event/target'); +const { on, off, emit } = require('../event/core'); +const { + requiresAddonGlobal, + attach, detach, destroy +} = require('./utils'); +const { delay: async } = require('../lang/functional'); +const { Ci, Cu, Cc } = require('chrome'); +const timer = require('../timers'); +const { URL } = require('../url'); +const { sandbox, evaluate, load } = require('../loader/sandbox'); +const { merge } = require('../util/object'); +const xulApp = require('../system/xul-app'); +const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion, + '17.0a2', '*'); +const { getTabForContentWindow } = require('../tabs/utils'); + +// WeakMap of sandboxes so we can access private values +const sandboxes = new WeakMap(); + +/* Trick the linker in order to ensure shipping these files in the XPI. + require('./content-worker.js'); + Then, retrieve URL of these files in the XPI: +*/ +let prefix = module.uri.split('sandbox.js')[0]; +const CONTENT_WORKER_URL = prefix + 'content-worker.js'; + +// Fetch additional list of domains to authorize access to for each content +// script. It is stored in manifest `metadata` field which contains +// package.json data. This list is originaly defined by authors in +// `permissions` attribute of their package.json addon file. +const permissions = require('@loader/options').metadata['permissions'] || {}; +const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || []; + +const JS_VERSION = '1.8'; + +const WorkerSandbox = Class({ + + implements: [ + EventTarget + ], + + /** + * Emit a message to the worker content sandbox + */ + emit: function emit(...args) { + // Ensure having an asynchronous behavior + let self = this; + async(function () { + emitToContent(self, JSON.stringify(args, replacer)); + }); + }, + + /** + * Synchronous version of `emit`. + * /!\ Should only be used when it is strictly mandatory /!\ + * Doesn't ensure passing only JSON values. + * Mainly used by context-menu in order to avoid breaking it. + */ + emitSync: function emitSync(...args) { + return emitToContent(this, args); + }, + + /** + * Tells if content script has at least one listener registered for one event, + * through `self.on('xxx', ...)`. + * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API. + */ + hasListenerFor: function hasListenerFor(name) { + return modelFor(this).hasListenerFor(name); + }, + + /** + * Configures sandbox and loads content scripts into it. + * @param {Worker} worker + * content worker + */ + initialize: function WorkerSandbox(worker, window) { + let model = {}; + sandboxes.set(this, model); + model.worker = worker; + // We receive a wrapped window, that may be an xraywrapper if it's content + let proto = window; + + // TODO necessary? + // Ensure that `emit` has always the right `this` + this.emit = this.emit.bind(this); + this.emitSync = this.emitSync.bind(this); + + // Eventually use expanded principal sandbox feature, if some are given. + // + // But prevent it when the Worker isn't used for a content script but for + // injecting `addon` object into a Panel, Widget, ... scope. + // That's because: + // 1/ It is useless to use multiple domains as the worker is only used + // to communicate with the addon, + // 2/ By using it it would prevent the document to have access to any JS + // value of the worker. As JS values coming from multiple domain principals + // can't be accessed by 'mono-principals' (principal with only one domain). + // Even if this principal is for a domain that is specified in the multiple + // domain principal. + let principals = window; + let wantGlobalProperties = []; + if (EXPANDED_PRINCIPALS.length > 0 && !requiresAddonGlobal(worker)) { + principals = EXPANDED_PRINCIPALS.concat(window); + // We have to replace XHR constructor of the content document + // with a custom cross origin one, automagically added by platform code: + delete proto.XMLHttpRequest; + wantGlobalProperties.push('XMLHttpRequest'); + } + + // Instantiate trusted code in another Sandbox in order to prevent content + // script from messing with standard classes used by proxy and API code. + let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window }); + apiSandbox.console = console; + + // Create the sandbox and bind it to window in order for content scripts to + // have access to all standard globals (window, document, ...) + let content = sandbox(principals, { + sandboxPrototype: proto, + wantXrays: true, + wantGlobalProperties: wantGlobalProperties, + sameZoneAs: window, + metadata: { SDKContentScript: true } + }); + model.sandbox = content; + + // We have to ensure that window.top and window.parent are the exact same + // object than window object, i.e. the sandbox global object. But not + // always, in case of iframes, top and parent are another window object. + let top = window.top === window ? content : content.top; + let parent = window.parent === window ? content : content.parent; + merge(content, { + // We need 'this === window === top' to be true in toplevel scope: + get window() content, + get top() top, + get parent() parent, + // Use the Greasemonkey naming convention to provide access to the + // unwrapped window object so the content script can access document + // JavaScript values. + // NOTE: this functionality is experimental and may change or go away + // at any time! + get unsafeWindow() window.wrappedJSObject + }); + + // Load trusted code that will inject content script API. + // We need to expose JS objects defined in same principal in order to + // avoid having any kind of wrapper. + load(apiSandbox, CONTENT_WORKER_URL); + + // prepare a clean `self.options` + let options = 'contentScriptOptions' in worker ? + JSON.stringify(worker.contentScriptOptions) : + undefined; + + // Then call `inject` method and communicate with this script + // by trading two methods that allow to send events to the other side: + // - `onEvent` called by content script + // - `result.emitToContent` called by addon script + // Bug 758203: We have to explicitely define `__exposedProps__` in order + // to allow access to these chrome object attributes from this sandbox with + // content priviledges + // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers + let onEvent = onContentEvent.bind(null, this); + // `ContentWorker` is defined in CONTENT_WORKER_URL file + let chromeAPI = createChromeAPI(); + let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options); + + // Merge `emitToContent` and `hasListenerFor` into our private + // model of the WorkerSandbox so we can communicate with content + // script + merge(model, result); + + // Handle messages send by this script: + setListeners(this); + + // Inject `addon` global into target document if document is trusted, + // `addon` in document is equivalent to `self` in content script. + if (requiresAddonGlobal(worker)) { + Object.defineProperty(getUnsafeWindow(window), 'addon', { + value: content.self + } + ); + } + + // Inject our `console` into target document if worker doesn't have a tab + // (e.g Panel, PageWorker, Widget). + // `worker.tab` can't be used because bug 804935. + if (!getTabForContentWindow(window)) { + let win = getUnsafeWindow(window); + + // export our chrome console to content window, using the same approach + // of `ConsoleAPI`: + // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150 + // + // and described here: + // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn + let con = Cu.createObjectIn(win); + + let genPropDesc = function genPropDesc(fun) { + return { enumerable: true, configurable: true, writable: true, + value: console[fun] }; + } + + const properties = { + log: genPropDesc('log'), + info: genPropDesc('info'), + warn: genPropDesc('warn'), + error: genPropDesc('error'), + debug: genPropDesc('debug'), + trace: genPropDesc('trace'), + dir: genPropDesc('dir'), + group: genPropDesc('group'), + groupCollapsed: genPropDesc('groupCollapsed'), + groupEnd: genPropDesc('groupEnd'), + time: genPropDesc('time'), + timeEnd: genPropDesc('timeEnd'), + profile: genPropDesc('profile'), + profileEnd: genPropDesc('profileEnd'), + __noSuchMethod__: { enumerable: true, configurable: true, writable: true, + value: function() {} } + }; + + Object.defineProperties(con, properties); + Cu.makeObjectPropsNormal(con); + + win.console = con; + }; + + // The order of `contentScriptFile` and `contentScript` evaluation is + // intentional, so programs can load libraries like jQuery from script URLs + // and use them in scripts. + let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile + : null, + contentScript = ('contentScript' in worker) ? worker.contentScript : null; + + if (contentScriptFile) + importScripts.apply(null, [this].concat(contentScriptFile)); + if (contentScript) { + evaluateIn( + this, + Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript + ); + } + }, + destroy: function destroy() { + this.emitSync('detach'); + let model = modelFor(this); + model.sandbox = null + model.worker = null; + }, + +}); + +exports.WorkerSandbox = WorkerSandbox; + +/** + * Imports scripts to the sandbox by reading files under urls and + * evaluating its source. If exception occurs during evaluation + * `'error'` event is emitted on the worker. + * This is actually an analog to the `importScript` method in web + * workers but in our case it's not exposed even though content + * scripts may be able to do it synchronously since IO operation + * takes place in the UI process. + */ +function importScripts (workerSandbox, ...urls) { + let { worker, sandbox } = modelFor(workerSandbox); + for (let i in urls) { + let contentScriptFile = urls[i]; + try { + let uri = URL(contentScriptFile); + if (uri.scheme === 'resource') + load(sandbox, String(uri)); + else + throw Error('Unsupported `contentScriptFile` url: ' + String(uri)); + } + catch(e) { + emit(worker, 'error', e); + } + } +} + +function setListeners (workerSandbox) { + let { worker } = modelFor(workerSandbox); + // console.xxx calls + workerSandbox.on('console', function consoleListener (kind, ...args) { + console[kind].apply(console, args); + }); + + // self.postMessage calls + workerSandbox.on('message', function postMessage(data) { + // destroyed? + if (worker) + emit(worker, 'message', data); + }); + + // self.port.emit calls + workerSandbox.on('event', function portEmit (...eventArgs) { + // If not destroyed, emit event information to worker + // `eventArgs` has the event name as first element, + // and remaining elements are additional arguments to pass + if (worker) + emit.apply(null, [worker.port].concat(eventArgs)); + }); + + // unwrap, recreate and propagate async Errors thrown from content-script + workerSandbox.on('error', function onError({instanceOfError, value}) { + if (worker) { + let error = value; + if (instanceOfError) { + error = new Error(value.message, value.fileName, value.lineNumber); + error.stack = value.stack; + error.name = value.name; + } + emit(worker, 'error', error); + } + }); +} + +/** + * Evaluates code in the sandbox. + * @param {String} code + * JavaScript source to evaluate. + * @param {String} [filename='javascript:' + code] + * Name of the file + */ +function evaluateIn (workerSandbox, code, filename) { + let { worker, sandbox } = modelFor(workerSandbox); + try { + evaluate(sandbox, code, filename || 'javascript:' + code); + } + catch(e) { + emit(worker, 'error', e); + } +} + +/** + * Method called by the worker sandbox when it needs to send a message + */ +function onContentEvent (workerSandbox, args) { + // As `emit`, we ensure having an asynchronous behavior + async(function () { + // We emit event to chrome/addon listeners + emit.apply(null, [workerSandbox].concat(JSON.parse(args))); + }); +} + + +function modelFor (workerSandbox) { + return sandboxes.get(workerSandbox); +} + +/** + * JSON.stringify is buggy with cross-sandbox values, + * it may return '{}' on functions. Use a replacer to match them correctly. + */ +function replacer (k, v) { + return typeof v === 'function' ? undefined : v; +} + +function getUnsafeWindow (win) { + return win.wrappedJSObject || win; +} + +function emitToContent (workerSandbox, args) { + return modelFor(workerSandbox).emitToContent(args); +} + +function createChromeAPI () { + return { + timers: { + setTimeout: timer.setTimeout, + setInterval: timer.setInterval, + clearTimeout: timer.clearTimeout, + clearInterval: timer.clearInterval, + __exposedProps__: { + setTimeout: 'r', + setInterval: 'r', + clearTimeout: 'r', + clearInterval: 'r' + }, + }, + sandbox: { + evaluate: evaluate, + __exposedProps__: { + evaluate: 'r' + } + }, + __exposedProps__: { + timers: 'r', + sandbox: 'r' + } + }; +}
deleted file mode 100644 --- a/addon-sdk/source/lib/sdk/content/symbiont.js +++ /dev/null @@ -1,229 +0,0 @@ -/* 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"; - -module.metadata = { - "stability": "unstable" -}; - -const { Worker } = require('./worker'); -const { Loader } = require('./loader'); -const hiddenFrames = require('../frame/hidden-frame'); -const observers = require('../deprecated/observer-service'); -const unload = require('../system/unload'); -const { getDocShell } = require("../frame/utils"); -const { ignoreWindow } = require('../private-browsing/utils'); - -// Everything coming from add-on's xpi considered an asset. -const assetsURI = require('../self').data.url().replace(/data\/$/, ""); - -/** - * This trait is layered on top of `Worker` and in contrast to symbiont - * Worker constructor requires `content` option that represents content - * that will be loaded in the provided frame, if frame is not provided - * Worker will create hidden one. - */ -const Symbiont = Worker.resolve({ - constructor: '_initWorker', - destroy: '_workerDestroy' - }).compose(Loader, { - - /** - * The constructor requires all the options that are required by - * `require('content').Worker` with the difference that the `frame` option - * is optional. If `frame` is not provided, `contentURL` is expected. - * @param {Object} options - * @param {String} options.contentURL - * URL of a content to load into `this._frame` and create worker for. - * @param {Element} [options.frame] - * iframe element that is used to load `options.contentURL` into. - * if frame is not provided hidden iframe will be created. - */ - constructor: function Symbiont(options) { - options = options || {}; - - if ('contentURL' in options) - this.contentURL = options.contentURL; - if ('contentScriptWhen' in options) - this.contentScriptWhen = options.contentScriptWhen; - if ('contentScriptOptions' in options) - this.contentScriptOptions = options.contentScriptOptions; - if ('contentScriptFile' in options) - this.contentScriptFile = options.contentScriptFile; - if ('contentScript' in options) - this.contentScript = options.contentScript; - if ('allow' in options) - this.allow = options.allow; - if ('onError' in options) - this.on('error', options.onError); - if ('onMessage' in options) - this.on('message', options.onMessage); - if ('frame' in options) { - this._initFrame(options.frame); - } - else { - let self = this; - this._hiddenFrame = hiddenFrames.HiddenFrame({ - onReady: function onFrame() { - self._initFrame(this.element); - }, - onUnload: function onUnload() { - // Bug 751211: Remove reference to _frame when hidden frame is - // automatically removed on unload, otherwise we are going to face - // "dead object" exception - self.destroy(); - } - }); - hiddenFrames.add(this._hiddenFrame); - } - - unload.ensure(this._public, "destroy"); - }, - - destroy: function destroy() { - this._workerDestroy(); - this._unregisterListener(); - this._frame = null; - if (this._hiddenFrame) { - hiddenFrames.remove(this._hiddenFrame); - this._hiddenFrame = null; - } - }, - - /** - * XUL iframe or browser elements with attribute `type` being `content`. - * Used to create `ContentSymbiont` from. - * @type {nsIFrame|nsIBrowser} - */ - _frame: null, - - /** - * Listener to the `'frameReady"` event (emitted when `iframe` is ready). - * Removes listener, sets right permissions to the frame and loads content. - */ - _initFrame: function _initFrame(frame) { - if (this._loadListener) - this._unregisterListener(); - - this._frame = frame; - - if (getDocShell(frame)) { - this._reallyInitFrame(frame); - } - else { - if (this._waitForFrame) { - observers.remove('content-document-global-created', this._waitForFrame); - } - this._waitForFrame = this.__waitForFrame.bind(this, frame); - observers.add('content-document-global-created', this._waitForFrame); - } - }, - - __waitForFrame: function _waitForFrame(frame, win, topic) { - if (frame.contentWindow == win) { - observers.remove('content-document-global-created', this._waitForFrame); - delete this._waitForFrame; - this._reallyInitFrame(frame); - } - }, - - _reallyInitFrame: function _reallyInitFrame(frame) { - getDocShell(frame).allowJavascript = this.allow.script; - frame.setAttribute("src", this._contentURL); - - // Inject `addon` object in document if we load a document from - // one of our addon folder and if no content script are defined. bug 612726 - let isDataResource = - typeof this._contentURL == "string" && - this._contentURL.indexOf(assetsURI) == 0; - let hasContentScript = - (Array.isArray(this.contentScript) ? this.contentScript.length > 0 - : !!this.contentScript) || - (Array.isArray(this.contentScriptFile) ? this.contentScriptFile.length > 0 - : !!this.contentScriptFile); - // If we have to inject `addon` we have to do it before document - // script execution, so during `start`: - this._injectInDocument = isDataResource && !hasContentScript; - if (this._injectInDocument) - this.contentScriptWhen = "start"; - - if ((frame.contentDocument.readyState == "complete" || - (frame.contentDocument.readyState == "interactive" && - this.contentScriptWhen != 'end' )) && - frame.contentDocument.location == this._contentURL) { - // In some cases src doesn't change and document is already ready - // (for ex: when the user moves a widget while customizing toolbars.) - this._onInit(); - return; - } - - let self = this; - - if ('start' == this.contentScriptWhen) { - this._loadEvent = 'start'; - observers.add('document-element-inserted', - this._loadListener = function onStart(doc) { - let window = doc.defaultView; - - if (ignoreWindow(window)) { - return; - } - - if (window && window == frame.contentWindow) { - self._unregisterListener(); - self._onInit(); - } - - }); - return; - } - - let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded'; - let self = this; - this._loadEvent = eventName; - frame.addEventListener(eventName, - this._loadListener = function _onReady(event) { - - if (event.target != frame.contentDocument) - return; - self._unregisterListener(); - - self._onInit(); - - }, true); - - }, - - /** - * Unregister listener that watchs for document being ready to be injected. - * This listener is registered in `Symbiont._initFrame`. - */ - _unregisterListener: function _unregisterListener() { - if (this._waitForFrame) { - observers.remove('content-document-global-created', this._waitForFrame); - delete this._waitForFrame; - } - - if (!this._loadListener) - return; - if (this._loadEvent == "start") { - observers.remove('document-element-inserted', this._loadListener); - } - else { - this._frame.removeEventListener(this._loadEvent, this._loadListener, - true); - } - this._loadListener = null; - }, - - /** - * Called by Symbiont itself when the frame is ready to load - * content scripts according to contentScriptWhen. Overloaded by Panel. - */ - _onInit: function () { - this._initWorker({ window: this._frame.contentWindow }); - } - -}); -exports.Symbiont = Symbiont;
--- a/addon-sdk/source/lib/sdk/content/utils.js +++ b/addon-sdk/source/lib/sdk/content/utils.js @@ -1,41 +1,82 @@ /* 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"; +'use strict'; module.metadata = { - "stability": "unstable" + 'stability': 'unstable' }; -let assetsURI = require("../self").data.url(); +let { merge } = require('../util/object'); +let assetsURI = require('../self').data.url(); let isArray = Array.isArray; +let method = require('method/core'); function isAddonContent({ contentURL }) { - return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0; + return typeof(contentURL) === 'string' && contentURL.indexOf(assetsURI) === 0; } exports.isAddonContent = isAddonContent; function hasContentScript({ contentScript, contentScriptFile }) { return (isArray(contentScript) ? contentScript.length > 0 : !!contentScript) || (isArray(contentScriptFile) ? contentScriptFile.length > 0 : !!contentScriptFile); } exports.hasContentScript = hasContentScript; function requiresAddonGlobal(model) { - return isAddonContent(model) && !hasContentScript(model); + return model.injectInDocument || (isAddonContent(model) && !hasContentScript(model)); } exports.requiresAddonGlobal = requiresAddonGlobal; function getAttachEventType(model) { if (!model) return null; let when = model.contentScriptWhen; - return requiresAddonGlobal(model) ? "document-element-inserted" : - when === "start" ? "document-element-inserted" : - when === "end" ? "load" : - when === "ready" ? "DOMContentLoaded" : + return requiresAddonGlobal(model) ? 'document-element-inserted' : + when === 'start' ? 'document-element-inserted' : + when === 'end' ? 'load' : + when === 'ready' ? 'DOMContentLoaded' : null; } exports.getAttachEventType = getAttachEventType; +let attach = method('worker-attach'); +exports.attach = attach; + +let detach = method('worker-detach'); +exports.detach = detach; + +let destroy = method('worker-destroy'); +exports.destroy = destroy; + +function WorkerHost (workerFor) { + // Define worker properties that just proxy to underlying worker + return ['postMessage', 'port', 'url', 'tab'].reduce(function(proto, name) { + // Use descriptor properties instead so we can call + // the worker function in the context of the worker so we + // don't have to create new functions with `fn.bind(worker)` + let descriptorProp = { + value: function (...args) { + let worker = workerFor(this); + return worker[name].apply(worker, args); + } + }; + + let accessorProp = { + get: function () { return workerFor(this)[name]; }, + set: function (value) { workerFor(this)[name] = value; } + }; + + Object.defineProperty(proto, name, merge({ + enumerable: true, + configurable: false, + }, isDescriptor(name) ? descriptorProp : accessorProp)); + return proto; + }, {}); + + function isDescriptor (prop) { + return ~['postMessage'].indexOf(prop); + } +} +exports.WorkerHost = WorkerHost;
--- a/addon-sdk/source/lib/sdk/content/worker.js +++ b/addon-sdk/source/lib/sdk/content/worker.js @@ -2,650 +2,281 @@ * 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"; module.metadata = { "stability": "unstable" }; -const { Trait } = require('../deprecated/traits'); -const { EventEmitter, EventEmitterTrait } = require('../deprecated/events'); +const { Class } = require('../core/heritage'); +const { EventTarget } = require('../event/target'); +const { on, off, emit, setListeners } = require('../event/core'); +const { + attach, detach, destroy +} = require('./utils'); +const { method } = require('../lang/functional'); const { Ci, Cu, Cc } = require('chrome'); -const timer = require('../timers'); -const { URL } = require('../url'); const unload = require('../system/unload'); -const observers = require('../deprecated/observer-service'); -const { Cortex } = require('../deprecated/cortex'); -const { sandbox, evaluate, load } = require("../loader/sandbox"); -const { merge } = require('../util/object'); -const xulApp = require("../system/xul-app"); -const { getInnerId } = require("../window/utils") -const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion, - "17.0a2", "*"); +const events = require('../system/events'); +const { getInnerId } = require("../window/utils"); +const { WorkerSandbox } = require('./sandbox'); const { getTabForWindow } = require('../tabs/helpers'); -const { getTabForContentWindow } = require('../tabs/utils'); - -/* Trick the linker in order to ensure shipping these files in the XPI. - require('./content-worker.js'); - Then, retrieve URL of these files in the XPI: -*/ -let prefix = module.uri.split('worker.js')[0]; -const CONTENT_WORKER_URL = prefix + 'content-worker.js'; -// Fetch additional list of domains to authorize access to for each content -// script. It is stored in manifest `metadata` field which contains -// package.json data. This list is originaly defined by authors in -// `permissions` attribute of their package.json addon file. -const permissions = require('@loader/options').metadata['permissions'] || {}; -const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || []; +// A weak map of workers to hold private attributes that +// should not be exposed +const workers = new WeakMap(); -const JS_VERSION = '1.8'; +let 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."; -const WorkerSandbox = EventEmitter.compose({ - - /** - * Emit a message to the worker content sandbox - */ - emit: function emit() { - // First ensure having a regular array - // (otherwise, `arguments` would be mapped to an object by `stringify`) - let array = Array.slice(arguments); - // JSON.stringify is buggy with cross-sandbox values, - // it may return "{}" on functions. Use a replacer to match them correctly. - function replacer(k, v) { - return typeof v === "function" ? undefined : v; - } - // Ensure having an asynchronous behavior - let self = this; - timer.setTimeout(function () { - self._emitToContent(JSON.stringify(array, replacer)); - }, 0); - }, - - /** - * Synchronous version of `emit`. - * /!\ Should only be used when it is strictly mandatory /!\ - * Doesn't ensure passing only JSON values. - * Mainly used by context-menu in order to avoid breaking it. - */ - emitSync: function emitSync() { - let args = Array.slice(arguments); - return this._emitToContent(args); - }, - - /** - * Tells if content script has at least one listener registered for one event, - * through `self.on('xxx', ...)`. - * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API. - */ - hasListenerFor: function hasListenerFor(name) { - return this._hasListenerFor(name); - }, - - /** - * Method called by the worker sandbox when it needs to send a message - */ - _onContentEvent: function onContentEvent(args) { - // As `emit`, we ensure having an asynchronous behavior - let self = this; - timer.setTimeout(function () { - // We emit event to chrome/addon listeners - self._emit.apply(self, JSON.parse(args)); - }, 0); - }, - - /** - * Configures sandbox and loads content scripts into it. - * @param {Worker} worker - * content worker - */ - constructor: function WorkerSandbox(worker) { - this._addonWorker = worker; - - // Ensure that `emit` has always the right `this` - this.emit = this.emit.bind(this); - this.emitSync = this.emitSync.bind(this); - - // We receive a wrapped window, that may be an xraywrapper if it's content - let window = worker._window; - let proto = window; - - // Eventually use expanded principal sandbox feature, if some are given. - // - // But prevent it when the Worker isn't used for a content script but for - // injecting `addon` object into a Panel, Widget, ... scope. - // That's because: - // 1/ It is useless to use multiple domains as the worker is only used - // to communicate with the addon, - // 2/ By using it it would prevent the document to have access to any JS - // value of the worker. As JS values coming from multiple domain principals - // can't be accessed by "mono-principals" (principal with only one domain). - // Even if this principal is for a domain that is specified in the multiple - // domain principal. - let principals = window; - let wantGlobalProperties = [] - if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) { - principals = EXPANDED_PRINCIPALS.concat(window); - // We have to replace XHR constructor of the content document - // with a custom cross origin one, automagically added by platform code: - delete proto.XMLHttpRequest; - wantGlobalProperties.push("XMLHttpRequest"); - } - - // Instantiate trusted code in another Sandbox in order to prevent content - // script from messing with standard classes used by proxy and API code. - let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window }); - apiSandbox.console = console; - - // Create the sandbox and bind it to window in order for content scripts to - // have access to all standard globals (window, document, ...) - let content = this._sandbox = sandbox(principals, { - sandboxPrototype: proto, - wantXrays: true, - wantGlobalProperties: wantGlobalProperties, - sameZoneAs: window, - metadata: { SDKContentScript: true } - }); - // We have to ensure that window.top and window.parent are the exact same - // object than window object, i.e. the sandbox global object. But not - // always, in case of iframes, top and parent are another window object. - let top = window.top === window ? content : content.top; - let parent = window.parent === window ? content : content.parent; - merge(content, { - // We need "this === window === top" to be true in toplevel scope: - get window() content, - get top() top, - get parent() parent, - // Use the Greasemonkey naming convention to provide access to the - // unwrapped window object so the content script can access document - // JavaScript values. - // NOTE: this functionality is experimental and may change or go away - // at any time! - get unsafeWindow() window.wrappedJSObject - }); - - // Load trusted code that will inject content script API. - // We need to expose JS objects defined in same principal in order to - // avoid having any kind of wrapper. - load(apiSandbox, CONTENT_WORKER_URL); - - // prepare a clean `self.options` - let options = 'contentScriptOptions' in worker ? - JSON.stringify( worker.contentScriptOptions ) : - undefined; - - // Then call `inject` method and communicate with this script - // by trading two methods that allow to send events to the other side: - // - `onEvent` called by content script - // - `result.emitToContent` called by addon script - // Bug 758203: We have to explicitely define `__exposedProps__` in order - // to allow access to these chrome object attributes from this sandbox with - // content priviledges - // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers - let chromeAPI = { - timers: { - setTimeout: timer.setTimeout, - setInterval: timer.setInterval, - clearTimeout: timer.clearTimeout, - clearInterval: timer.clearInterval, - __exposedProps__: { - setTimeout: 'r', - setInterval: 'r', - clearTimeout: 'r', - clearInterval: 'r' - } - }, - sandbox: { - evaluate: evaluate, - __exposedProps__: { - evaluate: 'r', - } - }, - __exposedProps__: { - timers: 'r', - sandbox: 'r', - } - }; - let onEvent = this._onContentEvent.bind(this); - // `ContentWorker` is defined in CONTENT_WORKER_URL file - let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options); - this._emitToContent = result.emitToContent; - this._hasListenerFor = result.hasListenerFor; - - // Handle messages send by this script: - let self = this; - // console.xxx calls - this.on("console", function consoleListener(kind) { - console[kind].apply(console, Array.slice(arguments, 1)); - }); - - // self.postMessage calls - this.on("message", function postMessage(data) { - // destroyed? - if (self._addonWorker) - self._addonWorker._emit('message', data); - }); - - // self.port.emit calls - this.on("event", function portEmit(name, args) { - // destroyed? - if (self._addonWorker) - self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments); - }); - - // unwrap, recreate and propagate async Errors thrown from content-script - this.on("error", function onError({instanceOfError, value}) { - if (self._addonWorker) { - let error = value; - if (instanceOfError) { - error = new Error(value.message, value.fileName, value.lineNumber); - error.stack = value.stack; - error.name = value.name; - } - self._addonWorker._emit('error', error); - } - }); - - // Inject `addon` global into target document if document is trusted, - // `addon` in document is equivalent to `self` in content script. - if (worker._injectInDocument) { - let win = window.wrappedJSObject ? window.wrappedJSObject : window; - Object.defineProperty(win, "addon", { - value: content.self - } - ); - } - - // Inject our `console` into target document if worker doesn't have a tab - // (e.g Panel, PageWorker, Widget). - // `worker.tab` can't be used because bug 804935. - if (!getTabForContentWindow(window)) { - let win = window.wrappedJSObject ? window.wrappedJSObject : window; - - // export our chrome console to content window, using the same approach - // of `ConsoleAPI`: - // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150 - // - // and described here: - // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn - let con = Cu.createObjectIn(win); - - let genPropDesc = function genPropDesc(fun) { - return { enumerable: true, configurable: true, writable: true, - value: console[fun] }; - } - - const properties = { - log: genPropDesc('log'), - info: genPropDesc('info'), - warn: genPropDesc('warn'), - error: genPropDesc('error'), - debug: genPropDesc('debug'), - trace: genPropDesc('trace'), - dir: genPropDesc('dir'), - group: genPropDesc('group'), - groupCollapsed: genPropDesc('groupCollapsed'), - groupEnd: genPropDesc('groupEnd'), - time: genPropDesc('time'), - timeEnd: genPropDesc('timeEnd'), - profile: genPropDesc('profile'), - profileEnd: genPropDesc('profileEnd'), - __noSuchMethod__: { enumerable: true, configurable: true, writable: true, - value: function() {} } - }; - - Object.defineProperties(con, properties); - Cu.makeObjectPropsNormal(con); - - win.console = con; - }; - - // The order of `contentScriptFile` and `contentScript` evaluation is - // intentional, so programs can load libraries like jQuery from script URLs - // and use them in scripts. - let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile - : null, - contentScript = ('contentScript' in worker) ? worker.contentScript : null; - - if (contentScriptFile) { - if (Array.isArray(contentScriptFile)) - this._importScripts.apply(this, contentScriptFile); - else - this._importScripts(contentScriptFile); - } - if (contentScript) { - this._evaluate( - Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript - ); - } - }, - destroy: function destroy() { - this.emitSync("detach"); - this._sandbox = null; - this._addonWorker = null; - }, - - /** - * JavaScript sandbox where all the content scripts are evaluated. - * {Sandbox} - */ - _sandbox: null, - - /** - * Reference to the addon side of the worker. - * @type {Worker} - */ - _addonWorker: null, - - /** - * Evaluates code in the sandbox. - * @param {String} code - * JavaScript source to evaluate. - * @param {String} [filename='javascript:' + code] - * Name of the file - */ - _evaluate: function(code, filename) { - try { - evaluate(this._sandbox, code, filename || 'javascript:' + code); - } - catch(e) { - this._addonWorker._emit('error', e); - } - }, - /** - * Imports scripts to the sandbox by reading files under urls and - * evaluating its source. If exception occurs during evaluation - * `"error"` event is emitted on the worker. - * This is actually an analog to the `importScript` method in web - * workers but in our case it's not exposed even though content - * scripts may be able to do it synchronously since IO operation - * takes place in the UI process. - */ - _importScripts: function _importScripts(url) { - let urls = Array.slice(arguments, 0); - for each (let contentScriptFile in urls) { - try { - let uri = URL(contentScriptFile); - if (uri.scheme === 'resource') - load(this._sandbox, String(uri)); - else - throw Error("Unsupported `contentScriptFile` url: " + String(uri)); - } - catch(e) { - this._addonWorker._emit('error', e); - } - } - } -}); - /** * Message-passing facility for communication between code running * in the content and add-on process. * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html */ -const Worker = EventEmitter.compose({ - on: Trait.required, - _removeAllListeners: Trait.required, +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 || {}; - // List of messages fired before worker is initialized - get _earlyEvents() { - delete this._earlyEvents; - this._earlyEvents = []; - return this._earlyEvents; + 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 * symbiont. * * `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 args = ['message'].concat(Array.slice(arguments)); - if (!this._inited) { - this._earlyEvents.push(args); + postMessage: function (...data) { + let model = modelFor(this); + let args = ['message'].concat(data); + if (!model.inited) { + model.earlyEvents.push(args); return; } - processMessage.apply(this, args); - }, - - /** - * EventEmitter, that behaves (calls listeners) asynchronously. - * A way to send customized messages to / from the worker. - * Events from in the worker can be observed / emitted via - * worker.on / worker.emit. - */ - get port() { - // We generate dynamically this attribute as it needs to be accessible - // before Worker.constructor gets called. (For ex: Panel) - - // create an event emitter that receive and send events from/to the worker - this._port = EventEmitterTrait.create({ - emit: this._emitEventToContent.bind(this) - }); - - // expose wrapped port, that exposes only public properties: - // We need to destroy this getter in order to be able to set the - // final value. We need to update only public port attribute as we never - // try to access port attribute from private API. - delete this._public.port; - this._public.port = Cortex(this._port); - // Replicate public port to the private object - delete this.port; - this.port = this._public.port; - - return this._port; - }, - - /** - * Same object than this.port but private API. - * Allow access to _emit, in order to send event to port. - */ - _port: null, - - /** - * Emit a custom event to the content script, - * i.e. emit this event on `self.port` - */ - _emitEventToContent: function () { - let args = ['event'].concat(Array.slice(arguments)); - if (!this._inited) { - this._earlyEvents.push(args); - return; - } - processMessage.apply(this, args); + processMessage.apply(null, [this].concat(args)); }, - // 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, - - constructor: function Worker(options) { - 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; - - this._setListeners(options); - - unload.ensure(this._public, "destroy"); - - // Ensure that worker._port is initialized for contentWorker to be able - // to send events during worker initialization. - this.port; - - this._documentUnload = this._documentUnload.bind(this); - this._pageShow = this._pageShow.bind(this); - this._pageHide = this._pageHide.bind(this); - - if ("window" in options) this._attach(options.window); - }, - - _setListeners: function(options) { - if ('onError' in options) - this.on('error', options.onError); - if ('onMessage' in options) - this.on('message', options.onMessage); - if ('onDetach' in options) - this.on('detach', options.onDetach); + get url () { + let model = modelFor(this); + // model.window will be null after detach + return model.window ? model.window.document.location.href : null; }, - _attach: function(window) { - this._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 - this._windowID = getInnerId(this._window); - observers.add("inner-window-destroyed", this._documentUnload); - - // Listen to pagehide event in order to freeze the content script - // while the document is frozen in bfcache: - this._window.addEventListener("pageshow", this._pageShow, true); - this._window.addEventListener("pagehide", this._pageHide, true); - - // will set this._contentWorker pointing to the private API: - this._contentWorker = WorkerSandbox(this); - - // Mainly enable worker.port.emit to send event to the content worker - this._inited = true; - this._frozen = false; - - // Process all events and messages that were fired before the - // worker was initialized. - this._earlyEvents.forEach((function (args) { - processMessage.apply(this, args); - }).bind(this)); + get contentURL () { + let model = modelFor(this); + return model.window ? model.window.document.URL : null; }, - _documentUnload: function _documentUnload(subject, topic, data) { - let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; - if (innerWinID != this._windowID) return false; - this._workerCleanup(); - return true; - }, - - _pageShow: function _pageShow() { - this._contentWorker.emitSync("pageshow"); - this._emit("pageshow"); - this._frozen = false; - }, - - _pageHide: function _pageHide() { - this._contentWorker.emitSync("pagehide"); - this._emit("pagehide"); - this._frozen = true; - }, - - get url() { - // this._window will be null after detach - return this._window ? this._window.document.location.href : null; - }, - - get tab() { - // this._window will be null after detach - if (this._window) - return getTabForWindow(this._window); + get tab () { + let model = modelFor(this); + // model.window will be null after detach + if (model.window) + return getTabForWindow(model.window); return null; }, - /** - * Tells content worker to unload itself and - * removes all the references from itself. - */ - destroy: function destroy() { - this._workerCleanup(); - this._inited = true; - this._removeAllListeners(); + // Implemented to provide some of the previous features of exposing sandbox + // so that Worker can be extended + getSandbox: function () { + return modelFor(this).contentWorker; }, - /** - * Remove all internal references to the attached document - * Tells _port to unload itself and removes all the references from itself. - */ - _workerCleanup: function _workerCleanup() { - // maybe unloaded before content side is created - // As Symbiont call worker.constructor on document load - if (this._contentWorker) - this._contentWorker.destroy(); - this._contentWorker = null; - if (this._window) { - this._window.removeEventListener("pageshow", this._pageShow, true); - this._window.removeEventListener("pagehide", this._pageHide, true); - } - this._window = null; - // This method may be called multiple times, - // avoid dispatching `detach` event more than once - if (this._windowID) { - this._windowID = null; - observers.remove("inner-window-destroyed", this._documentUnload); - this._earlyEvents.length = 0; - this._emit("detach"); - } - this._inited = false; - }, + 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); - /** - * Receive an event from the content script that need to be sent to - * worker.port. Provide a way for composed object to catch all events. - */ - _onContentScriptEvent: function _onContentScriptEvent() { - this._port._emit.apply(this._port, arguments); - }, + // 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); + + // will set model.contentWorker pointing to the private API: + model.contentWorker = WorkerSandbox(worker, model.window); - /** - * Reference to the content side of the worker. - * @type {WorkerGlobalScope} - */ - _contentWorker: null, + // Mainly enable worker.port.emit to send event to the content worker + model.inited = true; + model.frozen = false; - /** - * Reference to the window that is accessible from - * the content scripts. - * @type {Object} - */ - _window: null, + // Fire off `attach` event + emit(worker, 'attach', window); - /** - * Flag to enable `addon` object injection in document. (bug 612726) - * @type {Boolean} - */ - _injectInDocument: false + // Process all events and messages that were fired before the + // worker was initialized. + model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args))); }); /** - * Fired from postMessage and _emitEventToContent, or from the _earlyMessage + * 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) { + let model = modelFor(worker); + // maybe unloaded before content side is created + if (model.contentWorker) + model.contentWorker.destroy(); + 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; +}); + +/** + * Tells content worker to unload itself and + * removes all the references from itself. + */ +destroy.define(Worker, function (worker) { + detach(worker); + modelFor(worker).inited = true; + // Specifying no type or listener removes all listeners + // from target + off(worker); +}); + +/** + * 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 () { - if (!this._contentWorker) +function processMessage (worker, ...args) { + let model = modelFor(worker) || {}; + if (!model.contentWorker) throw new Error(ERR_DESTROYED); - if (this._frozen) + if (model.frozen) throw new Error(ERR_FROZEN); - this._contentWorker.emit.apply(null, Array.slice(arguments)); + model.contentWorker.emit.apply(null, args); } -exports.Worker = Worker; +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)); +} +
--- a/addon-sdk/source/lib/sdk/context-menu.js +++ b/addon-sdk/source/lib/sdk/context-menu.js @@ -358,36 +358,38 @@ let menuRules = mix(labelledItemRules, { return item instanceof BaseItem; }); }, msg: "items must be an array, and each element in the array must be an " + "Item, Menu, or Separator." } }); -let ContextWorker = Worker.compose({ +let ContextWorker = Class({ + implements: [ Worker ], + //Returns true if any context listeners are defined in the worker's port. anyContextListeners: function anyContextListeners() { - return this._contentWorker.hasListenerFor("context"); + return this.getSandbox().hasListenerFor("context"); }, // Calls the context workers context listeners and returns the first result // that is either a string or a value that evaluates to true. If all of the // listeners returned false then returns false. If there are no listeners // then returns null. getMatchedContext: function getCurrentContexts(popupNode) { - let results = this._contentWorker.emitSync("context", popupNode); + let results = this.getSandbox().emitSync("context", popupNode); return results.reduce(function(val, result) val || result, null); }, // Emits a click event in the worker's port. popupNode is the node that was // context-clicked, and clickedItemData is the data of the item that was // clicked. fireClick: function fireClick(popupNode, clickedItemData) { - this._contentWorker.emitSync("click", popupNode, clickedItemData); + this.getSandbox().emitSync("click", popupNode, clickedItemData); } }); // Returns true if any contexts match. If there are no contexts then a // PageContext is tested instead function hasMatchingContext(contexts, popupNode) { for (let context in contexts) { if (!context.isCurrent(popupNode))
--- a/addon-sdk/source/lib/sdk/deprecated/api-utils.js +++ b/addon-sdk/source/lib/sdk/deprecated/api-utils.js @@ -23,39 +23,16 @@ const VALID_TYPES = [ "object", "string", "undefined", ]; const { isArray } = Array; /** - * Returns a function C that creates instances of privateCtor. C may be called - * with or without the new keyword. The prototype of each instance returned - * from C is C.prototype, and C.prototype is an object whose prototype is - * privateCtor.prototype. Instances returned from C will therefore be instances - * of both C and privateCtor. Additionally, the constructor of each instance - * returned from C is C. - * - * @param privateCtor - * A constructor. - * @return A function that makes new instances of privateCtor. - */ -exports.publicConstructor = function publicConstructor(privateCtor) { - function PublicCtor() { - let obj = { constructor: PublicCtor, __proto__: PublicCtor.prototype }; - memory.track(obj, privateCtor.name); - privateCtor.apply(obj, arguments); - return obj; - } - PublicCtor.prototype = { __proto__: privateCtor.prototype }; - return PublicCtor; -}; - -/** * Returns a validated options dictionary given some requirements. If any of * the requirements are not met, an exception is thrown. * * @param options * An object, the options dictionary to validate. It's not modified. * If it's null or otherwise falsey, an empty object is assumed. * @param requirements * An object whose keys are the expected keys in options. Any key in
deleted file mode 100644 --- a/addon-sdk/source/lib/sdk/deprecated/app-strings.js +++ /dev/null @@ -1,67 +0,0 @@ -/* 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"; - -module.metadata = { - "stability": "deprecated" -}; - -const {Cc,Ci} = require("chrome"); -const apiUtils = require("./api-utils"); - -/** - * A bundle of strings. - * - * @param url {String} - * the URL of the string bundle - */ -exports.StringBundle = apiUtils.publicConstructor(function StringBundle(url) { - - let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. - getService(Ci.nsIStringBundleService). - createBundle(url); - - this.__defineGetter__("url", function () url); - - /** - * Get a string from the bundle. - * - * @param name {String} - * the name of the string to get - * @param args {array} [optional] - * an array of arguments that replace occurrences of %S in the string - * - * @returns {String} the value of the string - */ - this.get = function strings_get(name, args) { - try { - if (args) - return stringBundle.formatStringFromName(name, args, args.length); - else - return stringBundle.GetStringFromName(name); - } - catch(ex) { - // f.e. "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) - // [nsIStringBundle.GetStringFromName]" - throw new Error("String '" + name + "' could not be retrieved from the " + - "bundle due to an unknown error (it doesn't exist?)."); - } - }, - - /** - * Iterate the strings in the bundle. - * - */ - apiUtils.addIterator( - this, - function keysValsGen() { - let enumerator = stringBundle.getSimpleEnumeration(); - while (enumerator.hasMoreElements()) { - let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); - yield [elem.key, elem.value]; - } - } - ); -});
--- a/addon-sdk/source/lib/sdk/deprecated/memory.js +++ b/addon-sdk/source/lib/sdk/deprecated/memory.js @@ -1,53 +1,57 @@ /* 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"; module.metadata = { "stability": "deprecated" }; -const {Cc,Ci,Cu,components} = require("chrome"); -var trackedObjects = {}; +const { Cc, Ci, Cu, components } = require("chrome"); +const { when: unload } = require("../system/unload") -var Compacter = { - INTERVAL: 5000, - notify: function(timer) { +var trackedObjects = {}; +const Compacter = { + notify: function() { var newTrackedObjects = {}; + for (let name in trackedObjects) { - var oldBin = trackedObjects[name]; - var newBin = []; - var strongRefs = []; - for (var i = 0; i < oldBin.length; i++) { - var strongRef = oldBin[i].weakref.get(); + let oldBin = trackedObjects[name]; + let newBin = []; + let strongRefs = []; + + for (let i = 0, l = oldBin.length; i < l; i++) { + let strongRef = oldBin[i].weakref.get(); + if (strongRef && strongRefs.indexOf(strongRef) == -1) { strongRefs.push(strongRef); newBin.push(oldBin[i]); } } + if (newBin.length) newTrackedObjects[name] = newBin; } + trackedObjects = newTrackedObjects; } }; var timer = Cc["@mozilla.org/timer;1"] .createInstance(Ci.nsITimer); - timer.initWithCallback(Compacter, - Compacter.INTERVAL, + 5000, Ci.nsITimer.TYPE_REPEATING_SLACK); -var track = exports.track = function track(object, bin, stackFrameNumber) { +function track(object, bin, stackFrameNumber) { var frame = components.stack.caller; var weakref = Cu.getWeakReference(object); + if (!bin && 'constructor' in object) bin = object.constructor.name; if (bin == "Object") bin = frame.name; if (!bin) bin = "generic"; if (!(bin in trackedObjects)) trackedObjects[bin] = []; @@ -56,63 +60,70 @@ var track = exports.track = function tra for (var i = 0; i < stackFrameNumber; i++) frame = frame.caller; trackedObjects[bin].push({weakref: weakref, created: new Date(), filename: frame.filename, lineNo: frame.lineNumber, bin: bin}); -}; +} +exports.track = track; var getBins = exports.getBins = function getBins() { var names = []; for (let name in trackedObjects) names.push(name); return names; }; -var getObjects = exports.getObjects = function getObjects(bin) { - function getLiveObjectsInBin(bin, array) { - for (var i = 0; i < bin.length; i++) { - var object = bin[i].weakref.get(); - if (object) - array.push(bin[i]); +function getObjects(bin) { + var results = []; + + function getLiveObjectsInBin(bin) { + for (let i = 0, l = bin.length; i < l; i++) { + let object = bin[i].weakref.get(); + + if (object) { + results.push(bin[i]); + } } } - var results = []; if (bin) { if (bin in trackedObjects) - getLiveObjectsInBin(trackedObjects[bin], results); - } else + getLiveObjectsInBin(trackedObjects[bin]); + } + else { for (let name in trackedObjects) - getLiveObjectsInBin(trackedObjects[name], results); + getLiveObjectsInBin(trackedObjects[name]); + } + return results; -}; +} +exports.getObjects = getObjects; -var gc = exports.gc = function gc() { +function gc() { // Components.utils.forceGC() doesn't currently perform // cycle collection, which means that e.g. DOM elements // won't be collected by it. Fortunately, there are // other ways... - - var window = Cc["@mozilla.org/appshell/appShellService;1"] + var test_utils = Cc["@mozilla.org/appshell/appShellService;1"] .getService(Ci.nsIAppShellService) - .hiddenDOMWindow; - var test_utils = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); + .hiddenDOMWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); test_utils.garbageCollect(); + // Clean metadata for dead objects Compacter.notify(); - // Not sure why, but sometimes it appears that we don't get // them all with just one CC, so let's do it again. test_utils.garbageCollect(); }; +exports.gc = gc; -require("../system/unload").when( - function() { - trackedObjects = {}; - if (timer) { - timer.cancel(); - timer = null; - } - }); +unload(_ => { + trackedObjects = {}; + if (timer) { + timer.cancel(); + timer = null; + } +});
deleted file mode 100644 --- a/addon-sdk/source/lib/sdk/deprecated/observer-service.js +++ /dev/null @@ -1,134 +0,0 @@ -/* 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"; - -module.metadata = { - "stability": "deprecated" -}; - -const { Cc, Ci } = require("chrome"); -const { when: unload } = require("../system/unload"); -const { ns } = require("../core/namespace"); -const { on, off, emit, once } = require("../system/events"); -const { id } = require("../self"); - -const subscribers = ns(); -const cache = []; - -/** - * Topics specifically available to Jetpack-generated extensions. - * - * Using these predefined consts instead of the platform strings is good: - * - allows us to scope topics specifically for Jetpacks - * - addons aren't dependent on strings nor behavior of core platform topics - * - the core platform topics are not clearly named - * - */ -exports.topics = { - /** - * A topic indicating that the application is in a state usable - * by add-ons. - */ - APPLICATION_READY: id + "_APPLICATION_READY" -}; - -function Listener(callback, target) { - return function listener({ subject, data }) { - callback.call(target || callback, subject, data); - } -} - -/** - * Register the given callback as an observer of the given topic. - * - * @param topic {String} - * the topic to observe - * - * @param callback {Object} - * the callback; an Object that implements nsIObserver or a Function - * that gets called when the notification occurs - * - * @param target {Object} [optional] - * the object to use as |this| when calling a Function callback - * - * @returns the observer - */ -function add(topic, callback, target) { - let listeners = subscribers(callback); - if (!(topic in listeners)) { - let listener = Listener(callback, target); - listeners[topic] = listener; - - // Cache callback unless it's already cached. - if (!~cache.indexOf(callback)) - cache.push(callback); - - on(topic, listener); - } -}; -exports.add = add; - -/** - * Unregister the given callback as an observer of the given topic. - * - * @param topic {String} - * the topic being observed - * - * @param callback {Object} - * the callback doing the observing - * - * @param target {Object} [optional] - * the object being used as |this| when calling a Function callback - */ -function remove(topic, callback, target) { - let listeners = subscribers(callback); - if (topic in listeners) { - let listener = listeners[topic]; - delete listeners[topic]; - - // If no more observers are registered and callback is still in cache - // then remove it. - let index = cache.indexOf(callback); - if (~index && !Object.keys(listeners).length) - cache.splice(index, 1) - - off(topic, listener); - } -}; -exports.remove = remove; - -/** - * Notify observers about something. - * - * @param topic {String} - * the topic to notify observers about - * - * @param subject {Object} [optional] - * some information about the topic; can be any JS object or primitive - * - * @param data {String} [optional] [deprecated] - * some more information about the topic; deprecated as the subject - * is sufficient to pass all needed information to the JS observers - * that this module targets; if you have multiple values to pass to - * the observer, wrap them in an object and pass them via the subject - * parameter (i.e.: { foo: 1, bar: "some string", baz: myObject }) - */ -function notify(topic, subject, data) { - emit(topic, { - subject: subject === undefined ? null : subject, - data: data === undefined ? null : data - }); -} -exports.notify = notify; - -unload(function() { - // Make a copy of cache first, since cache will be changing as we - // iterate through it. - cache.slice().forEach(function(callback) { - Object.keys(subscribers(callback)).forEach(function(topic) { - remove(topic, callback); - }); - }); -})
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/lib/sdk/deprecated/symbiont.js @@ -0,0 +1,229 @@ +/* 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"; + +module.metadata = { + "stability": "deprecated" +}; + +const { Worker } = require('./traits-worker'); +const { Loader } = require('../content/loader'); +const hiddenFrames = require('../frame/hidden-frame'); +const { on, off } = require('../system/events'); +const unload = require('../system/unload'); +const { getDocShell } = require("../frame/utils"); +const { ignoreWindow } = require('../private-browsing/utils'); + +// Everything coming from add-on's xpi considered an asset. +const assetsURI = require('../self').data.url().replace(/data\/$/, ""); + +/** + * This trait is layered on top of `Worker` and in contrast to symbiont + * Worker constructor requires `content` option that represents content + * that will be loaded in the provided frame, if frame is not provided + * Worker will create hidden one. + */ +const Symbiont = Worker.resolve({ + constructor: '_initWorker', + destroy: '_workerDestroy' + }).compose(Loader, { + + /** + * The constructor requires all the options that are required by + * `require('content').Worker` with the difference that the `frame` option + * is optional. If `frame` is not provided, `contentURL` is expected. + * @param {Object} options + * @param {String} options.contentURL + * URL of a content to load into `this._frame` and create worker for. + * @param {Element} [options.frame] + * iframe element that is used to load `options.contentURL` into. + * if frame is not provided hidden iframe will be created. + */ + constructor: function Symbiont(options) { + options = options || {}; + + if ('contentURL' in options) + this.contentURL = options.contentURL; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + if ('contentScriptOptions' in options) + this.contentScriptOptions = options.contentScriptOptions; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('allow' in options) + this.allow = options.allow; + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + if ('frame' in options) { + this._initFrame(options.frame); + } + else { + let self = this; + this._hiddenFrame = hiddenFrames.HiddenFrame({ + onReady: function onFrame() { + self._initFrame(this.element); + }, + onUnload: function onUnload() { + // Bug 751211: Remove reference to _frame when hidden frame is + // automatically removed on unload, otherwise we are going to face + // "dead object" exception + self.destroy(); + } + }); + hiddenFrames.add(this._hiddenFrame); + } + + unload.ensure(this._public, "destroy"); + }, + + destroy: function destroy() { + this._workerDestroy(); + this._unregisterListener(); + this._frame = null; + if (this._hiddenFrame) { + hiddenFrames.remove(this._hiddenFrame); + this._hiddenFrame = null; + } + }, + + /** + * XUL iframe or browser elements with attribute `type` being `content`. + * Used to create `ContentSymbiont` from. + * @type {nsIFrame|nsIBrowser} + */ + _frame: null, + + /** + * Listener to the `'frameReady"` event (emitted when `iframe` is ready). + * Removes listener, sets right permissions to the frame and loads content. + */ + _initFrame: function _initFrame(frame) { + if (this._loadListener) + this._unregisterListener(); + + this._frame = frame; + + if (getDocShell(frame)) { + this._reallyInitFrame(frame); + } + else { + if (this._waitForFrame) { + off('content-document-global-created', this._waitForFrame); + } + this._waitForFrame = this.__waitForFrame.bind(this, frame); + on('content-document-global-created', this._waitForFrame); + } + }, + + __waitForFrame: function _waitForFrame(frame, { subject: win }) { + if (frame.contentWindow == win) { + off('content-document-global-created', this._waitForFrame); + delete this._waitForFrame; + this._reallyInitFrame(frame); + } + }, + + _reallyInitFrame: function _reallyInitFrame(frame) { + getDocShell(frame).allowJavascript = this.allow.script; + frame.setAttribute("src", this._contentURL); + + // Inject `addon` object in document if we load a document from + // one of our addon folder and if no content script are defined. bug 612726 + let isDataResource = + typeof this._contentURL == "string" && + this._contentURL.indexOf(assetsURI) == 0; + let hasContentScript = + (Array.isArray(this.contentScript) ? this.contentScript.length > 0 + : !!this.contentScript) || + (Array.isArray(this.contentScriptFile) ? this.contentScriptFile.length > 0 + : !!this.contentScriptFile); + // If we have to inject `addon` we have to do it before document + // script execution, so during `start`: + this._injectInDocument = isDataResource && !hasContentScript; + if (this._injectInDocument) + this.contentScriptWhen = "start"; + + if ((frame.contentDocument.readyState == "complete" || + (frame.contentDocument.readyState == "interactive" && + this.contentScriptWhen != 'end' )) && + frame.contentDocument.location == this._contentURL) { + // In some cases src doesn't change and document is already ready + // (for ex: when the user moves a widget while customizing toolbars.) + this._onInit(); + return; + } + + let self = this; + + if ('start' == this.contentScriptWhen) { + this._loadEvent = 'start'; + on('document-element-inserted', + this._loadListener = function onStart({ subject: doc }) { + let window = doc.defaultView; + + if (ignoreWindow(window)) { + return; + } + + if (window && window == frame.contentWindow) { + self._unregisterListener(); + self._onInit(); + } + + }); + return; + } + + let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded'; + let self = this; + this._loadEvent = eventName; + frame.addEventListener(eventName, + this._loadListener = function _onReady(event) { + + if (event.target != frame.contentDocument) + return; + self._unregisterListener(); + + self._onInit(); + + }, true); + + }, + + /** + * Unregister listener that watchs for document being ready to be injected. + * This listener is registered in `Symbiont._initFrame`. + */ + _unregisterListener: function _unregisterListener() { + if (this._waitForFrame) { + off('content-document-global-created', this._waitForFrame); + delete this._waitForFrame; + } + + if (!this._loadListener) + return; + if (this._loadEvent == "start") { + off('document-element-inserted', this._loadListener); + } + else { + this._frame.removeEventListener(this._loadEvent, this._loadListener, + true); + } + this._loadListener = null; + }, + + /** + * Called by Symbiont itself when the frame is ready to load + * content scripts according to contentScriptWhen. Overloaded by Panel. + */ + _onInit: function () { + this._initWorker({ window: this._frame.contentWindow }); + } + +}); +exports.Symbiont = Symbiont;
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/lib/sdk/deprecated/traits-worker.js @@ -0,0 +1,660 @@ +/* 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/traits-worker` was previously `content/worker` and kept + * only due to `deprecated/symbiont` using it, which is necessary for + * `widget`, until that reaches deprecation EOL. + * + */ + +"use strict"; + +module.metadata = { + "stability": "deprecated" +}; + +const { Trait } = require('./traits'); +const { EventEmitter, EventEmitterTrait } = require('./events'); +const { Ci, Cu, Cc } = require('chrome'); +const timer = require('../timers'); +const { URL } = require('../url'); +const unload = require('../system/unload'); +const observers = require('../system/events'); +const { Cortex } = require('./cortex'); +const { sandbox, evaluate, load } = require("../loader/sandbox"); +const { merge } = require('../util/object'); +const xulApp = require("../system/xul-app"); +const { getInnerId } = require("../window/utils") +const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion, + "17.0a2", "*"); +const { getTabForWindow } = require('../tabs/helpers'); +const { getTabForContentWindow } = require('../tabs/utils'); + +/* Trick the linker in order to ensure shipping these files in the XPI. + require('../content/content-worker.js'); + Then, retrieve URL of these files in the XPI: +*/ +let prefix = module.uri.split('deprecated/traits-worker.js')[0]; +const CONTENT_WORKER_URL = prefix + 'content/content-worker.js'; + +// Fetch additional list of domains to authorize access to for each content +// script. It is stored in manifest `metadata` field which contains +// package.json data. This list is originaly defined by authors in +// `permissions` attribute of their package.json addon file. +const permissions = require('@loader/options').metadata['permissions'] || {}; +const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || []; + +const JS_VERSION = '1.8'; + +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."; + + +const WorkerSandbox = EventEmitter.compose({ + + /** + * Emit a message to the worker content sandbox + */ + emit: function emit() { + // First ensure having a regular array + // (otherwise, `arguments` would be mapped to an object by `stringify`) + let array = Array.slice(arguments); + // JSON.stringify is buggy with cross-sandbox values, + // it may return "{}" on functions. Use a replacer to match them correctly. + function replacer(k, v) { + return typeof v === "function" ? undefined : v; + } + // Ensure having an asynchronous behavior + let self = this; + timer.setTimeout(function () { + self._emitToContent(JSON.stringify(array, replacer)); + }, 0); + }, + + /** + * Synchronous version of `emit`. + * /!\ Should only be used when it is strictly mandatory /!\ + * Doesn't ensure passing only JSON values. + * Mainly used by context-menu in order to avoid breaking it. + */ + emitSync: function emitSync() { + let args = Array.slice(arguments); + return this._emitToContent(args); + }, + + /** + * Tells if content script has at least one listener registered for one event, + * through `self.on('xxx', ...)`. + * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API. + */ + hasListenerFor: function hasListenerFor(name) { + return this._hasListenerFor(name); + }, + + /** + * Method called by the worker sandbox when it needs to send a message + */ + _onContentEvent: function onContentEvent(args) { + // As `emit`, we ensure having an asynchronous behavior + let self = this; + timer.setTimeout(function () { + // We emit event to chrome/addon listeners + self._emit.apply(self, JSON.parse(args)); + }, 0); + }, + + /** + * Configures sandbox and loads content scripts into it. + * @param {Worker} worker + * content worker + */ + constructor: function WorkerSandbox(worker) { + this._addonWorker = worker; + + // Ensure that `emit` has always the right `this` + this.emit = this.emit.bind(this); + this.emitSync = this.emitSync.bind(this); + + // We receive a wrapped window, that may be an xraywrapper if it's content + let window = worker._window; + let proto = window; + + // Eventually use expanded principal sandbox feature, if some are given. + // + // But prevent it when the Worker isn't used for a content script but for + // injecting `addon` object into a Panel, Widget, ... scope. + // That's because: + // 1/ It is useless to use multiple domains as the worker is only used + // to communicate with the addon, + // 2/ By using it it would prevent the document to have access to any JS + // value of the worker. As JS values coming from multiple domain principals + // can't be accessed by "mono-principals" (principal with only one domain). + // Even if this principal is for a domain that is specified in the multiple + // domain principal. + let principals = window; + let wantGlobalProperties = [] + if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) { + principals = EXPANDED_PRINCIPALS.concat(window); + // We have to replace XHR constructor of the content document + // with a custom cross origin one, automagically added by platform code: + delete proto.XMLHttpRequest; + wantGlobalProperties.push("XMLHttpRequest"); + } + + // Instantiate trusted code in another Sandbox in order to prevent content + // script from messing with standard classes used by proxy and API code. + let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window }); + apiSandbox.console = console; + + // Create the sandbox and bind it to window in order for content scripts to + // have access to all standard globals (window, document, ...) + let content = this._sandbox = sandbox(principals, { + sandboxPrototype: proto, + wantXrays: true, + wantGlobalProperties: wantGlobalProperties, + sameZoneAs: window, + metadata: { SDKContentScript: true } + }); + // We have to ensure that window.top and window.parent are the exact same + // object than window object, i.e. the sandbox global object. But not + // always, in case of iframes, top and parent are another window object. + let top = window.top === window ? content : content.top; + let parent = window.parent === window ? content : content.parent; + merge(content, { + // We need "this === window === top" to be true in toplevel scope: + get window() content, + get top() top, + get parent() parent, + // Use the Greasemonkey naming convention to provide access to the + // unwrapped window object so the content script can access document + // JavaScript values. + // NOTE: this functionality is experimental and may change or go away + // at any time! + get unsafeWindow() window.wrappedJSObject + }); + + // Load trusted code that will inject content script API. + // We need to expose JS objects defined in same principal in order to + // avoid having any kind of wrapper. + load(apiSandbox, CONTENT_WORKER_URL); + + // prepare a clean `self.options` + let options = 'contentScriptOptions' in worker ? + JSON.stringify( worker.contentScriptOptions ) : + undefined; + + // Then call `inject` method and communicate with this script + // by trading two methods that allow to send events to the other side: + // - `onEvent` called by content script + // - `result.emitToContent` called by addon script + // Bug 758203: We have to explicitely define `__exposedProps__` in order + // to allow access to these chrome object attributes from this sandbox with + // content priviledges + // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers + let chromeAPI = { + timers: { + setTimeout: timer.setTimeout, + setInterval: timer.setInterval, + clearTimeout: timer.clearTimeout, + clearInterval: timer.clearInterval, + __exposedProps__: { + setTimeout: 'r', + setInterval: 'r', + clearTimeout: 'r', + clearInterval: 'r' + } + }, + sandbox: { + evaluate: evaluate, + __exposedProps__: { + evaluate: 'r', + } + }, + __exposedProps__: { + timers: 'r', + sandbox: 'r', + } + }; + let onEvent = this._onContentEvent.bind(this); + // `ContentWorker` is defined in CONTENT_WORKER_URL file + let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options); + this._emitToContent = result.emitToContent; + this._hasListenerFor = result.hasListenerFor; + + // Handle messages send by this script: + let self = this; + // console.xxx calls + this.on("console", function consoleListener(kind) { + console[kind].apply(console, Array.slice(arguments, 1)); + }); + + // self.postMessage calls + this.on("message", function postMessage(data) { + // destroyed? + if (self._addonWorker) + self._addonWorker._emit('message', data); + }); + + // self.port.emit calls + this.on("event", function portEmit(name, args) { + // destroyed? + if (self._addonWorker) + self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments); + }); + + // unwrap, recreate and propagate async Errors thrown from content-script + this.on("error", function onError({instanceOfError, value}) { + if (self._addonWorker) { + let error = value; + if (instanceOfError) { + error = new Error(value.message, value.fileName, value.lineNumber); + error.stack = value.stack; + error.name = value.name; + } + self._addonWorker._emit('error', error); + } + }); + + // Inject `addon` global into target document if document is trusted, + // `addon` in document is equivalent to `self` in content script. + if (worker._injectInDocument) { + let win = window.wrappedJSObject ? window.wrappedJSObject : window; + Object.defineProperty(win, "addon", { + value: content.self + } + ); + } + + // Inject our `console` into target document if worker doesn't have a tab + // (e.g Panel, PageWorker, Widget). + // `worker.tab` can't be used because bug 804935. + if (!getTabForContentWindow(window)) { + let win = window.wrappedJSObject ? window.wrappedJSObject : window; + + // export our chrome console to content window, using the same approach + // of `ConsoleAPI`: + // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150 + // + // and described here: + // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn + let con = Cu.createObjectIn(win); + + let genPropDesc = function genPropDesc(fun) { + return { enumerable: true, configurable: true, writable: true, + value: console[fun] }; + } + + const properties = { + log: genPropDesc('log'), + info: genPropDesc('info'), + warn: genPropDesc('warn'), + error: genPropDesc('error'), + debug: genPropDesc('debug'), + trace: genPropDesc('trace'), + dir: genPropDesc('dir'), + group: genPropDesc('group'), + groupCollapsed: genPropDesc('groupCollapsed'), + groupEnd: genPropDesc('groupEnd'), + time: genPropDesc('time'), + timeEnd: genPropDesc('timeEnd'), + profile: genPropDesc('profile'), + profileEnd: genPropDesc('profileEnd'), + __noSuchMethod__: { enumerable: true, configurable: true, writable: true, + value: function() {} } + }; + + Object.defineProperties(con, properties); + Cu.makeObjectPropsNormal(con); + + win.console = con; + }; + + // The order of `contentScriptFile` and `contentScript` evaluation is + // intentional, so programs can load libraries like jQuery from script URLs + // and use them in scripts. + let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile + : null, + contentScript = ('contentScript' in worker) ? worker.contentScript : null; + + if (contentScriptFile) { + if (Array.isArray(contentScriptFile)) + this._importScripts.apply(this, contentScriptFile); + else + this._importScripts(contentScriptFile); + } + if (contentScript) { + this._evaluate( + Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript + ); + } + }, + destroy: function destroy() { + this.emitSync("detach"); + this._sandbox = null; + this._addonWorker = null; + }, + + /** + * JavaScript sandbox where all the content scripts are evaluated. + * {Sandbox} + */ + _sandbox: null, + + /** + * Reference to the addon side of the worker. + * @type {Worker} + */ + _addonWorker: null, + + /** + * Evaluates code in the sandbox. + * @param {String} code + * JavaScript source to evaluate. + * @param {String} [filename='javascript:' + code] + * Name of the file + */ + _evaluate: function(code, filename) { + try { + evaluate(this._sandbox, code, filename || 'javascript:' + code); + } + catch(e) { + this._addonWorker._emit('error', e); + } + }, + /** + * Imports scripts to the sandbox by reading files under urls and + * evaluating its source. If exception occurs during evaluation + * `"error"` event is emitted on the worker. + * This is actually an analog to the `importScript` method in web + * workers but in our case it's not exposed even though content + * scripts may be able to do it synchronously since IO operation + * takes place in the UI process. + */ + _importScripts: function _importScripts(url) { + let urls = Array.slice(arguments, 0); + for each (let contentScriptFile in urls) { + try { + let uri = URL(contentScriptFile); + if (uri.scheme === 'resource') + load(this._sandbox, String(uri)); + else + throw Error("Unsupported `contentScriptFile` url: " + String(uri)); + } + catch(e) { + this._addonWorker._emit('error', e); + } + } + } +}); + +/** + * Message-passing facility for communication between code running + * in the content and add-on process. + * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html + */ +const Worker = EventEmitter.compose({ + on: Trait.required, + _removeAllListeners: Trait.required, + + // List of messages fired before worker is initialized + get _earlyEvents() { + delete this._earlyEvents; + this._earlyEvents = []; + return this._earlyEvents; + }, + + /** + * 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 + * symbiont. + * + * `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 args = ['message'].concat(Array.slice(arguments)); + if (!this._inited) { + this._earlyEvents.push(args); + return; + } + processMessage.apply(this, args); + }, + + /** + * EventEmitter, that behaves (calls listeners) asynchronously. + * A way to send customized messages to / from the worker. + * Events from in the worker can be observed / emitted via + * worker.on / worker.emit. + */ + get port() { + // We generate dynamically this attribute as it needs to be accessible + // before Worker.constructor gets called. (For ex: Panel) + + // create an event emitter that receive and send events from/to the worker + this._port = EventEmitterTrait.create({ + emit: this._emitEventToContent.bind(this) + }); + + // expose wrapped port, that exposes only public properties: + // We need to destroy this getter in order to be able to set the + // final value. We need to update only public port attribute as we never + // try to access port attribute from private API. + delete this._public.port; + this._public.port = Cortex(this._port); + // Replicate public port to the private object + delete this.port; + this.port = this._public.port; + + return this._port; + }, + + /** + * Same object than this.port but private API. + * Allow access to _emit, in order to send event to port. + */ + _port: null, + + /** + * Emit a custom event to the content script, + * i.e. emit this event on `self.port` + */ + _emitEventToContent: function () { + let args = ['event'].concat(Array.slice(arguments)); + if (!this._inited) { + this._earlyEvents.push(args); + return; + } + processMessage.apply(this, args); + }, + + // 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, + + constructor: function Worker(options) { + 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; + + this._setListeners(options); + + unload.ensure(this._public, "destroy"); + + // Ensure that worker._port is initialized for contentWorker to be able + // to send events during worker initialization. + this.port; + + this._documentUnload = this._documentUnload.bind(this); + this._pageShow = this._pageShow.bind(this); + this._pageHide = this._pageHide.bind(this); + + if ("window" in options) this._attach(options.window); + }, + + _setListeners: function(options) { + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + if ('onDetach' in options) + this.on('detach', options.onDetach); + }, + + _attach: function(window) { + this._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 + this._windowID = getInnerId(this._window); + observers.on("inner-window-destroyed", this._documentUnload); + + // Listen to pagehide event in order to freeze the content script + // while the document is frozen in bfcache: + this._window.addEventListener("pageshow", this._pageShow, true); + this._window.addEventListener("pagehide", this._pageHide, true); + + // will set this._contentWorker pointing to the private API: + this._contentWorker = WorkerSandbox(this); + + // Mainly enable worker.port.emit to send event to the content worker + this._inited = true; + this._frozen = false; + + // Process all events and messages that were fired before the + // worker was initialized. + this._earlyEvents.forEach((function (args) { + processMessage.apply(this, args); + }).bind(this)); + }, + + _documentUnload: function _documentUnload({ subject, data }) { + let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (innerWinID != this._windowID) return false; + this._workerCleanup(); + return true; + }, + + _pageShow: function _pageShow() { + this._contentWorker.emitSync("pageshow"); + this._emit("pageshow"); + this._frozen = false; + }, + + _pageHide: function _pageHide() { + this._contentWorker.emitSync("pagehide"); + this._emit("pagehide"); + this._frozen = true; + }, + + get url() { + // this._window will be null after detach + return this._window ? this._window.document.location.href : null; + }, + + get tab() { + // this._window will be null after detach + if (this._window) + return getTabForWindow(this._window); + return null; + }, + + /** + * Tells content worker to unload itself and + * removes all the references from itself. + */ + destroy: function destroy() { + this._workerCleanup(); + this._inited = true; + this._removeAllListeners(); + }, + + /** + * Remove all internal references to the attached document + * Tells _port to unload itself and removes all the references from itself. + */ + _workerCleanup: function _workerCleanup() { + // maybe unloaded before content side is created + // As Symbiont call worker.constructor on document load + if (this._contentWorker) + this._contentWorker.destroy(); + this._contentWorker = null; + if (this._window) { + this._window.removeEventListener("pageshow", this._pageShow, true); + this._window.removeEventListener("pagehide", this._pageHide, true); + } + this._window = null; + // This method may be called multiple times, + // avoid dispatching `detach` event more than once + if (this._windowID) { + this._windowID = null; + observers.off("inner-window-destroyed", this._documentUnload); + this._earlyEvents.length = 0; + this._emit("detach"); + } + this._inited = false; + }, + + /** + * Receive an event from the content script that need to be sent to + * worker.port. Provide a way for composed object to catch all events. + */ + _onContentScriptEvent: function _onContentScriptEvent() { + this._port._emit.apply(this._port, arguments); + }, + + /** + * 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, + + /** + * Flag to enable `addon` object injection in document. (bug 612726) + * @type {Boolean} + */ + _injectInDocument: false +}); + +/** + * 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 () { + if (!this._contentWorker) + throw new Error(ERR_DESTROYED); + if (this._frozen) + throw new Error(ERR_FROZEN); + + this._contentWorker.emit.apply(null, Array.slice(arguments)); +} + +exports.Worker = Worker;
--- a/addon-sdk/source/lib/sdk/event/core.js +++ b/addon-sdk/source/lib/sdk/event/core.js @@ -11,16 +11,17 @@ module.metadata = { const UNCAUGHT_ERROR = 'An error event was emitted for which there was no listener.'; const BAD_LISTENER = 'The event listener must be a function.'; const { ns } = require('../core/namespace'); const event = ns(); const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/; +exports.EVENT_TYPE_PATTERN = EVENT_TYPE_PATTERN; // Utility function to access given event `target` object's event listeners for // the specific event `type`. If listeners for this type does not exists they // will be created. const observers = function observers(target, type) { if (!target) throw TypeError("Event target must be an object"); let listeners = event(target); return type in listeners ? listeners[type] : listeners[type] = []; @@ -156,15 +157,16 @@ exports.count = count; * The type of event. * @param {Object} listeners * Dictionary of listeners. */ function setListeners(target, listeners) { Object.keys(listeners || {}).forEach(key => { let match = EVENT_TYPE_PATTERN.exec(key); let type = match && match[1].toLowerCase(); - let listener = listeners[key]; + if (!type) return; - if (type && typeof(listener) === 'function') + let listener = listeners[key]; + if (typeof(listener) === 'function') on(target, type, listener); }); } exports.setListeners = setListeners;
--- a/addon-sdk/source/lib/sdk/event/utils.js +++ b/addon-sdk/source/lib/sdk/event/utils.js @@ -2,17 +2,17 @@ * 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"; module.metadata = { "stability": "unstable" }; -let { emit, on, once, off } = require("./core"); +let { emit, on, once, off, EVENT_TYPE_PATTERN } = require("./core"); // This module provides set of high order function for working with event // streams (streams in a NodeJS style that dispatch data, end and error // events). // Function takes a `target` object and returns set of implicit references // (non property references) it keeps. This basically allows defining // references between objects without storing the explicitly. See transform for @@ -250,8 +250,26 @@ Reactor.prototype.onNext = function(pres Reactor.prototype.run = function(input) { on(input, "data", message => this.onNext(message, input.value)); on(input, "end", () => this.onEnd(input.value)); start(input); this.value = input.value; this.onStart(input.value); }; exports.Reactor = Reactor; + +/** + * Takes an object used as options with potential keys like 'onMessage', + * used to be called `require('sdk/event/core').setListeners` on. + * This strips all keys that would trigger a listener to be set. + * + * @params {Object} object + * @return {Object} + */ + +function stripListeners (object) { + return Object.keys(object).reduce((agg, key) => { + if (!EVENT_TYPE_PATTERN.test(key)) + agg[key] = object[key]; + return agg; + }, {}); +} +exports.stripListeners = stripListeners;
--- a/addon-sdk/source/lib/sdk/l10n/prefs.js +++ b/addon-sdk/source/lib/sdk/l10n/prefs.js @@ -1,21 +1,20 @@ /* 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 observers = require("../deprecated/observer-service"); +const { on } = require("../system/events"); const core = require("./core"); const { id: jetpackId} = require('../self'); const OPTIONS_DISPLAYED = "addon-options-displayed"; -function onOptionsDisplayed(document, addonId) { +function onOptionsDisplayed({ subjec: document, data: addonId }) { if (addonId !== jetpackId) return; let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' + 'button[data-jetpack-id="' + jetpackId + '"][pref-name]'; let nodes = document.querySelectorAll(query); for (let node of nodes) { let name = node.getAttribute("pref-name"); if (node.tagName == "setting") { @@ -35,10 +34,9 @@ function onOptionsDisplayed(document, ad } else if (node.tagName == "button") { let label = core.get(name + "_label"); if (label) node.setAttribute("label", label); } } } - -observers.add(OPTIONS_DISPLAYED, onOptionsDisplayed); +on(OPTIONS_DISPLAYED, onOptionsDisplayed);
--- a/addon-sdk/source/lib/sdk/page-mod.js +++ b/addon-sdk/source/lib/sdk/page-mod.js @@ -2,17 +2,17 @@ * 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"; module.metadata = { "stability": "stable" }; -const observers = require('./deprecated/observer-service'); +const observers = require('./system/events'); const { Loader, validationAttributes } = require('./content/loader'); const { Worker } = require('./content/worker'); const { Registry } = require('./util/registry'); const { EventEmitter } = require('./deprecated/events'); const { on, emit } = require('./event/core'); const { validateOptions : validate } = require('./deprecated/api-utils'); const { Cc, Ci } = require('chrome'); const { merge } = require('./util/object'); @@ -95,17 +95,17 @@ const PageMod = Loader.compose(EventEmit ' `top` or `frame` value'); } else { this.attachTo = ["top", "frame"]; } let include = options.include; let rules = this.include = Rules(); - + if (!include) throw new Error('The `include` option must always contain atleast one rule'); rules.add.apply(rules, [].concat(include)); if (contentStyle || contentStyleFile) { this._style = Style({ uri: contentStyleFile, @@ -212,34 +212,34 @@ exports.PageMod = function(options) Page exports.PageMod.prototype = PageMod.prototype; const PageModManager = Registry.resolve({ constructor: '_init', _destructor: '_registryDestructor' }).compose({ constructor: function PageModRegistry(constructor) { this._init(PageMod); - observers.add( + observers.on( 'document-element-inserted', this._onContentWindow = this._onContentWindow.bind(this) ); }, _destructor: function _destructor() { - observers.remove('document-element-inserted', this._onContentWindow); + observers.off('document-element-inserted', this._onContentWindow); this._removeAllListeners(); // We need to do some cleaning er PageMods, like unregistering any // `contentStyle*` this._registry.forEach(function(pageMod) { pageMod.destroy(); }); this._registryDestructor(); }, - _onContentWindow: function _onContentWindow(document) { + _onContentWindow: function _onContentWindow({ subject: document }) { let window = document.defaultView; // XML documents don't have windows, and we don't yet support them. if (!window) return; // We apply only on documents in tabs of Firefox if (!getTabForContentWindow(window)) return;
--- a/addon-sdk/source/lib/sdk/page-worker.js +++ b/addon-sdk/source/lib/sdk/page-worker.js @@ -4,18 +4,19 @@ "use strict"; module.metadata = { "stability": "stable" }; const { Class } = require('./core/heritage'); const { on, emit, off, setListeners } = require('./event/core'); -const { filter, pipe, map, merge: streamMerge } = require('./event/utils'); -const { WorkerHost, Worker, detach, attach, destroy } = require('./worker/utils'); +const { filter, pipe, map, merge: streamMerge, stripListeners } = require('./event/utils'); +const { detach, attach, destroy, WorkerHost } = require('./content/utils'); +const { Worker } = require('./content/worker'); const { Disposable } = require('./core/disposable'); const { EventTarget } = require('./event/target'); const { unload } = require('./system/unload'); const { events, streamEventsFrom } = require('./content/events'); const { getAttachEventType } = require('./content/utils'); const { window } = require('./addon/window'); const { getParentWindow } = require('./window/utils'); const { create: makeFrame, getDocShell } = require('./frame/utils'); @@ -61,18 +62,18 @@ function enableScript (page) { } function disableScript (page) { getDocShell(viewFor(page)).allowJavascript = false; } function Allow (page) { return { - get script() getDocShell(viewFor(page)).allowJavascript, - set script(value) value ? enableScript(page) : disableScript(page) + get script() { return getDocShell(viewFor(page)).allowJavascript; }, + set script(value) { return value ? enableScript(page) : disableScript(page); } }; } function injectWorker ({page}) { let worker = workerFor(page); let view = viewFor(page); if (isValidURL(page, view.contentDocument.URL)) attach(worker, view.contentWindow); @@ -84,42 +85,44 @@ const Page = Class({ implements: [ EventTarget, Disposable ], extends: WorkerHost(workerFor), setup: function Page(options) { let page = this; options = pageContract(options); - setListeners(this, options); let view = makeFrame(window.document, { nodeName: 'iframe', type: 'content', uri: options.contentURL, allowJavascript: options.allow.script, allowPlugins: true, allowAuth: true }); ['contentScriptFile', 'contentScript', 'contentScriptWhen'] - .forEach(function (prop) page[prop] = options[prop]); + .forEach(prop => page[prop] = options[prop]); views.set(this, view); pages.set(view, this); - let worker = new Worker(options); + // Set listeners on the {Page} object itself, not the underlying worker, + // like `onMessage`, as it gets piped + setListeners(this, options); + let worker = new Worker(stripListeners(options)); workers.set(this, worker); pipe(worker, this); if (this.include || options.include) { this.rules = Rules(); this.rules.add.apply(this.rules, [].concat(this.include || options.include)); } }, - get allow() Allow(this), + get allow() { return Allow(this); }, set allow(value) { let allowJavascript = pageContract({ allow: value }).allow.script; return allowJavascript ? enableScript(this) : disableScript(this); }, get contentURL() { return viewFor(this).getAttribute('src'); }, set contentURL(value) { if (!isValidURL(this, value)) return; let view = viewFor(this); @@ -128,17 +131,17 @@ const Page = Class({ }, dispose: function () { if (isDisposed(this)) return; let view = viewFor(this); if (view.parentNode) view.parentNode.removeChild(view); views.delete(this); destroy(workers.get(this)); }, - toString: function () '[object Page]' + toString: function () { return '[object Page]' } }); exports.Page = Page; let pageEvents = streamMerge([events, streamEventsFrom(window)]); let readyEvents = filter(pageEvents, isReadyEvent); let formattedEvents = map(readyEvents, function({target, type}) { return { type: type, page: pageFromDoc(target) };
--- a/addon-sdk/source/lib/sdk/panel.js +++ b/addon-sdk/source/lib/sdk/panel.js @@ -14,27 +14,27 @@ module.metadata = { const { Ci } = require("chrome"); const { validateOptions: valid } = require('./deprecated/api-utils'); const { setTimeout } = require('./timers'); const { isPrivateBrowsingSupported } = require('./self'); const { isWindowPBSupported } = require('./private-browsing/utils'); const { Class } = require("./core/heritage"); const { merge } = require("./util/object"); -const { WorkerHost, Worker, detach, attach, destroy, - requiresAddonGlobal } = require("./worker/utils"); +const { WorkerHost, detach, attach, destroy } = require("./content/utils"); +const { Worker } = require("./content/worker"); const { Disposable } = require("./core/disposable"); const { contract: loaderContract } = require("./content/loader"); const { contract } = require("./util/contract"); const { on, off, emit, setListeners } = require("./event/core"); const { EventTarget } = require("./event/target"); const domPanel = require("./panel/utils"); const { events } = require("./panel/events"); const systemEvents = require("./system/events"); -const { filter, pipe } = require("./event/utils"); +const { filter, pipe, stripListeners } = require("./event/utils"); const { getNodeView, getActiveView } = require("./view/core"); const { isNil, isObject } = require("./lang/type"); const { getAttachEventType } = require("./content/utils"); let number = { is: ['number', 'undefined', 'null'] }; let boolean = { is: ['boolean', 'undefined', 'null'] }; let rectContract = contract({ @@ -112,30 +112,30 @@ const Panel = Class({ let model = merge({ defaultWidth: 320, defaultHeight: 240, focus: true, position: Object.freeze({}), }, panelContract(options)); models.set(this, model); - // Setup listeners. - setListeners(this, options); // Setup view let view = domPanel.make(); panels.set(view, this); views.set(this, view); // Load panel content. domPanel.setURL(view, model.contentURL); setupAutoHide(this); - let worker = new Worker(options); + // Setup listeners. + setListeners(this, options); + let worker = new Worker(stripListeners(options)); workers.set(this, worker); // pipe events from worker to a panel. pipe(worker, this); }, dispose: function dispose() { this.hide(); off(this);
--- a/addon-sdk/source/lib/sdk/simple-prefs.js +++ b/addon-sdk/source/lib/sdk/simple-prefs.js @@ -3,30 +3,24 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict'; module.metadata = { "stability": "experimental" }; const { emit, off } = require("./event/core"); -const { when: unload } = require("./system/unload"); const { PrefsTarget } = require("./preferences/event-target"); const { id } = require("./self"); -const observers = require("./deprecated/observer-service"); +const { on } = require("./system/events"); const ADDON_BRANCH = "extensions." + id + "."; const BUTTON_PRESSED = id + "-cmdPressed"; const target = PrefsTarget({ branchName: ADDON_BRANCH }); // Listen to clicks on buttons -function buttonClick(subject, data) { +function buttonClick({ data }) { emit(target, data); } -observers.add(BUTTON_PRESSED, buttonClick); - -// Make sure we cleanup listeners on unload. -unload(function() { - observers.remove(BUTTON_PRESSED, buttonClick); -}); +on(BUTTON_PRESSED, buttonClick); module.exports = target;
--- a/addon-sdk/source/lib/sdk/test/harness.js +++ b/addon-sdk/source/lib/sdk/test/harness.js @@ -1,33 +1,40 @@ /* 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"; module.metadata = { "stability": "experimental" }; const { Cc, Ci, Cu } = require("chrome"); const { Loader } = require('./loader'); const { serializeStack, parseStack } = require("toolkit/loader"); const { setTimeout } = require('../timers'); -const memory = require('../deprecated/memory'); const { PlainTextConsole } = require("../console/plain-text"); const { when: unload } = require("../system/unload"); const { format, fromException } = require("../console/traceback"); const system = require("../system"); +const memory = require('../deprecated/memory'); +const { gc: gcPromise } = require('./memory'); +const { defer } = require('../core/promise'); // Trick manifest builder to make it think we need these modules ? const unit = require("../deprecated/unit-test"); const test = require("../../test"); const url = require("../url"); +function emptyPromise() { + let { promise, resolve } = defer(); + resolve(); + return promise; +} + var cService = Cc['@mozilla.org/consoleservice;1'].getService() .QueryInterface(Ci.nsIConsoleService); // The console used to log messages var testConsole; // Cuddlefish loader in which we load and execute tests. var loader; @@ -138,67 +145,61 @@ function dictDiff(last, curr) { var result = curr[name] - (last[name] || 0); if (result) diff[name] = (result > 0 ? "+" : "") + result; } return diff; } function reportMemoryUsage() { - memory.gc(); - - var mgr = Cc["@mozilla.org/memory-reporter-manager;1"] - .getService(Ci.nsIMemoryReporterManager); - - // Bug 916501: this code is *so* bogus -- nsIMemoryReporter changed its |memoryUsed| - // field to |amount| *years* ago, and even bigger changes have happened - // since -- that it must just never be run. - var reporters = mgr.enumerateReporters(); - if (reporters.hasMoreElements()) - print("\n"); - - while (reporters.hasMoreElements()) { - var reporter = reporters.getNext(); - reporter.QueryInterface(Ci.nsIMemoryReporter); - print(reporter.description + ": " + reporter.memoryUsed + "\n"); + if (!profileMemory) { + return emptyPromise(); } - var weakrefs = [info.weakref.get() - for each (info in memory.getObjects())]; - weakrefs = [weakref for each (weakref in weakrefs) if (weakref)]; - print("Tracked memory objects in testing sandbox: " + - weakrefs.length + "\n"); + return gcPromise().then((function () { + var mgr = Cc["@mozilla.org/memory-reporter-manager;1"] + .getService(Ci.nsIMemoryReporterManager); + let count = 0; + function logReporter(process, path, kind, units, amount, description) { + print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n"); + } + mgr.getReportsForThisProcess(logReporter, null); + + var weakrefs = [info.weakref.get() + for each (info in memory.getObjects())]; + weakrefs = [weakref for each (weakref in weakrefs) if (weakref)]; + print("Tracked memory objects in testing sandbox: " + weakrefs.length + "\n"); + })); } var gWeakrefInfo; function checkMemory() { - memory.gc(); - Cu.schedulePreciseGC(function () { + return gcPromise().then(_ => { let leaks = getPotentialLeaks(); let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) { return !(url in startLeaks.compartments); }); let windowURLs = Object.keys(leaks.windows).filter(function(url) { return !(url in startLeaks.windows); }); for (let url of compartmentURLs) console.warn("LEAKED", leaks.compartments[url]); for (let url of windowURLs) console.warn("LEAKED", leaks.windows[url]); - - showResults(); - }); + }).then(showResults); } function showResults() { + let { promise, resolve } = defer(); + if (gWeakrefInfo) { gWeakrefInfo.forEach( function(info) { var ref = info.weakref.get(); if (ref !== null) { var data = ref.__url__ ? ref.__url__ : ref; var warning = data == "[object Object]" ? "[object " + data.constructor.name + "(" + @@ -206,16 +207,19 @@ function showResults() { : data; console.warn("LEAK", warning, info.bin); } } ); } onDone(results); + + resolve(); + return promise; } function cleanup() { let coverObject = {}; try { for (let name in loader.modules) memory.track(loader.modules[name], "module global scope: " + name); @@ -245,17 +249,18 @@ function cleanup() { if (typeof loader.globals.global == "object") { coverObject = loader.globals.global['__$coverObject'] || {}; } consoleListener.errorsLogged = 0; loader = null; memory.gc(); - } catch (e) { + } + catch (e) { results.failed++; console.error("unload.send() threw an exception."); console.exception(e); }; setTimeout(require('@test/options').checkMemory ? checkMemory : showResults, 1); // dump the coverobject @@ -328,34 +333,33 @@ function getPotentialLeaks() { if (matches[1] in compartments) return; let details = compartmentDetails.exec(matches[1]); if (!details) { console.error("Unable to parse compartment detail " + matches[1]); return; } - + let item = { path: matches[1], principal: details[1], location: details[2] ? details[2].replace("\\", "/", "g") : undefined, source: details[3] ? details[3].split(" -> ").reverse() : undefined, toString: function() this.location }; if (!isPossibleLeak(item)) return; compartments[matches[1]] = item; return; } - matches = windowRegexp.exec(path); - if (matches) { + if (matches = windowRegexp.exec(path)) { if (matches[1] in windows) return; let details = windowDetails.exec(matches[1]); if (!details) { console.error("Unable to parse window detail " + matches[1]); return; } @@ -369,45 +373,50 @@ function getPotentialLeaks() { if (!isPossibleLeak(item)) return; windows[matches[1]] = item; } } - let mgr = Cc["@mozilla.org/memory-reporter-manager;1"]. - getService(Ci.nsIMemoryReporterManager); - - mgr.getReportsForThisProcess(logReporter, null); + Cc["@mozilla.org/memory-reporter-manager;1"] + .getService(Ci.nsIMemoryReporterManager) + .getReportsForThisProcess(logReporter, null); return { compartments: compartments, windows: windows }; } function nextIteration(tests) { if (tests) { results.passed += tests.passed; results.failed += tests.failed; - if (profileMemory) - reportMemoryUsage(); + reportMemoryUsage().then(_ => { + let testRun = []; + for each (let test in tests.testRunSummary) { + let testCopy = {}; + for (let info in test) { + testCopy[info] = test[info]; + } + testRun.push(testCopy); + } - let testRun = []; - for each (let test in tests.testRunSummary) { - let testCopy = {}; - for (let info in test) { - testCopy[info] = test[info]; - } - testRun.push(testCopy); - } + results.testRuns.push(testRun); + iterationsLeft--; - results.testRuns.push(testRun); - iterationsLeft--; + checkForEnd(); + }) } + else { + checkForEnd(); + } +} +function checkForEnd() { if (iterationsLeft && (!stopOnError || results.failed == 0)) { // Pass the loader which has a hooked console that doesn't dispatch // errors to the JS console and avoid firing false alarm in our // console listener findAndRunTests(loader, nextIteration); } else { setTimeout(cleanup, 0);
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/lib/sdk/test/memory.js @@ -0,0 +1,17 @@ +'use strict'; + +const { Cu } = require("chrome"); +const memory = require('../deprecated/memory'); +const { defer } = require('../core/promise'); + +function gc() { + let { promise, resolve } = defer(); + + Cu.forceGC(); + memory.gc(); + + Cu.schedulePreciseGC(_ => resolve()); + + return promise; +} +exports.gc = gc;
--- a/addon-sdk/source/lib/sdk/ui/button/action.js +++ b/addon-sdk/source/lib/sdk/ui/button/action.js @@ -5,16 +5,27 @@ module.metadata = { 'stability': 'experimental', 'engines': { 'Firefox': '> 28' } }; +// Because Firefox Holly, we still need to check if `CustomizableUI` is +// available. Once Australis will officially land, we can safely remove it. +// See Bug 959142 +try { + require('chrome').Cu.import('resource:///modules/CustomizableUI.jsm', {}); +} +catch (e) { + throw Error('Unsupported Application: The module ' + module.id + + ' does not support this application.'); +} + const { Class } = require('../../core/heritage'); const { merge } = require('../../util/object'); const { Disposable } = require('../../core/disposable'); const { on, off, emit, setListeners } = require('../../event/core'); const { EventTarget } = require('../../event/target'); const view = require('./view'); const { buttonContract, stateContract } = require('./contract');
--- a/addon-sdk/source/lib/sdk/ui/button/toggle.js +++ b/addon-sdk/source/lib/sdk/ui/button/toggle.js @@ -5,16 +5,27 @@ module.metadata = { 'stability': 'experimental', 'engines': { 'Firefox': '> 28' } }; +// Because Firefox Holly, we still need to check if `CustomizableUI` is +// available. Once Australis will officially land, we can safely remove it. +// See Bug 959142 +try { + require('chrome').Cu.import('resource:///modules/CustomizableUI.jsm', {}); +} +catch (e) { + throw Error('Unsupported Application: The module ' + module.id + + ' does not support this application.'); +} + const { Class } = require('../../core/heritage'); const { merge } = require('../../util/object'); const { Disposable } = require('../../core/disposable'); const { on, off, emit, setListeners } = require('../../event/core'); const { EventTarget } = require('../../event/target'); const view = require('./view'); const { toggleButtonContract, toggleStateContract } = require('./contract');
--- a/addon-sdk/source/lib/sdk/ui/button/view.js +++ b/addon-sdk/source/lib/sdk/ui/button/view.js @@ -9,17 +9,18 @@ module.metadata = { 'Firefox': '> 28' } }; const { Cu } = require('chrome'); const { on, off, emit } = require('../../event/core'); const { id: addonID, data } = require('sdk/self'); -const buttonPrefix = 'button--' + addonID.replace(/@/g, '-at-'); +const buttonPrefix = + 'button--' + addonID.toLowerCase().replace(/[^a-z0-9-_]/g, ''); const { isObject } = require('../../lang/type'); const { getMostRecentBrowserWindow } = require('../../window/utils'); const { ignoreWindow } = require('../../private-browsing/utils'); const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {}); const { AREA_PANEL, AREA_NAVBAR } = CustomizableUI;
--- a/addon-sdk/source/lib/sdk/ui/frame.js +++ b/addon-sdk/source/lib/sdk/ui/frame.js @@ -5,12 +5,23 @@ module.metadata = { "stability": "experimental", "engines": { "Firefox": "> 28" } }; +// Because Firefox Holly, we still need to check if `CustomizableUI` is +// available. Once Australis will officially land, we can safely remove it. +// See Bug 959142 +try { + require("chrome").Cu.import("resource:///modules/CustomizableUI.jsm", {}); +} +catch (e) { + throw Error("Unsupported Application: The module" + module.id + + " does not support this application."); +} + require("./frame/view"); const { Frame } = require("./frame/model"); exports.Frame = Frame;
--- a/addon-sdk/source/lib/sdk/ui/sidebar.js +++ b/addon-sdk/source/lib/sdk/ui/sidebar.js @@ -19,32 +19,26 @@ const { URL } = require('../url'); const { add, remove, has, clear, iterator } = require('../lang/weak-set'); const { id: addonID } = require('../self'); const { WindowTracker } = require('../deprecated/window-utils'); const { isShowing } = require('./sidebar/utils'); const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = require('../window/utils'); const { ns } = require('../core/namespace'); const { remove: removeFromArray } = require('../util/array'); const { show, hide, toggle } = require('./sidebar/actions'); -const { Worker: WorkerTrait } = require('../content/worker'); +const { Worker } = require('../content/worker'); const { contract: sidebarContract } = require('./sidebar/contract'); const { create, dispose, updateTitle, updateURL, isSidebarShowing, showSidebar, hideSidebar } = require('./sidebar/view'); const { defer } = require('../core/promise'); const { models, views, viewsFor, modelFor } = require('./sidebar/namespace'); const { isLocalURL } = require('../url'); const { ensure } = require('../system/unload'); const { identify } = require('./id'); const { uuid } = require('../util/uuid'); -const Worker = WorkerTrait.resolve({ - _injectInDocument: '__injectInDocument' -}).compose({ - get _injectInDocument() true -}); - const sidebarNS = ns(); const WEB_PANEL_BROWSER_ID = 'web-panels-browser'; let sidebars = {}; const Sidebar = Class({ implements: [ Disposable ], @@ -113,17 +107,18 @@ const Sidebar = Class({ let sbTitle = window.document.getElementById('sidebar-title'); function onWebPanelSidebarCreated() { if (panelBrowser.contentWindow.location != model.url || sbTitle.value != model.title) { return; } let worker = windowNS(window).worker = Worker({ - window: panelBrowser.contentWindow + window: panelBrowser.contentWindow, + injectInDocument: true }); function onWebPanelSidebarUnload() { windowNS(window).onWebPanelSidebarUnload = null; // uncheck the associated menuitem bar.setAttribute('checked', 'false');
--- a/addon-sdk/source/lib/sdk/ui/toolbar.js +++ b/addon-sdk/source/lib/sdk/ui/toolbar.js @@ -5,12 +5,23 @@ module.metadata = { "stability": "experimental", "engines": { "Firefox": "> 28" } }; +// Because Firefox Holly, we still need to check if `CustomizableUI` is +// available. Once Australis will officially land, we can safely remove it. +// See Bug 959142 +try { + require("chrome").Cu.import("resource:///modules/CustomizableUI.jsm", {}); +} +catch (e) { + throw Error("Unsupported Application: The module" + module.id + + " does not support this application."); +} + const { Toolbar } = require("./toolbar/model"); require("./toolbar/view"); exports.Toolbar = Toolbar;
--- a/addon-sdk/source/lib/sdk/window/utils.js +++ b/addon-sdk/source/lib/sdk/window/utils.js @@ -4,17 +4,16 @@ 'use strict'; module.metadata = { 'stability': 'unstable' }; const { Cc, Ci } = require('chrome'); const array = require('../util/array'); -const observers = require('../deprecated/observer-service'); const { defer } = require('sdk/core/promise'); const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1']. getService(Ci.nsIWindowWatcher); const appShellService = Cc['@mozilla.org/appshell/appShellService;1']. getService(Ci.nsIAppShellService); const WM = Cc['@mozilla.org/appshell/window-mediator;1']. getService(Ci.nsIWindowMediator); @@ -147,35 +146,16 @@ function getWindowLoadingContext(window) QueryInterface(Ci.nsILoadContext); } exports.getWindowLoadingContext = getWindowLoadingContext; const isTopLevel = window => window && getToplevelWindow(window) === window; exports.isTopLevel = isTopLevel; /** - * Removes given window from the application's window registry. Unless - * `options.close` is `false` window is automatically closed on application - * quit. - * @params {nsIDOMWindow} window - * @params {Boolean} options.close - */ -function backgroundify(window, options) { - let base = getBaseWindow(window); - base.visibility = false; - base.enabled = false; - appShellService.unregisterTopLevelWindow(getXULWindow(window)); - if (!options || options.close !== false) - observers.add('quit-application-granted', window.close.bind(window)); - - return window; -} -exports.backgroundify = backgroundify; - -/** * Takes hash of options and serializes it to a features string that * can be used passed to `window.open`. For more details on features string see: * https://developer.mozilla.org/en/DOM/window.open#Position_and_size_features */ function serializeFeatures(options) { return Object.keys(options).reduce(function(result, name) { let value = options[name];
--- a/addon-sdk/source/lib/sdk/worker/utils.js +++ b/addon-sdk/source/lib/sdk/worker/utils.js @@ -1,103 +1,19 @@ /* 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"; +'use strict'; module.metadata = { - "stability": "unstable" + 'stability': 'deprecated' }; -// This module attempts to hide trait based nature of the worker so that -// code depending on workers could be de-trait-ified without changing worker -// implementation. - -const { Worker: WorkerTrait } = require("../content/worker"); -const { Loader } = require("../content/loader"); -const { merge } = require("../util/object"); -const { emit } = require("../event/core"); - -let assetsURI = require("../self").data.url(); -let isArray = Array.isArray; - -function isAddonContent({ contentURL }) { - return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0; -} - -function hasContentScript({ contentScript, contentScriptFile }) { - return (isArray(contentScript) ? contentScript.length > 0 : - !!contentScript) || - (isArray(contentScriptFile) ? contentScriptFile.length > 0 : - !!contentScriptFile); -} - -function requiresAddonGlobal(model) { - return isAddonContent(model) && !hasContentScript(model); -} -exports.requiresAddonGlobal = requiresAddonGlobal; - - -const LegacyWorker = WorkerTrait.compose(Loader).resolve({ - _setListeners: "__setListeners", - _injectInDocument: "__injectInDocument", - contentURL: "__contentURL" -}).compose({ - _setListeners: function() {}, - get contentURL() this._window.document.URL, - get _injectInDocument() requiresAddonGlobal(this), - attach: function(window) this._attach(window), - detach: function() this._workerCleanup() -}); - -// Weak map that stores mapping between regular worker instances and -// legacy trait based worker instances. -let traits = new WeakMap(); - -function traitFor(worker) traits.get(worker, null); +const { + requiresAddonGlobal, attach, detach, destroy, WorkerHost +} = require('../content/utils'); -function WorkerHost(workerFor) { - // Define worker properties that just proxy to a wrapped trait. - return ["postMessage", "port", "url", "tab"].reduce(function(proto, name) { - Object.defineProperty(proto, name, { - enumerable: true, - configurable: false, - get: function() traitFor(workerFor(this))[name], - set: function(value) traitFor(workerFor(this))[name] = value - }); - return proto; - }, {}); -} exports.WorkerHost = WorkerHost; - -// Type representing worker instance. -function Worker(options) { - let worker = Object.create(Worker.prototype); - let trait = new LegacyWorker(options); - ["pageshow", "pagehide", "detach", "message", "error"].forEach(function(key) { - trait.on(key, function() { - emit.apply(emit, [worker, key].concat(Array.slice(arguments))); - }); - }); - traits.set(worker, trait); - return worker; -} -exports.Worker = Worker; - -function detach(worker) { - let trait = traitFor(worker); - if (trait) trait.detach(); -} exports.detach = detach; - -function attach(worker, window) { - let trait = traitFor(worker); - // Cleanup the worker before injecting the content script into a new document. - trait.attach(window); -} exports.attach = attach; - -function destroy(worker) { - let trait = traitFor(worker); - if (trait) trait.destroy(); -} exports.destroy = destroy; +exports.requiresAddonGlobal = requiresAddonGlobal;
--- a/addon-sdk/source/lib/toolkit/loader.js +++ b/addon-sdk/source/lib/toolkit/loader.js @@ -608,18 +608,30 @@ const Require = iced(function Require(lo throw err; uri = uri + '.js'; } } // If not yet cached, load and cache it. // We also freeze module to prevent it from further changes // at runtime. if (!(uri in modules)) { + // Many of the loader's functionalities are dependent + // on modules[uri] being set before loading, so we set it and + // remove it if we have any errors. module = modules[uri] = Module(requirement, uri); - freeze(load(loader, module)); + try { + freeze(load(loader, module)); + } + catch (e) { + // Clear out modules cache so we can throw on a second invalid require + delete modules[uri]; + // Also clear out the Sandbox that was created + delete loader.sandboxes[uri]; + throw e; + } } return module.exports; } // Make `require.main === module` evaluate to true in main module scope. require.main = loader.main === requirer ? requirer : undefined; return iced(require); });
--- a/addon-sdk/source/mapping.json +++ b/addon-sdk/source/mapping.json @@ -10,17 +10,16 @@ "l10n/html": "sdk/l10n/html", "l10n/loader": "sdk/l10n/loader", "l10n/locale": "sdk/l10n/locale", "l10n/prefs": "sdk/l10n/prefs", "list": "sdk/util/list", "loader": "sdk/loader/loader", "memory": "sdk/deprecated/memory", "namespace": "sdk/core/namespace", - "observer-service": "sdk/deprecated/observer-service", "preferences-service": "sdk/preferences/service", "promise": "sdk/core/promise", "system": "sdk/system", "system/events": "sdk/system/events", "tabs/tab": "sdk/tabs/tab", "tabs/utils": "sdk/tabs/utils", "timer": "sdk/timers", "traits": "sdk/deprecated/traits",
--- a/addon-sdk/source/test/addons/layout-change/main.js +++ b/addon-sdk/source/test/addons/layout-change/main.js @@ -91,19 +91,16 @@ exports["test compatibility"] = function require("sdk/page-worker"), "sdk/page-worker -> page-worker"); assert.equal(require("timer"), require("sdk/timers"), "sdk/timers -> timer"); assert.equal(require("xhr"), require("sdk/net/xhr"), "sdk/io/xhr -> xhr"); - assert.equal(require("observer-service"), - require("sdk/deprecated/observer-service"), "sdk/deprecated/observer-service -> observer-service"); - assert.equal(require("private-browsing"), require("sdk/private-browsing"), "sdk/private-browsing -> private-browsing"); assert.equal(require("passwords"), require("sdk/passwords"), "sdk/passwords -> passwords"); assert.equal(require("events"), require("sdk/deprecated/events"), "sdk/deprecated/events -> events"); @@ -142,19 +139,16 @@ exports["test compatibility"] = function require("sdk/querystring"), "sdk/querystring -> querystring"); assert.equal(loader.require("addon-page"), loader.require("sdk/addon-page"), "sdk/addon-page -> addon-page"); assert.equal(require("tabs/utils"), require("sdk/tabs/utils"), "sdk/tabs/utils -> tabs/utils"); - assert.equal(require("app-strings"), - require("sdk/deprecated/app-strings"), "sdk/deprecated/app-strings -> app-strings"); - assert.equal(require("dom/events"), require("sdk/dom/events"), "sdk/dom/events -> dom/events"); assert.equal(require("tabs/tab.js"), require("sdk/tabs/tab"), "sdk/tabs/tab -> tabs/tab.js"); assert.equal(require("memory"), require("sdk/deprecated/memory"), "sdk/deprecated/memory -> memory");
--- a/addon-sdk/source/test/addons/symbiont/main.js +++ b/addon-sdk/source/test/addons/symbiont/main.js @@ -1,16 +1,16 @@ /* 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 { data } = require("sdk/self"); -const { Symbiont } = require("sdk/content/symbiont"); +const { Symbiont } = require("sdk/deprecated/symbiont"); exports["test:direct communication with trusted document"] = function(assert, done) { let worker = Symbiont({ contentURL: data.url("test-trusted-document.html") }); worker.port.on('document-to-addon', function (arg) { assert.equal(arg, "ok", "Received an event from the document");
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/fixtures/loader/missing-twice/file.json @@ -0,0 +1,1 @@ +an invalid json file
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/fixtures/loader/missing-twice/main.js @@ -0,0 +1,32 @@ +/* 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'; + +try { + require('./not-found'); +} +catch (e1) { + exports.firstError = e1; + // It should throw again and not be cached + try { + require('./not-found'); + } + catch (e2) { + exports.secondError = e2; + } +} + +try { + require('./file.json'); +} +catch (e) { + exports.invalidJSON1 = e; + try { + require('./file.json'); + } + catch (e) { + exports.invalidJSON2 = e; + } +}
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js +++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js @@ -4,33 +4,36 @@ 'use strict'; const { Cc, Ci } = require('chrome'); const { Loader } = require('sdk/test/loader'); const timer = require('sdk/timers'); const { getOwnerWindow } = require('sdk/private-browsing/window/utils'); const { windows, onFocus, getMostRecentBrowserWindow } = require('sdk/window/utils'); const { open, focus, close } = require('sdk/window/helpers'); -const { StringBundle } = require('sdk/deprecated/app-strings'); const tabs = require('sdk/tabs'); const { browserWindows } = require('sdk/windows'); const { set: setPref } = require("sdk/preferences/service"); const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings"; const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAA"; // Bug 682681 - tab.title should never be empty exports.testBug682681_aboutURI = function(assert, done) { - let tabStrings = StringBundle('chrome://browser/locale/tabbrowser.properties'); + let url = 'chrome://browser/locale/tabbrowser.properties'; + let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(url); + let emptyTabTitle = stringBundle.GetStringFromName('tabs.emptyTabTitle'); tabs.on('ready', function onReady(tab) { tabs.removeListener('ready', onReady); assert.equal(tab.title, - tabStrings.get('tabs.emptyTabTitle'), + emptyTabTitle, "title of about: tab is not blank"); tab.close(done); }); // open a about: url tabs.open({ url: "about:blank",
--- a/addon-sdk/source/test/test-addon-installer.js +++ b/addon-sdk/source/test/test-addon-installer.js @@ -1,56 +1,55 @@ /* 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 { Cc, Ci, Cu } = require("chrome"); const AddonInstaller = require("sdk/addon/installer"); -const observers = require("sdk/deprecated/observer-service"); +const { on, off } = require("sdk/system/events"); const { setTimeout } = require("sdk/timers"); const tmp = require("sdk/test/tmp-file"); const system = require("sdk/system"); const fixtures = require("./fixtures"); const testFolderURL = module.uri.split('test-addon-installer.js')[0]; const ADDON_URL = testFolderURL + "fixtures/addon-install-unit-test@mozilla.com.xpi"; const ADDON_PATH = tmp.createFromURL(ADDON_URL); exports["test Install"] = function (assert, done) { // Save all events distpatched by bootstrap.js of the installed addon let events = []; - function eventsObserver(subject, data) { + function eventsObserver({ data }) { events.push(data); } - observers.add("addon-install-unit-test", eventsObserver, false); + on("addon-install-unit-test", eventsObserver); // Install the test addon AddonInstaller.install(ADDON_PATH).then( function onInstalled(id) { assert.equal(id, "addon-install-unit-test@mozilla.com", "`id` is valid"); // Now uninstall it AddonInstaller.uninstall(id).then(function () { // Ensure that bootstrap.js methods of the addon have been called // successfully and in the right order let expectedEvents = ["install", "startup", "shutdown", "uninstall"]; assert.equal(JSON.stringify(events), JSON.stringify(expectedEvents), "addon's bootstrap.js functions have been called"); - observers.remove("addon-install-unit-test", eventsObserver); + off("addon-install-unit-test", eventsObserver); done(); }); }, function onFailure(code) { assert.fail("Install failed: "+code); - observers.remove("addon-install-unit-test", eventsObserver); + off("addon-install-unit-test", eventsObserver); done(); } ); }; exports["test Failing Install With Invalid Path"] = function (assert, done) { AddonInstaller.install("invalid-path").then( function onInstalled(id) { @@ -79,20 +78,18 @@ exports["test Failing Install With Inval } ); } exports["test Update"] = function (assert, done) { // Save all events distpatched by bootstrap.js of the installed addon let events = []; let iteration = 1; - function eventsObserver(subject, data) { - events.push(data); - } - observers.add("addon-install-unit-test", eventsObserver); + let eventsObserver = ({data}) => events.push(data); + on("addon-install-unit-test", eventsObserver); function onInstalled(id) { let prefix = "[" + iteration + "] "; assert.equal(id, "addon-install-unit-test@mozilla.com", prefix + "`id` is valid"); // On 2nd and 3rd iteration, we receive uninstall events from the last // previously installed addon @@ -110,24 +107,24 @@ exports["test Update"] = function (asser else { events = []; AddonInstaller.uninstall(id).then(function() { let expectedEvents = ["shutdown", "uninstall"]; assert.equal(JSON.stringify(events), JSON.stringify(expectedEvents), prefix + "addon's bootstrap.js functions have been called"); - observers.remove("addon-install-unit-test", eventsObserver); + off("addon-install-unit-test", eventsObserver); done(); }); } } function onFailure(code) { assert.fail("Install failed: "+code); - observers.remove("addon-install-unit-test", eventsObserver); + off("addon-install-unit-test", eventsObserver); done(); } function next() { events = []; AddonInstaller.install(ADDON_PATH).then(onInstalled, onFailure); }
--- a/addon-sdk/source/test/test-api-utils.js +++ b/addon-sdk/source/test/test-api-utils.js @@ -1,39 +1,14 @@ /* 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/. */ const apiUtils = require("sdk/deprecated/api-utils"); -exports.testPublicConstructor = function (assert) { - function PrivateCtor() {} - PrivateCtor.prototype = {}; - - let PublicCtor = apiUtils.publicConstructor(PrivateCtor); - assert.ok( - PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype), - "PrivateCtor.prototype should be prototype of PublicCtor.prototype" - ); - - function testObj(useNew) { - let obj = useNew ? new PublicCtor() : PublicCtor(); - assert.ok(obj instanceof PublicCtor, - "Object should be instance of PublicCtor"); - assert.ok(obj instanceof PrivateCtor, - "Object should be instance of PrivateCtor"); - assert.ok(PublicCtor.prototype.isPrototypeOf(obj), - "PublicCtor's prototype should be prototype of object"); - assert.equal(obj.constructor, PublicCtor, - "Object constructor should be PublicCtor"); - } - testObj(true); - testObj(false); -}; - exports.testValidateOptionsEmpty = function (assert) { let val = apiUtils.validateOptions(null, {}); assert.deepEqual(val, {}); val = apiUtils.validateOptions(null, { foo: {} }); assert.deepEqual(val, {});
deleted file mode 100644 --- a/addon-sdk/source/test/test-app-strings.js +++ /dev/null @@ -1,68 +0,0 @@ -/* 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 { Cc, Ci } = require("chrome"); -const { StringBundle } = require("sdk/deprecated/app-strings"); - -exports.testStringBundle = function(assert) { - let url = "chrome://global/locale/security/caps.properties"; - - let strings = StringBundle(url); - - assert.equal(strings.url, url, - "'url' property contains correct URL of string bundle"); - - let appLocale = Cc["@mozilla.org/intl/nslocaleservice;1"]. - getService(Ci.nsILocaleService). - getApplicationLocale(); - - let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. - getService(Ci.nsIStringBundleService). - createBundle(url, appLocale); - - let (name = "CheckMessage") { - assert.equal(strings.get(name), stringBundle.GetStringFromName(name), - "getting a string returns the string"); - } - - let (name = "CreateWrapperDenied", args = ["foo"]) { - assert.equal(strings.get(name, args), - stringBundle.formatStringFromName(name, args, args.length), - "getting a formatted string returns the formatted string"); - } - - assert.throws(function () strings.get("nonexistentString"), - RegExp("String 'nonexistentString' could not be retrieved from " + - "the bundle due to an unknown error \\(it doesn't exist\\?\\)\\."), - "retrieving a nonexistent string throws an exception"); - - let a = [], b = []; - let enumerator = stringBundle.getSimpleEnumeration(); - while (enumerator.hasMoreElements()) { - let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); - a.push([elem.key, elem.value]); - } - - for (let key in strings) { - b.push([ key, strings.get(key) ]); - } - - // Sort the arrays, because we don't assume enumeration has a set order. - // Sort compares [key, val] as string "key,val", which sorts the way we want - // it to, so there is no need to provide a custom compare function. - a.sort(); - b.sort(); - - assert.equal(a.length, b.length, - "the iterator returns the correct number of items"); - - for (let i = 0; i < a.length; i++) { - assert.equal(a[i][0], b[i][0], "the iterated string's name is correct"); - assert.equal(a[i][1], b[i][1], - "the iterated string's value is correct"); - } -}; - -require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-content-symbiont.js +++ b/addon-sdk/source/test/test-content-symbiont.js @@ -1,15 +1,15 @@ /* 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 { Cc, Ci } = require('chrome'); -const { Symbiont } = require('sdk/content/symbiont'); +const { Symbiont } = require('sdk/deprecated/symbiont'); const self = require('sdk/self'); const fixtures = require("./fixtures"); const { close } = require('sdk/window/helpers'); const app = require("sdk/system/xul-app"); function makeWindow() { let content = '<?xml version="1.0"?>' +
--- a/addon-sdk/source/test/test-content-worker.js +++ b/addon-sdk/source/test/test-content-worker.js @@ -7,16 +7,17 @@ // Skipping due to window creation being unsupported in Fennec module.metadata = { engines: { 'Firefox': '*' } }; const { Cc, Ci } = require("chrome"); +const { on } = require("sdk/event/core"); const { setTimeout } = require("sdk/timers"); const { LoaderWithHookedConsole } = require("sdk/test/loader"); const { Worker } = require("sdk/content/worker"); const { close } = require("sdk/window/helpers"); const { set: setPref } = require("sdk/preferences/service"); const { isArray } = require("sdk/lang/type"); const { URL } = require('sdk/url'); const fixtures = require("./fixtures"); @@ -111,16 +112,18 @@ exports["test:sample"] = WorkerTest( assert.equal(worker.url, window.location.href, "worker.url still works"); done(); } }); assert.equal(worker.url, window.location.href, "worker.url works"); + assert.equal(worker.contentURL, window.location.href, + "worker.contentURL works"); worker.postMessage("hi!"); } ); exports["test:emit"] = WorkerTest( DEFAULT_CONTENT_URL, function(assert, browser, done) { @@ -221,17 +224,17 @@ exports["test:post-json-values-only"] = let worker = Worker({ window: browser.contentWindow, contentScript: "new " + function WorkerScope() { self.on("message", function (message) { self.postMessage([ message.fun === undefined, typeof message.w, message.w && "port" in message.w, - message.w.url, + message.w._url, Array.isArray(message.array), JSON.stringify(message.array)]); }); } }); // Validate worker.onMessage let array = [1, 2, 3]; @@ -242,16 +245,20 @@ exports["test:post-json-values-only"] = assert.equal(message[3], DEFAULT_CONTENT_URL, "jsonable attributes are accessible"); // See bug 714891, Arrays may be broken over compartements: assert.ok(message[4], "Array keeps being an array"); assert.equal(message[5], JSON.stringify(array), "Array is correctly serialized"); done(); }); + // Add a new url property sa the Class function used by + // Worker doesn't set enumerables to true for non-functions + worker._url = DEFAULT_CONTENT_URL; + worker.postMessage({ fun: function () {}, w: worker, array: array }); } ); exports["test:emit-json-values-only"] = WorkerTest( DEFAULT_CONTENT_URL, function(assert, browser, done) { @@ -259,17 +266,17 @@ exports["test:emit-json-values-only"] = window: browser.contentWindow, contentScript: "new " + function WorkerScope() { // Validate self.on and self.emit self.port.on("addon-to-content", function (fun, w, obj, array) { self.port.emit("content-to-addon", [ fun === null, typeof w, "port" in w, - w.url, + w._url, "fun" in obj, Object.keys(obj.dom).length, Array.isArray(array), JSON.stringify(array) ]); }); } }); @@ -290,16 +297,19 @@ exports["test:emit-json-values-only"] = "Array is correctly serialized"); done(); }); let obj = { fun: function () {}, dom: browser.contentWindow.document.createElement("div") }; + // Add a new url property sa the Class function used by + // Worker doesn't set enumerables to true for non-functions + worker._url = DEFAULT_CONTENT_URL; worker.port.emit("addon-to-content", function () {}, worker, obj, array); } ); exports["test:content is wrapped"] = WorkerTest( "data:text/html;charset=utf-8,<script>var documentValue=true;</script>", function(assert, browser, done) { @@ -824,9 +834,42 @@ exports['test:conentScriptFile as URL in assert.equal(msg, "msg from contentScriptFile", "received a wrong message from contentScriptFile"); done(); } }); } ); +exports.testWorkerEvents = WorkerTest(DEFAULT_CONTENT_URL, function (assert, browser, done) { + let window = browser.contentWindow; + let events = []; + let worker = Worker({ + window: window, + contentScript: 'new ' + function WorkerScope() { + self.postMessage('start'); + }, + onAttach: win => { + events.push('attach'); + assert.pass('attach event called when attached'); + assert.equal(window, win, 'attach event passes in attached window'); + }, + onError: err => { + assert.equal(err.message, 'Custom', + 'Error passed into error event'); + worker.detach(); + }, + onMessage: msg => { + assert.pass('`onMessage` handles postMessage') + throw new Error('Custom'); + }, + onDetach: _ => { + assert.pass('`onDetach` called when worker detached'); + done(); + } + }); + // `attach` event is called synchronously during instantiation, + // so we can't listen to that, TODO FIX? + // worker.on('attach', obj => console.log('attach', obj)); +}); + + require("test").run(exports);
--- a/addon-sdk/source/test/test-event-core.js +++ b/addon-sdk/source/test/test-event-core.js @@ -1,15 +1,15 @@ /* 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 { on, once, off, emit, count, amass } = require('sdk/event/core'); +const { on, once, off, emit, count } = require('sdk/event/core'); const { LoaderWithHookedConsole } = require("sdk/test/loader"); exports['test add a listener'] = function(assert) { let events = [ { name: 'event#1' }, 'event#2' ]; let target = { name: 'target' }; on(target, 'message', function(message) { assert.equal(this, target, 'this is a target object');
--- a/addon-sdk/source/test/test-event-utils.js +++ b/addon-sdk/source/test/test-event-utils.js @@ -1,16 +1,16 @@ /* 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 { on, emit } = require("sdk/event/core"); -const { filter, map, merge, expand, pipe } = require("sdk/event/utils"); +const { filter, map, merge, expand, pipe, stripListeners } = require("sdk/event/utils"); const $ = require("./event/helpers"); function isEven(x) !(x % 2) function inc(x) x + 1 exports["test filter events"] = function(assert) { let input = {}; let evens = filter(input, isEven); @@ -163,55 +163,55 @@ exports["test expand"] = function(assert assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"], "all inputs data merged into one"); }; exports["test pipe"] = function (assert, done) { let src = {}; let dest = {}; - + let aneventCount = 0, multiargsCount = 0; let wildcardCount = {}; on(dest, 'an-event', arg => { assert.equal(arg, 'my-arg', 'piped argument to event'); ++aneventCount; check(); }); on(dest, 'multiargs', (...data) => { assert.equal(data[0], 'a', 'multiple arguments passed via pipe'); assert.equal(data[1], 'b', 'multiple arguments passed via pipe'); assert.equal(data[2], 'c', 'multiple arguments passed via pipe'); ++multiargsCount; check(); }); - + on(dest, '*', (name, ...data) => { wildcardCount[name] = (wildcardCount[name] || 0) + 1; if (name === 'multiargs') { assert.equal(data[0], 'a', 'multiple arguments passed via pipe, wildcard'); assert.equal(data[1], 'b', 'multiple arguments passed via pipe, wildcard'); assert.equal(data[2], 'c', 'multiple arguments passed via pipe, wildcard'); } if (name === 'an-event') assert.equal(data[0], 'my-arg', 'argument passed via pipe, wildcard'); check(); }); pipe(src, dest); for (let i = 0; i < 3; i++) emit(src, 'an-event', 'my-arg'); - + emit(src, 'multiargs', 'a', 'b', 'c'); function check () { if (aneventCount === 3 && multiargsCount === 1 && - wildcardCount['an-event'] === 3 && + wildcardCount['an-event'] === 3 && wildcardCount['multiargs'] === 1) done(); } }; exports["test pipe multiple targets"] = function (assert) { let src1 = {}; let src2 = {}; @@ -232,27 +232,51 @@ exports["test pipe multiple targets"] = on(middle, '*', () => middleFired++); on(dest, '*', () => destFired++); emit(src1, 'ev'); assert.equal(src1Fired, 1, 'event triggers in source in pipe chain'); assert.equal(middleFired, 1, 'event passes through the middle of pipe chain'); assert.equal(destFired, 1, 'event propagates to end of pipe chain'); assert.equal(src2Fired, 0, 'event does not fire on alternative chain routes'); - + emit(src2, 'ev'); assert.equal(src2Fired, 1, 'event triggers in source in pipe chain'); assert.equal(middleFired, 2, 'event passes through the middle of pipe chain from different src'); assert.equal(destFired, 2, 'event propagates to end of pipe chain from different src'); assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes'); - + emit(middle, 'ev'); assert.equal(middleFired, 3, 'event triggers in source of pipe chain'); assert.equal(destFired, 3, 'event propagates to end of pipe chain from middle src'); assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes'); assert.equal(src2Fired, 1, 'event does not fire on alternative chain routes'); }; +exports['test stripListeners'] = function (assert) { + var options = { + onAnEvent: noop1, + onMessage: noop2, + verb: noop1, + value: 100 + }; + + var stripped = stripListeners(options); + assert.ok(stripped !== options, 'stripListeners should return a new object'); + assert.equal(options.onAnEvent, noop1, 'stripListeners does not affect original'); + assert.equal(options.onMessage, noop2, 'stripListeners does not affect original'); + assert.equal(options.verb, noop1, 'stripListeners does not affect original'); + assert.equal(options.value, 100, 'stripListeners does not affect original'); + + assert.ok(!stripped.onAnEvent, 'stripListeners removes `on*` values'); + assert.ok(!stripped.onMessage, 'stripListeners removes `on*` values'); + assert.equal(stripped.verb, noop1, 'stripListeners leaves not `on*` values'); + assert.equal(stripped.value, 100, 'stripListeners leaves not `on*` values'); + + function noop1 () {} + function noop2 () {} +}; + require('test').run(exports);
--- a/addon-sdk/source/test/test-loader.js +++ b/addon-sdk/source/test/test-loader.js @@ -96,16 +96,23 @@ exports['test syntax errors'] = function assert.equal(stack.pop().fileName, module.uri, "previous to it is a test module"); } finally { unload(loader); } } +exports['test sandboxes are not added if error'] = function (assert) { + let uri = root + '/fixtures/loader/missing-twice/'; + let loader = Loader({ paths: { '': uri } }); + let program = main(loader, 'main'); + assert.ok(!(uri + 'not-found.js' in loader.sandboxes), 'not-found.js not in loader.sandboxes'); +} + exports['test missing module'] = function(assert) { let uri = root + '/fixtures/loader/missing/' let loader = Loader({ paths: { '': uri } }); try { let program = main(loader, 'main') } catch (error) { assert.equal(error.message, "Module `not-found` is not found at " + @@ -123,16 +130,36 @@ exports['test missing module'] = functio assert.equal(stack.pop().fileName, module.uri, "previous in the stack is test module"); } finally { unload(loader); } } +exports["test invalid module not cached and throws everytime"] = function(assert) { + let uri = root + "/fixtures/loader/missing-twice/"; + let loader = Loader({ paths: { "": uri } }); + + let { firstError, secondError, invalidJSON1, invalidJSON2 } = main(loader, "main"); + assert.equal(firstError.message, "Module `not-found` is not found at " + + uri + "not-found.js", "throws on first invalid require"); + assert.equal(firstError.lineNumber, 8, "first error is on line 7"); + assert.equal(secondError.message, "Module `not-found` is not found at " + + uri + "not-found.js", "throws on second invalid require"); + assert.equal(secondError.lineNumber, 14, "second error is on line 14"); + + assert.equal(invalidJSON1.message, + "JSON.parse: unexpected character at line 1 column 1 of the JSON data", + "throws on invalid JSON"); + assert.equal(invalidJSON2.message, + "JSON.parse: unexpected character at line 1 column 1 of the JSON data", + "throws on invalid JSON second time"); +}; + exports['test exceptions in modules'] = function(assert) { let uri = root + '/fixtures/loader/exceptions/' let loader = Loader({ paths: { '': uri } }); try { let program = main(loader, 'main') } catch (error) {
--- a/addon-sdk/source/test/test-memory.js +++ b/addon-sdk/source/test/test-memory.js @@ -1,21 +1,22 @@ /* 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"; -var memory = require("sdk/deprecated/memory"); +const memory = require("sdk/deprecated/memory"); +const { gc } = require("sdk/test/memory"); exports.testMemory = function(assert) { - assert.pass("Skipping this test until Gecko memory debugging issues " + - "are resolved (see bug 592774)."); - return; - var obj = {}; memory.track(obj, "testMemory.testObj"); + var objs = memory.getObjects("testMemory.testObj"); assert.equal(objs[0].weakref.get(), obj); obj = null; - memory.gc(); - assert.equal(objs[0].weakref.get(), null); + + gc().then(function() { + assert.equal(objs[0].weakref.get(), null); + }); }; require('sdk/test').run(exports);
deleted file mode 100644 --- a/addon-sdk/source/test/test-observer-service.js +++ /dev/null @@ -1,79 +0,0 @@ -/* 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/. */ - -const observers = require("sdk/deprecated/observer-service"); -const { Cc, Ci } = require("chrome"); -const { LoaderWithHookedConsole2 } = require("sdk/test/loader"); - -exports.testUnloadAndErrorLogging = function(assert) { - let { loader, messages } = LoaderWithHookedConsole2(module); - var sbobsvc = loader.require("sdk/deprecated/observer-service"); - - var timesCalled = 0; - var cb = function(subject, data) { - timesCalled++; - }; - var badCb = function(subject, data) { - throw new Error("foo"); - }; - sbobsvc.add("blarg", cb); - observers.notify("blarg", "yo yo"); - assert.equal(timesCalled, 1); - sbobsvc.add("narg", badCb); - observers.notify("narg", "yo yo"); - - assert.equal(messages[0], "console.error: " + require("sdk/self").name + ": \n"); - var lines = messages[1].split("\n"); - assert.equal(lines[0], " Message: Error: foo"); - assert.equal(lines[1], " Stack:"); - // Keep in mind to update "18" to the line of "throw new Error("foo")" - assert.ok(lines[2].indexOf(module.uri + ":18") != -1); - - loader.unload(); - observers.notify("blarg", "yo yo"); - assert.equal(timesCalled, 1); -}; - -exports.testObserverService = function(assert) { - var ios = Cc['@mozilla.org/network/io-service;1'] - .getService(Ci.nsIIOService); - var service = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - var uri = ios.newURI("http://www.foo.com", null, null); - var timesCalled = 0; - var lastSubject = null; - var lastData = null; - - var cb = function(subject, data) { - timesCalled++; - lastSubject = subject; - lastData = data; - }; - - observers.add("blarg", cb); - service.notifyObservers(uri, "blarg", "some data"); - assert.equal(timesCalled, 1, - "observer-service.add() should call callback"); - assert.equal(lastSubject, uri, - "observer-service.add() should pass subject"); - assert.equal(lastData, "some data", - "observer-service.add() should pass data"); - - function customSubject() {} - function customData() {} - observers.notify("blarg", customSubject, customData); - assert.equal(timesCalled, 2, - "observer-service.notify() should work"); - assert.equal(lastSubject, customSubject, - "observer-service.notify() should pass+wrap subject"); - assert.equal(lastData, customData, - "observer-service.notify() should pass data"); - - observers.remove("blarg", cb); - service.notifyObservers(null, "blarg", "some data"); - assert.equal(timesCalled, 2, - "observer-service.remove() should work"); -}; - -require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-simple-prefs.js +++ b/addon-sdk/source/test/test-simple-prefs.js @@ -1,16 +1,16 @@ /* 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 { Loader } = require("sdk/test/loader"); const { setTimeout } = require("sdk/timers"); -const { notify } = require("sdk/deprecated/observer-service"); +const { emit } = require("sdk/system/events"); const { id } = require("sdk/self"); const simplePrefs = require("sdk/simple-prefs"); const { prefs: sp } = simplePrefs; const specialChars = "!@#$%^&*()_-=+[]{}~`\'\"<>,./?;:"; exports.testIterations = function(assert) { sp["test"] = true; @@ -127,17 +127,17 @@ exports.testPrefListener = function(asse exports.testBtnListener = function(assert, done) { let name = "test-btn-listen"; simplePrefs.on(name, function listener() { simplePrefs.removeListener(name, listener); assert.pass("Button press event was heard"); done(); }); - notify((id + "-cmdPressed"), "", name); + emit((id + "-cmdPressed"), { subject: "", data: name }); }; exports.testPrefRemoveListener = function(assert, done) { let counter = 0; let listener = function() { assert.pass("The prefs listener was not removed yet");
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/test-test-memory.js @@ -0,0 +1,23 @@ +/* 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 { Cc, Ci, Cu, components } = require('chrome'); +const { gc } = require('sdk/test/memory'); + +exports.testGC = function(assert, done) { + let weakref; + let (tempObj = {}) { + weakref = Cu.getWeakReference(tempObj); + assert.equal(weakref.get(), tempObj, 'the weakref returned the tempObj'); + } + + gc().then(function(arg) { + assert.equal(arg, undefined, 'there is no argument'); + assert.pass('gc() returns a promise which eventually resolves'); + assert.equal(weakref.get(), undefined, 'the weakref returned undefined'); + }).then(done).then(null, assert.fail); +}; + +require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-traceback.js +++ b/addon-sdk/source/test/test-traceback.js @@ -1,46 +1,47 @@ /* 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"; var traceback = require("sdk/console/traceback"); var {Cc,Ci,Cr,Cu} = require("chrome"); +const { on, off } = require("sdk/system/events"); function throwNsIException() { var ios = Cc['@mozilla.org/network/io-service;1'] .getService(Ci.nsIIOService); ios.newURI("i'm a malformed URI", null, null); } function throwError() { throw new Error("foob"); } exports.testFormatDoesNotFetchRemoteFiles = function(assert) { - var observers = require("sdk/deprecated/observer-service"); ["http", "https"].forEach( function(scheme) { var httpRequests = 0; function onHttp() { httpRequests++; } - observers.add("http-on-modify-request", onHttp); + on("http-on-modify-request", onHttp); try { var tb = [{filename: scheme + "://www.mozilla.org/", lineNumber: 1, name: "blah"}]; traceback.format(tb); } catch (e) { assert.fail(e); } - observers.remove("http-on-modify-request", onHttp); + off("http-on-modify-request", onHttp); assert.equal(httpRequests, 0, "traceback.format() does not make " + scheme + " request"); }); }; exports.testFromExceptionWithString = function(assert) {
--- a/addon-sdk/source/test/test-window-utils2.js +++ b/addon-sdk/source/test/test-window-utils2.js @@ -6,17 +6,17 @@ // Opening new windows in Fennec causes issues module.metadata = { engines: { 'Firefox': '*' } }; const { Ci } = require('chrome'); -const { open, backgroundify, windows, isBrowser, +const { open, windows, isBrowser, getXULWindow, getBaseWindow, getToplevelWindow, getMostRecentWindow, getMostRecentBrowserWindow } = require('sdk/window/utils'); const { close } = require('sdk/window/helpers'); const windowUtils = require('sdk/deprecated/window-utils'); exports['test get nsIBaseWindow from nsIDomWindow'] = function(assert) { let active = windowUtils.activeBrowserWindow; @@ -73,50 +73,34 @@ exports['test new top window with variou assert.throws(function () { open('foo'); }, msg); assert.throws(function () { open('http://foo'); }, msg); assert.throws(function () { open('https://foo'); - }, msg); + }, msg); assert.throws(function () { open('ftp://foo'); }, msg); assert.throws(function () { open('//foo'); }, msg); let chromeWindow = open('chrome://foo/content/'); assert.ok(~windows().indexOf(chromeWindow), 'chrome URI works'); - + let resourceWindow = open('resource://foo'); assert.ok(~windows().indexOf(resourceWindow), 'resource URI works'); // Wait for the window unload before ending test close(chromeWindow).then(close.bind(null, resourceWindow)).then(done); }; -exports.testBackgroundify = function(assert, done) { - let window = open('data:text/html;charset=utf-8,backgroundy'); - assert.ok(~windows().indexOf(window), - 'window is in the list of windows'); - let backgroundy = backgroundify(window); - assert.equal(backgroundy, window, 'backgroundify returs give window back'); - assert.ok(!~windows().indexOf(window), - 'backgroundifyied window is in the list of windows'); - - // Wait for the window unload before ending test - // backgroundified windows doesn't dispatch domwindowclosed event - // so that we have to manually wait for unload event - window.onunload = done; - window.close(); -}; - exports.testIsBrowser = function(assert) { // dummy window, bad type assert.equal(isBrowser({ document: { documentElement: { getAttribute: function() { return 'navigator:browserx'; }}}}), false, 'dummy object with correct stucture and bad type does not pass'); assert.ok(isBrowser(getMostRecentBrowserWindow()), 'active browser window is a browser window'); assert.ok(!isBrowser({}), 'non window is not a browser window');
--- a/addon-sdk/source/test/test-windows-common.js +++ b/addon-sdk/source/test/test-windows-common.js @@ -1,20 +1,16 @@ /* 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 { Loader } = require('sdk/test/loader'); const { browserWindows } = require('sdk/windows'); -const { viewFor } = require('sdk/view/core'); const { Ci } = require("chrome"); -const { isBrowser, getWindowTitle } = require("sdk/window/utils"); -const { defer } = require("sdk/lang/functional"); - // TEST: browserWindows Iterator exports.testBrowserWindowsIterator = function(assert) { let activeWindowCount = 0; let windows = []; let i = 0; for each (let window in browserWindows) { if (window === browserWindows.activeWindow) @@ -56,31 +52,9 @@ exports.testWindowActivateMethod_simple window.activate(); assert.equal(browserWindows.activeWindow, window, 'Active window is active after window.activate() call'); assert.equal(window.tabs.activeTab, tab, 'Active tab is active after window.activate() call'); }; - -exports["test getView(window)"] = function(assert, done) { - browserWindows.once("open", window => { - const view = viewFor(window); - - assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window"); - assert.ok(isBrowser(view), "view is a browser window"); - assert.equal(getWindowTitle(view), window.title, - "window has a right title"); - - window.close(); - // Defer handler cause window is destroyed after event is dispatched. - browserWindows.once("close", defer(_ => { - assert.equal(viewFor(window), null, "window view is gone"); - done(); - })); - }); - - - browserWindows.open({ url: "data:text/html,<title>yo</title>" }); -}; - require('sdk/test').run(exports);
--- a/addon-sdk/source/test/windows/test-firefox-windows.js +++ b/addon-sdk/source/test/windows/test-firefox-windows.js @@ -1,24 +1,26 @@ /* 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 { Cc, Ci } = require('chrome'); const { setTimeout } = require('sdk/timers'); const { Loader } = require('sdk/test/loader'); -const { onFocus, getMostRecentWindow, windows } = require('sdk/window/utils'); +const { onFocus, getMostRecentWindow, windows, isBrowser, getWindowTitle } = require('sdk/window/utils'); const { open, close, focus } = require('sdk/window/helpers'); const { browserWindows } = require("sdk/windows"); const tabs = require("sdk/tabs"); const winUtils = require("sdk/deprecated/window-utils"); const { WindowTracker } = winUtils; const { isPrivate } = require('sdk/private-browsing'); const { isWindowPBSupported } = require('sdk/private-browsing/utils'); +const { viewFor } = require("sdk/view/core"); +const { defer } = require("sdk/lang/functional"); // TEST: open & close window exports.testOpenAndCloseWindow = function(assert, done) { assert.equal(browserWindows.length, 1, "Only one window open"); let title = 'testOpenAndCloseWindow'; browserWindows.open({ url: "data:text/html;charset=utf-8,<title>" + title + "</title>", @@ -413,11 +415,31 @@ exports.testWindowIteratorPrivateDefault assert.equal(windows(null, { includePrivate: true }).length, 2); // test that all windows in iterator are not private for (let window of browserWindows) assert.ok(!isPrivate(window), 'no window in browserWindows is private'); close(window).then(done); }); -} +}; + +exports["test getView(window)"] = function(assert, done) { + browserWindows.once("open", window => { + const view = viewFor(window); + + assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window"); + assert.ok(isBrowser(view), "view is a browser window"); + assert.equal(getWindowTitle(view), window.title, + "window has a right title"); + + window.close(); + // Defer handler cause window is destroyed after event is dispatched. + browserWindows.once("close", defer(_ => { + assert.equal(viewFor(window), null, "window view is gone"); + done(); + })); + }); + + browserWindows.open({ url: "data:text/html,<title>yo</title>" }); +}; require('sdk/test').run(exports);