Bug 960771 - Uplift Add-on SDK

This commit is contained in:
Erik Vold 2014-01-16 17:29:40 -08:00
parent 1e38ac2acc
commit e1390cbc10
51 changed files with 1827 additions and 1360 deletions

View File

@ -8,6 +8,6 @@ module.metadata = {
};
exports.Loader = require('./loader').Loader;
exports.Symbiont = require('./symbiont').Symbiont;
exports.Symbiont = require('../deprecated/symbiont').Symbiont;
exports.Worker = require('./worker').Worker;

View File

@ -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'
}
};
}

View File

@ -1,17 +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': '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;
@ -24,18 +26,57 @@ function hasContentScript({ contentScript, 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;

View File

@ -7,38 +7,25 @@ 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';
// A weak map of workers to hold private attributes that
// should not be exposed
const workers = new WeakMap();
// 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';
let modelFor = (worker) => workers.get(worker);
const ERR_DESTROYED =
"Couldn't find the worker to receive this message. " +
@ -48,357 +35,43 @@ 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);
// List of messages fired before worker is initialized
get _earlyEvents() {
delete this._earlyEvents;
this._earlyEvents = [];
return this._earlyEvents;
options = options || {};
if ('contentScriptFile' in options)
this.contentScriptFile = options.contentScriptFile;
if ('contentScriptOptions' in options)
this.contentScriptOptions = options.contentScriptOptions;
if ('contentScript' in options)
this.contentScript = options.contentScript;
if ('injectInDocument' in options)
this.injectInDocument = !!options.injectInDocument;
setListeners(this, options);
unload.ensure(this, "destroy");
// Ensure that worker.port is initialized for contentWorker to be able
// to send events during worker initialization.
this.port = createPort(this);
model.documentUnload = documentUnload.bind(this);
model.pageShow = pageShow.bind(this);
model.pageHide = pageHide.bind(this);
if ('window' in options)
attach(this, options.window);
},
/**
@ -413,239 +86,197 @@ const Worker = EventEmitter.compose({
* 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);
processMessage.apply(null, [this].concat(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;
get url () {
let model = modelFor(this);
// model.window will be null after detach
return model.window ? model.window.document.location.href : null;
},
/**
* 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);
get contentURL () {
let model = modelFor(this);
return model.window ? model.window.document.URL : null;
},
// 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.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));
},
_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;
/**
* 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);
},
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);
/**
* Reference to the content side of the worker.
* @type {WorkerGlobalScope}
*/
_contentWorker: null,
// 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);
/**
* Reference to the window that is accessible from
* the content scripts.
* @type {Object}
*/
_window: null,
// will set model.contentWorker pointing to the private API:
model.contentWorker = WorkerSandbox(worker, model.window);
/**
* Flag to enable `addon` object injection in document. (bug 612726)
* @type {Boolean}
*/
_injectInDocument: false
// Mainly enable worker.port.emit to send event to the content worker
model.inited = true;
model.frozen = false;
// Fire off `attach` event
emit(worker, 'attach', window);
// Process all events and messages that were fired before the
// worker was initialized.
model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args)));
});
/**
* 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);
}
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));
}
exports.Worker = Worker;

View File

@ -363,10 +363,12 @@ let menuRules = mix(labelledItemRules, {
}
});
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
@ -374,7 +376,7 @@ let ContextWorker = Worker.compose({
// 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);
},
@ -382,7 +384,7 @@ let ContextWorker = Worker.compose({
// 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);
}
});

View File

@ -27,29 +27,6 @@ const VALID_TYPES = [
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.

View File

@ -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];
}
}
);
});

View File

@ -1,48 +1,52 @@
/* 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")
@ -61,7 +65,8 @@ var track = exports.track = function track(object, bin, stackFrameNumber) {
filename: frame.filename,
lineNo: frame.lineNumber,
bin: bin});
};
}
exports.track = track;
var getBins = exports.getBins = function getBins() {
var names = [];
@ -70,49 +75,55 @@ var getBins = exports.getBins = function getBins() {
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);
return results;
};
getLiveObjectsInBin(trackedObjects[name]);
}
var gc = exports.gc = function gc() {
return results;
}
exports.getObjects = getObjects;
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;
}
});

View File

@ -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);
});
});
})

View File

@ -4,13 +4,13 @@
"use strict";
module.metadata = {
"stability": "unstable"
"stability": "deprecated"
};
const { Worker } = require('./worker');
const { Loader } = require('./loader');
const { Worker } = require('./traits-worker');
const { Loader } = require('../content/loader');
const hiddenFrames = require('../frame/hidden-frame');
const observers = require('../deprecated/observer-service');
const { on, off } = require('../system/events');
const unload = require('../system/unload');
const { getDocShell } = require("../frame/utils");
const { ignoreWindow } = require('../private-browsing/utils');
@ -28,7 +28,7 @@ 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
@ -80,7 +80,7 @@ const Symbiont = Worker.resolve({
unload.ensure(this._public, "destroy");
},
destroy: function destroy() {
this._workerDestroy();
this._unregisterListener();
@ -90,14 +90,14 @@ const Symbiont = Worker.resolve({
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.
@ -105,7 +105,7 @@ const Symbiont = Worker.resolve({
_initFrame: function _initFrame(frame) {
if (this._loadListener)
this._unregisterListener();
this._frame = frame;
if (getDocShell(frame)) {
@ -113,16 +113,16 @@ const Symbiont = Worker.resolve({
}
else {
if (this._waitForFrame) {
observers.remove('content-document-global-created', this._waitForFrame);
off('content-document-global-created', this._waitForFrame);
}
this._waitForFrame = this.__waitForFrame.bind(this, frame);
observers.add('content-document-global-created', this._waitForFrame);
on('content-document-global-created', this._waitForFrame);
}
},
__waitForFrame: function _waitForFrame(frame, win, topic) {
__waitForFrame: function _waitForFrame(frame, { subject: win }) {
if (frame.contentWindow == win) {
observers.remove('content-document-global-created', this._waitForFrame);
off('content-document-global-created', this._waitForFrame);
delete this._waitForFrame;
this._reallyInitFrame(frame);
}
@ -157,13 +157,13 @@ const Symbiont = Worker.resolve({
this._onInit();
return;
}
let self = this;
if ('start' == this.contentScriptWhen) {
this._loadEvent = 'start';
observers.add('document-element-inserted',
this._loadListener = function onStart(doc) {
on('document-element-inserted',
this._loadListener = function onStart({ subject: doc }) {
let window = doc.defaultView;
if (ignoreWindow(window)) {
@ -174,41 +174,41 @@ const Symbiont = Worker.resolve({
self._unregisterListener();
self._onInit();
}
});
return;
}
let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded';
let self = this;
this._loadEvent = eventName;
frame.addEventListener(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);
off('content-document-global-created', this._waitForFrame);
delete this._waitForFrame;
}
if (!this._loadListener)
return;
if (this._loadEvent == "start") {
observers.remove('document-element-inserted', this._loadListener);
off('document-element-inserted', this._loadListener);
}
else {
this._frame.removeEventListener(this._loadEvent, this._loadListener,
@ -216,14 +216,14 @@ const Symbiont = Worker.resolve({
}
this._loadListener = null;
},
/**
* Called by Symbiont itself when the frame is ready to load
* content scripts according to contentScriptWhen. Overloaded by Panel.
* 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;

View File

@ -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;

View File

@ -16,6 +16,7 @@ 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
@ -161,9 +162,10 @@ 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);
});
}

View File

@ -7,7 +7,7 @@ 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
@ -255,3 +255,21 @@ Reactor.prototype.run = function(input) {
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;

View File

@ -1,16 +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 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], ' +
@ -40,5 +39,4 @@ function onOptionsDisplayed(document, addonId) {
}
}
}
observers.add(OPTIONS_DISPLAYED, onOptionsDisplayed);
on(OPTIONS_DISPLAYED, onOptionsDisplayed);

View File

@ -7,7 +7,7 @@ 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');
@ -100,7 +100,7 @@ const PageMod = Loader.compose(EventEmitter, {
let include = options.include;
let rules = this.include = Rules();
if (!include)
throw new Error('The `include` option must always contain atleast one rule');
@ -217,13 +217,13 @@ const PageModManager = Registry.resolve({
}).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
@ -234,7 +234,7 @@ const PageModManager = Registry.resolve({
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)

View File

@ -9,8 +9,9 @@ module.metadata = {
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');
@ -66,8 +67,8 @@ function disableScript (page) {
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); }
};
}
@ -89,7 +90,6 @@ const Page = Class({
setup: function Page(options) {
let page = this;
options = pageContract(options);
setListeners(this, options);
let view = makeFrame(window.document, {
nodeName: 'iframe',
type: 'content',
@ -100,12 +100,15 @@ const Page = Class({
});
['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);
@ -114,7 +117,7 @@ const Page = Class({
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);
@ -133,7 +136,7 @@ const Page = Class({
views.delete(this);
destroy(workers.get(this));
},
toString: function () '[object Page]'
toString: function () { return '[object Page]' }
});
exports.Page = Page;

View File

@ -19,8 +19,8 @@ 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");
@ -29,7 +29,7 @@ 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");
@ -117,8 +117,6 @@ const Panel = Class({
}, panelContract(options));
models.set(this, model);
// Setup listeners.
setListeners(this, options);
// Setup view
let view = domPanel.make();
@ -130,7 +128,9 @@ const Panel = Class({
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.

View File

@ -8,10 +8,9 @@ module.metadata = {
};
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";
@ -19,14 +18,9 @@ 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;

View File

@ -1,7 +1,6 @@
/* 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 = {
@ -12,17 +11,25 @@ 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);
@ -143,36 +150,30 @@ function dictDiff(last, curr) {
}
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) {
@ -188,12 +189,12 @@ function checkMemory() {
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) {
@ -211,6 +212,9 @@ function showResults() {
}
onDone(results);
resolve();
return promise;
}
function cleanup() {
@ -250,7 +254,8 @@ function cleanup() {
loader = null;
memory.gc();
} catch (e) {
}
catch (e) {
results.failed++;
console.error("unload.send() threw an exception.");
console.exception(e);
@ -333,7 +338,7 @@ function getPotentialLeaks() {
console.error("Unable to parse compartment detail " + matches[1]);
return;
}
let item = {
path: matches[1],
principal: details[1],
@ -349,8 +354,7 @@ function getPotentialLeaks() {
return;
}
matches = windowRegexp.exec(path);
if (matches) {
if (matches = windowRegexp.exec(path)) {
if (matches[1] in windows)
return;
@ -374,10 +378,9 @@ function getPotentialLeaks() {
}
}
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 };
}
@ -387,22 +390,28 @@ function nextIteration(tests) {
results.passed += tests.passed;
results.failed += tests.failed;
if (profileMemory)
reportMemoryUsage();
let testRun = [];
for each (let test in tests.testRunSummary) {
let testCopy = {};
for (let info in test) {
testCopy[info] = test[info];
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);
}
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

View File

@ -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;

View File

@ -10,6 +10,17 @@ module.metadata = {
}
};
// 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');

View File

@ -10,6 +10,17 @@ module.metadata = {
}
};
// 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');

View File

@ -14,7 +14,8 @@ 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');

View File

@ -10,6 +10,17 @@ module.metadata = {
}
};
// 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");

View File

@ -24,7 +24,7 @@ const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = requ
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');
@ -34,12 +34,6 @@ 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';
@ -118,7 +112,8 @@ const Sidebar = Class({
}
let worker = windowNS(window).worker = Worker({
window: panelBrowser.contentWindow
window: panelBrowser.contentWindow,
injectInDocument: true
});
function onWebPanelSidebarUnload() {

View File

@ -10,6 +10,17 @@ module.metadata = {
}
};
// 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");

View File

@ -9,7 +9,6 @@ module.metadata = {
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'].
@ -151,25 +150,6 @@ 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:

View File

@ -2,102 +2,18 @@
* 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 {
requiresAddonGlobal, attach, detach, destroy, WorkerHost
} = require('../content/utils');
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);
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;

View File

@ -613,8 +613,20 @@ const Require = iced(function Require(loader, requirer) {
// 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;

View File

@ -15,7 +15,6 @@
"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",

View File

@ -96,9 +96,6 @@ exports["test compatibility"] = function(assert) {
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");
@ -147,9 +144,6 @@ exports["test compatibility"] = function(assert) {
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");

View File

@ -5,7 +5,7 @@
"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({

View File

@ -0,0 +1 @@
an invalid json file

View File

@ -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;
}
}

View File

@ -9,7 +9,6 @@ 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");
@ -19,13 +18,17 @@ const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAA
// 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);

View File

@ -1,12 +1,11 @@
/* 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");
@ -20,10 +19,10 @@ 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(
@ -39,13 +38,13 @@ exports["test Install"] = function (assert, done) {
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();
}
);
@ -84,10 +83,8 @@ 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 + "] ";
@ -115,14 +112,14 @@ exports["test Update"] = function (assert, done) {
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();
}

View File

@ -4,31 +4,6 @@
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, {});

View File

@ -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);

View File

@ -4,7 +4,7 @@
"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');

View File

@ -12,6 +12,7 @@ module.metadata = {
};
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");
@ -116,6 +117,8 @@ exports["test:sample"] = WorkerTest(
assert.equal(worker.url, window.location.href,
"worker.url works");
assert.equal(worker.contentURL, window.location.href,
"worker.contentURL works");
worker.postMessage("hi!");
}
);
@ -226,7 +229,7 @@ exports["test:post-json-values-only"] = WorkerTest(
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)]);
});
@ -247,6 +250,10 @@ exports["test:post-json-values-only"] = WorkerTest(
"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 });
}
);
@ -264,7 +271,7 @@ exports["test:emit-json-values-only"] = WorkerTest(
fun === null,
typeof w,
"port" in w,
w.url,
w._url,
"fun" in obj,
Object.keys(obj.dom).length,
Array.isArray(array),
@ -295,6 +302,9 @@ exports["test:emit-json-values-only"] = WorkerTest(
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);
}
);
@ -829,4 +839,37 @@ exports['test:conentScriptFile as URL instance'] = WorkerTest(
}
);
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);

View File

@ -4,7 +4,7 @@
'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) {

View File

@ -5,7 +5,7 @@
'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)
@ -168,7 +168,7 @@ exports["test expand"] = function(assert) {
exports["test pipe"] = function (assert, done) {
let src = {};
let dest = {};
let aneventCount = 0, multiargsCount = 0;
let wildcardCount = {};
@ -184,7 +184,7 @@ exports["test pipe"] = function (assert, done) {
++multiargsCount;
check();
});
on(dest, '*', (name, ...data) => {
wildcardCount[name] = (wildcardCount[name] || 0) + 1;
if (name === 'multiargs') {
@ -201,12 +201,12 @@ exports["test pipe"] = function (assert, done) {
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();
}
@ -237,7 +237,7 @@ exports["test pipe multiple targets"] = function (assert) {
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,
@ -245,7 +245,7 @@ exports["test pipe multiple targets"] = function (assert) {
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');
@ -255,4 +255,28 @@ exports["test pipe multiple targets"] = function (assert) {
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);

View File

@ -101,6 +101,13 @@ exports['test syntax errors'] = function(assert) {
}
}
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 } });
@ -128,6 +135,26 @@ exports['test missing module'] = function(assert) {
}
}
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/'

View File

@ -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);

View File

@ -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);

View File

@ -5,7 +5,7 @@
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;
@ -132,7 +132,7 @@ exports.testBtnListener = function(assert, done) {
assert.pass("Button press event was heard");
done();
});
notify((id + "-cmdPressed"), "", name);
emit((id + "-cmdPressed"), { subject: "", data: name });
};
exports.testPrefRemoveListener = function(assert, done) {

View File

@ -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);

View File

@ -1,9 +1,11 @@
/* 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']
@ -16,7 +18,6 @@ function throwError() {
}
exports.testFormatDoesNotFetchRemoteFiles = function(assert) {
var observers = require("sdk/deprecated/observer-service");
["http", "https"].forEach(
function(scheme) {
var httpRequests = 0;
@ -24,7 +25,7 @@ exports.testFormatDoesNotFetchRemoteFiles = function(assert) {
httpRequests++;
}
observers.add("http-on-modify-request", onHttp);
on("http-on-modify-request", onHttp);
try {
var tb = [{filename: scheme + "://www.mozilla.org/",
@ -35,7 +36,7 @@ exports.testFormatDoesNotFetchRemoteFiles = function(assert) {
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 " +

View File

@ -11,7 +11,7 @@ module.metadata = {
};
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');
@ -78,7 +78,7 @@ exports['test new top window with various URIs'] = function(assert, done) {
}, msg);
assert.throws(function () {
open('https://foo');
}, msg);
}, msg);
assert.throws(function () {
open('ftp://foo');
}, msg);
@ -88,7 +88,7 @@ exports['test new top window with various URIs'] = function(assert, done) {
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');
@ -96,22 +96,6 @@ exports['test new top window with various URIs'] = function(assert, done) {
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() {

View File

@ -5,11 +5,7 @@
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) {
@ -61,26 +57,4 @@ exports.testWindowActivateMethod_simple = function(assert) {
'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);

View File

@ -6,7 +6,7 @@
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");
@ -14,6 +14,8 @@ 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) {
@ -418,6 +420,26 @@ exports.testWindowIteratorPrivateDefault = function(assert, done) {
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);