Move Add-on SDK source to toolkit/jetpack

This commit is contained in:
NTD 2018-02-09 06:46:43 -05:00 • committed by Roy Tam
commit 23b67db438
993 changed files with 484 additions and 100663 deletions

View file

@ -0,0 +1,11 @@
[App]
Vendor=Varma
Name=Test App
Version=1.0
BuildID=20060101
Copyright=Copyright (c) 2009 Atul Varma
ID=xulapp@toolness.com
[Gecko]
MinVersion=1.9.2.0
MaxVersion=2.0.*

View file

@ -0,0 +1,362 @@
/* 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/. */
// @see http://dxr.mozilla.org/mozilla-central/source/js/src/xpconnect/loader/mozJSComponentLoader.cpp
'use strict';
// IMPORTANT: Avoid adding any initialization tasks here, if you need to do
// something before add-on is loaded consider addon/runner module instead!
const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu,
results: Cr, manager: Cm } = Components;
const ioService = Cc['@mozilla.org/network/io-service;1'].
getService(Ci.nsIIOService);
const resourceHandler = ioService.getProtocolHandler('resource').
QueryInterface(Ci.nsIResProtocolHandler);
const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1'].
getService(Ci.mozIJSSubScriptLoader);
const prefService = Cc['@mozilla.org/preferences-service;1'].
getService(Ci.nsIPrefService).
QueryInterface(Ci.nsIPrefBranch);
const appInfo = Cc["@mozilla.org/xre/app-info;1"].
getService(Ci.nsIXULAppInfo);
const vc = Cc["@mozilla.org/xpcom/version-comparator;1"].
getService(Ci.nsIVersionComparator);
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable',
'install', 'uninstall', 'upgrade', 'downgrade' ];
const bind = Function.call.bind(Function.bind);
var loader = null;
var unload = null;
var cuddlefishSandbox = null;
var nukeTimer = null;
var resourceDomains = [];
function setResourceSubstitution(domain, uri) {
resourceDomains.push(domain);
resourceHandler.setSubstitution(domain, uri);
}
// Utility function that synchronously reads local resource from the given
// `uri` and returns content string.
function readURI(uri) {
let channel = NetUtil.newChannel({
uri: NetUtil.newURI(uri, 'UTF-8'),
loadUsingSystemPrincipal: true
});
let stream = channel.open2();
let cstream = Cc['@mozilla.org/intl/converter-input-stream;1'].
createInstance(Ci.nsIConverterInputStream);
cstream.init(stream, 'UTF-8', 0, 0);
let str = {};
let data = '';
let read = 0;
do {
read = cstream.readString(0xffffffff, str);
data += str.value;
} while (read != 0);
cstream.close();
return data;
}
// We don't do anything on install & uninstall yet, but in a future
// we should allow add-ons to cleanup after uninstall.
function install(data, reason) {}
function uninstall(data, reason) {}
function startup(data, reasonCode) {
try {
let reason = REASON[reasonCode];
// URI for the root of the XPI file.
// 'jar:' URI if the addon is packed, 'file:' URI otherwise.
// (Used by l10n module in order to fetch `locale` folder)
let rootURI = data.resourceURI.spec;
// TODO: Maybe we should perform read harness-options.json asynchronously,
// since we can't do anything until 'sessionstore-windows-restored' anyway.
let options = JSON.parse(readURI(rootURI + './harness-options.json'));
let id = options.jetpackID;
let name = options.name;
// Clean the metadata
options.metadata[name]['permissions'] = options.metadata[name]['permissions'] || {};
// freeze the permissionss
Object.freeze(options.metadata[name]['permissions']);
// freeze the metadata
Object.freeze(options.metadata[name]);
// Register a new resource 'domain' for this addon which is mapping to
// XPI's `resources` folder.
// Generate the domain name by using jetpack ID, which is the extension ID
// by stripping common characters that doesn't work as a domain name:
let uuidRe =
/^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/;
let domain = id.
toLowerCase().
replace(/@/g, '-at-').
replace(/\./g, '-dot-').
replace(uuidRe, '$1');
let prefixURI = 'resource://' + domain + '/';
let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null);
setResourceSubstitution(domain, resourcesURI);
// Create path to URLs mapping supported by loader.
let paths = {
// Relative modules resolve to add-on package lib
'./': prefixURI + name + '/lib/',
'./tests/': prefixURI + name + '/tests/',
'': 'resource://gre/modules/commonjs/'
};
// Maps addon lib and tests ressource folders for each package
paths = Object.keys(options.metadata).reduce(function(result, name) {
result[name + '/'] = prefixURI + name + '/lib/'
result[name + '/tests/'] = prefixURI + name + '/tests/'
return result;
}, paths);
// We need to map tests folder when we run sdk tests whose package name
// is stripped
if (name == 'addon-sdk')
paths['tests/'] = prefixURI + name + '/tests/';
let useBundledSDK = options['force-use-bundled-sdk'];
if (!useBundledSDK) {
try {
useBundledSDK = prefService.getBoolPref("extensions.addon-sdk.useBundledSDK");
}
catch (e) {
// Pref doesn't exist, allow using Firefox shipped SDK
}
}
// Starting with Firefox 21.0a1, we start using modules shipped into firefox
// Still allow using modules from the xpi if the manifest tell us to do so.
// And only try to look for sdk modules in xpi if the xpi actually ship them
if (options['is-sdk-bundled'] &&
(vc.compare(appInfo.version, '21.0a1') < 0 || useBundledSDK)) {
// Maps sdk module folders to their resource folder
paths[''] = prefixURI + 'addon-sdk/lib/';
// test.js is usually found in root commonjs or SDK_ROOT/lib/ folder,
// so that it isn't shipped in the xpi. Keep a copy of it in sdk/ folder
// until we no longer support SDK modules in XPI:
paths['test'] = prefixURI + 'addon-sdk/lib/sdk/test.js';
}
// Retrieve list of module folder overloads based on preferences in order to
// eventually used a local modules instead of files shipped into Firefox.
let branch = prefService.getBranch('extensions.modules.' + id + '.path');
paths = branch.getChildList('', {}).reduce(function (result, name) {
// Allows overloading of any sub folder by replacing . by / in pref name
let path = name.substr(1).split('.').join('/');
// Only accept overloading folder by ensuring always ending with `/`
if (path) path += '/';
let fileURI = branch.getCharPref(name);
// On mobile, file URI has to end with a `/` otherwise, setSubstitution
// takes the parent folder instead.
if (fileURI[fileURI.length-1] !== '/')
fileURI += '/';
// Maps the given file:// URI to a resource:// in order to avoid various
// failure that happens with file:// URI and be close to production env
let resourcesURI = ioService.newURI(fileURI, null, null);
let resName = 'extensions.modules.' + domain + '.commonjs.path' + name;
setResourceSubstitution(resName, resourcesURI);
result[path] = 'resource://' + resName + '/';
return result;
}, paths);
// Make version 2 of the manifest
let manifest = options.manifest;
// Import `cuddlefish.js` module using a Sandbox and bootstrap loader.
let cuddlefishPath = 'loader/cuddlefish.js';
let cuddlefishURI = 'resource://gre/modules/commonjs/sdk/' + cuddlefishPath;
if (paths['sdk/']) { // sdk folder has been overloaded
// (from pref, or cuddlefish is still in the xpi)
cuddlefishURI = paths['sdk/'] + cuddlefishPath;
}
else if (paths['']) { // root modules folder has been overloaded
cuddlefishURI = paths[''] + 'sdk/' + cuddlefishPath;
}
cuddlefishSandbox = loadSandbox(cuddlefishURI);
let cuddlefish = cuddlefishSandbox.exports;
// Normalize `options.mainPath` so that it looks like one that will come
// in a new version of linker.
let main = options.mainPath;
unload = cuddlefish.unload;
loader = cuddlefish.Loader({
paths: paths,
// modules manifest.
manifest: manifest,
// Add-on ID used by different APIs as a unique identifier.
id: id,
// Add-on name.
name: name,
// Add-on version.
version: options.metadata[name].version,
// Add-on package descriptor.
metadata: options.metadata[name],
// Add-on load reason.
loadReason: reason,
prefixURI: prefixURI,
// Add-on URI.
rootURI: rootURI,
// options used by system module.
// File to write 'OK' or 'FAIL' (exit code emulation).
resultFile: options.resultFile,
// Arguments passed as --static-args
staticArgs: options.staticArgs,
// Option to prevent automatic kill of firefox during tests
noQuit: options.no_quit,
// Add-on preferences branch name
preferencesBranch: options.preferencesBranch,
// Arguments related to test runner.
modules: {
'@test/options': {
iterations: options.iterations,
filter: options.filter,
profileMemory: options.profileMemory,
stopOnError: options.stopOnError,
verbose: options.verbose,
parseable: options.parseable,
checkMemory: options.check_memory,
}
}
});
let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI);
let require = cuddlefish.Require(loader, module);
// Init the 'sdk/webextension' module from the bootstrap addon parameter.
require("sdk/webextension").initFromBootstrapAddonParam(data);
require('sdk/addon/runner').startup(reason, {
loader: loader,
main: main,
prefsURI: rootURI + 'defaults/preferences/prefs.js'
});
} catch (error) {
dump('Bootstrap error: ' +
(error.message ? error.message : String(error)) + '\n' +
(error.stack || error.fileName + ': ' + error.lineNumber) + '\n');
throw error;
}
};
function loadSandbox(uri) {
let proto = {
sandboxPrototype: {
loadSandbox: loadSandbox,
ChromeWorker: ChromeWorker
}
};
let sandbox = Cu.Sandbox(systemPrincipal, proto);
// Create a fake commonjs environnement just to enable loading loader.js
// correctly
sandbox.exports = {};
sandbox.module = { uri: uri, exports: sandbox.exports };
sandbox.require = function (id) {
if (id !== "chrome")
throw new Error("Bootstrap sandbox `require` method isn't implemented.");
return Object.freeze({ Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm,
CC: bind(CC, Components), components: Components,
ChromeWorker: ChromeWorker });
};
scriptLoader.loadSubScript(uri, sandbox, 'UTF-8');
return sandbox;
}
function unloadSandbox(sandbox) {
if (Cu.getClassName(sandbox, true) == "Sandbox")
Cu.nukeSandbox(sandbox);
}
function setTimeout(callback, delay) {
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback({ notify: callback }, delay,
Ci.nsITimer.TYPE_ONE_SHOT);
return timer;
}
function shutdown(data, reasonCode) {
let reason = REASON[reasonCode];
if (loader) {
unload(loader, reason);
unload = null;
// Don't waste time cleaning up if the application is shutting down
if (reason != "shutdown") {
// Avoid leaking all modules when something goes wrong with one particular
// module. Do not clean it up immediatly in order to allow executing some
// actions on addon disabling.
// We need to keep a reference to the timer, otherwise it is collected
// and won't ever fire.
nukeTimer = setTimeout(nukeModules, 1000);
// Bug 944951 - bootstrap.js must remove the added resource: URIs on unload
resourceDomains.forEach(domain => {
resourceHandler.setSubstitution(domain, null);
})
}
}
};
function nukeModules() {
nukeTimer = null;
// module objects store `exports` which comes from sandboxes
// We should avoid keeping link to these object to avoid leaking sandboxes
for (let key in loader.modules) {
delete loader.modules[key];
}
// Direct links to sandboxes should be removed too
for (let key in loader.sandboxes) {
let sandbox = loader.sandboxes[key];
delete loader.sandboxes[key];
// Bug 775067: From FF17 we can kill all CCW from a given sandbox
unloadSandbox(sandbox);
}
unloadSandbox(loader.sharedGlobalSandbox);
loader = null;
// both `toolkit/loader` and `system/xul-app` are loaded as JSM's via
// `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when
// the addon is unload.
unloadSandbox(cuddlefishSandbox.loaderSandbox);
// Bug 764840: We need to unload cuddlefish otherwise it will stay alive
// and keep a reference to this compartment.
unloadSandbox(cuddlefishSandbox);
cuddlefishSandbox = null;
}

View file

@ -0,0 +1,33 @@
<?xml version="1.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/. -->
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>xulapp@toolness.com</em:id>
<em:version>1.0</em:version>
<em:type>2</em:type>
<em:bootstrap>true</em:bootstrap>
<em:unpack>false</em:unpack>
<!-- Firefox -->
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>26.0</em:minVersion>
<em:maxVersion>30.0</em:maxVersion>
</Description>
</em:targetApplication>
<!-- Front End MetaData -->
<em:name>Test App</em:name>
<em:description>Harness for tests.</em:description>
<em:creator>Mozilla Corporation</em:creator>
<em:homepageURL></em:homepageURL>
<em:optionsType></em:optionsType>
<em:optionsURL></em:optionsURL>
<em:updateURL></em:updateURL>
</Description>
</RDF>

View file

@ -0,0 +1,95 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Cu } = require("chrome");
const { Class } = require("../sdk/core/heritage");
const { MessagePort, MessageChannel } = require("../sdk/messaging");
const { require: devtoolsRequire } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const { DebuggerServer } = devtoolsRequire("devtools/server/main");
const outputs = new WeakMap();
const inputs = new WeakMap();
const targets = new WeakMap();
const transports = new WeakMap();
const inputFor = port => inputs.get(port);
const outputFor = port => outputs.get(port);
const transportFor = port => transports.get(port);
const fromTarget = target => {
const debuggee = new Debuggee();
const { port1, port2 } = new MessageChannel();
inputs.set(debuggee, port1);
outputs.set(debuggee, port2);
targets.set(debuggee, target);
return debuggee;
};
exports.fromTarget = fromTarget;
const Debuggee = Class({
extends: MessagePort.prototype,
close: function() {
const server = transportFor(this);
if (server) {
transports.delete(this);
server.close();
}
outputFor(this).close();
},
start: function() {
const target = targets.get(this);
if (target.isLocalTab) {
// Since a remote protocol connection will be made, let's start the
// DebuggerServer here, once and for all tools.
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
transports.set(this, DebuggerServer.connectPipe());
}
// TODO: Implement support for remote connections (See Bug 980421)
else {
throw Error("Remote targets are not yet supported");
}
// pipe messages send to the debuggee to an actual
// server via remote debugging protocol transport.
inputFor(this).addEventListener("message", ({data}) =>
transportFor(this).send(data));
// pipe messages received from the remote debugging
// server transport onto the this debuggee.
transportFor(this).hooks = {
onPacket: packet => inputFor(this).postMessage(packet),
onClosed: () => inputFor(this).close()
};
inputFor(this).start();
outputFor(this).start();
},
postMessage: function(data) {
return outputFor(this).postMessage(data);
},
get onmessage() {
return outputFor(this).onmessage;
},
set onmessage(onmessage) {
outputFor(this).onmessage = onmessage;
},
addEventListener: function(...args) {
return outputFor(this).addEventListener(...args);
},
removeEventListener: function(...args) {
return outputFor(this).removeEventListener(...args);
}
});
exports.Debuggee = Debuggee;

View file

@ -0,0 +1,120 @@
/* 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";
(function({content, sendSyncMessage, addMessageListener, sendAsyncMessage}) {
const Cc = Components.classes;
const Ci = Components.interfaces;
const observerService = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
const channels = new Map();
const handles = new WeakMap();
// Takes remote port handle and creates a local one.
// also set's up a messaging channel between them.
// This is temporary workaround until Bug 914974 is fixed
// and port can be transfered through message manager.
const demarshal = (handle) => {
if (handle.type === "MessagePort") {
if (!channels.has(handle.id)) {
const channel = new content.MessageChannel();
channels.set(handle.id, channel);
handles.set(channel.port1, handle);
channel.port1.onmessage = onOutPort;
}
return channels.get(handle.id).port2;
}
return null;
};
const onOutPort = event => {
const handle = handles.get(event.target);
sendAsyncMessage("sdk/port/message", {
port: handle,
message: event.data
});
};
const onInPort = ({data}) => {
const channel = channels.get(data.port.id);
if (channel)
channel.port1.postMessage(data.message);
};
const onOutEvent = event =>
sendSyncMessage("sdk/event/" + event.type,
{ type: event.type,
data: event.data });
const onInMessage = (message) => {
const {type, data, origin, bubbles, cancelable, ports} = message.data;
const event = new content.MessageEvent(type, {
bubbles: bubbles,
cancelable: cancelable,
data: data,
origin: origin,
target: content,
source: content,
ports: ports.map(demarshal)
});
content.dispatchEvent(event);
};
const onReady = event => {
channels.clear();
};
addMessageListener("sdk/event/message", onInMessage);
addMessageListener("sdk/port/message", onInPort);
const observer = {
handleEvent: ({target, type}) => {
observer.observe(target, type);
},
observe: (document, topic, data) => {
// When frame associated with message manager is removed from document `docShell`
// is set to `null` but observer is still kept alive. At this point accesing
// `content.document` throws "can't access dead object" exceptions. In order to
// avoid leaking observer and logged errors observer is going to be removed when
// `docShell` is set to `null`.
if (!docShell) {
observerService.removeObserver(observer, topic);
}
else if (document === content.document) {
if (topic.endsWith("-document-interactive")) {
sendAsyncMessage("sdk/event/ready", {
type: "ready",
readyState: document.readyState,
uri: document.documentURI
});
}
if (topic.endsWith("-document-loaded")) {
sendAsyncMessage("sdk/event/load", {
type: "load",
readyState: document.readyState,
uri: document.documentURI
});
}
if (topic === "unload") {
channels.clear();
sendAsyncMessage("sdk/event/unload", {
type: "unload",
readyState: "uninitialized",
uri: document.documentURI
});
}
}
}
};
observerService.addObserver(observer, "content-document-interactive", false);
observerService.addObserver(observer, "content-document-loaded", false);
observerService.addObserver(observer, "chrome-document-interactive", false);
observerService.addObserver(observer, "chrome-document-loaded", false);
addEventListener("unload", observer, false);
})(this);

View file

@ -0,0 +1,259 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Cu } = require("chrome");
const { Class } = require("../sdk/core/heritage");
const { curry } = require("../sdk/lang/functional");
const { EventTarget } = require("../sdk/event/target");
const { Disposable, setup, dispose } = require("../sdk/core/disposable");
const { emit, off, setListeners } = require("../sdk/event/core");
const { when } = require("../sdk/event/utils");
const { getFrameElement } = require("../sdk/window/utils");
const { contract, validate } = require("../sdk/util/contract");
const { data: { url: resolve }} = require("../sdk/self");
const { identify } = require("../sdk/ui/id");
const { isLocalURL, URL } = require("../sdk/url");
const { encode } = require("../sdk/base64");
const { marshal, demarshal } = require("./ports");
const { fromTarget } = require("./debuggee");
const { removed } = require("../sdk/dom/events");
const { id: addonID } = require("../sdk/self");
const { viewFor } = require("../sdk/view/core");
const { createView } = require("./panel/view");
const OUTER_FRAME_URI = module.uri.replace(/\.js$/, ".html");
const FRAME_SCRIPT = module.uri.replace("/panel.js", "/frame-script.js");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const HTML_NS = "http://www.w3.org/1999/xhtml";
const makeID = name =>
("dev-panel-" + addonID + "-" + name).
split("/").join("-").
split(".").join("-").
split(" ").join("-").
replace(/[^A-Za-z0-9_\-]/g, "");
// Weak mapping between `Panel` instances and their frame's
// `nsIMessageManager`.
const managers = new WeakMap();
// Return `nsIMessageManager` for the given `Panel` instance.
const managerFor = x => managers.get(x);
// Weak mappinging between iframe's and their owner
// `Panel` instances.
const panels = new WeakMap();
const panelFor = frame => panels.get(frame);
// Weak mapping between panels and debugees they're targeting.
const debuggees = new WeakMap();
const debuggeeFor = panel => debuggees.get(panel);
const frames = new WeakMap();
const frameFor = panel => frames.get(panel);
const setAttributes = (node, attributes) => {
for (var key in attributes)
node.setAttribute(key, attributes[key]);
};
const onStateChange = ({target, data}) => {
const panel = panelFor(target);
panel.readyState = data.readyState;
emit(panel, data.type, { target: panel, type: data.type });
};
// port event listener on the message manager that demarshalls
// and forwards to the actual receiver. This is a workaround
// until Bug 914974 is fixed.
const onPortMessage = ({data, target}) => {
const port = demarshal(target, data.port);
if (port)
port.postMessage(data.message);
};
// When frame is removed from the toolbox destroy panel
// associated with it to release all the resources.
const onFrameRemove = frame => {
panelFor(frame).destroy();
};
const onFrameInited = frame => {
frame.style.visibility = "visible";
}
const inited = frame => new Promise(resolve => {
const { messageManager } = frame.frameLoader;
const listener = message => {
messageManager.removeMessageListener("sdk/event/ready", listener);
resolve(frame);
};
messageManager.addMessageListener("sdk/event/ready", listener);
});
const getTarget = ({target}) => target;
const Panel = Class({
extends: Disposable,
implements: [EventTarget],
get id() {
return makeID(this.name || this.label);
},
readyState: "uninitialized",
ready: function() {
const { readyState } = this;
const isReady = readyState === "complete" ||
readyState === "interactive";
return isReady ? Promise.resolve(this) :
when(this, "ready").then(getTarget);
},
loaded: function() {
const { readyState } = this;
const isLoaded = readyState === "complete";
return isLoaded ? Promise.resolve(this) :
when(this, "load").then(getTarget);
},
unloaded: function() {
const { readyState } = this;
const isUninitialized = readyState === "uninitialized";
return isUninitialized ? Promise.resolve(this) :
when(this, "unload").then(getTarget);
},
postMessage: function(data, ports=[]) {
const manager = managerFor(this);
manager.sendAsyncMessage("sdk/event/message", {
type: "message",
bubbles: false,
cancelable: false,
data: data,
origin: this.url,
ports: ports.map(marshal(manager))
});
}
});
exports.Panel = Panel;
validate.define(Panel, contract({
label: {
is: ["string"],
msg: "The `option.label` must be a provided"
},
tooltip: {
is: ["string", "undefined"],
msg: "The `option.tooltip` must be a string"
},
icon: {
is: ["string"],
map: x => x && resolve(x),
ok: x => isLocalURL(x),
msg: "The `options.icon` must be a valid local URI."
},
url: {
map: x => resolve(x.toString()),
is: ["string"],
ok: x => isLocalURL(x),
msg: "The `options.url` must be a valid local URI."
},
invertIconForLightTheme: {
is: ["boolean", "undefined"],
msg: "The `options.invertIconForLightTheme` must be a boolean."
},
invertIconForDarkTheme: {
is: ["boolean", "undefined"],
msg: "The `options.invertIconForDarkTheme` must be a boolean."
}
}));
setup.define(Panel, (panel, {window, toolbox, url}) => {
// Hack: Given that iframe created by devtools API is no good for us,
// we obtain original iframe and replace it with the one that has
// desired configuration.
const original = getFrameElement(window);
const container = original.parentNode;
original.remove();
const frame = createView(panel, container.ownerDocument);
// Following modifications are a temporary workaround until Bug 1049188
// is fixed.
// Enforce certain iframe customizations regardless of users request.
setAttributes(frame, {
"id": original.id,
"src": url,
"flex": 1,
"forceOwnRefreshDriver": "",
"tooltip": "aHTMLTooltip"
});
frame.style.visibility = "hidden";
frame.classList.add("toolbox-panel-iframe");
// Inject iframe into designated node until add-on author decides
// to inject it elsewhere instead.
if (!frame.parentNode)
container.appendChild(frame);
// associate view with a panel
frames.set(panel, frame);
// associate panel model with a frame view.
panels.set(frame, panel);
const debuggee = fromTarget(toolbox.target);
// associate debuggee with a panel.
debuggees.set(panel, debuggee);
// Setup listeners for the frame message manager.
const { messageManager } = frame.frameLoader;
messageManager.addMessageListener("sdk/event/ready", onStateChange);
messageManager.addMessageListener("sdk/event/load", onStateChange);
messageManager.addMessageListener("sdk/event/unload", onStateChange);
messageManager.addMessageListener("sdk/port/message", onPortMessage);
messageManager.loadFrameScript(FRAME_SCRIPT, false);
managers.set(panel, messageManager);
// destroy panel if frame is removed.
removed(frame).then(onFrameRemove);
// show frame when it is initialized.
inited(frame).then(onFrameInited);
// set listeners if there are ones defined on the prototype.
setListeners(panel, Object.getPrototypeOf(panel));
panel.setup({ debuggee: debuggee });
});
createView.define(Panel, (panel, document) => {
const frame = document.createElement("iframe");
setAttributes(frame, {
"sandbox": "allow-scripts",
// We end up using chrome iframe with forced message manager
// as fixing a swapFrameLoader seemed like a giant task (see
// Bug 1075490).
"type": "chrome",
"forcemessagemanager": true,
"transparent": true,
"seamless": "seamless",
});
return frame;
});
dispose.define(Panel, function(panel) {
debuggeeFor(panel).close();
debuggees.delete(panel);
managers.delete(panel);
frames.delete(panel);
panel.readyState = "destroyed";
panel.dispose();
});
viewFor.define(Panel, frameFor);

View file

@ -0,0 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { method } = require("method/core");
const createView = method("dev/panel/view#createView");
exports.createView = createView;

View file

@ -0,0 +1,64 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
// This module provides `marshal` and `demarshal` functions
// that can be used to send MessagePort's over `nsIFrameMessageManager`
// until Bug 914974 is fixed.
const { add, iterator } = require("../sdk/lang/weak-set");
const { curry } = require("../sdk/lang/functional");
var id = 0;
const ports = new WeakMap();
// Takes `nsIFrameMessageManager` and `MessagePort` instances
// and returns a handle representing given `port`. Messages
// received on given `port` will be forwarded to a message
// manager under `sdk/port/message` and messages like:
// { port: { type: "MessagePort", id: 2}, data: data }
// Where id is an identifier associated with a given `port`
// and `data` is an `event.data` received on port.
const marshal = curry((manager, port) => {
if (!ports.has(port)) {
id = id + 1;
const handle = {type: "MessagePort", id: id};
// Bind id to the given port
ports.set(port, handle);
// Obtain a weak reference to a port.
add(exports, port);
port.onmessage = event => {
manager.sendAsyncMessage("sdk/port/message", {
port: handle,
message: event.data
});
};
return handle;
}
return ports.get(port);
});
exports.marshal = marshal;
// Takes `nsIFrameMessageManager` instance and a handle returned
// `marshal(manager, port)` returning a `port` that was passed
// to it. Note that `port` may be GC-ed in which case returned
// value will be `null`.
const demarshal = curry((manager, {type, id}) => {
if (type === "MessagePort") {
for (let port of iterator(exports)) {
if (id === ports.get(port).id)
return port;
}
}
return null;
});
exports.demarshal = demarshal;

View file

@ -0,0 +1,135 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Class } = require("../sdk/core/heritage");
const { EventTarget } = require("../sdk/event/target");
const { Disposable, setup, dispose } = require("../sdk/core/disposable");
const { contract, validate } = require("../sdk/util/contract");
const { id: addonID } = require("../sdk/self");
const { onEnable, onDisable } = require("dev/theme/hooks");
const { isString, instanceOf, isFunction } = require("sdk/lang/type");
const { add } = require("sdk/util/array");
const { data } = require("../sdk/self");
const { isLocalURL } = require("../sdk/url");
const makeID = name =>
("dev-theme-" + addonID + (name ? "-" + name : "")).
split(/[ . /]/).join("-").
replace(/[^A-Za-z0-9_\-]/g, "");
const Theme = Class({
extends: Disposable,
implements: [EventTarget],
initialize: function(options) {
this.name = options.name;
this.label = options.label;
this.styles = options.styles;
// Event handlers
this.onEnable = options.onEnable;
this.onDisable = options.onDisable;
},
get id() {
return makeID(this.name || this.label);
},
setup: function() {
// Any initialization steps done at the registration time.
},
getStyles: function() {
if (!this.styles) {
return [];
}
if (isString(this.styles)) {
if (isLocalURL(this.styles)) {
return [data.url(this.styles)];
}
}
let result = [];
for (let style of this.styles) {
if (isString(style)) {
if (isLocalURL(style)) {
style = data.url(style);
}
add(result, style);
} else if (instanceOf(style, Theme)) {
result = result.concat(style.getStyles());
}
}
return result;
},
getClassList: function() {
let result = [];
for (let style of this.styles) {
if (instanceOf(style, Theme)) {
result = result.concat(style.getClassList());
}
}
if (this.name) {
add(result, this.name);
}
return result;
}
});
exports.Theme = Theme;
// Initialization & dispose
setup.define(Theme, (theme) => {
theme.classList = [];
theme.setup();
});
dispose.define(Theme, function(theme) {
theme.dispose();
});
// Validation
validate.define(Theme, contract({
label: {
is: ["string"],
msg: "The `option.label` must be a provided"
},
}));
// Support theme events: apply and unapply the theme.
onEnable.define(Theme, (theme, {window, oldTheme}) => {
if (isFunction(theme.onEnable)) {
theme.onEnable(window, oldTheme);
}
});
onDisable.define(Theme, (theme, {window, newTheme}) => {
if (isFunction(theme.onDisable)) {
theme.onDisable(window, newTheme);
}
});
// Support for built-in themes
const LightTheme = Theme({
name: "theme-light",
styles: "chrome://devtools/skin/light-theme.css",
});
const DarkTheme = Theme({
name: "theme-dark",
styles: "chrome://devtools/skin/dark-theme.css",
});
exports.LightTheme = LightTheme;
exports.DarkTheme = DarkTheme;

View file

@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { method } = require("method/core");
const onEnable = method("dev/theme/hooks#onEnable");
const onDisable = method("dev/theme/hooks#onDisable");
exports.onEnable = onEnable;
exports.onDisable = onDisable;

View file

@ -0,0 +1,107 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Cu, Cc, Ci } = require("chrome");
const { Class } = require("../sdk/core/heritage");
const { Disposable, setup } = require("../sdk/core/disposable");
const { contract, validate } = require("../sdk/util/contract");
const { each, pairs, values } = require("../sdk/util/sequence");
const { onEnable, onDisable } = require("../dev/theme/hooks");
const { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
// This is temporary workaround to allow loading of the developer tools client - volcan
// into a toolbox panel, this hack won't be necessary as soon as devtools patch will be
// shipped in nightly, after which it can be removed. Bug 1038517
const registerSDKURI = () => {
const ioService = Cc['@mozilla.org/network/io-service;1']
.getService(Ci.nsIIOService);
const resourceHandler = ioService.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler);
const uri = module.uri.replace("dev/toolbox.js", "");
resourceHandler.setSubstitution("sdk", ioService.newURI(uri, null, null));
};
registerSDKURI();
const Tool = Class({
extends: Disposable,
setup: function(params={}) {
const { panels } = validate(this, params);
const { themes } = validate(this, params);
this.panels = panels;
this.themes = themes;
each(([key, Panel]) => {
const { url, label, tooltip, icon, invertIconForLightTheme,
invertIconForDarkTheme } = validate(Panel.prototype);
const { id } = Panel.prototype;
gDevTools.registerTool({
id: id,
url: "about:blank",
label: label,
tooltip: tooltip,
icon: icon,
invertIconForLightTheme: invertIconForLightTheme,
invertIconForDarkTheme: invertIconForDarkTheme,
isTargetSupported: target => target.isLocalTab,
build: (window, toolbox) => {
const panel = new Panel();
setup(panel, { window: window,
toolbox: toolbox,
url: url });
return panel.ready();
}
});
}, pairs(panels));
each(([key, theme]) => {
validate(theme);
setup(theme);
gDevTools.registerTheme({
id: theme.id,
label: theme.label,
stylesheets: theme.getStyles(),
classList: theme.getClassList(),
onApply: (window, oldTheme) => {
onEnable(theme, { window: window,
oldTheme: oldTheme });
},
onUnapply: (window, newTheme) => {
onDisable(theme, { window: window,
newTheme: newTheme });
}
});
}, pairs(themes));
},
dispose: function() {
each(Panel => gDevTools.unregisterTool(Panel.prototype.id),
values(this.panels));
each(Theme => gDevTools.unregisterTheme(Theme.prototype.id),
values(this.themes));
}
});
validate.define(Tool, contract({
panels: {
is: ["object", "undefined"]
},
themes: {
is: ["object", "undefined"]
}
}));
exports.Tool = Tool;

View file

@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu } = require("chrome");
const { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
const { devtools } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const { getActiveTab } = require("../sdk/tabs/utils");
const { getMostRecentBrowserWindow } = require("../sdk/window/utils");
const targetFor = target => {
target = target || getActiveTab(getMostRecentBrowserWindow());
return devtools.TargetFactory.forTab(target);
};
const getId = id => ((id.prototype && id.prototype.id) || id.id || id);
const getCurrentPanel = toolbox => toolbox.getCurrentPanel();
exports.getCurrentPanel = getCurrentPanel;
const openToolbox = (id, tab) => {
id = getId(id);
return gDevTools.showToolbox(targetFor(tab), id);
};
exports.openToolbox = openToolbox;
const closeToolbox = tab => gDevTools.closeToolbox(targetFor(tab));
exports.closeToolbox = closeToolbox;
const getToolbox = tab => gDevTools.getToolbox(targetFor(tab));
exports.getToolbox = getToolbox;
const openToolboxPanel = (id, tab) => {
id = getId(id);
return gDevTools.showToolbox(targetFor(tab), id).then(getCurrentPanel);
};
exports.openToolboxPanel = openToolboxPanel;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
language: node_js
node_js:
- 0.4
- 0.5
- 0.6

View file

@ -0,0 +1,14 @@
# Changes
## 1.0.1 / 2013-05-01
- Update method library version.
## 1.0.0 / 2012-11-09
- Test integration for browsers.
- New method library.
## 0.0.1 / 2012-10-22
- Initial release

View file

@ -0,0 +1,18 @@
Copyright 2012 Irakli Gozalishvili. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

View file

@ -0,0 +1,70 @@
# diffpatcher
[![Build Status](https://secure.travis-ci.org/Gozala/diffpatcher.png)](http://travis-ci.org/Gozala/diffpatcher)
[![Browser support](https://ci.testling.com/Gozala/diffpatcher.png)](http://ci.testling.com/Gozala/diffpatcher)
Diffpatcher is a small library that lets you treat hashes as if they were
git repositories.
## diff
Diff function that takes two hashes and returns delta hash.
```js
var diff = require("diffpatcher/diff")
diff({ a: { b: 1 }, c: { d: 2 } }, // hash#1
{ a: { e: 3 }, c: { d: 4 } }) // hash#2
// => { // delta
// a: {
// b: null, // -
// e: 3 // +
// },
// c: {
// d: 4 // ±
// }
// }
```
As you can see from the example above `delta` makes no real distinction between
proprety upadate and property addition. Try to think of additions as an update
from `undefined` to whatever it's being updated to.
## patch
Patch fuction takes a `hash` and a `delta` and returns a new `hash` which is
just like orginial but with delta applied to it. Let's apply delta from the
previous example to the first hash from the same example
```js
var patch = require("diffpatcher/patch")
patch({ a: { b: 1 }, c: { d: 2 } }, // hash#1
{ // delta
a: {
b: null, // -
e: 3 // +
},
c: {
d: 4 // ±
}
})
// => { a: { e: 3 }, c: { d: 4 } } // hash#2
```
That's about it really, just diffing hashes and applying thes diffs on them.
### rebase
And as Linus mentioned everything in git can be expressed with `rebase`, that
also pretty much the case for `diffpatcher`. `rebase` takes `target` hash,
and rebases `parent` onto it with `diff` applied.
## Install
npm install diffpatcher

View file

@ -0,0 +1,45 @@
"use strict";
var method = require("../method/core")
// Method is designed to work with data structures representing application
// state. Calling it with a state should return object representing `delta`
// that has being applied to a previous state to get to a current state.
//
// Example
//
// diff(state) // => { "item-id-1": { title: "some title" } "item-id-2": null }
var diff = method("diff@diffpatcher")
// diff between `null` / `undefined` to any hash is a hash itself.
diff.define(null, function(from, to) { return to })
diff.define(undefined, function(from, to) { return to })
diff.define(Object, function(from, to) {
return calculate(from, to || {}) || {}
})
function calculate(from, to) {
var diff = {}
var changes = 0
Object.keys(from).forEach(function(key) {
changes = changes + 1
if (!(key in to) && from[key] != null) diff[key] = null
else changes = changes - 1
})
Object.keys(to).forEach(function(key) {
changes = changes + 1
var previous = from[key]
var current = to[key]
if (previous === current) return (changes = changes - 1)
if (typeof(current) !== "object") return diff[key] = current
if (typeof(previous) !== "object") return diff[key] = current
var delta = calculate(previous, current)
if (delta) diff[key] = delta
else changes = changes - 1
})
return changes ? diff : null
}
diff.calculate = calculate
module.exports = diff

View file

@ -0,0 +1,5 @@
"use strict";
exports.diff = require("./diff")
exports.patch = require("./patch")
exports.rebase = require("./rebase")

View file

@ -0,0 +1,54 @@
{
"name": "diffpatcher",
"id": "diffpatcher",
"version": "1.2.0",
"description": "Utilities for diff-ing & patch-ing hashes",
"keywords": [
"diff", "patch", "rebase", "hash", "changes", "versions"
],
"author": "Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)",
"homepage": "https://github.com/Gozala/diffpatcher",
"repository": {
"type": "git",
"url": "https://github.com/Gozala/diffpatcher.git",
"web": "https://github.com/Gozala/diffpatcher"
},
"bugs": {
"url": "http://github.com/Gozala/diffpatcher/issues/"
},
"dependencies": {
"method": "~2.0.0"
},
"devDependencies": {
"test": "~0.x.0",
"phantomify": "~0.x.0",
"retape": "~0.x.0",
"tape": "~0.1.5"
},
"main": "./index.js",
"scripts": {
"test": "npm run test-node && npm run test-browser",
"test-browser": "node ./node_modules/phantomify/bin/cmd.js ./test/common.js",
"test-node": "node ./test/common.js",
"test-tap": "node ./test/tap.js"
},
"testling": {
"files": "test/tap.js",
"browsers": [
"ie/9..latest",
"chrome/25..latest",
"firefox/20..latest",
"safari/6..latest",
"opera/11.0..latest",
"iphone/6..latest",
"ipad/6..latest",
"android-browser/4.2..latest"
]
},
"licenses": [
{
"type": "MIT",
"url": "https://github.com/Gozala/diffpatcher/License.md"
}
]
}

View file

@ -0,0 +1,21 @@
"use strict";
var method = require("../method/core")
var rebase = require("./rebase")
// Method is designed to work with data structures representing application
// state. Calling it with a state and delta should return object representing
// new state, with changes in `delta` being applied to previous.
//
// ## Example
//
// patch(state, {
// "item-id-1": { completed: false }, // update
// "item-id-2": null // delete
// })
var patch = method("patch@diffpatcher")
patch.define(Object, function patch(hash, delta) {
return rebase({}, hash, delta)
})
module.exports = patch

View file

@ -0,0 +1,36 @@
"use strict";
var nil = {}
var owns = ({}).hasOwnProperty
function rebase(result, parent, delta) {
var key, current, previous, update
for (key in parent) {
if (owns.call(parent, key)) {
previous = parent[key]
update = owns.call(delta, key) ? delta[key] : nil
if (previous === null) continue
else if (previous === void(0)) continue
else if (update === null) continue
else if (update === void(0)) continue
else result[key] = previous
}
}
for (key in delta) {
if (owns.call(delta, key)) {
update = delta[key]
current = owns.call(result, key) ? result[key] : nil
if (current === update) continue
else if (update === null) continue
else if (update === void(0)) continue
else if (current === nil) result[key] = update
else if (typeof(update) !== "object") result[key] = update
else if (typeof(current) !== "object") result[key] = update
else result[key]= rebase({}, current, update)
}
}
return result
}
module.exports = rebase

View file

@ -0,0 +1,3 @@
"use strict";
require("test").run(require("./index"))

View file

@ -0,0 +1,59 @@
"use strict";
var diff = require("../diff")
exports["test diff from null"] = function(assert) {
var to = { a: 1, b: 2 }
assert.equal(diff(null, to), to, "diff null to x returns x")
assert.equal(diff(void(0), to), to, "diff undefined to x returns x")
}
exports["test diff to null"] = function(assert) {
var from = { a: 1, b: 2 }
assert.deepEqual(diff({ a: 1, b: 2 }, null),
{ a: null, b: null },
"diff x null returns x with all properties nullified")
}
exports["test diff identical"] = function(assert) {
assert.deepEqual(diff({}, {}), {}, "diff on empty objects is {}")
assert.deepEqual(diff({ a: 1, b: 2 }, { a: 1, b: 2 }), {},
"if properties match diff is {}")
assert.deepEqual(diff({ a: 1, b: { c: { d: 3, e: 4 } } },
{ a: 1, b: { c: { d: 3, e: 4 } } }), {},
"diff between identical nested hashes is {}")
}
exports["test diff delete"] = function(assert) {
assert.deepEqual(diff({ a: 1, b: 2 }, { b: 2 }), { a: null },
"missing property is deleted")
assert.deepEqual(diff({ a: 1, b: 2 }, { a: 2 }), { a: 2, b: null },
"missing property is deleted another updated")
assert.deepEqual(diff({ a: 1, b: 2 }, {}), { a: null, b: null },
"missing propertes are deleted")
assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, {}),
{ a: null, b: null },
"missing deep propertes are deleted")
assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, { b: { c: {} } }),
{ a: null, b: { c: { d: null } } },
"missing nested propertes are deleted")
}
exports["test add update"] = function(assert) {
assert.deepEqual(diff({ a: 1, b: 2 }, { b: 2, c: 3 }), { a: null, c: 3 },
"delete and add")
assert.deepEqual(diff({ a: 1, b: 2 }, { a: 2, c: 3 }), { a: 2, b: null, c: 3 },
"delete and adds")
assert.deepEqual(diff({}, { a: 1, b: 2 }), { a: 1, b: 2 },
"diff on empty objcet returns equivalen of to")
assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, { d: 3 }),
{ a: null, b: null, d: 3 },
"missing deep propertes are deleted")
assert.deepEqual(diff({ b: { c: {} }, d: null }, { a: 1, b: { c: { d: 2 } } }),
{ a: 1, b: { c: { d: 2 } } },
"missing nested propertes are deleted")
}

View file

@ -0,0 +1,14 @@
"use strict";
var diff = require("../diff")
var patch = require("../patch")
exports["test diff"] = require("./diff")
exports["test patch"] = require("./patch")
exports["test patch(a, diff(a, b)) => b"] = function(assert) {
var a = { a: { b: 1 }, c: { d: 2 } }
var b = { a: { e: 3 }, c: { d: 4 } }
assert.deepEqual(patch(a, diff(a, b)), b, "patch(a, diff(a, b)) => b")
}

View file

@ -0,0 +1,83 @@
"use strict";
var patch = require("../patch")
exports["test patch delete"] = function(assert) {
var hash = { a: 1, b: 2 }
assert.deepEqual(patch(hash, { a: null }), { b: 2 }, "null removes property")
}
exports["test patch delete with void"] = function(assert) {
var hash = { a: 1, b: 2 }
assert.deepEqual(patch(hash, { a: void(0) }), { b: 2 },
"void(0) removes property")
}
exports["test patch delete missing"] = function(assert) {
assert.deepEqual(patch({ a: 1, b: 2 }, { c: null }),
{ a: 1, b: 2 },
"null removes property if exists");
assert.deepEqual(patch({ a: 1, b: 2 }, { c: void(0) }),
{ a: 1, b: 2 },
"void removes property if exists");
}
exports["test delete deleted"] = function(assert) {
assert.deepEqual(patch({ a: null, b: 2, c: 3, d: void(0)},
{ a: void(0), b: null, d: null }),
{c: 3},
"removed all existing and non existing");
}
exports["test update deleted"] = function(assert) {
assert.deepEqual(patch({ a: null, b: void(0), c: 3},
{ a: { b: 2 } }),
{ a: { b: 2 }, c: 3 },
"replace deleted");
}
exports["test patch delete with void"] = function(assert) {
var hash = { a: 1, b: 2 }
assert.deepEqual(patch(hash, { a: void(0) }), { b: 2 },
"void(0) removes property")
}
exports["test patch addition"] = function(assert) {
var hash = { a: 1, b: 2 }
assert.deepEqual(patch(hash, { c: 3 }), { a: 1, b: 2, c: 3 },
"new properties are added")
}
exports["test patch addition"] = function(assert) {
var hash = { a: 1, b: 2 }
assert.deepEqual(patch(hash, { c: 3 }), { a: 1, b: 2, c: 3 },
"new properties are added")
}
exports["test hash on itself"] = function(assert) {
var hash = { a: 1, b: 2 }
assert.deepEqual(patch(hash, hash), hash,
"applying hash to itself returns hash itself")
}
exports["test patch with empty delta"] = function(assert) {
var hash = { a: 1, b: 2 }
assert.deepEqual(patch(hash, {}), hash,
"applying empty delta results in no changes")
}
exports["test patch nested data"] = function(assert) {
assert.deepEqual(patch({ a: { b: 1 }, c: { d: 2 } },
{ a: { b: null, e: 3 }, c: { d: 4 } }),
{ a: { e: 3 }, c: { d: 4 } },
"nested structures can also be patched")
}

View file

@ -0,0 +1,3 @@
"use strict";
require("retape")(require("./index"))

View file

@ -0,0 +1,27 @@
/* 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 globalMM = Components.classes["@mozilla.org/globalmessagemanager;1"].
getService(Components.interfaces.nsIMessageListenerManager);
// Load frame scripts from the same dir as this module.
// Since this JSM will be loaded using require(), PATH will be
// overridden while running tests, just like any other module.
const PATH = __URI__.replace('framescript/FrameScriptManager.jsm', '');
// Builds a unique loader ID for this runtime. We prefix with the SDK path so
// overriden versions of the SDK don't conflict
var LOADER_ID = 0;
this.getNewLoaderID = () => {
return PATH + ":" + LOADER_ID++;
}
const frame_script = function(contentFrame, PATH) {
let { registerContentFrame } = Components.utils.import(PATH + 'framescript/content.jsm', {});
registerContentFrame(contentFrame);
}
globalMM.loadFrameScript("data:,(" + frame_script.toString() + ")(this, " + JSON.stringify(PATH) + ");", true);
this.EXPORTED_SYMBOLS = ['getNewLoaderID'];

View file

@ -0,0 +1,94 @@
/* 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 { utils: Cu, classes: Cc, interfaces: Ci } = Components;
const { Services } = Cu.import('resource://gre/modules/Services.jsm');
const cpmm = Cc['@mozilla.org/childprocessmessagemanager;1'].
getService(Ci.nsISyncMessageSender);
this.EXPORTED_SYMBOLS = ["registerContentFrame"];
// This may be an overriden version of the SDK so use the PATH as a key for the
// initial messages before we have a loaderID.
const PATH = __URI__.replace('framescript/content.jsm', '');
const { Loader } = Cu.import(PATH + 'toolkit/loader.js', {});
// one Loader instance per addon (per @loader/options to be precise)
var addons = new Map();
// Tell the parent that a new process is ready
cpmm.sendAsyncMessage('sdk/remote/process/start', {
modulePath: PATH
});
// Load a child process module loader with the given loader options
cpmm.addMessageListener('sdk/remote/process/load', ({ data: { modulePath, loaderID, options, reason } }) => {
if (modulePath != PATH)
return;
// During startup races can mean we get a second load message
if (addons.has(loaderID))
return;
options.waiveInterposition = true;
let loader = Loader.Loader(options);
let addon = {
loader,
require: Loader.Require(loader, { id: 'LoaderHelper' }),
}
addons.set(loaderID, addon);
cpmm.sendAsyncMessage('sdk/remote/process/attach', {
loaderID,
processID: Services.appinfo.processID,
isRemote: Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
});
addon.child = addon.require('sdk/remote/child');
for (let contentFrame of frames.values())
addon.child.registerContentFrame(contentFrame);
});
// Unload a child process loader
cpmm.addMessageListener('sdk/remote/process/unload', ({ data: { loaderID, reason } }) => {
if (!addons.has(loaderID))
return;
let addon = addons.get(loaderID);
Loader.unload(addon.loader, reason);
// We want to drop the reference to the loader but never allow creating a new
// loader with the same ID
addons.set(loaderID, {});
})
var frames = new Set();
this.registerContentFrame = contentFrame => {
contentFrame.addEventListener("unload", () => {
unregisterContentFrame(contentFrame);
}, false);
frames.add(contentFrame);
for (let addon of addons.values()) {
if ("child" in addon)
addon.child.registerContentFrame(contentFrame);
}
};
function unregisterContentFrame(contentFrame) {
frames.delete(contentFrame);
for (let addon of addons.values()) {
if ("child" in addon)
addon.child.unregisterContentFrame(contentFrame);
}
}

View file

@ -0,0 +1,215 @@
/* 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 { query, constant, cache } = require("sdk/lang/functional");
const { pairs, each, map, object } = require("sdk/util/sequence");
const { nodeToMessageManager } = require("./util");
// Decorator function that takes `f` function and returns one that attempts
// to run `f` with given arguments. In case of exception error is logged
// and `fallback` is returned instead.
const Try = (fn, fallback=null) => (...args) => {
try {
return fn(...args);
} catch(error) {
console.error(error);
return fallback;
}
};
// Decorator funciton that takes `f` function and returns one that returns
// JSON cloned result of whatever `f` returns for given arguments.
const JSONReturn = f => (...args) => JSON.parse(JSON.stringify(f(...args)));
const Null = constant(null);
// Table of readers mapped to field names they're going to be reading.
const readers = Object.create(null);
// Read function takes "contextmenu" event target `node` and returns table of
// read field names mapped to appropriate values. Read uses above defined read
// table to read data for all registered readers.
const read = node =>
object(...map(([id, read]) => [id, read(node, id)], pairs(readers)));
// Table of built-in readers, each takes a descriptor and returns a reader:
// descriptor -> node -> JSON
const parsers = Object.create(null)
// Function takes a descriptor of the remotely defined reader and parsese it
// to construct a local reader that's going to read out data from context menu
// target.
const parse = descriptor => {
const parser = parsers[descriptor.category];
if (!parser) {
console.error("Unknown reader descriptor was received", descriptor, `"${descriptor.category}"`);
return Null
}
return Try(parser(descriptor));
}
// TODO: Test how chrome's mediaType behaves to try and match it's behavior.
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const SVG_NS = "http://www.w3.org/2000/svg";
// Firefox always creates a HTMLVideoElement when loading an ogg file
// directly. If the media is actually audio, be smarter and provide a
// context menu with audio operations.
// Source: https://github.com/mozilla/gecko-dev/blob/28c2fca3753c5371643843fc2f2f205146b083b7/browser/base/content/nsContextMenu.js#L632-L637
const isVideoLoadingAudio = node =>
node.readyState >= node.HAVE_METADATA &&
(node.videoWidth == 0 || node.videoHeight == 0)
const isVideo = node =>
node instanceof node.ownerDocument.defaultView.HTMLVideoElement &&
!isVideoLoadingAudio(node);
const isAudio = node => {
const {HTMLVideoElement, HTMLAudioElement} = node.ownerDocument.defaultView;
return node instanceof HTMLAudioElement ? true :
node instanceof HTMLVideoElement ? isVideoLoadingAudio(node) :
false;
};
const isImage = ({namespaceURI, localName}) =>
namespaceURI === HTML_NS && localName === "img" ? true :
namespaceURI === XUL_NS && localName === "image" ? true :
namespaceURI === SVG_NS && localName === "image" ? true :
false;
parsers["reader/MediaType()"] = constant(node =>
isImage(node) ? "image" :
isAudio(node) ? "audio" :
isVideo(node) ? "video" :
null);
const readLink = node =>
node.namespaceURI === HTML_NS && node.localName === "a" ? node.href :
readLink(node.parentNode);
parsers["reader/LinkURL()"] = constant(node =>
node.matches("a, a *") ? readLink(node) : null);
// Reader that reads out `true` if "contextmenu" `event.target` matches
// `descriptor.selector` and `false` if it does not.
parsers["reader/SelectorMatch()"] = ({selector}) =>
node => node.matches(selector);
// Accessing `selectionStart` and `selectionEnd` properties on non
// editable input nodes throw exceptions, there for we need this util
// function to guard us against them.
const getInputSelection = node => {
try {
if ("selectionStart" in node && "selectionEnd" in node) {
const {selectionStart, selectionEnd} = node;
return {selectionStart, selectionEnd}
}
}
catch(_) {}
return null;
}
// Selection reader does not really cares about descriptor so it is
// a constant function returning selection reader. Selection reader
// returns string of the selected text or `null` if there is no selection.
parsers["reader/Selection()"] = constant(node => {
const selection = node.ownerDocument.getSelection();
if (!selection.isCollapsed) {
return selection.toString();
}
// If target node is editable (text, input, textarea, etc..) document does
// not really handles selections there. There for we fallback to checking
// `selectionStart` `selectionEnd` properties and if they are present we
// extract selections manually from the `node.value`.
else {
const selection = getInputSelection(node);
const isSelected = selection &&
Number.isInteger(selection.selectionStart) &&
Number.isInteger(selection.selectionEnd) &&
selection.selectionStart !== selection.selectionEnd;
return isSelected ? node.value.substring(selection.selectionStart,
selection.selectionEnd) :
null;
}
});
// Query reader just reads out properties from the node, so we just use `query`
// utility function.
parsers["reader/Query()"] = ({path}) => JSONReturn(query(path));
// Attribute reader just reads attribute of the event target node.
parsers["reader/Attribute()"] = ({name}) => node => node.getAttribute(name);
// Extractor reader defines generates a reader out of serialized function, who's
// return value is JSON cloned. Note: We do know source will evaluate to function
// as that's what we serialized on the other end, it's also ok if generated function
// is going to throw as registered readers are wrapped in try catch to avoid breakting
// unrelated readers.
parsers["reader/Extractor()"] = ({source}) =>
JSONReturn(new Function("return (" + source + ")")());
// If the context-menu target node or any of its ancestors is one of these,
// Firefox uses a tailored context menu, and so the page context doesn't apply.
// There for `reader/isPage()` will read `false` in that case otherwise it's going
// to read `true`.
const nonPageElements = ["a", "applet", "area", "button", "canvas", "object",
"embed", "img", "input", "map", "video", "audio", "menu",
"option", "select", "textarea", "[contenteditable=true]"];
const nonPageSelector = nonPageElements.
concat(nonPageElements.map(tag => `${tag} *`)).
join(", ");
// Note: isPageContext implementation could have actually used SelectorMatch reader,
// but old implementation was also checked for collapsed selection there for to keep
// the behavior same we end up implementing a new reader.
parsers["reader/isPage()"] = constant(node =>
node.ownerDocument.defaultView.getSelection().isCollapsed &&
!node.matches(nonPageSelector));
// Reads `true` if node is in an iframe otherwise returns true.
parsers["reader/isFrame()"] = constant(node =>
!!node.ownerDocument.defaultView.frameElement);
parsers["reader/isEditable()"] = constant(node => {
const selection = getInputSelection(node);
return selection ? !node.readOnly && !node.disabled : node.isContentEditable;
});
// TODO: Add some reader to read out tab id.
const onReadersUpdate = message => {
each(([id, descriptor]) => {
if (descriptor) {
readers[id] = parse(descriptor);
}
else {
delete readers[id];
}
}, pairs(message.data));
};
exports.onReadersUpdate = onReadersUpdate;
const onContextMenu = event => {
if (!event.defaultPrevented) {
const manager = nodeToMessageManager(event.target);
manager.sendSyncMessage("sdk/context-menu/read", read(event.target), readers);
}
};
exports.onContextMenu = onContextMenu;
const onContentFrame = (frame) => {
// Listen for contextmenu events in on this frame.
frame.addEventListener("contextmenu", onContextMenu);
// Listen to registered reader changes and update registry.
frame.addMessageListener("sdk/context-menu/readers", onReadersUpdate);
// Request table of readers (if this is loaded in a new process some table
// changes may be missed, this is way to sync up).
frame.sendAsyncMessage("sdk/context-menu/readers?");
};
exports.onContentFrame = onContentFrame;

View file

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "unstable"
};
const mime = "application/javascript";
const requireURI = module.uri.replace("framescript/manager.js",
"toolkit/require.js");
const requireLoadURI = `data:${mime},this["Components"].utils.import("${requireURI}")`
// Loads module with given `id` into given `messageManager` via shared module loader. If `init`
// string is passed, will call module export with that name and pass frame script environment
// of the `messageManager` into it. Since module will load only once per process (which is
// once for chrome proces & second for content process) it is useful to have an init function
// to setup event listeners on each content frame.
const loadModule = (messageManager, id, allowDelayed, init) => {
const moduleLoadURI = `${requireLoadURI}.require("${id}")`
const uri = init ? `${moduleLoadURI}.${init}(this)` : moduleLoadURI;
messageManager.loadFrameScript(uri, allowDelayed);
};
exports.loadModule = loadModule;

View file

@ -0,0 +1,25 @@
/* 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 { Ci } = require("chrome");
const windowToMessageManager = window =>
window.
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDocShell).
sameTypeRootTreeItem.
QueryInterface(Ci.nsIDocShell).
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIContentFrameMessageManager);
exports.windowToMessageManager = windowToMessageManager;
const nodeToMessageManager = node =>
windowToMessageManager(node.ownerDocument.defaultView);
exports.nodeToMessageManager = nodeToMessageManager;

3
toolkit/jetpack/index.js Normal file
View file

@ -0,0 +1,3 @@
/* 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/. */

View file

@ -0,0 +1,53 @@
/* 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/. */
/**
* Takes parsed `package.json` manifest and returns
* valid add-on id for it.
*/
function getID(manifest) {
manifest = manifest || {};
if (manifest.id) {
if (typeof manifest.id !== "string") {
return null;
}
// If manifest.id is already valid (as domain or GUID), use it
if (isValidAOMName(manifest.id)) {
return manifest.id;
}
// Otherwise, this ID is invalid so return `null`
return null;
}
// If no `id` defined, turn `name` into a domain ID,
// as we transition to `name` being an id, similar to node/npm, but
// append a '@' to make it compatible with Firefox requirements
if (manifest.name) {
if (typeof manifest.name !== "string") {
return null;
}
var modifiedName = "@" + manifest.name;
return isValidAOMName(modifiedName) ? modifiedName : null;
}
// If no `id` or `name` property, return null as this manifest
// is invalid
return null;
}
module.exports = getID;
/**
* Regex taken from XPIProvider.jsm in the Addon Manager to validate proper
* IDs that are able to be used.
* http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm#209
*/
function isValidAOMName (s) {
return /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i.test(s || "");
}

View file

@ -0,0 +1,28 @@
{
"name": "jetpack-id",
"version": "1.0.0",
"description": "Creates an ID from a Firefox Jetpack manifest",
"main": "index.js",
"repository": {
"type": "git",
"url": "http://github.com/jsantell/jetpack-id"
},
"author": {
"name": "Jordan Santell",
"url": "http://github.com/jsantell"
},
"license": "MPL-2.0",
"scripts": {
"test": "./node_modules/.bin/mocha --reporter spec --ui bdd"
},
"keywords": [
"jetpack",
"addon",
"mozilla",
"firefox"
],
"devDependencies": {
"mocha": "*",
"chai": "*"
}
}

View file

@ -0,0 +1,5 @@
language: node_js
node_js:
- 0.4
- 0.5
- 0.6

View file

@ -0,0 +1,55 @@
# Changes
## 1.0.2 / 2012-12-26
- Delegate to polymorphic methods from `.define` and `.implement` so, they
can be overidden.
## 1.0.1 / 2012-11-11
- Fix issues with different `Error` types as they all inherit from
`Error`.
## 1.0.0 / 2012-11-09
- Add browser test integration.
- Fix cross-browser incompatibilities & test failures.
- Add support for host objects.
- Add optional `hint` argument for method to ease debugging.
- Remove default implementation at definition time.
## 0.1.1 / 2012-10-15
- Fix regression causing custom type implementation to be stored on objects.
## 0.1.0 / 2012-10-15
- Remove dependency on name module.
- Implement fallback for engines that do not support ES5.
- Add support for built-in type extensions without extending their prototypes.
- Make API for default definitions more intuitive.
Skipping type argument now defines default:
isFoo.define(function(value) {
return false
})
- Make exposed `define` and `implement` polymorphic.
- Removed dev dependency on swank-js.
- Primitive types `string, number, boolean` no longer inherit method
implementations from `Object`.
## 0.0.3 / 2012-07-17
- Remove module boilerplate
## 0.0.2 / 2012-06-26
- Name changes to make it less conflicting with other library conventions.
- Expose function version of `define` & `implement` methods.
- Expose `Null` and `Undefined` object holding implementations for an
associated types.
## 0.0.1 / 2012-06-25
- Initial release

View file

@ -0,0 +1,18 @@
Copyright 2012 Irakli Gozalishvili. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

View file

@ -0,0 +1,117 @@
# method
[![Build Status](https://secure.travis-ci.org/Gozala/method.png)](http://travis-ci.org/Gozala/method)
Library provides an API for defining polymorphic methods that dispatch on the
first argument type. This provides a powerful way for decouple abstraction
interface definition from an actual implementation per type, without risks
of interference with other libraries.
### Motivation
- Provide a high-performance, dynamic polymorphism construct as an
alternative to existing object methods that does not provides any
mechanics for guarding against name conflicts.
- Allow independent extension of types, and implementations of methods
on types, by different parties.
## Install
npm install method
## Use
```js
var method = require("method")
// Define `isWatchable` method that can be implemented for any type.
var isWatchable = method("isWatchable")
// If you call it on any object it will
// throw as nothing implements that method yet.
//isWatchable({}) // => Exception: method is not implemented
// If you define private method on `Object.prototype`
// all objects will inherit it.
Object.prototype[isWatchable] = function() {
return false;
}
isWatchable({}) // => false
// Although `isWatchable` property above will be enumerable and there for
// may damage some assumbtions made by other libraries. There for it"s
// recomended to use built-in helpers methods that will define extension
// without breaking assumbtions made by other libraries:
isWatchable.define(Object, function() { return false })
// There are primitive types in JS that won"t inherit methods from Object:
isWatchable(null) // => Exception: method is not implemented
// One could either implement methods for such types:
isWatchable.define(null, function() { return false })
isWatchable.define(undefined, function() { return false })
// Or simply define default implementation:
isWatchable.define(function() { return false })
// Alternatively default implementation may be provided at creation:
isWatchable = method(function() { return false })
// Method dispatches on an first argument type. That allows us to create
// new types with an alternative implementations:
function Watchable() {}
isWatchable.define(Watchable, function() { return true })
// This will make all `Watchable` instances watchable!
isWatchable(new Watchable()) // => true
// Arbitrary objects can also be extended to implement given method. For example
// any object can simply made watchable:
function watchable(object) {
return isWatchable.implement(objct, function() { return true })
}
isWatchable(watchable({})) // => true
// Full protocols can be defined with such methods:
var observers = "observers@" + module.filename
var watchers = method("watchers")
var watch = method("watch")
var unwatch = method("unwatch")
watchers.define(Watchable, function(target) {
return target[observers] || (target[observers] = [])
})
watch.define(Watchable, function(target, watcher) {
var observers = watchers(target)
if (observers.indexOf(watcher) < 0) observers.push(watcher)
return target
})
unwatch.define(Watchable, function(target, watcher) {
var observers = watchers(target)
var index = observers.indexOf(watcher)
if (observers.indexOf(watcher) >= 0) observers.unshift(watcher)
return target
})
// Define type Port that inherits form Watchable
function Port() {}
Port.prototype = Object.create(Watchable.prototype)
var emit = method("emit")
emit.define(Port, function(port, message) {
watchers(port).slice().forEach(function(watcher) {
watcher(message)
})
})
var p = new Port()
watch(p, console.log)
emit(p, "hello world") // => info: "hello world"
```

View file

@ -0,0 +1,225 @@
"use strict";
var defineProperty = Object.defineProperty || function(object, name, property) {
object[name] = property.value
return object
}
// Shortcut for `Object.prototype.toString` for faster access.
var typefy = Object.prototype.toString
// Map to for jumping from typeof(value) to associated type prefix used
// as a hash in the map of builtin implementations.
var types = { "function": "Object", "object": "Object" }
// Array is used to save method implementations for the host objects in order
// to avoid extending them with non-primitive values that could cause leaks.
var host = []
// Hash map is used to save method implementations for builtin types in order
// to avoid extending their prototypes. This also allows to share method
// implementations for types across diff contexts / frames / compartments.
var builtin = {}
function Primitive() {}
function ObjectType() {}
ObjectType.prototype = new Primitive()
function ErrorType() {}
ErrorType.prototype = new ObjectType()
var Default = builtin.Default = Primitive.prototype
var Null = builtin.Null = new Primitive()
var Void = builtin.Void = new Primitive()
builtin.String = new Primitive()
builtin.Number = new Primitive()
builtin.Boolean = new Primitive()
builtin.Object = ObjectType.prototype
builtin.Error = ErrorType.prototype
builtin.EvalError = new ErrorType()
builtin.InternalError = new ErrorType()
builtin.RangeError = new ErrorType()
builtin.ReferenceError = new ErrorType()
builtin.StopIteration = new ErrorType()
builtin.SyntaxError = new ErrorType()
builtin.TypeError = new ErrorType()
builtin.URIError = new ErrorType()
function Method(hint) {
/**
Private Method is a callable private name that dispatches on the first
arguments same named Method:
method(object, ...rest) => object[method](...rest)
Optionally hint string may be provided that will be used in generated names
to ease debugging.
## Example
var foo = Method()
// Implementation for any types
foo.define(function(value, arg1, arg2) {
// ...
})
// Implementation for a specific type
foo.define(BarType, function(bar, arg1, arg2) {
// ...
})
**/
// Create an internal unique name if `hint` is provided it is used to
// prefix name to ease debugging.
var name = (hint || "") + "#" + Math.random().toString(32).substr(2)
function dispatch(value) {
// Method dispatches on type of the first argument.
// If first argument is `null` or `void` associated implementation is
// looked up in the `builtin` hash where implementations for built-ins
// are stored.
var type = null
var method = value === null ? Null[name] :
value === void(0) ? Void[name] :
// Otherwise attempt to use method with a generated private
// `name` that is supposedly in the prototype chain of the
// `target`.
value[name] ||
// Otherwise assume it's one of the built-in type instances,
// in which case implementation is stored in a `builtin` hash.
// Attempt to find a implementation for the given built-in
// via constructor name and method name.
((type = builtin[(value.constructor || "").name]) &&
type[name]) ||
// Otherwise assume it's a host object. For host objects
// actual method implementations are stored in the `host`
// array and only index for the implementation is stored
// in the host object's prototype chain. This avoids memory
// leaks that otherwise could happen when saving JS objects
// on host object.
host[value["!" + name] || void(0)] ||
// Otherwise attempt to lookup implementation for builtins by
// a type of the value. This basically makes sure that all
// non primitive values will delegate to an `Object`.
((type = builtin[types[typeof(value)]]) && type[name])
// If method implementation for the type is still not found then
// just fallback for default implementation.
method = method || Default[name]
// If implementation is still not found (which also means there is no
// default) just throw an error with a descriptive message.
if (!method) throw TypeError("Type does not implements method: " + name)
// If implementation was found then just delegate.
return method.apply(method, arguments)
}
// Make `toString` of the dispatch return a private name, this enables
// method definition without sugar:
//
// var method = Method()
// object[method] = function() { /***/ }
dispatch.toString = function toString() { return name }
// Copy utility methods for convenient API.
dispatch.implement = implementMethod
dispatch.define = defineMethod
return dispatch
}
// Create method shortcuts form functions.
var defineMethod = function defineMethod(Type, lambda) {
return define(this, Type, lambda)
}
var implementMethod = function implementMethod(object, lambda) {
return implement(this, object, lambda)
}
// Define `implement` and `define` polymorphic methods to allow definitions
// and implementations through them.
var implement = Method("implement")
var define = Method("define")
function _implement(method, object, lambda) {
/**
Implements `Method` for the given `object` with a provided `implementation`.
Calling `Method` with `object` as a first argument will dispatch on provided
implementation.
**/
return defineProperty(object, method.toString(), {
enumerable: false,
configurable: false,
writable: false,
value: lambda
})
}
function _define(method, Type, lambda) {
/**
Defines `Method` for the given `Type` with a provided `implementation`.
Calling `Method` with a first argument of this `Type` will dispatch on
provided `implementation`. If `Type` is a `Method` default implementation
is defined. If `Type` is a `null` or `undefined` `Method` is implemented
for that value type.
**/
// Attempt to guess a type via `Object.prototype.toString.call` hack.
var type = Type && typefy.call(Type.prototype)
// If only two arguments are passed then `Type` is actually an implementation
// for a default type.
if (!lambda) Default[method] = Type
// If `Type` is `null` or `void` store implementation accordingly.
else if (Type === null) Null[method] = lambda
else if (Type === void(0)) Void[method] = lambda
// If `type` hack indicates built-in type and type has a name us it to
// store a implementation into associated hash. If hash for this type does
// not exists yet create one.
else if (type !== "[object Object]" && Type.name) {
var Bulitin = builtin[Type.name] || (builtin[Type.name] = new ObjectType())
Bulitin[method] = lambda
}
// If `type` hack indicates an object, that may be either object or any
// JS defined "Class". If name of the constructor is `Object`, assume it's
// built-in `Object` and store implementation accordingly.
else if (Type.name === "Object")
builtin.Object[method] = lambda
// Host objects are pain!!! Every browser does some crazy stuff for them
// So far all browser seem to not implement `call` method for host object
// constructors. If that is a case here, assume it's a host object and
// store implementation in a `host` array and store `index` in the array
// in a `Type.prototype` itself. This avoids memory leaks that could be
// caused by storing JS objects on a host objects.
else if (Type.call === void(0)) {
var index = host.indexOf(lambda)
if (index < 0) index = host.push(lambda) - 1
// Prefix private name with `!` so it can be dispatched from the method
// without type checks.
implement("!" + method, Type.prototype, index)
}
// If Got that far `Type` is user defined JS `Class`. Define private name
// as hidden property on it's prototype.
else
implement(method, Type.prototype, lambda)
}
// And provided implementations for a polymorphic equivalents.
_define(define, _define)
_define(implement, _implement)
// Define exports on `Method` as it's only thing being exported.
Method.implement = implement
Method.define = define
Method.Method = Method
Method.method = Method
Method.builtin = builtin
Method.host = host
module.exports = Method

View file

@ -0,0 +1,41 @@
{
"name": "method",
"id": "method",
"version": "1.0.2",
"description": "Functional polymorphic method dispatch",
"keywords": [
"method",
"dispatch",
"protocol",
"polymorphism",
"type dispatch"
],
"author": "Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)",
"homepage": "https://github.com/Gozala/method",
"main": "./core.js",
"repository": {
"type": "git",
"url": "https://github.com/Gozala/method.git",
"web": "https://github.com/Gozala/method"
},
"bugs": {
"url": "http://github.com/Gozala/method/issues/"
},
"devDependencies": {
"test": "~0.x.0",
"repl-utils": "~2.0.1",
"phantomify": "~0.1.0"
},
"scripts": {
"test": "npm run test-node && npm run test-browser",
"test-browser": "node ./node_modules/phantomify/bin/cmd.js ./test/browser.js",
"test-node": "node ./test/common.js",
"repl": "node node_modules/repl-utils"
},
"licenses": [
{
"type": "MIT",
"url": "https://github.com/Gozala/method/License.md"
}
]
}

View file

@ -0,0 +1,20 @@
"use strict";
exports["test common"] = require("./common")
var Method = require("../core")
exports["test host objects"] = function(assert) {
var isElement = Method("is-element")
isElement.define(function() { return false })
isElement.define(Element, function() { return true })
assert.notDeepEqual(typeof(Element.prototype[isElement]), "number",
"Host object's prototype is extended with a number value")
assert.ok(!isElement({}), "object is not an Element")
assert.ok(document.createElement("div"), "Element is an element")
}
require("test").run(exports)

View file

@ -0,0 +1,272 @@
"use strict";
var Method = require("../core")
function type(value) {
return Object.prototype.toString.call(value).
split(" ").
pop().
split("]").
shift().
toLowerCase()
}
var values = [
null, // 0
undefined, // 1
Infinity, // 2
NaN, // 3
5, // 4
{}, // 5
Object.create({}), // 6
Object.create(null), // 7
[], // 8
/foo/, // 9
new Date(), // 10
Function, // 11
function() {}, // 12
true, // 13
false, // 14
"string" // 15
]
function True() { return true }
function False() { return false }
var trues = values.map(True)
var falses = values.map(False)
exports["test throws if not implemented"] = function(assert) {
var method = Method("nope")
assert.throws(function() {
method({})
}, /not implement/i, "method throws if not implemented")
assert.throws(function() {
method(null)
}, /not implement/i, "method throws on null")
}
exports["test all types inherit from default"] = function(assert) {
var isImplemented = Method("isImplemented")
isImplemented.define(function() { return true })
values.forEach(function(value) {
assert.ok(isImplemented(value),
type(value) + " inherits deafult implementation")
})
}
exports["test default can be implemented later"] = function(assert) {
var isImplemented = Method("isImplemented")
isImplemented.define(function() {
return true
})
values.forEach(function(value) {
assert.ok(isImplemented(value),
type(value) + " inherits deafult implementation")
})
}
exports["test dispatch not-implemented"] = function(assert) {
var isDefault = Method("isDefault")
values.forEach(function(value) {
assert.throws(function() {
isDefault(value)
}, /not implement/, type(value) + " throws if not implemented")
})
}
exports["test dispatch default"] = function(assert) {
var isDefault = Method("isDefault")
// Implement default
isDefault.define(True)
assert.deepEqual(values.map(isDefault), trues,
"all implementation inherit from default")
}
exports["test dispatch null"] = function(assert) {
var isNull = Method("isNull")
// Implement default
isNull.define(False)
isNull.define(null, True)
assert.deepEqual(values.map(isNull),
[ true ].
concat(falses.slice(1)),
"only null gets methods defined for null")
}
exports["test dispatch undefined"] = function(assert) {
var isUndefined = Method("isUndefined")
// Implement default
isUndefined.define(False)
isUndefined.define(undefined, True)
assert.deepEqual(values.map(isUndefined),
[ false, true ].
concat(falses.slice(2)),
"only undefined gets methods defined for undefined")
}
exports["test dispatch object"] = function(assert) {
var isObject = Method("isObject")
// Implement default
isObject.define(False)
isObject.define(Object, True)
assert.deepEqual(values.map(isObject),
[ false, false, false, false, false ].
concat(trues.slice(5, 13)).
concat([false, false, false]),
"all values except primitives inherit Object methods")
}
exports["test dispatch number"] = function(assert) {
var isNumber = Method("isNumber")
isNumber.define(False)
isNumber.define(Number, True)
assert.deepEqual(values.map(isNumber),
falses.slice(0, 2).
concat(true, true, true).
concat(falses.slice(5)),
"all numbers inherit from Number method")
}
exports["test dispatch string"] = function(assert) {
var isString = Method("isString")
isString.define(False)
isString.define(String, True)
assert.deepEqual(values.map(isString),
falses.slice(0, 15).
concat(true),
"all strings inherit from String method")
}
exports["test dispatch function"] = function(assert) {
var isFunction = Method("isFunction")
isFunction.define(False)
isFunction.define(Function, True)
assert.deepEqual(values.map(isFunction),
falses.slice(0, 11).
concat(true, true).
concat(falses.slice(13)),
"all functions inherit from Function method")
}
exports["test dispatch date"] = function(assert) {
var isDate = Method("isDate")
isDate.define(False)
isDate.define(Date, True)
assert.deepEqual(values.map(isDate),
falses.slice(0, 10).
concat(true).
concat(falses.slice(11)),
"all dates inherit from Date method")
}
exports["test dispatch RegExp"] = function(assert) {
var isRegExp = Method("isRegExp")
isRegExp.define(False)
isRegExp.define(RegExp, True)
assert.deepEqual(values.map(isRegExp),
falses.slice(0, 9).
concat(true).
concat(falses.slice(10)),
"all regexps inherit from RegExp method")
}
exports["test redefine for descendant"] = function(assert) {
var isFoo = Method("isFoo")
var ancestor = {}
isFoo.implement(ancestor, function() { return true })
var descendant = Object.create(ancestor)
isFoo.implement(descendant, function() { return false })
assert.ok(isFoo(ancestor), "defined on ancestor")
assert.ok(!isFoo(descendant), "overrided for descendant")
}
exports["test on custom types"] = function(assert) {
function Bar() {}
var isBar = Method("isBar")
isBar.define(function() { return false })
isBar.define(Bar, function() { return true })
assert.ok(!isBar({}), "object is get's default implementation")
assert.ok(isBar(new Bar()), "Foo type objects get own implementation")
var isObject = Method("isObject")
isObject.define(function() { return false })
isObject.define(Object, function() { return true })
assert.ok(isObject(new Bar()), "foo inherits implementation from object")
isObject.define(Bar, function() { return false })
assert.ok(!isObject(new Bar()),
"implementation inherited form object can be overrided")
}
exports["test error types"] = function(assert) {
var isError = Method("isError")
isError.define(function() { return false })
isError.define(Error, function() { return true })
assert.ok(isError(Error("boom")), "error is error")
assert.ok(isError(TypeError("boom")), "type error is an error")
assert.ok(isError(EvalError("boom")), "eval error is an error")
assert.ok(isError(RangeError("boom")), "range error is an error")
assert.ok(isError(ReferenceError("boom")), "reference error is an error")
assert.ok(isError(SyntaxError("boom")), "syntax error is an error")
assert.ok(isError(URIError("boom")), "URI error is an error")
}
exports["test override define polymorphic method"] = function(assert) {
var define = Method.define
var implement = Method.implement
var fn = Method("fn")
var methods = {}
implement(define, fn, function(method, label, implementation) {
methods[label] = implementation
})
function foo() {}
define(fn, "foo-case", foo)
assert.equal(methods["foo-case"], foo, "define set property")
}
exports["test override define via method API"] = function(assert) {
var define = Method.define
var implement = Method.implement
var fn = Method("fn")
var methods = {}
define.implement(fn, function(method, label, implementation) {
methods[label] = implementation
})
function foo() {}
define(fn, "foo-case", foo)
assert.equal(methods["foo-case"], foo, "define set property")
}
require("test").run(exports)

View file

@ -0,0 +1,57 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["Startup"];
var { utils: Cu, interfaces: Ci, classes: Cc } = Components;
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const { defer } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const { XulApp } = Cu.import("resource://gre/modules/commonjs/sdk/system/xul-app.jsm", {});
const appStartupSrv = Cc["@mozilla.org/toolkit/app-startup;1"]
.getService(Ci.nsIAppStartup);
const NAME2TOPIC = {
'Firefox': 'sessionstore-windows-restored',
'Fennec': 'sessionstore-windows-restored',
'SeaMonkey': 'sessionstore-windows-restored',
'Thunderbird': 'mail-startup-done',
'Instantbird': 'xul-window-visible'
};
var Startup = {
initialized: !appStartupSrv.startingUp
};
var exports = Startup;
var gOnceInitializedDeferred = defer();
exports.onceInitialized = gOnceInitializedDeferred.promise;
// Set 'final-ui-startup' as default topic for unknown applications
var appStartup = 'final-ui-startup';
if (Startup.initialized) {
gOnceInitializedDeferred.resolve()
}
else {
// Gets the topic that fit best as application startup event, in according with
// the current application (e.g. Firefox, Fennec, Thunderbird...)
for (let name of Object.keys(NAME2TOPIC)) {
if (XulApp.is(name)) {
appStartup = NAME2TOPIC[name];
break;
}
}
let listener = function (subject, topic) {
Services.obs.removeObserver(this, topic);
Startup.initialized = true;
Services.tm.currentThread.dispatch(() => gOnceInitializedDeferred.resolve(),
Ci.nsIThread.DISPATCH_NORMAL);
}
Services.obs.addObserver(listener, appStartup, false);
}

View file

@ -0,0 +1,9 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
EXTRA_JS_MODULES.sdk.system += [
'Startup.js',
]

481
toolkit/jetpack/moz.build Normal file
View file

@ -0,0 +1,481 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
EXTRA_JS_MODULES.sdk += [
'app-extension/bootstrap.js',
]
EXTRA_JS_MODULES.sdk.system += [
'modules/system/Startup.js',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk":
EXTRA_JS_MODULES.commonjs.method.test += [
'method/test/browser.js',
'method/test/common.js',
]
EXTRA_JS_MODULES.commonjs.sdk.deprecated += [
'sdk/deprecated/api-utils.js',
'sdk/deprecated/sync-worker.js',
'sdk/deprecated/unit-test-finder.js',
'sdk/deprecated/unit-test.js',
'sdk/deprecated/window-utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.frame += [
'sdk/frame/hidden-frame.js',
'sdk/frame/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.panel += [
'sdk/panel/events.js',
'sdk/panel/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.places += [
'sdk/places/bookmarks.js',
'sdk/places/contract.js',
'sdk/places/events.js',
'sdk/places/favicon.js',
'sdk/places/history.js',
'sdk/places/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.places.host += [
'sdk/places/host/host-bookmarks.js',
'sdk/places/host/host-query.js',
'sdk/places/host/host-tags.js',
]
EXTRA_JS_MODULES.commonjs.sdk.tabs += [
'sdk/tabs/common.js',
'sdk/tabs/events.js',
'sdk/tabs/helpers.js',
'sdk/tabs/namespace.js',
'sdk/tabs/observer.js',
'sdk/tabs/tab-fennec.js',
'sdk/tabs/tab-firefox.js',
'sdk/tabs/tab.js',
'sdk/tabs/tabs-firefox.js',
'sdk/tabs/utils.js',
'sdk/tabs/worker.js',
]
EXTRA_JS_MODULES.commonjs.sdk.test += [
'sdk/test/assert.js',
'sdk/test/harness.js',
'sdk/test/httpd.js',
'sdk/test/loader.js',
'sdk/test/memory.js',
'sdk/test/options.js',
'sdk/test/runner.js',
'sdk/test/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.ui += [
'sdk/ui/component.js',
'sdk/ui/frame.js',
'sdk/ui/id.js',
'sdk/ui/sidebar.js',
'sdk/ui/state.js',
'sdk/ui/toolbar.js',
]
EXTRA_JS_MODULES.commonjs.sdk.ui.button += [
'sdk/ui/button/action.js',
'sdk/ui/button/contract.js',
'sdk/ui/button/toggle.js',
'sdk/ui/button/view.js',
]
EXTRA_JS_MODULES.commonjs.sdk.ui.sidebar += [
'sdk/ui/sidebar/actions.js',
'sdk/ui/sidebar/contract.js',
'sdk/ui/sidebar/namespace.js',
'sdk/ui/sidebar/utils.js',
'sdk/ui/sidebar/view.js',
]
EXTRA_JS_MODULES.commonjs.sdk.window += [
'sdk/window/browser.js',
'sdk/window/events.js',
'sdk/window/helpers.js',
'sdk/window/namespace.js',
'sdk/window/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.windows += [
'sdk/windows/fennec.js',
'sdk/windows/firefox.js',
'sdk/windows/observer.js',
'sdk/windows/tabs-fennec.js',
]
EXTRA_JS_MODULES.commonjs += [
'index.js',
'test.js',
]
EXTRA_JS_MODULES.commonjs.sdk += [
'sdk/webextension.js',
]
EXTRA_JS_MODULES.commonjs.dev += [
'dev/debuggee.js',
'dev/frame-script.js',
'dev/panel.js',
'dev/ports.js',
'dev/theme.js',
'dev/toolbox.js',
'dev/utils.js',
'dev/volcan.js',
]
EXTRA_JS_MODULES.commonjs.dev.panel += [
'dev/panel/view.js',
]
EXTRA_JS_MODULES.commonjs.dev.theme += [
'dev/theme/hooks.js',
]
EXTRA_JS_MODULES.commonjs.diffpatcher += [
'diffpatcher/diff.js',
'diffpatcher/index.js',
'diffpatcher/patch.js',
'diffpatcher/rebase.js',
]
EXTRA_JS_MODULES.commonjs.diffpatcher.test += [
'diffpatcher/test/common.js',
'diffpatcher/test/diff.js',
'diffpatcher/test/index.js',
'diffpatcher/test/patch.js',
'diffpatcher/test/tap.js',
]
EXTRA_JS_MODULES.commonjs.framescript += [
'framescript/content.jsm',
'framescript/context-menu.js',
'framescript/FrameScriptManager.jsm',
'framescript/manager.js',
'framescript/util.js',
]
EXTRA_JS_MODULES.commonjs['jetpack-id'] += [
'jetpack-id/index.js',
]
EXTRA_JS_MODULES.commonjs.method += [
'method/core.js',
]
EXTRA_JS_MODULES.commonjs['mozilla-toolkit-versioning'] += [
'mozilla-toolkit-versioning/index.js',
]
EXTRA_JS_MODULES.commonjs['mozilla-toolkit-versioning'].lib += [
'mozilla-toolkit-versioning/lib/utils.js',
]
EXTRA_JS_MODULES.commonjs.node += [
'node/os.js',
]
EXTRA_JS_MODULES.commonjs.sdk += [
'sdk/base64.js',
'sdk/clipboard.js',
'sdk/context-menu.js',
'sdk/context-menu@2.js',
'sdk/hotkeys.js',
'sdk/indexed-db.js',
'sdk/l10n.js',
'sdk/messaging.js',
'sdk/notifications.js',
'sdk/page-mod.js',
'sdk/page-worker.js',
'sdk/panel.js',
'sdk/passwords.js',
'sdk/private-browsing.js',
'sdk/querystring.js',
'sdk/request.js',
'sdk/selection.js',
'sdk/self.js',
'sdk/simple-prefs.js',
'sdk/simple-storage.js',
'sdk/system.js',
'sdk/tabs.js',
'sdk/test.js',
'sdk/timers.js',
'sdk/ui.js',
'sdk/url.js',
'sdk/windows.js',
]
EXTRA_JS_MODULES.commonjs.sdk.addon += [
'sdk/addon/bootstrap.js',
'sdk/addon/events.js',
'sdk/addon/host.js',
'sdk/addon/installer.js',
'sdk/addon/manager.js',
'sdk/addon/runner.js',
'sdk/addon/window.js',
]
EXTRA_JS_MODULES.commonjs.sdk.browser += [
'sdk/browser/events.js',
]
EXTRA_JS_MODULES.commonjs.sdk.console += [
'sdk/console/plain-text.js',
'sdk/console/traceback.js',
]
EXTRA_JS_MODULES.commonjs.sdk.content += [
'sdk/content/content-worker.js',
'sdk/content/content.js',
'sdk/content/context-menu.js',
'sdk/content/events.js',
'sdk/content/l10n-html.js',
'sdk/content/loader.js',
'sdk/content/mod.js',
'sdk/content/page-mod.js',
'sdk/content/page-worker.js',
'sdk/content/sandbox.js',
'sdk/content/tab-events.js',
'sdk/content/thumbnail.js',
'sdk/content/utils.js',
'sdk/content/worker-child.js',
'sdk/content/worker.js',
]
EXTRA_JS_MODULES.commonjs.sdk.content.sandbox += [
'sdk/content/sandbox/events.js',
]
EXTRA_JS_MODULES.commonjs.sdk['context-menu'] += [
'sdk/context-menu/context.js',
'sdk/context-menu/core.js',
'sdk/context-menu/readers.js',
]
EXTRA_JS_MODULES.commonjs.sdk.core += [
'sdk/core/disposable.js',
'sdk/core/heritage.js',
'sdk/core/namespace.js',
'sdk/core/observer.js',
'sdk/core/promise.js',
'sdk/core/reference.js',
]
EXTRA_JS_MODULES.commonjs.sdk.deprecated.events += [
'sdk/deprecated/events/assembler.js',
]
EXTRA_JS_MODULES.commonjs.sdk.dom += [
'sdk/dom/events-shimmed.js',
'sdk/dom/events.js',
]
EXTRA_JS_MODULES.commonjs.sdk.dom.events += [
'sdk/dom/events/keys.js',
]
EXTRA_JS_MODULES.commonjs.sdk.event += [
'sdk/event/chrome.js',
'sdk/event/core.js',
'sdk/event/dom.js',
'sdk/event/target.js',
'sdk/event/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.fs += [
'sdk/fs/path.js',
]
EXTRA_JS_MODULES.commonjs.sdk.input += [
'sdk/input/browser.js',
'sdk/input/customizable-ui.js',
'sdk/input/frame.js',
'sdk/input/system.js',
]
EXTRA_JS_MODULES.commonjs.sdk.io += [
'sdk/io/buffer.js',
'sdk/io/byte-streams.js',
'sdk/io/file.js',
'sdk/io/fs.js',
'sdk/io/stream.js',
'sdk/io/text-streams.js',
]
EXTRA_JS_MODULES.commonjs.sdk.keyboard += [
'sdk/keyboard/hotkeys.js',
'sdk/keyboard/observer.js',
'sdk/keyboard/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.l10n += [
'sdk/l10n/core.js',
'sdk/l10n/html.js',
'sdk/l10n/loader.js',
'sdk/l10n/locale.js',
'sdk/l10n/plural-rules.js',
'sdk/l10n/prefs.js',
]
EXTRA_JS_MODULES.commonjs.sdk.l10n.json += [
'sdk/l10n/json/core.js',
]
EXTRA_JS_MODULES.commonjs.sdk.l10n.properties += [
'sdk/l10n/properties/core.js',
]
EXTRA_JS_MODULES.commonjs.sdk.lang += [
'sdk/lang/functional.js',
'sdk/lang/type.js',
'sdk/lang/weak-set.js',
]
EXTRA_JS_MODULES.commonjs.sdk.lang.functional += [
'sdk/lang/functional/concurrent.js',
'sdk/lang/functional/core.js',
'sdk/lang/functional/helpers.js',
]
EXTRA_JS_MODULES.commonjs.sdk.loader += [
'sdk/loader/cuddlefish.js',
'sdk/loader/sandbox.js',
]
EXTRA_JS_MODULES.commonjs.sdk.model += [
'sdk/model/core.js',
]
EXTRA_JS_MODULES.commonjs.sdk.net += [
'sdk/net/url.js',
'sdk/net/xhr.js',
]
EXTRA_JS_MODULES.commonjs.sdk.output += [
'sdk/output/system.js',
]
EXTRA_JS_MODULES.commonjs.sdk['page-mod'] += [
'sdk/page-mod/match-pattern.js',
]
EXTRA_JS_MODULES.commonjs.sdk.passwords += [
'sdk/passwords/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.platform += [
'sdk/platform/xpcom.js',
]
EXTRA_JS_MODULES.commonjs.sdk.preferences += [
'sdk/preferences/event-target.js',
'sdk/preferences/native-options.js',
'sdk/preferences/service.js',
'sdk/preferences/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk['private-browsing'] += [
'sdk/private-browsing/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.remote += [
'sdk/remote/child.js',
'sdk/remote/core.js',
'sdk/remote/parent.js',
'sdk/remote/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.stylesheet += [
'sdk/stylesheet/style.js',
'sdk/stylesheet/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.system += [
'sdk/system/child_process.js',
'sdk/system/environment.js',
'sdk/system/events-shimmed.js',
'sdk/system/events.js',
'sdk/system/globals.js',
'sdk/system/process.js',
'sdk/system/runtime.js',
'sdk/system/unload.js',
'sdk/system/xul-app.js',
'sdk/system/xul-app.jsm',
]
EXTRA_JS_MODULES.commonjs.sdk.system.child_process += [
'sdk/system/child_process/subprocess.js',
]
EXTRA_JS_MODULES.commonjs.sdk.tab += [
'sdk/tab/events.js',
]
EXTRA_JS_MODULES.commonjs.sdk.ui.button.view += [
'sdk/ui/button/view/events.js',
]
EXTRA_JS_MODULES.commonjs.sdk.ui.frame += [
'sdk/ui/frame/model.js',
'sdk/ui/frame/view.html',
'sdk/ui/frame/view.js',
]
EXTRA_JS_MODULES.commonjs.sdk.ui.state += [
'sdk/ui/state/events.js',
]
EXTRA_JS_MODULES.commonjs.sdk.ui.toolbar += [
'sdk/ui/toolbar/model.js',
'sdk/ui/toolbar/view.js',
]
EXTRA_JS_MODULES.commonjs.sdk.uri += [
'sdk/uri/resource.js',
]
EXTRA_JS_MODULES.commonjs.sdk.url += [
'sdk/url/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.util += [
'sdk/util/array.js',
'sdk/util/collection.js',
'sdk/util/contract.js',
'sdk/util/deprecate.js',
'sdk/util/dispatcher.js',
'sdk/util/list.js',
'sdk/util/match-pattern.js',
'sdk/util/object.js',
'sdk/util/rules.js',
'sdk/util/sequence.js',
'sdk/util/uuid.js',
]
EXTRA_JS_MODULES.commonjs.sdk.view += [
'sdk/view/core.js',
]
EXTRA_JS_MODULES.commonjs.sdk.worker += [
'sdk/worker/utils.js',
]
EXTRA_JS_MODULES.commonjs.sdk.zip += [
'sdk/zip/utils.js',
]
EXTRA_JS_MODULES.commonjs.toolkit += [
'toolkit/loader.js',
'toolkit/require.js',
]

View file

@ -0,0 +1,112 @@
/* 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/. */
var versionParse = require('./lib/utils').versionParse;
var COMPARATORS = ['>=', '<=', '>', '<', '=', '~', '^'];
exports.parse = function (input) {
input = input || '';
input = input.trim();
if (!input)
throw new Error('`parse` argument must be a populated string.');
// Handle the "*" case
if (input === "*") {
return { min: undefined, max: undefined };
}
var inputs = input.split(' ');
var min;
var max;
// 1.2.3 - 2.3.4
if (inputs.length === 3 && inputs[1] === '-') {
return { min: inputs[0], max: inputs[2] };
}
inputs.forEach(function (input) {
var parsed = parseExpression(input);
var version = parsed.version;
var comparator = parsed.comparator;
// 1.2.3
if (inputs.length === 1 && !comparator)
min = max = version;
// Parse min
if (~comparator.indexOf('>')) {
if (~comparator.indexOf('='))
min = version; // >=1.2.3
else
min = increment(version); // >1.2.3
}
else if (~comparator.indexOf('<')) {
if (~comparator.indexOf('='))
max = version; // <=1.2.3
else
max = decrement(version); // <1.2.3
}
});
return {
min: min,
max : max
};
};
function parseExpression (input) {
for (var i = 0; i < COMPARATORS.length; i++)
if (~input.indexOf(COMPARATORS[i]))
return {
comparator: COMPARATORS[i],
version: input.substr(COMPARATORS[i].length)
};
return { version: input, comparator: '' };
}
/**
* Takes a version string ('1.2.3') and returns a version string
* that'll parse as one less than the input string ('1.2.3.-1').
*
* @param {String} vString
* @return {String}
*/
function decrement (vString) {
return vString + (vString.charAt(vString.length - 1) === '.' ? '' : '.') + '-1';
}
exports.decrement = decrement;
/**
* Takes a version string ('1.2.3') and returns a version string
* that'll parse as greater than the input string by the smallest margin
* possible ('1.2.3.1').
* listed as number-A, string-B, number-C, string-D in
* Mozilla's Toolkit Format.
* https://developer.mozilla.org/en-US/docs/Toolkit_version_format
*
* @param {String} vString
* @return {String}
*/
function increment (vString) {
var match = versionParse(vString);
var a = match[1];
var b = match[2];
var c = match[3];
var d = match[4];
var lastPos = vString.length - 1;
var lastChar = vString.charAt(lastPos);
if (!b) {
return vString + (lastChar === '.' ? '' : '.') + '1';
}
if (!c) {
return vString + '1';
}
if (!d) {
return vString.substr(0, lastPos) + (++lastChar);
}
return vString.substr(0, lastPos) + String.fromCharCode(lastChar.charCodeAt(0) + 1);
}
exports.increment = increment;

View file

@ -0,0 +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/. */
/**
* Breaks up a version string into the 4 components
* defined in:
* https://developer.mozilla.org/en-US/docs/Toolkit_version_format
* @params {String} val
* @return {String}
*/
function versionParse (val) {
return val.match(/^([0-9\.]*)([a-zA-Z]*)([0-9\.]*)([a-zA-Z]*)$/);
}
exports.versionParse = versionParse;

View file

@ -0,0 +1,21 @@
{
"name": "mozilla-toolkit-versioning",
"version": "0.0.2",
"description": "Parser for Mozilla's toolkit version format",
"main": "index.js",
"scripts": {
"test": "./node_modules/.bin/mocha --reporter spec --ui bdd"
},
"repository": {
"type": "git",
"url": "git://github.com/jsantell/mozilla-toolkit-versioning.git"
},
"author": "Jordan Santell",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"mocha": "^1.21.4",
"chai": "^1.9.1",
"mozilla-version-comparator": "^1.0.2"
}
}

View file

@ -0,0 +1,90 @@
/* 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 { Cc, Ci } = require('chrome');
const system = require('../sdk/system');
const runtime = require('../sdk/system/runtime');
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
const isWindows = system.platform === 'win32';
const endianness = ((new Uint32Array((new Uint8Array([1,2,3,4])).buffer))[0] === 0x04030201) ? 'LE' : 'BE';
XPCOMUtils.defineLazyGetter(this, "oscpu", () => {
try {
return Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
} catch (e) {
return "";
}
});
XPCOMUtils.defineLazyGetter(this, "hostname", () => {
try {
// On some platforms (Linux according to try), this service does not exist and fails.
return Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService).myHostName;
} catch (e) {
return "";
}
});
/**
* Returns a path to a temp directory
*/
exports.tmpdir = () => system.pathFor('TmpD');
/**
* Returns the endianness of the architecture: either 'LE' or 'BE'
*/
exports.endianness = () => endianness;
/**
* Returns hostname of the machine
*/
exports.hostname = () => hostname;
/**
* Name of the OS type
* Possible values:
* https://developer.mozilla.org/en/OS_TARGET
*/
exports.type = () => runtime.OS;
/**
* Name of the OS Platform in lower case string.
* Possible values:
* https://developer.mozilla.org/en/OS_TARGET
*/
exports.platform = () => system.platform;
/**
* Type of processor architecture running:
* 'arm', 'ia32', 'x86', 'x64'
*/
exports.arch = () => system.architecture;
/**
* Returns the operating system release.
*/
exports.release = () => {
let match = oscpu.match(/(\d[\.\d]*)/);
return match && match.length > 1 ? match[1] : oscpu;
};
/**
* Returns EOL character for the OS
*/
exports.EOL = isWindows ? '\r\n' : '\n';
/**
* Returns [0, 0, 0], as this is not implemented.
*/
exports.loadavg = () => [0, 0, 0];
['uptime', 'totalmem', 'freemem', 'cpus'].forEach(method => {
exports[method] = () => { throw new Error('os.' + method + ' is not supported.'); };
});

182
toolkit/jetpack/sdk/addon/bootstrap.js vendored Normal file
View file

@ -0,0 +1,182 @@
/* 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 { Cu } = require("chrome");
const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
const { Task: { spawn } } = require("resource://gre/modules/Task.jsm");
const { readURI } = require("sdk/net/url");
const { mount, unmount } = require("sdk/uri/resource");
const { setTimeout } = require("sdk/timers");
const { Loader, Require, Module, main, unload } = require("toolkit/loader");
const prefs = require("sdk/preferences/service");
// load below now, so that it can be used by sdk/addon/runner
// see bug https://bugzilla.mozilla.org/show_bug.cgi?id=1042239
const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {});
const REASON = [ "unknown", "startup", "shutdown", "enable", "disable",
"install", "uninstall", "upgrade", "downgrade" ];
const UUID_PATTERN = /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/;
// Takes add-on ID and normalizes it to a domain name so that add-on
// can be mapped to resource://domain/
const readDomain = id =>
// If only `@` character is the first one, than just substract it,
// otherwise fallback to legacy normalization code path. Note: `.`
// is valid character for resource substitutaiton & we intend to
// make add-on URIs intuitive, so it's best to just stick to an
// add-on author typed input.
id.lastIndexOf("@") === 0 ? id.substr(1).toLowerCase() :
id.toLowerCase().
replace(/@/g, "-at-").
replace(/\./g, "-dot-").
replace(UUID_PATTERN, "$1");
const readPaths = id => {
const base = `extensions.modules.${id}.path.`;
const domain = readDomain(id);
return prefs.keys(base).reduce((paths, key) => {
const value = prefs.get(key);
const name = key.replace(base, "");
const path = name.split(".").join("/");
const prefix = path.length ? `${path}/` : path;
const uri = value.endsWith("/") ? value : `${value}/`;
const root = `extensions.modules.${domain}.commonjs.path.${name}`;
mount(root, uri);
paths[prefix] = `resource://${root}/`;
return paths;
}, {});
};
const Bootstrap = function(mountURI) {
this.mountURI = mountURI;
this.install = this.install.bind(this);
this.uninstall = this.uninstall.bind(this);
this.startup = this.startup.bind(this);
this.shutdown = this.shutdown.bind(this);
};
Bootstrap.prototype = {
constructor: Bootstrap,
mount(domain, rootURI) {
mount(domain, rootURI);
this.domain = domain;
},
unmount() {
if (this.domain) {
unmount(this.domain);
this.domain = null;
}
},
install(addon, reason) {
return new Promise(resolve => resolve());
},
uninstall(addon, reason) {
return new Promise(resolve => {
const {id} = addon;
prefs.reset(`extensions.${id}.sdk.domain`);
prefs.reset(`extensions.${id}.sdk.version`);
prefs.reset(`extensions.${id}.sdk.rootURI`);
prefs.reset(`extensions.${id}.sdk.baseURI`);
prefs.reset(`extensions.${id}.sdk.load.reason`);
resolve();
});
},
startup(addon, reasonCode) {
const { id, version, resourceURI: { spec: addonURI } } = addon;
const rootURI = this.mountURI || addonURI;
const reason = REASON[reasonCode];
const self = this;
return spawn(function*() {
const metadata = JSON.parse(yield readURI(`${rootURI}package.json`));
const domain = readDomain(id);
const baseURI = `resource://${domain}/`;
this.mount(domain, rootURI);
prefs.set(`extensions.${id}.sdk.domain`, domain);
prefs.set(`extensions.${id}.sdk.version`, version);
prefs.set(`extensions.${id}.sdk.rootURI`, rootURI);
prefs.set(`extensions.${id}.sdk.baseURI`, baseURI);
prefs.set(`extensions.${id}.sdk.load.reason`, reason);
const command = prefs.get(`extensions.${id}.sdk.load.command`);
const loader = Loader({
id,
isNative: true,
checkCompatibility: true,
prefixURI: baseURI,
rootURI: baseURI,
name: metadata.name,
paths: Object.assign({
"": "resource://gre/modules/commonjs/",
"devtools/": "resource://devtools/",
"./": baseURI
}, readPaths(id)),
manifest: metadata,
metadata: metadata,
modules: {
"@test/options": {},
},
noQuit: prefs.get(`extensions.${id}.sdk.test.no-quit`, false)
});
self.loader = loader;
const module = Module("package.json", `${baseURI}package.json`);
const require = Require(loader, module);
const main = command === "test" ? "sdk/test/runner" : null;
const prefsURI = `${baseURI}defaults/preferences/prefs.js`;
// Init the 'sdk/webextension' module from the bootstrap addon parameter.
require("sdk/webextension").initFromBootstrapAddonParam(addon);
const { startup } = require("sdk/addon/runner");
startup(reason, {loader, main, prefsURI});
}.bind(this)).catch(error => {
console.error(`Failed to start ${id} addon`, error);
throw error;
});
},
shutdown(addon, code) {
this.unmount();
return this.unload(REASON[code]);
},
unload(reason) {
return new Promise(resolve => {
const { loader } = this;
if (loader) {
this.loader = null;
unload(loader, reason);
setTimeout(() => {
for (let uri of Object.keys(loader.sandboxes)) {
let sandbox = loader.sandboxes[uri];
if (Cu.getClassName(sandbox, true) == "Sandbox")
Cu.nukeSandbox(sandbox);
delete loader.sandboxes[uri];
delete loader.modules[uri];
}
try {
Cu.nukeSandbox(loader.sharedGlobalSandbox);
} catch (e) {
Cu.reportError(e);
}
resolve();
}, 1000);
}
else {
resolve();
}
});
}
};
exports.Bootstrap = Bootstrap;

View file

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.metadata = {
'stability': 'experimental'
};
var { request: hostReq, response: hostRes } = require('./host');
var { defer: async } = require('../lang/functional');
var { defer } = require('../core/promise');
var { emit: emitSync, on, off } = require('../event/core');
var { uuid } = require('../util/uuid');
var emit = async(emitSync);
// Map of IDs to deferreds
var requests = new Map();
// May not be necessary to wrap this in `async`
// once promises are async via bug 881047
var receive = async(function ({data, id, error}) {
let request = requests.get(id);
if (request) {
if (error) request.reject(error);
else request.resolve(clone(data));
requests.delete(id);
}
});
on(hostRes, 'data', receive);
/*
* Send is a helper to be used in client APIs to send
* a request to host
*/
function send (eventName, data) {
let id = uuid();
let deferred = defer();
requests.set(id, deferred);
emit(hostReq, 'data', {
id: id,
data: clone(data),
event: eventName
});
return deferred.promise;
}
exports.send = send;
/*
* Implement internal structured cloning algorithm in the future?
* http://www.whatwg.org/specs/web-apps/current-work/multipage/common-dom-interfaces.html#internal-structured-cloning-algorithm
*/
function clone (obj) {
return JSON.parse(JSON.stringify(obj || {}));
}

View file

@ -0,0 +1,12 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
exports.request = {};
exports.response = {};

View file

@ -0,0 +1,121 @@
/* 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/. */
module.metadata = {
"stability": "experimental"
};
const { Cc, Ci, Cu } = require("chrome");
const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm");
const { defer } = require("../core/promise");
const { setTimeout } = require("../timers");
/**
* `install` method error codes:
*
* https://developer.mozilla.org/en/Addons/Add-on_Manager/AddonManager#AddonInstall_errors
*/
exports.ERROR_NETWORK_FAILURE = AddonManager.ERROR_NETWORK_FAILURE;
exports.ERROR_INCORRECT_HASH = AddonManager.ERROR_INCORRECT_HASH;
exports.ERROR_CORRUPT_FILE = AddonManager.ERROR_CORRUPT_FILE;
exports.ERROR_FILE_ACCESS = AddonManager.ERROR_FILE_ACCESS;
/**
* Immediatly install an addon.
*
* @param {String} xpiPath
* file path to an xpi file to install
* @return {Promise}
* A promise resolved when the addon is finally installed.
* Resolved with addon id as value or rejected with an error code.
*/
exports.install = function install(xpiPath) {
let { promise, resolve, reject } = defer();
// Create nsIFile for the xpi file
let file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile);
try {
file.initWithPath(xpiPath);
}
catch(e) {
reject(exports.ERROR_FILE_ACCESS);
return promise;
}
// Listen for installation end
let listener = {
onInstallEnded: function(aInstall, aAddon) {
aInstall.removeListener(listener);
// Bug 749745: on FF14+, onInstallEnded is called just before `startup()`
// is called, but we expect to resolve the promise only after it.
// As startup is called synchronously just after onInstallEnded,
// a simple setTimeout(0) is enough
setTimeout(resolve, 0, aAddon.id);
},
onInstallFailed: function (aInstall) {
aInstall.removeListener(listener);
reject(aInstall.error);
},
onDownloadFailed: function(aInstall) {
this.onInstallFailed(aInstall);
}
};
// Order AddonManager to install the addon
AddonManager.getInstallForFile(file, function(install) {
if (install.error == 0) {
install.addListener(listener);
install.install();
} else {
reject(install.error);
}
});
return promise;
};
exports.uninstall = function uninstall(addonId) {
let { promise, resolve, reject } = defer();
// Listen for uninstallation end
let listener = {
onUninstalled: function onUninstalled(aAddon) {
if (aAddon.id != addonId)
return;
AddonManager.removeAddonListener(listener);
resolve();
}
};
AddonManager.addAddonListener(listener);
// Order Addonmanager to uninstall the addon
getAddon(addonId).then(addon => addon.uninstall(), reject);
return promise;
};
exports.disable = function disable(addonId) {
return getAddon(addonId).then(addon => {
addon.userDisabled = true;
return addonId;
});
};
exports.enable = function enabled(addonId) {
return getAddon(addonId).then(addon => {
addon.userDisabled = false;
return addonId;
});
};
exports.isActive = function isActive(addonId) {
return getAddon(addonId).then(addon => addon.isActive && !addon.appDisabled);
};
const getAddon = function getAddon (id) {
let { promise, resolve, reject } = defer();
AddonManager.getAddonByID(id, addon => addon ? resolve(addon) : reject());
return promise;
}
exports.getAddon = getAddon;

View file

@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
const { defer } = require("../core/promise");
function getAddonByID(id) {
let { promise, resolve } = defer();
AddonManager.getAddonByID(id, resolve);
return promise;
}
exports.getAddonByID = getAddonByID;

View file

@ -0,0 +1,180 @@
/* 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/. */
module.metadata = {
"stability": "experimental"
};
const { Cc, Ci, Cu } = require('chrome');
const { rootURI, metadata, isNative } = require('@loader/options');
const { id, loadReason } = require('../self');
const { descriptor, Sandbox, evaluate, main, resolveURI } = require('toolkit/loader');
const { once } = require('../system/events');
const { exit, env, staticArgs } = require('../system');
const { when: unload } = require('../system/unload');
const globals = require('../system/globals');
const xulApp = require('../system/xul-app');
const { get } = require('../preferences/service');
const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
getService(Ci.nsIAppShellService);
const { preferences } = metadata;
const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function () {
return Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {}).
BrowserToolboxProcess;
});
// Initializes default preferences
function setDefaultPrefs(prefsURI) {
const prefs = Cc['@mozilla.org/preferences-service;1'].
getService(Ci.nsIPrefService).
QueryInterface(Ci.nsIPrefBranch2);
const branch = prefs.getDefaultBranch('');
const sandbox = Sandbox({
name: prefsURI,
prototype: {
pref: function(key, val) {
switch (typeof val) {
case 'boolean':
branch.setBoolPref(key, val);
break;
case 'number':
if (val % 1 == 0) // number must be a integer, otherwise ignore it
branch.setIntPref(key, val);
break;
case 'string':
branch.setCharPref(key, val);
break;
}
}
}
});
// load preferences.
evaluate(sandbox, prefsURI);
}
function definePseudo(loader, id, exports) {
let uri = resolveURI(id, loader.mapping);
loader.modules[uri] = { exports: exports };
}
function startup(reason, options) {
return Startup.onceInitialized.then(() => {
// Inject globals ASAP in order to have console API working ASAP
Object.defineProperties(options.loader.globals, descriptor(globals));
// NOTE: Module is intentionally required only now because it relies
// on existence of hidden window, which does not exists until startup.
let { ready } = require('../addon/window');
// Load localization manifest and .properties files.
// Run the addon even in case of error (best effort approach)
require('../l10n/loader').
load(rootURI).
then(null, function failure(error) {
if (!isNative)
console.info("Error while loading localization: " + error.message);
}).
then(function onLocalizationReady(data) {
// Exports data to a pseudo module so that api-utils/l10n/core
// can get access to it
definePseudo(options.loader, '@l10n/data', data ? data : null);
return ready;
}).then(function() {
run(options);
}).then(null, console.exception);
return void 0; // otherwise we raise a warning, see bug 910304
});
}
function run(options) {
try {
// Try initializing HTML localization before running main module. Just print
// an exception in case of error, instead of preventing addon to be run.
try {
// Do not enable HTML localization while running test as it is hard to
// disable. Because unit tests are evaluated in a another Loader who
// doesn't have access to this current loader.
if (options.main !== 'sdk/test/runner') {
require('../l10n/html').enable();
}
}
catch(error) {
console.exception(error);
}
// native-options does stuff directly with preferences key from package.json
if (preferences && preferences.length > 0) {
try {
require('../preferences/native-options').
enable({ preferences: preferences, id: id }).
catch(console.exception);
}
catch (error) {
console.exception(error);
}
}
else {
// keeping support for addons packaged with older SDK versions,
// when cfx didn't include the 'preferences' key in @loader/options
// Initialize inline options localization, without preventing addon to be
// run in case of error
try {
require('../l10n/prefs').enable();
}
catch(error) {
console.exception(error);
}
// TODO: When bug 564675 is implemented this will no longer be needed
// Always set the default prefs, because they disappear on restart
if (options.prefsURI) {
// Only set if `prefsURI` specified
try {
setDefaultPrefs(options.prefsURI);
}
catch (err) {
// cfx bootstrap always passes prefsURI, even in addons without prefs
}
}
}
// this is where the addon's main.js finally run.
let program = main(options.loader, options.main);
if (typeof(program.onUnload) === 'function')
unload(program.onUnload);
if (typeof(program.main) === 'function') {
program.main({
loadReason: loadReason,
staticArgs: staticArgs
}, {
print: function print(_) { dump(_ + '\n') },
quit: exit
});
}
if (get("extensions." + id + ".sdk.debug.show", false)) {
BrowserToolboxProcess.init({ addonID: id });
}
} catch (error) {
console.exception(error);
throw error;
}
}
exports.startup = startup;
// If add-on is lunched via `cfx run` we need to use `system.exit` to let
// cfx know we're done (`cfx test` will take care of exit so we don't do
// anything here).
if (env.CFX_COMMAND === 'run') {
unload(function(reason) {
if (reason === 'shutdown')
exit(0);
});
}

View file

@ -0,0 +1,66 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Ci, Cc } = require("chrome");
const { make: makeWindow, getHiddenWindow } = require("../window/utils");
const { create: makeFrame, getDocShell } = require("../frame/utils");
const { defer } = require("../core/promise");
const { when: unload } = require("../system/unload");
const cfxArgs = require("../test/options");
var addonPrincipal = Cc["@mozilla.org/systemprincipal;1"].
createInstance(Ci.nsIPrincipal);
var hiddenWindow = getHiddenWindow();
if (cfxArgs.parseable) {
console.info("hiddenWindow document.documentURI:" +
hiddenWindow.document.documentURI);
console.info("hiddenWindow document.readyState:" +
hiddenWindow.document.readyState);
}
// Once Bug 565388 is fixed and shipped we'll be able to make invisible,
// permanent docShells. Meanwhile we create hidden top level window and
// use it's docShell.
var frame = makeFrame(hiddenWindow.document, {
nodeName: "iframe",
namespaceURI: "http://www.w3.org/1999/xhtml",
allowJavascript: true,
allowPlugins: true
})
var docShell = getDocShell(frame);
var eventTarget = docShell.chromeEventHandler;
// We need to grant docShell system principals in order to load XUL document
// from data URI into it.
docShell.createAboutBlankContentViewer(addonPrincipal);
// Get a reference to the DOM window of the given docShell and load
// such document into that would allow us to create XUL iframes, that
// are necessary for hidden frames etc..
var window = docShell.contentViewer.DOMDocument.defaultView;
window.location = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window/>";
// Create a promise that is delivered once add-on window is interactive,
// used by add-on runner to defer add-on loading until window is ready.
var { promise, resolve } = defer();
eventTarget.addEventListener("DOMContentLoaded", function handler(event) {
eventTarget.removeEventListener("DOMContentLoaded", handler, false);
resolve();
}, false);
exports.ready = promise;
exports.window = window;
// Still close window on unload to claim memory back early.
unload(function() {
window.close()
frame.parentNode.removeChild(frame);
});

View file

@ -0,0 +1,47 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "unstable"
};
const { Cu } = require("chrome");
// Passing an empty object as second argument to avoid scope's pollution
// (devtools loader injects these symbols as global and prevent using
// const here)
var { atob, btoa } = Cu.import("resource://gre/modules/Services.jsm", {});
function isUTF8(charset) {
let type = typeof charset;
if (type === "undefined")
return false;
if (type === "string" && charset.toLowerCase() === "utf-8")
return true;
throw new Error("The charset argument can be only 'utf-8'");
}
function toOctetChar(c) {
return String.fromCharCode(c.charCodeAt(0) & 0xFF);
}
exports.decode = function (data, charset) {
if (isUTF8(charset))
return decodeURIComponent(escape(atob(data)))
return atob(data);
}
exports.encode = function (data, charset) {
if (isUTF8(charset))
return btoa(unescape(encodeURIComponent(data)))
data = data.replace(/[^\x00-\xFF]/g, toOctetChar);
return btoa(data);
}

View file

@ -0,0 +1,20 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "unstable"
};
const { events } = require("../window/events");
const { filter } = require("../event/utils");
const { isBrowser } = require("../window/utils");
// TODO: `isBrowser` detects weather window is a browser by checking
// `windowtype` attribute, which means that all 'open' events will be
// filtered out since document is not loaded yet. Maybe we can find a better
// implementation for `isBrowser`. Either way it's not really needed yet
// neither window tracker provides this event.
exports.events = filter(events, ({target}) => isBrowser(target));

View file

@ -0,0 +1,337 @@
/* 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": "stable",
"engines": {
// TODO Fennec Support 789757
"Firefox": "*",
"SeaMonkey": "*",
"Thunderbird": "*"
}
};
const { Cc, Ci } = require("chrome");
const { DataURL } = require("./url");
const apiUtils = require("./deprecated/api-utils");
/*
While these data flavors resemble Internet media types, they do
no directly map to them.
*/
const kAllowableFlavors = [
"text/unicode",
"text/html",
"image/png"
/* CURRENTLY UNSUPPORTED FLAVORS
"text/plain",
"image/jpg",
"image/jpeg",
"image/gif",
"text/x-moz-text-internal",
"AOLMAIL",
"application/x-moz-file",
"text/x-moz-url",
"text/x-moz-url-data",
"text/x-moz-url-desc",
"text/x-moz-url-priv",
"application/x-moz-nativeimage",
"application/x-moz-nativehtml",
"application/x-moz-file-promise-url",
"application/x-moz-file-promise-dest-filename",
"application/x-moz-file-promise",
"application/x-moz-file-promise-dir"
*/
];
/*
Aliases for common flavors. Not all flavors will
get an alias. New aliases must be approved by a
Jetpack API druid.
*/
const kFlavorMap = [
{ short: "text", long: "text/unicode" },
{ short: "html", long: "text/html" },
{ short: "image", long: "image/png" }
];
var clipboardService = Cc["@mozilla.org/widget/clipboard;1"].
getService(Ci.nsIClipboard);
var clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].
getService(Ci.nsIClipboardHelper);
var imageTools = Cc["@mozilla.org/image/tools;1"].
getService(Ci.imgITools);
exports.set = function(aData, aDataType) {
let options = {
data: aData,
datatype: aDataType || "text"
};
// If `aDataType` is not given or if it's "image", the data is parsed as
// data URL to detect a better datatype
if (aData && (!aDataType || aDataType === "image")) {
try {
let dataURL = new DataURL(aData);
options.datatype = dataURL.mimeType;
options.data = dataURL.data;
}
catch (e) {
// Ignore invalid URIs
if (e.name !== "URIError") {
throw e;
}
}
}
options = apiUtils.validateOptions(options, {
data: {
is: ["string"]
},
datatype: {
is: ["string"]
}
});
let flavor = fromJetpackFlavor(options.datatype);
if (!flavor)
throw new Error("Invalid flavor for " + options.datatype);
// Additional checks for using the simple case
if (flavor == "text/unicode") {
clipboardHelper.copyString(options.data);
return true;
}
// Below are the more complex cases where we actually have to work with a
// nsITransferable object
var xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
if (!xferable)
throw new Error("Couldn't set the clipboard due to an internal error " +
"(couldn't create a Transferable object).");
// Bug 769440: Starting with FF16, transferable have to be inited
if ("init" in xferable)
xferable.init(null);
switch (flavor) {
case "text/html":
// add text/html flavor
let str = Cc["@mozilla.org/supports-string;1"].
createInstance(Ci.nsISupportsString);
str.data = options.data;
xferable.addDataFlavor(flavor);
xferable.setTransferData(flavor, str, str.data.length * 2);
// add a text/unicode flavor (html converted to plain text)
str = Cc["@mozilla.org/supports-string;1"].
createInstance(Ci.nsISupportsString);
let converter = Cc["@mozilla.org/feed-textconstruct;1"].
createInstance(Ci.nsIFeedTextConstruct);
converter.type = "html";
converter.text = options.data;
str.data = converter.plainText();
xferable.addDataFlavor("text/unicode");
xferable.setTransferData("text/unicode", str, str.data.length * 2);
break;
// Set images to the clipboard is not straightforward, to have an idea how
// it works on platform side, see:
// http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530
case "image/png":
let image = options.data;
let container = {};
try {
let input = Cc["@mozilla.org/io/string-input-stream;1"].
createInstance(Ci.nsIStringInputStream);
input.setData(image, image.length);
imageTools.decodeImageData(input, flavor, container);
}
catch (e) {
throw new Error("Unable to decode data given in a valid image.");
}
// Store directly the input stream makes the cliboard's data available
// for Firefox but not to the others application or to the OS. Therefore,
// a `nsISupportsInterfacePointer` object that reference an `imgIContainer`
// with the image is needed.
var imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"].
createInstance(Ci.nsISupportsInterfacePointer);
imgPtr.data = container.value;
xferable.addDataFlavor(flavor);
xferable.setTransferData(flavor, imgPtr, -1);
break;
default:
throw new Error("Unable to handle the flavor " + flavor + ".");
}
// TODO: Not sure if this will ever actually throw. -zpao
try {
clipboardService.setData(
xferable,
null,
clipboardService.kGlobalClipboard
);
} catch (e) {
throw new Error("Couldn't set clipboard data due to an internal error: " + e);
}
return true;
};
exports.get = function(aDataType) {
let options = {
datatype: aDataType
};
// Figure out the best data type for the clipboard's data, if omitted
if (!aDataType) {
if (~currentFlavors().indexOf("image"))
options.datatype = "image";
else
options.datatype = "text";
}
options = apiUtils.validateOptions(options, {
datatype: {
is: ["string"]
}
});
var xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
if (!xferable)
throw new Error("Couldn't set the clipboard due to an internal error " +
"(couldn't create a Transferable object).");
// Bug 769440: Starting with FF16, transferable have to be inited
if ("init" in xferable)
xferable.init(null);
var flavor = fromJetpackFlavor(options.datatype);
// Ensure that the user hasn't requested a flavor that we don't support.
if (!flavor)
throw new Error("Getting the clipboard with the flavor '" + flavor +
"' is not supported.");
// TODO: Check for matching flavor first? Probably not worth it.
xferable.addDataFlavor(flavor);
// Get the data into our transferable.
clipboardService.getData(
xferable,
clipboardService.kGlobalClipboard
);
var data = {};
var dataLen = {};
try {
xferable.getTransferData(flavor, data, dataLen);
} catch (e) {
// Clipboard doesn't contain data in flavor, return null.
return null;
}
// There's no data available, return.
if (data.value === null)
return null;
// TODO: Add flavors here as we support more in kAllowableFlavors.
switch (flavor) {
case "text/unicode":
case "text/html":
data = data.value.QueryInterface(Ci.nsISupportsString).data;
break;
case "image/png":
let dataURL = new DataURL();
dataURL.mimeType = flavor;
dataURL.base64 = true;
let image = data.value;
// Due to the differences in how images could be stored in the clipboard
// the checks below are needed. The clipboard could already provide the
// image as byte streams, but also as pointer, or as image container.
// If it's not possible obtain a byte stream, the function returns `null`.
if (image instanceof Ci.nsISupportsInterfacePointer)
image = image.data;
if (image instanceof Ci.imgIContainer)
image = imageTools.encodeImage(image, flavor);
if (image instanceof Ci.nsIInputStream) {
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
createInstance(Ci.nsIBinaryInputStream);
binaryStream.setInputStream(image);
dataURL.data = binaryStream.readBytes(binaryStream.available());
data = dataURL.toString();
}
else
data = null;
break;
default:
data = null;
}
return data;
};
function currentFlavors() {
// Loop over kAllowableFlavors, calling hasDataMatchingFlavors for each.
// This doesn't seem like the most efficient way, but we can't get
// confirmation for specific flavors any other way. This is supposed to be
// an inexpensive call, so performance shouldn't be impacted (much).
var currentFlavors = [];
for (var flavor of kAllowableFlavors) {
var matches = clipboardService.hasDataMatchingFlavors(
[flavor],
1,
clipboardService.kGlobalClipboard
);
if (matches)
currentFlavors.push(toJetpackFlavor(flavor));
}
return currentFlavors;
};
Object.defineProperty(exports, "currentFlavors", { get : currentFlavors });
// SUPPORT FUNCTIONS ////////////////////////////////////////////////////////
function toJetpackFlavor(aFlavor) {
for (let flavorMap of kFlavorMap)
if (flavorMap.long == aFlavor)
return flavorMap.short;
// Return null in the case where we don't match
return null;
}
function fromJetpackFlavor(aJetpackFlavor) {
// TODO: Handle proper flavors better
for (let flavorMap of kFlavorMap)
if (flavorMap.short == aJetpackFlavor || flavorMap.long == aJetpackFlavor)
return flavorMap.long;
// Return null in the case where we don't match.
return null;
}

View file

@ -0,0 +1,78 @@
/* 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 { Cc, Ci, Cu, Cr } = require("chrome");
const self = require("../self");
const prefs = require("../preferences/service");
const { merge } = require("../util/object");
const { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {});
const DEFAULT_LOG_LEVEL = "error";
const ADDON_LOG_LEVEL_PREF = "extensions." + self.id + ".sdk.console.logLevel";
const SDK_LOG_LEVEL_PREF = "extensions.sdk.console.logLevel";
var logLevel = DEFAULT_LOG_LEVEL;
function setLogLevel() {
logLevel = prefs.get(ADDON_LOG_LEVEL_PREF,
prefs.get(SDK_LOG_LEVEL_PREF,
DEFAULT_LOG_LEVEL));
}
setLogLevel();
var logLevelObserver = {
QueryInterface: function(iid) {
if (!iid.equals(Ci.nsIObserver) &&
!iid.equals(Ci.nsISupportsWeakReference) &&
!iid.equals(Ci.nsISupports))
throw Cr.NS_ERROR_NO_INTERFACE;
return this;
},
observe: function(subject, topic, data) {
setLogLevel();
}
};
var branch = Cc["@mozilla.org/preferences-service;1"].
getService(Ci.nsIPrefService).
getBranch(null);
branch.addObserver(ADDON_LOG_LEVEL_PREF, logLevelObserver, true);
branch.addObserver(SDK_LOG_LEVEL_PREF, logLevelObserver, true);
function PlainTextConsole(print, innerID) {
let consoleOptions = {
prefix: self.name,
maxLogLevel: logLevel,
dump: print,
innerID: innerID,
consoleID: "addon/" + self.id
};
let console = new ConsoleAPI(consoleOptions);
// As we freeze the console object, we can't modify this property afterward
Object.defineProperty(console, "maxLogLevel", {
get: function() {
return logLevel;
}
});
// We defined the `__exposedProps__` in our console chrome object.
//
// Meanwhile we're investigating with the platform team if `__exposedProps__`
// are needed, or are just a left-over.
console.__exposedProps__ = Object.keys(ConsoleAPI.prototype).reduce(function(exposed, prop) {
exposed[prop] = "r";
return exposed;
}, {});
Object.freeze(console);
return console;
};
exports.PlainTextConsole = PlainTextConsole;

View file

@ -0,0 +1,86 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Ci, components } = require("chrome");
const { parseStack, sourceURI } = require("toolkit/loader");
const { readURISync } = require("../net/url");
function safeGetFileLine(path, line) {
try {
var scheme = require("../url").URL(path).scheme;
// TODO: There should be an easier, more accurate way to figure out
// what's the case here.
if (!(scheme == "http" || scheme == "https"))
return readURISync(path).split("\n")[line - 1];
} catch (e) {}
return null;
}
function nsIStackFramesToJSON(frame) {
var stack = [];
while (frame) {
if (frame.filename) {
stack.unshift({
fileName: sourceURI(frame.filename),
lineNumber: frame.lineNumber,
name: frame.name
});
}
frame = frame.caller;
}
return stack;
};
var fromException = exports.fromException = function fromException(e) {
if (e instanceof Ci.nsIException)
return nsIStackFramesToJSON(e.location);
if (e.stack && e.stack.length)
return parseStack(e.stack);
if (e.fileName && typeof(e.lineNumber == "number"))
return [{fileName: sourceURI(e.fileName),
lineNumber: e.lineNumber,
name: null}];
return [];
};
var get = exports.get = function get() {
return nsIStackFramesToJSON(components.stack.caller);
};
var format = exports.format = function format(tbOrException) {
if (tbOrException === undefined) {
tbOrException = get();
tbOrException.pop();
}
var tb;
if (typeof(tbOrException) == "object" &&
tbOrException.constructor.name == "Array")
tb = tbOrException;
else
tb = fromException(tbOrException);
var lines = ["Traceback (most recent call last):"];
tb.forEach(
function(frame) {
if (!(frame.fileName || frame.lineNumber || frame.name))
return;
lines.push(' File "' + frame.fileName + '", line ' +
frame.lineNumber + ', in ' + frame.name);
var sourceLine = safeGetFileLine(frame.fileName, frame.lineNumber);
if (sourceLine)
lines.push(' ' + sourceLine.trim());
});
return lines.join("\n");
};

View file

@ -0,0 +1,305 @@
/* 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/. */
Object.freeze({
// TODO: Bug 727854 Use same implementation than common JS modules,
// i.e. EventEmitter module
/**
* Create an EventEmitter instance.
*/
createEventEmitter: function createEventEmitter(emit) {
let listeners = Object.create(null);
let eventEmitter = Object.freeze({
emit: emit,
on: function on(name, callback) {
if (typeof callback !== "function")
return this;
if (!(name in listeners))
listeners[name] = [];
listeners[name].push(callback);
return this;
},
once: function once(name, callback) {
eventEmitter.on(name, function onceCallback() {
eventEmitter.removeListener(name, onceCallback);
callback.apply(callback, arguments);
});
},
removeListener: function removeListener(name, callback) {
if (!(name in listeners))
return;
let index = listeners[name].indexOf(callback);
if (index == -1)
return;
listeners[name].splice(index, 1);
}
});
function onEvent(name) {
if (!(name in listeners))
return [];
let args = Array.slice(arguments, 1);
let results = [];
for (let callback of listeners[name]) {
results.push(callback.apply(null, args));
}
return results;
}
return {
eventEmitter: eventEmitter,
emit: onEvent
};
},
/**
* Create an EventEmitter instance to communicate with chrome module
* by passing only strings between compartments.
* This function expects `emitToChrome` function, that allows to send
* events to the chrome module. It returns the EventEmitter as `pipe`
* attribute, and, `onChromeEvent` a function that allows chrome module
* to send event into the EventEmitter.
*
* pipe.emit --> emitToChrome
* onChromeEvent --> callback registered through pipe.on
*/
createPipe: function createPipe(emitToChrome) {
let ContentWorker = this;
function onEvent(type, ...args) {
// JSON.stringify is buggy with cross-sandbox values,
// it may return "{}" on functions. Use a replacer to match them correctly.
let replacer = (k, v) =>
typeof(v) === "function"
? (type === "console" ? Function.toString.call(v) : void(0))
: v;
let str = JSON.stringify([type, ...args], replacer);
emitToChrome(str);
}
let { eventEmitter, emit } =
ContentWorker.createEventEmitter(onEvent);
return {
pipe: eventEmitter,
onChromeEvent: function onChromeEvent(array) {
// We either receive a stringified array, or a real array.
// We still allow to pass an array of objects, in WorkerSandbox.emitSync
// in order to allow sending DOM node reference between content script
// and modules (only used for context-menu API)
let args = typeof array == "string" ? JSON.parse(array) : array;
return emit.apply(null, args);
}
};
},
injectConsole: function injectConsole(exports, pipe) {
exports.console = Object.freeze({
log: pipe.emit.bind(null, "console", "log"),
info: pipe.emit.bind(null, "console", "info"),
warn: pipe.emit.bind(null, "console", "warn"),
error: pipe.emit.bind(null, "console", "error"),
debug: pipe.emit.bind(null, "console", "debug"),
exception: pipe.emit.bind(null, "console", "exception"),
trace: pipe.emit.bind(null, "console", "trace"),
time: pipe.emit.bind(null, "console", "time"),
timeEnd: pipe.emit.bind(null, "console", "timeEnd")
});
},
injectTimers: function injectTimers(exports, chromeAPI, pipe, console) {
// wrapped functions from `'timer'` module.
// Wrapper adds `try catch` blocks to the callbacks in order to
// emit `error` event if exception is thrown in
// the Worker global scope.
// @see http://www.w3.org/TR/workers/#workerutils
// List of all living timeouts/intervals
let _timers = Object.create(null);
// Keep a reference to original timeout functions
let {
setTimeout: chromeSetTimeout,
setInterval: chromeSetInterval,
clearTimeout: chromeClearTimeout,
clearInterval: chromeClearInterval
} = chromeAPI.timers;
function registerTimer(timer) {
let registerMethod = null;
if (timer.kind == "timeout")
registerMethod = chromeSetTimeout;
else if (timer.kind == "interval")
registerMethod = chromeSetInterval;
else
throw new Error("Unknown timer kind: " + timer.kind);
if (typeof timer.fun == 'string') {
let code = timer.fun;
timer.fun = () => chromeAPI.sandbox.evaluate(exports, code);
} else if (typeof timer.fun != 'function') {
throw new Error('Unsupported callback type' + typeof timer.fun);
}
let id = registerMethod(onFire, timer.delay);
function onFire() {
try {
if (timer.kind == "timeout")
delete _timers[id];
timer.fun.apply(null, timer.args);
} catch(e) {
console.exception(e);
let wrapper = {
instanceOfError: instanceOf(e, Error),
value: e,
};
if (wrapper.instanceOfError) {
wrapper.value = {
message: e.message,
fileName: e.fileName,
lineNumber: e.lineNumber,
stack: e.stack,
name: e.name,
};
}
pipe.emit('error', wrapper);
}
}
_timers[id] = timer;
return id;
}
// copied from sdk/lang/type.js since modules are not available here
function instanceOf(value, Type) {
var isConstructorNameSame;
var isConstructorSourceSame;
// If `instanceof` returned `true` we know result right away.
var isInstanceOf = value instanceof Type;
// If `instanceof` returned `false` we do ducktype check since `Type` may be
// from a different sandbox. If a constructor of the `value` or a constructor
// of the value's prototype has same name and source we assume that it's an
// instance of the Type.
if (!isInstanceOf && value) {
isConstructorNameSame = value.constructor.name === Type.name;
isConstructorSourceSame = String(value.constructor) == String(Type);
isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) ||
instanceOf(Object.getPrototypeOf(value), Type);
}
return isInstanceOf;
}
function unregisterTimer(id) {
if (!(id in _timers))
return;
let { kind } = _timers[id];
delete _timers[id];
if (kind == "timeout")
chromeClearTimeout(id);
else if (kind == "interval")
chromeClearInterval(id);
else
throw new Error("Unknown timer kind: " + kind);
}
function disableAllTimers() {
Object.keys(_timers).forEach(unregisterTimer);
}
exports.setTimeout = function ContentScriptSetTimeout(callback, delay) {
return registerTimer({
kind: "timeout",
fun: callback,
delay: delay,
args: Array.slice(arguments, 2)
});
};
exports.clearTimeout = function ContentScriptClearTimeout(id) {
unregisterTimer(id);
};
exports.setInterval = function ContentScriptSetInterval(callback, delay) {
return registerTimer({
kind: "interval",
fun: callback,
delay: delay,
args: Array.slice(arguments, 2)
});
};
exports.clearInterval = function ContentScriptClearInterval(id) {
unregisterTimer(id);
};
// On page-hide, save a list of all existing timers before disabling them,
// in order to be able to restore them on page-show.
// These events are fired when the page goes in/out of bfcache.
// https://developer.mozilla.org/En/Working_with_BFCache
let frozenTimers = [];
pipe.on("pageshow", function onPageShow() {
frozenTimers.forEach(registerTimer);
});
pipe.on("pagehide", function onPageHide() {
frozenTimers = [];
for (let id in _timers)
frozenTimers.push(_timers[id]);
disableAllTimers();
// Some other pagehide listeners may register some timers that won't be
// frozen as this particular pagehide listener is called first.
// So freeze these timers on next cycle.
chromeSetTimeout(function () {
for (let id in _timers)
frozenTimers.push(_timers[id]);
disableAllTimers();
}, 0);
});
// Unregister all timers when the page is destroyed
// (i.e. when it is removed from bfcache)
pipe.on("detach", function clearTimeouts() {
disableAllTimers();
_timers = {};
frozenTimers = [];
});
},
injectMessageAPI: function injectMessageAPI(exports, pipe, console) {
let ContentWorker = this;
let { eventEmitter: port, emit : portEmit } =
ContentWorker.createEventEmitter(pipe.emit.bind(null, "event"));
pipe.on("event", portEmit);
let self = {
port: port,
postMessage: pipe.emit.bind(null, "message"),
on: pipe.on.bind(null),
once: pipe.once.bind(null),
removeListener: pipe.removeListener.bind(null),
};
Object.defineProperty(exports, "self", {
value: self
});
},
injectOptions: function (exports, options) {
Object.defineProperty( exports.self, "options", { value: JSON.parse( options ) });
},
inject: function (exports, chromeAPI, emitToChrome, options) {
let ContentWorker = this;
let { pipe, onChromeEvent } =
ContentWorker.createPipe(emitToChrome);
ContentWorker.injectConsole(exports, pipe);
ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console);
ContentWorker.injectMessageAPI(exports, pipe, exports.console);
if ( options !== undefined ) {
ContentWorker.injectOptions(exports, options);
}
Object.freeze( exports.self );
return onChromeEvent;
}
});

View file

@ -0,0 +1,17 @@
/* 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 { deprecateUsage } = require('../util/deprecate');
Object.defineProperty(exports, "Worker", {
get: function() {
deprecateUsage('`sdk/content/content` is deprecated. Please use `sdk/content/worker` directly.');
return require('./worker').Worker;
}
});

View file

@ -0,0 +1,408 @@
/* 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 { Class } = require("../core/heritage");
const self = require("../self");
const { WorkerChild } = require("./worker-child");
const { getInnerId } = require("../window/utils");
const { Ci } = require("chrome");
const { Services } = require("resource://gre/modules/Services.jsm");
const system = require('../system/events');
const { process } = require('../remote/child');
// These functions are roughly copied from sdk/selection which doesn't work
// in the content process
function getElementWithSelection(window) {
let element = Services.focus.getFocusedElementForWindow(window, false, {});
if (!element)
return null;
try {
// Accessing selectionStart and selectionEnd on e.g. a button
// results in an exception thrown as per the HTML5 spec. See
// http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#textFieldSelection
let { value, selectionStart, selectionEnd } = element;
let hasSelection = typeof value === "string" &&
!isNaN(selectionStart) &&
!isNaN(selectionEnd) &&
selectionStart !== selectionEnd;
return hasSelection ? element : null;
}
catch (err) {
console.exception(err);
return null;
}
}
function safeGetRange(selection, rangeNumber) {
try {
let { rangeCount } = selection;
let range = null;
for (let rangeNumber = 0; rangeNumber < rangeCount; rangeNumber++ ) {
range = selection.getRangeAt(rangeNumber);
if (range && range.toString())
break;
range = null;
}
return range;
}
catch (e) {
return null;
}
}
function getSelection(window) {
let selection = window.getSelection();
let range = safeGetRange(selection);
if (range)
return range.toString();
let node = getElementWithSelection(window);
if (!node)
return null;
return node.value.substring(node.selectionStart, node.selectionEnd);
}
//These are used by PageContext.isCurrent below. If the popupNode or any of
//its ancestors is one of these, Firefox uses a tailored context menu, and so
//the page context doesn't apply.
const NON_PAGE_CONTEXT_ELTS = [
Ci.nsIDOMHTMLAnchorElement,
Ci.nsIDOMHTMLAppletElement,
Ci.nsIDOMHTMLAreaElement,
Ci.nsIDOMHTMLButtonElement,
Ci.nsIDOMHTMLCanvasElement,
Ci.nsIDOMHTMLEmbedElement,
Ci.nsIDOMHTMLImageElement,
Ci.nsIDOMHTMLInputElement,
Ci.nsIDOMHTMLMapElement,
Ci.nsIDOMHTMLMediaElement,
Ci.nsIDOMHTMLMenuElement,
Ci.nsIDOMHTMLObjectElement,
Ci.nsIDOMHTMLOptionElement,
Ci.nsIDOMHTMLSelectElement,
Ci.nsIDOMHTMLTextAreaElement,
];
// List all editable types of inputs. Or is it better to have a list
// of non-editable inputs?
var editableInputs = {
email: true,
number: true,
password: true,
search: true,
tel: true,
text: true,
textarea: true,
url: true
};
var CONTEXTS = {};
var Context = Class({
initialize: function(id) {
this.id = id;
},
adjustPopupNode: function adjustPopupNode(popupNode) {
return popupNode;
},
// Gets state to pass through to the parent process for the node the user
// clicked on
getState: function(popupNode) {
return false;
}
});
// Matches when the context-clicked node doesn't have any of
// NON_PAGE_CONTEXT_ELTS in its ancestors
CONTEXTS.PageContext = Class({
extends: Context,
getState: function(popupNode) {
// If there is a selection in the window then this context does not match
if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
return false;
// If the clicked node or any of its ancestors is one of the blocked
// NON_PAGE_CONTEXT_ELTS then this context does not match
while (!(popupNode instanceof Ci.nsIDOMDocument)) {
if (NON_PAGE_CONTEXT_ELTS.some(type => popupNode instanceof type))
return false;
popupNode = popupNode.parentNode;
}
return true;
}
});
// Matches when there is an active selection in the window
CONTEXTS.SelectionContext = Class({
extends: Context,
getState: function(popupNode) {
if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
return true;
try {
// The node may be a text box which has selectionStart and selectionEnd
// properties. If not this will throw.
let { selectionStart, selectionEnd } = popupNode;
return !isNaN(selectionStart) && !isNaN(selectionEnd) &&
selectionStart !== selectionEnd;
}
catch (e) {
return false;
}
}
});
// Matches when the context-clicked node or any of its ancestors matches the
// selector given
CONTEXTS.SelectorContext = Class({
extends: Context,
initialize: function initialize(id, selector) {
Context.prototype.initialize.call(this, id);
this.selector = selector;
},
adjustPopupNode: function adjustPopupNode(popupNode) {
let selector = this.selector;
while (!(popupNode instanceof Ci.nsIDOMDocument)) {
if (popupNode.matches(selector))
return popupNode;
popupNode = popupNode.parentNode;
}
return null;
},
getState: function(popupNode) {
return !!this.adjustPopupNode(popupNode);
}
});
// Matches when the page url matches any of the patterns given
CONTEXTS.URLContext = Class({
extends: Context,
getState: function(popupNode) {
return popupNode.ownerDocument.URL;
}
});
// Matches when the user-supplied predicate returns true
CONTEXTS.PredicateContext = Class({
extends: Context,
getState: function(node) {
let window = node.ownerDocument.defaultView;
let data = {};
data.documentType = node.ownerDocument.contentType;
data.documentURL = node.ownerDocument.location.href;
data.targetName = node.nodeName.toLowerCase();
data.targetID = node.id || null ;
if ((data.targetName === 'input' && editableInputs[node.type]) ||
data.targetName === 'textarea') {
data.isEditable = !node.readOnly && !node.disabled;
}
else {
data.isEditable = node.isContentEditable;
}
data.selectionText = getSelection(window, "TEXT");
data.srcURL = node.src || null;
data.value = node.value || null;
while (!data.linkURL && node) {
data.linkURL = node.href || null;
node = node.parentNode;
}
return data;
},
});
function instantiateContext({ id, type, args }) {
if (!(type in CONTEXTS)) {
console.error("Attempt to use unknown context " + type);
return;
}
return new CONTEXTS[type](id, ...args);
}
var ContextWorker = Class({
implements: [ WorkerChild ],
// Calls the context workers context listeners and returns the first result
// that is either a string or a value that evaluates to true. If all of the
// listeners returned false then returns false. If there are no listeners,
// returns true (show the menu item by default).
getMatchedContext: function getCurrentContexts(popupNode) {
let results = this.sandbox.emitSync("context", popupNode);
if (!results.length)
return true;
return results.reduce((val, result) => val || result);
},
// Emits a click event in the worker's port. popupNode is the node that was
// context-clicked, and clickedItemData is the data of the item that was
// clicked.
fireClick: function fireClick(popupNode, clickedItemData) {
this.sandbox.emitSync("click", popupNode, clickedItemData);
}
});
// Gets the item's content script worker for a window, creating one if necessary
// Once created it will be automatically destroyed when the window unloads.
// If there is not content scripts for the item then null will be returned.
function getItemWorkerForWindow(item, window) {
if (!item.contentScript && !item.contentScriptFile)
return null;
let id = getInnerId(window);
let worker = item.workerMap.get(id);
if (worker)
return worker;
worker = ContextWorker({
id: item.id,
window,
manager: item.manager,
contentScript: item.contentScript,
contentScriptFile: item.contentScriptFile,
onDetach: function() {
item.workerMap.delete(id);
}
});
item.workerMap.set(id, worker);
return worker;
}
// A very simple remote proxy for every item. It's job is to provide data for
// the main process to use to determine visibility state and to call into
// content scripts when clicked.
var RemoteItem = Class({
initialize: function(options, manager) {
this.id = options.id;
this.contexts = options.contexts.map(instantiateContext);
this.contentScript = options.contentScript;
this.contentScriptFile = options.contentScriptFile;
this.manager = manager;
this.workerMap = new Map();
keepAlive.set(this.id, this);
},
destroy: function() {
for (let worker of this.workerMap.values()) {
worker.destroy();
}
keepAlive.delete(this.id);
},
activate: function(popupNode, data) {
let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
if (!worker)
return;
for (let context of this.contexts)
popupNode = context.adjustPopupNode(popupNode);
worker.fireClick(popupNode, data);
},
// Fills addonInfo with state data to send through to the main process
getContextState: function(popupNode, addonInfo) {
if (!(self.id in addonInfo)) {
addonInfo[self.id] = {
processID: process.id,
items: {}
};
}
let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
let contextStates = {};
for (let context of this.contexts)
contextStates[context.id] = context.getState(popupNode);
addonInfo[self.id].items[this.id] = {
// It isn't ideal to create a PageContext for every item but there isn't
// a good shared place to do it.
pageContext: (new CONTEXTS.PageContext()).getState(popupNode),
contextStates,
hasWorker: !!worker,
workerContext: worker ? worker.getMatchedContext(popupNode) : true
}
}
});
exports.RemoteItem = RemoteItem;
// Holds remote items for this frame.
var keepAlive = new Map();
// Called to create remote proxies for items. If they already exist we destroy
// and recreate. This can happen if the item changes in some way or in odd
// timing cases where the frame script is create around the same time as the
// item is created in the main process
process.port.on('sdk/contextmenu/createitems', (process, items) => {
for (let itemoptions of items) {
let oldItem = keepAlive.get(itemoptions.id);
if (oldItem) {
oldItem.destroy();
}
let item = new RemoteItem(itemoptions, this);
}
});
process.port.on('sdk/contextmenu/destroyitems', (process, items) => {
for (let id of items) {
let item = keepAlive.get(id);
item.destroy();
}
});
var lastPopupNode = null;
system.on('content-contextmenu', ({ subject }) => {
let { event: { target: popupNode }, addonInfo } = subject.wrappedJSObject;
lastPopupNode = popupNode;
for (let item of keepAlive.values()) {
item.getContextState(popupNode, addonInfo);
}
}, true);
process.port.on('sdk/contextmenu/activateitems', (process, items, data) => {
for (let id of items) {
let item = keepAlive.get(id);
if (!item)
continue;
item.activate(lastPopupNode, data);
}
});

View file

@ -0,0 +1,57 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Ci } = require("chrome");
const { open } = require("../event/dom");
const { observe } = require("../event/chrome");
const { filter, merge, map, expand } = require("../event/utils");
const { windows } = require("../window/utils");
const { events: windowEvents } = require("sdk/window/events");
// Note: Please note that even though pagehide event is included
// it's not observable reliably since it's not always triggered
// when closing tabs. Implementation can be imrpoved once that
// event will be necessary.
var TYPES = ["DOMContentLoaded", "load", "pageshow", "pagehide"];
var insert = observe("document-element-inserted");
var windowCreate = merge([
observe("content-document-global-created"),
observe("chrome-document-global-created")
]);
var create = map(windowCreate, function({target, data, type}) {
return { target: target.document, type: type, data: data }
});
function streamEventsFrom({document}) {
// Map supported event types to a streams of those events on the given
// `window` for the inserted document and than merge these streams into
// single form stream off all window state change events.
let stateChanges = TYPES.map(function(type) {
return open(document, type, { capture: true });
});
// Since load events on document occur for every loded resource
return filter(merge(stateChanges), function({target}) {
return target instanceof Ci.nsIDOMDocument
})
}
exports.streamEventsFrom = streamEventsFrom;
var opened = windows(null, { includePrivate: true });
var state = merge(opened.map(streamEventsFrom));
var futureReady = filter(windowEvents, ({type}) =>
type === "DOMContentLoaded");
var futureWindows = map(futureReady, ({target}) => target);
var futureState = expand(futureWindows, streamEventsFrom);
exports.events = merge([insert, create, state, futureState]);

View file

@ -0,0 +1,133 @@
/* 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 { Ci, Cc, Cu } = require("chrome");
const core = require("../l10n/core");
const { loadSheet, removeSheet } = require("../stylesheet/utils");
const { process, frames } = require("../remote/child");
var observerService = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
const addObserver = ShimWaiver.getProperty(observerService, "addObserver");
const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver");
const assetsURI = require('../self').data.url();
const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}";
function translateElementAttributes(element) {
// Translateable attributes
const attrList = ['title', 'accesskey', 'alt', 'label', 'placeholder'];
const ariaAttrMap = {
'ariaLabel': 'aria-label',
'ariaValueText': 'aria-valuetext',
'ariaMozHint': 'aria-moz-hint'
};
const attrSeparator = '.';
// Try to translate each of the attributes
for (let attribute of attrList) {
const data = core.get(element.dataset.l10nId + attrSeparator + attribute);
if (data)
element.setAttribute(attribute, data);
}
// Look for the aria attribute translations that match fxOS's aliases
for (let attrAlias in ariaAttrMap) {
const data = core.get(element.dataset.l10nId + attrSeparator + attrAlias);
if (data)
element.setAttribute(ariaAttrMap[attrAlias], data);
}
}
// Taken from Gaia:
// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
function translateElement(element) {
element = element || document;
// check all translatable children (= w/ a `data-l10n-id' attribute)
var children = element.querySelectorAll('*[data-l10n-id]');
var elementCount = children.length;
for (var i = 0; i < elementCount; i++) {
var child = children[i];
// translate the child
var key = child.dataset.l10nId;
var data = core.get(key);
if (data)
child.textContent = data;
translateElementAttributes(child);
}
}
exports.translateElement = translateElement;
function onDocumentReady2Translate(event) {
let document = event.target;
document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
false);
translateElement(document);
try {
// Finally display document when we finished replacing all text content
if (document.defaultView)
removeSheet(document.defaultView, hideSheetUri, 'user');
}
catch(e) {
console.exception(e);
}
}
function onContentWindow(document) {
// Accept only HTML documents
if (!(document instanceof Ci.nsIDOMHTMLDocument))
return;
// Bug 769483: data:URI documents instanciated with nsIDOMParser
// have a null `location` attribute at this time
if (!document.location)
return;
// Accept only document from this addon
if (document.location.href.indexOf(assetsURI) !== 0)
return;
try {
// First hide content of the document in order to have content blinking
// between untranslated and translated states
loadSheet(document.defaultView, hideSheetUri, 'user');
}
catch(e) {
console.exception(e);
}
// Wait for DOM tree to be built before applying localization
document.addEventListener("DOMContentLoaded", onDocumentReady2Translate,
false);
}
// Listen to creation of content documents in order to translate them as soon
// as possible in their loading process
const ON_CONTENT = "document-element-inserted";
let enabled = false;
function enable() {
if (enabled)
return;
addObserver(onContentWindow, ON_CONTENT, false);
enabled = true;
}
process.port.on("sdk/l10n/html/enable", enable);
function disable() {
if (!enabled)
return;
removeObserver(onContentWindow, ON_CONTENT);
enabled = false;
}
process.port.on("sdk/l10n/html/disable", disable);

View file

@ -0,0 +1,74 @@
/* 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 { isValidURI, isLocalURL, URL } = require('../url');
const { contract } = require('../util/contract');
const { isString, isNil, instanceOf, isJSONable } = require('../lang/type');
const { validateOptions,
string, array, object, either, required } = require('../deprecated/api-utils');
const isValidScriptFile = (value) =>
(isString(value) || instanceOf(value, URL)) && isLocalURL(value);
// map of property validations
const valid = {
contentURL: {
is: either(string, object),
ok: url => isNil(url) || isLocalURL(url) || isValidURI(url),
msg: 'The `contentURL` option must be a valid URL.'
},
contentScriptFile: {
is: either(string, object, array),
ok: value => isNil(value) || [].concat(value).every(isValidScriptFile),
msg: 'The `contentScriptFile` option must be a local URL or an array of URLs.'
},
contentScript: {
is: either(string, array),
ok: value => isNil(value) || [].concat(value).every(isString),
msg: 'The `contentScript` option must be a string or an array of strings.'
},
contentScriptWhen: {
is: required(string),
map: value => value || 'end',
ok: value => ~['start', 'ready', 'end'].indexOf(value),
msg: 'The `contentScriptWhen` option must be either "start", "ready" or "end".'
},
contentScriptOptions: {
ok: value => isNil(value) || isJSONable(value),
msg: 'The contentScriptOptions should be a jsonable value.'
}
};
exports.validationAttributes = valid;
/**
* Shortcut function to validate property with validation.
* @param {Object|Number|String} suspect
* value to validate
* @param {Object} validation
* validation rule passed to `api-utils`
*/
function validate(suspect, validation) {
return validateOptions(
{ $: suspect },
{ $: validation }
).$;
}
function Allow(script) {
return {
get script() {
return script;
},
set script(value) {
script = !!value;
}
};
}
exports.contract = contract(valid);

View file

@ -0,0 +1,68 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Ci } = require("chrome");
const { dispatcher } = require("../util/dispatcher");
const { add, remove, iterator } = require("../lang/weak-set");
var getTargetWindow = dispatcher("getTargetWindow");
getTargetWindow.define(function (target) {
if (target instanceof Ci.nsIDOMWindow)
return target;
if (target instanceof Ci.nsIDOMDocument)
return target.defaultView || null;
return null;
});
exports.getTargetWindow = getTargetWindow;
var attachTo = dispatcher("attachTo");
exports.attachTo = attachTo;
var detachFrom = dispatcher("detatchFrom");
exports.detachFrom = detachFrom;
function attach(modification, target) {
if (!modification)
return;
let window = getTargetWindow(target);
attachTo(modification, window);
// modification are stored per content; `window` reference can still be the
// same even if the content is changed, therefore `document` is used instead.
add(modification, window.document);
}
exports.attach = attach;
function detach(modification, target) {
if (!modification)
return;
if (target) {
let window = getTargetWindow(target);
detachFrom(modification, window);
remove(modification, window.document);
}
else {
let documents = iterator(modification);
for (let document of documents) {
let window = document.defaultView;
// The window might have already gone away
if (!window)
continue;
detachFrom(modification, document.defaultView);
remove(modification, document);
}
}
}
exports.detach = detach;

View file

@ -0,0 +1,236 @@
/* 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": "stable"
};
const { getAttachEventType } = require('../content/utils');
const { Class } = require('../core/heritage');
const { Disposable } = require('../core/disposable');
const { WeakReference } = require('../core/reference');
const { WorkerChild } = require('./worker-child');
const { EventTarget } = require('../event/target');
const { on, emit, once, setListeners } = require('../event/core');
const { on: domOn, removeListener: domOff } = require('../dom/events');
const { isRegExp, isUndefined } = require('../lang/type');
const { merge } = require('../util/object');
const { isBrowser, getFrames } = require('../window/utils');
const { getTabs, getURI: getTabURI } = require('../tabs/utils');
const { ignoreWindow } = require('../private-browsing/utils');
const { Style } = require("../stylesheet/style");
const { attach, detach } = require("../content/mod");
const { has, hasAny } = require("../util/array");
const { Rules } = require("../util/rules");
const { List, addListItem, removeListItem } = require('../util/list');
const { when } = require("../system/unload");
const { uuid } = require('../util/uuid');
const { frames, process } = require('../remote/child');
const pagemods = new Map();
const styles = new WeakMap();
var styleFor = (mod) => styles.get(mod);
// Helper functions
var modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri);
/**
* PageMod constructor (exported below).
* @constructor
*/
const ChildPageMod = Class({
implements: [
EventTarget,
Disposable,
],
setup: function PageMod(model) {
merge(this, model);
// Set listeners on {PageMod} itself, not the underlying worker,
// like `onMessage`, as it'll get piped.
setListeners(this, model);
function deserializeRules(rules) {
for (let rule of rules) {
yield rule.type == "string" ? rule.value
: new RegExp(rule.pattern, rule.flags);
}
}
let include = [...deserializeRules(this.include)];
this.include = Rules();
this.include.add.apply(this.include, include);
let exclude = [...deserializeRules(this.exclude)];
this.exclude = Rules();
this.exclude.add.apply(this.exclude, exclude);
if (this.contentStyle || this.contentStyleFile) {
styles.set(this, Style({
uri: this.contentStyleFile,
source: this.contentStyle
}));
}
pagemods.set(this.id, this);
this.seenDocuments = new WeakMap();
// `applyOnExistingDocuments` has to be called after `pagemods.add()`
// otherwise its calls to `onContent` method won't do anything.
if (has(this.attachTo, 'existing'))
applyOnExistingDocuments(this);
},
dispose: function() {
let style = styleFor(this);
if (style)
detach(style);
for (let i in this.include)
this.include.remove(this.include[i]);
pagemods.delete(this.id);
}
});
function onContentWindow({ target: document }) {
// Return if we have no pagemods
if (pagemods.size === 0)
return;
let window = document.defaultView;
// XML documents don't have windows, and we don't yet support them.
if (!window)
return;
// Frame event listeners are bound to the frame the event came from by default
let frame = this;
// We apply only on documents in tabs of Firefox
if (!frame.isTab)
return;
// When the tab is private, only addons with 'private-browsing' flag in
// their package.json can apply content script to private documents
if (ignoreWindow(window))
return;
for (let pagemod of pagemods.values()) {
if (modMatchesURI(pagemod, window.location.href))
onContent(pagemod, window);
}
}
frames.addEventListener("DOMDocElementInserted", onContentWindow, true);
function applyOnExistingDocuments (mod) {
for (let frame of frames) {
// Fake a newly created document
let window = frame.content;
// on startup with e10s, contentWindow might not exist yet,
// in which case we will get notified by "document-element-inserted".
if (!window || !window.frames)
return;
let uri = window.location.href;
if (has(mod.attachTo, "top") && modMatchesURI(mod, uri))
onContent(mod, window);
if (has(mod.attachTo, "frame"))
getFrames(window).
filter(iframe => modMatchesURI(mod, iframe.location.href)).
forEach(frame => onContent(mod, frame));
}
}
function createWorker(mod, window) {
let workerId = String(uuid());
// Instruct the parent to connect to this worker. Do this first so the parent
// side is connected before the worker attempts to send any messages there
let frame = frames.getFrameForWindow(window.top);
frame.port.emit('sdk/page-mod/worker-create', mod.id, {
id: workerId,
url: window.location.href
});
// Create a child worker and notify the parent
let worker = WorkerChild({
id: workerId,
window: window,
contentScript: mod.contentScript,
contentScriptFile: mod.contentScriptFile,
contentScriptOptions: mod.contentScriptOptions
});
once(worker, 'detach', () => worker.destroy());
}
function onContent (mod, window) {
let isTopDocument = window.top === window;
// Is a top level document and `top` is not set, ignore
if (isTopDocument && !has(mod.attachTo, "top"))
return;
// Is a frame document and `frame` is not set, ignore
if (!isTopDocument && !has(mod.attachTo, "frame"))
return;
// ensure we attach only once per document
let seen = mod.seenDocuments;
if (seen.has(window.document))
return;
seen.set(window.document, true);
let style = styleFor(mod);
if (style)
attach(style, window);
// Immediately evaluate content script if the document state is already
// matching contentScriptWhen expectations
if (isMatchingAttachState(mod, window)) {
createWorker(mod, window);
return;
}
let eventName = getAttachEventType(mod) || 'load';
domOn(window, eventName, function onReady (e) {
if (e.target.defaultView !== window)
return;
domOff(window, eventName, onReady, true);
createWorker(mod, window);
// Attaching is asynchronous so if the document is already loaded we will
// miss the pageshow event so send a synthetic one.
if (window.document.readyState == "complete") {
mod.on('attach', worker => {
try {
worker.send('pageshow');
emit(worker, 'pageshow');
}
catch (e) {
// This can fail if an earlier attach listener destroyed the worker
}
});
}
}, true);
}
function isMatchingAttachState (mod, window) {
let state = window.document.readyState;
return 'start' === mod.contentScriptWhen ||
// Is `load` event already dispatched?
'complete' === state ||
// Is DOMContentLoaded already dispatched and waiting for it?
('ready' === mod.contentScriptWhen && state === 'interactive')
}
process.port.on('sdk/page-mod/create', (process, model) => {
if (pagemods.has(model.id))
return;
new ChildPageMod(model);
});
process.port.on('sdk/page-mod/destroy', (process, id) => {
let mod = pagemods.get(id);
if (mod)
mod.destroy();
});

View file

@ -0,0 +1,154 @@
/* 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 { frames } = require("../remote/child");
const { Class } = require("../core/heritage");
const { Disposable } = require('../core/disposable');
const { data } = require("../self");
const { once } = require("../dom/events");
const { getAttachEventType } = require("./utils");
const { Rules } = require('../util/rules');
const { uuid } = require('../util/uuid');
const { WorkerChild } = require("./worker-child");
const { Cc, Ci, Cu } = require("chrome");
const { observe } = require("../event/chrome");
const { on } = require("../event/core");
const appShell = Cc["@mozilla.org/appshell/appShellService;1"].getService(Ci.nsIAppShellService);
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
const pages = new Map();
const DOC_INSERTED = "document-element-inserted";
function isValidURL(page, url) {
return !page.rules || page.rules.matchesAny(url);
}
const ChildPage = Class({
implements: [ Disposable ],
setup: function(frame, id, options) {
this.id = id;
this.frame = frame;
this.options = options;
this.webNav = appShell.createWindowlessBrowser(false);
this.docShell.allowJavascript = this.options.allow.script;
// Accessing the browser's window forces the initial about:blank document to
// be created before we start listening for notifications
this.contentWindow;
this.webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
pages.set(this.id, this);
this.contentURL = options.contentURL;
if (options.include) {
this.rules = Rules();
this.rules.add.apply(this.rules, [].concat(options.include));
}
},
dispose: function() {
pages.delete(this.id);
this.webProgress.removeProgressListener(this);
this.webNav.close();
this.webNav = null;
},
attachWorker: function() {
if (!isValidURL(this, this.contentWindow.location.href))
return;
this.options.id = uuid().toString();
this.options.window = this.contentWindow;
this.frame.port.emit("sdk/frame/connect", this.id, {
id: this.options.id,
url: this.contentWindow.document.documentURIObject.spec
});
new WorkerChild(this.options);
},
get docShell() {
return this.webNav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
},
get webProgress() {
return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
},
get contentWindow() {
return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
},
get contentURL() {
return this.options.contentURL;
},
set contentURL(url) {
this.options.contentURL = url;
url = this.options.contentURL ? data.url(this.options.contentURL) : "about:blank";
this.webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
},
onLocationChange: function(progress, request, location, flags) {
// Ignore inner-frame events
if (progress != this.webProgress)
return;
// Ignore events that don't change the document
if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
return;
let event = getAttachEventType(this.options);
// Attaching at the start of the load is handled by the
// document-element-inserted listener.
if (event == DOC_INSERTED)
return;
once(this.contentWindow, event, () => {
this.attachWorker();
}, false);
},
QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"])
});
on(observe(DOC_INSERTED), "data", ({ target }) => {
let page = Array.from(pages.values()).find(p => p.contentWindow.document === target);
if (!page)
return;
if (getAttachEventType(page.options) == DOC_INSERTED)
page.attachWorker();
});
frames.port.on("sdk/frame/create", (frame, id, options) => {
new ChildPage(frame, id, options);
});
frames.port.on("sdk/frame/set", (frame, id, params) => {
let page = pages.get(id);
if (!page)
return;
if ("allowScript" in params)
page.docShell.allowJavascript = params.allowScript;
if ("contentURL" in params)
page.contentURL = params.contentURL;
});
frames.port.on("sdk/frame/destroy", (frame, id) => {
let page = pages.get(id);
if (!page)
return;
page.destroy();
});

View file

@ -0,0 +1,426 @@
/* 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 { events } = require('./sandbox/events');
const { requiresAddonGlobal } = 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 { getTabForContentWindowNoShim } = require('../tabs/utils');
const { getInnerId } = require('../window/utils');
const { PlainTextConsole } = require('../console/plain-text');
const { data } = require('../self');const { isChildLoader } = require('../remote/core');
// 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:
*/
var prefix = module.uri.split('sandbox.js')[0];
const CONTENT_WORKER_URL = prefix + 'content-worker.js';
const metadata = require('@loader/options').metadata;
// 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 = (metadata && metadata['permissions']) || {};
const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
const waiveSecurityMembrane = !!permissions['unsafe-content-script'];
const nsIScriptSecurityManager = Ci.nsIScriptSecurityManager;
const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].
getService(Ci.nsIScriptSecurityManager);
const JS_VERSION = '1.8';
// Tests whether this window is loaded in a tab
function isWindowInTab(window) {
if (isChildLoader) {
let { frames } = require('../remote/child');
let frame = frames.getFrameForWindow(window.top);
return frame && frame.isTab;
}
else {
// The deprecated sync worker API still does everything in the main process
return getTabForContentWindowNoShim(window);
}
}
const WorkerSandbox = Class({
implements: [ EventTarget ],
/**
* Emit a message to the worker content sandbox
*/
emit: function emit(type, ...args) {
// JSON.stringify is buggy with cross-sandbox values,
// it may return "{}" on functions. Use a replacer to match them correctly.
let replacer = (k, v) =>
typeof(v) === "function"
? (type === "console" ? Function.toString.call(v) : void(0))
: v;
// Ensure having an asynchronous behavior
async(() =>
emitToContent(this, JSON.stringify([type, ...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) {
// because the arguments could be also non JSONable values,
// we need to ensure the array instance is created from
// the content's sandbox
return emitToContent(this, new modelFor(this).sandbox.Array(...args));
},
/**
* 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);
// Use expanded principal for content-script if the content is a
// regular web content for better isolation.
// (This behavior can be turned off for now with the unsafe-content-script
// flag to give addon developers time for making the necessary changes)
// But prevent it when the Worker isn't used for a content script but for
// injecting `addon` object into a Panel scope, for example.
// 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 = [];
let isSystemPrincipal = secMan.isSystemPrincipal(
window.document.nodePrincipal);
if (!isSystemPrincipal && !requiresAddonGlobal(worker)) {
if (EXPANDED_PRINCIPALS.length > 0) {
// 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');
}
if (!waiveSecurityMembrane)
principals = EXPANDED_PRINCIPALS.concat(window);
}
// 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: !requiresAddonGlobal(worker),
wantGlobalProperties: wantGlobalProperties,
wantExportHelpers: true,
sameZoneAs: window,
metadata: {
SDKContentScript: true,
'inner-window-id': getInnerId(window)
}
});
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() {
return content;
},
get top() {
return top;
},
get parent() {
return 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!
//
// Note that because waivers aren't propagated between origins, we
// need the unsafeWindow getter to live in the sandbox.
var unsafeWindowGetter =
new content.Function('return window.wrappedJSObject || window;');
Object.defineProperty(content, 'unsafeWindow', {get: unsafeWindowGetter});
// Load trusted code that will inject content script API.
let ContentWorker = load(content, 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
let onEvent = Cu.exportFunction(onContentEvent.bind(null, this), ContentWorker);
let chromeAPI = createChromeAPI(ContentWorker);
let result = Cu.waiveXrays(ContentWorker).inject(content, chromeAPI, onEvent, options);
// Merge `emitToContent` into our private model of the
// WorkerSandbox so we can communicate with content script
model.emitToContent = result;
let console = new PlainTextConsole(null, getInnerId(window));
// Handle messages send by this script:
setListeners(this, console);
// 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,
configurable: true
}
);
}
// Inject our `console` into target document if worker doesn't have a tab
// (e.g Panel, PageWorker).
// `worker.tab` can't be used because bug 804935.
if (!isWindowInTab(window)) {
let win = getUnsafeWindow(window);
// export our chrome console to content window, as 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'),
exception: genPropDesc('exception'),
assert: genPropDesc('assert'),
count: genPropDesc('count'),
table: genPropDesc('table'),
clear: genPropDesc('clear'),
dirxml: genPropDesc('dirxml'),
markTimeline: genPropDesc('markTimeline'),
timeline: genPropDesc('timeline'),
timelineEnd: genPropDesc('timelineEnd'),
timeStamp: genPropDesc('timeStamp'),
};
Object.defineProperties(con, properties);
Cu.makeObjectPropsNormal(con);
win.console = con;
};
emit(events, "content-script-before-inserted", {
window: window,
worker: worker
});
// 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(reason) {
if (typeof reason != 'string')
reason = '';
this.emitSync('event', 'detach', reason);
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 = data.url(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, console) {
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);
}
function getUnsafeWindow (win) {
return win.wrappedJSObject || win;
}
function emitToContent (workerSandbox, args) {
return modelFor(workerSandbox).emitToContent(args);
}
function createChromeAPI (scope) {
return Cu.cloneInto({
timers: {
setTimeout: timer.setTimeout.bind(timer),
setInterval: timer.setInterval.bind(timer),
clearTimeout: timer.clearTimeout.bind(timer),
clearInterval: timer.clearInterval.bind(timer),
},
sandbox: {
evaluate: evaluate,
},
}, scope, {cloneFunctions: true});
}

View file

@ -0,0 +1,12 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const events = {};
exports.events = events;

View file

@ -0,0 +1,58 @@
/* 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 { Ci } = require('chrome');
const system = require('sdk/system/events');
const { frames } = require('sdk/remote/child');
const { WorkerChild } = require('sdk/content/worker-child');
// map observer topics to tab event names
const EVENTS = {
'content-document-global-created': 'create',
'chrome-document-global-created': 'create',
'content-document-interactive': 'ready',
'chrome-document-interactive': 'ready',
'content-document-loaded': 'load',
'chrome-document-loaded': 'load',
// 'content-page-shown': 'pageshow', // bug 1024105
}
function topicListener({ subject, type }) {
// NOTE detect the window from the subject:
// - on *-global-created the subject is the window
// - in the other cases it is the document object
let window = subject instanceof Ci.nsIDOMWindow ? subject : subject.defaultView;
if (!window){
return;
}
let frame = frames.getFrameForWindow(window);
if (frame) {
let readyState = frame.content.document.readyState;
frame.port.emit('sdk/tab/event', EVENTS[type], { readyState });
}
}
for (let topic in EVENTS)
system.on(topic, topicListener, true);
// bug 1024105 - content-page-shown notification doesn't pass persisted param
function eventListener({target, type, persisted}) {
let frame = this;
if (target === frame.content.document) {
frame.port.emit('sdk/tab/event', type, persisted);
}
}
frames.addEventListener('pageshow', eventListener, true);
frames.port.on('sdk/tab/attach', (frame, options) => {
options.window = frame.content;
new WorkerChild(options);
});
// Forward the existent frames's readyState.
for (let frame of frames) {
let readyState = frame.content.document.readyState;
frame.port.emit('sdk/tab/event', 'init', { readyState });
}

View file

@ -0,0 +1,51 @@
/* 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 { Cc, Ci, Cu } = require('chrome');
const AppShellService = Cc['@mozilla.org/appshell/appShellService;1'].
getService(Ci.nsIAppShellService);
const NS = 'http://www.w3.org/1999/xhtml';
const COLOR = 'rgb(255,255,255)';
/**
* Creates canvas element with a thumbnail of the passed window.
* @param {Window} window
* @returns {Element}
*/
function getThumbnailCanvasForWindow(window) {
let aspectRatio = 0.5625; // 16:9
let thumbnail = AppShellService.hiddenDOMWindow.document
.createElementNS(NS, 'canvas');
thumbnail.mozOpaque = true;
thumbnail.width = Math.ceil(window.screen.availWidth / 5.75);
thumbnail.height = Math.round(thumbnail.width * aspectRatio);
let ctx = thumbnail.getContext('2d');
let snippetWidth = window.innerWidth * .6;
let scale = thumbnail.width / snippetWidth;
ctx.scale(scale, scale);
ctx.drawWindow(window, window.scrollX, window.scrollY, snippetWidth,
snippetWidth * aspectRatio, COLOR);
return thumbnail;
}
exports.getThumbnailCanvasForWindow = getThumbnailCanvasForWindow;
/**
* Creates Base64 encoded data URI of the thumbnail for the passed window.
* @param {Window} window
* @returns {String}
*/
exports.getThumbnailURIForWindow = function getThumbnailURIForWindow(window) {
return getThumbnailCanvasForWindow(window).toDataURL()
};
// default 80x45 blank when not available
exports.BLANK = 'data:image/png;base64,' +
'iVBORw0KGgoAAAANSUhEUgAAAFAAAAAtCAYAAAA5reyyAAAAJElEQVRoge3BAQ'+
'EAAACCIP+vbkhAAQAAAAAAAAAAAAAAAADXBjhtAAGQ0AF/AAAAAElFTkSuQmCC';

View file

@ -0,0 +1,105 @@
/* 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'
};
var { merge } = require('../util/object');
var { data } = require('../self');
var assetsURI = data.url();
var isArray = Array.isArray;
var method = require('../../method/core');
var { uuid } = require('../util/uuid');
const isAddonContent = ({ contentURL }) =>
contentURL && data.url(contentURL).startsWith(assetsURI);
exports.isAddonContent = isAddonContent;
function hasContentScript({ contentScript, contentScriptFile }) {
return (isArray(contentScript) ? contentScript.length > 0 :
!!contentScript) ||
(isArray(contentScriptFile) ? contentScriptFile.length > 0 :
!!contentScriptFile);
}
exports.hasContentScript = hasContentScript;
function requiresAddonGlobal(model) {
return 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 === 'ready' ? 'DOMContentLoaded' :
when === 'end' ? 'load' :
null;
}
exports.getAttachEventType = getAttachEventType;
var attach = method('worker-attach');
exports.attach = attach;
var connect = method('worker-connect');
exports.connect = connect;
var detach = method('worker-detach');
exports.detach = detach;
var 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;
function makeChildOptions(options) {
function makeStringArray(arrayOrValue) {
if (!arrayOrValue)
return [];
return [].concat(arrayOrValue).map(String);
}
return {
id: String(uuid()),
contentScript: makeStringArray(options.contentScript),
contentScriptFile: makeStringArray(options.contentScriptFile),
contentScriptOptions: options.contentScriptOptions ?
JSON.stringify(options.contentScriptOptions) :
null,
}
}
exports.makeChildOptions = makeChildOptions;

View file

@ -0,0 +1,158 @@
/* 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 { merge } = require('../util/object');
const { Class } = require('../core/heritage');
const { emit } = require('../event/core');
const { EventTarget } = require('../event/target');
const { getInnerId, getByInnerId } = require('../window/utils');
const { instanceOf, isObject } = require('../lang/type');
const system = require('../system/events');
const { when } = require('../system/unload');
const { WorkerSandbox } = require('./sandbox');
const { Ci } = require('chrome');
const { process, frames } = require('../remote/child');
const EVENTS = {
'chrome-page-shown': 'pageshow',
'content-page-shown': 'pageshow',
'chrome-page-hidden': 'pagehide',
'content-page-hidden': 'pagehide',
'inner-window-destroyed': 'detach',
}
// The parent Worker must have been created (or an async message sent to spawn
// its creation) before creating the WorkerChild or messages from the content
// script to the parent will get lost.
const WorkerChild = Class({
implements: [EventTarget],
initialize(options) {
merge(this, options);
keepAlive.set(this.id, this);
this.windowId = getInnerId(this.window);
if (this.contentScriptOptions)
this.contentScriptOptions = JSON.parse(this.contentScriptOptions);
this.port = EventTarget();
this.port.on('*', this.send.bind(this, 'event'));
this.on('*', this.send.bind(this));
this.observe = this.observe.bind(this);
for (let topic in EVENTS)
system.on(topic, this.observe);
this.receive = this.receive.bind(this);
process.port.on('sdk/worker/message', this.receive);
this.sandbox = WorkerSandbox(this, this.window);
// If the document has an unexpected readyState, its worker-child instance is initialized
// as frozen until one of the known readyState is reached.
let initialDocumentReadyState = this.window.document.readyState;
this.frozen = [
"loading", "interactive", "complete"
].includes(initialDocumentReadyState) ? false : true;
if (this.frozen) {
console.warn("SDK worker-child started as frozen on unexpected initial document.readyState", {
initialDocumentReadyState, windowLocation: this.window.location.href,
});
}
this.frozenMessages = [];
this.on('pageshow', () => {
this.frozen = false;
this.frozenMessages.forEach(args => this.sandbox.emit(...args));
this.frozenMessages = [];
});
this.on('pagehide', () => {
this.frozen = true;
});
},
// messages
receive(process, id, args) {
if (id !== this.id)
return;
args = JSON.parse(args);
if (this.frozen)
this.frozenMessages.push(args);
else
this.sandbox.emit(...args);
if (args[0] === 'detach')
this.destroy(args[1]);
},
send(...args) {
process.port.emit('sdk/worker/event', this.id, JSON.stringify(args, exceptions));
},
// notifications
observe({ type, subject }) {
if (!this.sandbox)
return;
if (subject.defaultView && getInnerId(subject.defaultView) === this.windowId) {
this.sandbox.emitSync(EVENTS[type]);
emit(this, EVENTS[type]);
}
if (type === 'inner-window-destroyed' &&
subject.QueryInterface(Ci.nsISupportsPRUint64).data === this.windowId) {
this.destroy();
}
},
get frame() {
return frames.getFrameForWindow(this.window.top);
},
// detach/destroy: unload and release the sandbox
destroy(reason) {
if (!this.sandbox)
return;
for (let topic in EVENTS)
system.off(topic, this.observe);
process.port.off('sdk/worker/message', this.receive);
this.sandbox.destroy(reason);
this.sandbox = null;
keepAlive.delete(this.id);
this.send('detach');
}
})
exports.WorkerChild = WorkerChild;
// Error instances JSON poorly
function exceptions(key, value) {
if (!isObject(value) || !instanceOf(value, Error))
return value;
let _errorType = value.constructor.name;
let { message, fileName, lineNumber, stack, name } = value;
return { _errorType, message, fileName, lineNumber, stack, name };
}
// workers for windows in this tab
var keepAlive = new Map();
process.port.on('sdk/worker/create', (process, options, cpows) => {
options.window = cpows.window;
let worker = new WorkerChild(options);
let frame = frames.getFrameForWindow(options.window.top);
frame.port.emit('sdk/worker/connect', options.id, options.window.location.href);
});
when(reason => {
for (let worker of keepAlive.values())
worker.destroy(reason);
});

View file

@ -0,0 +1,180 @@
/* 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 { emit } = require('../event/core');
const { omit, merge } = require('../util/object');
const { Class } = require('../core/heritage');
const { method } = require('../lang/functional');
const { getInnerId } = require('../window/utils');
const { EventTarget } = require('../event/target');
const { isPrivate } = require('../private-browsing/utils');
const { getTabForBrowser, getTabForContentWindowNoShim, getBrowserForTab } = require('../tabs/utils');
const { attach, connect, detach, destroy, makeChildOptions } = require('./utils');
const { ensure } = require('../system/unload');
const { on: observe } = require('../system/events');
const { Ci, Cu } = require('chrome');
const { modelFor: tabFor } = require('sdk/model/core');
const { remoteRequire, processes, frames } = require('../remote/parent');
remoteRequire('sdk/content/worker-child');
const workers = new WeakMap();
var modelFor = (worker) => workers.get(worker);
const ERR_DESTROYED = "Couldn't find the worker to receive this message. " +
"The script may not be initialized yet, or may already have been unloaded.";
// a handle for communication between content script and addon code
const Worker = Class({
implements: [EventTarget],
initialize(options = {}) {
ensure(this, 'detach');
let model = {
attached: false,
destroyed: false,
earlyEvents: [], // fired before worker was attached
frozen: true, // document is not yet active
options,
};
workers.set(this, model);
this.on('detach', this.detach);
EventTarget.prototype.initialize.call(this, options);
this.receive = this.receive.bind(this);
this.port = EventTarget();
this.port.emit = this.send.bind(this, 'event');
this.postMessage = this.send.bind(this, 'message');
if ('window' in options) {
let window = options.window;
delete options.window;
attach(this, window);
}
},
// messages
receive(process, id, args) {
let model = modelFor(this);
if (id !== model.id || !model.attached)
return;
args = JSON.parse(args);
if (model.destroyed && args[0] != 'detach')
return;
if (args[0] === 'event')
emit(this.port, ...args.slice(1))
else
emit(this, ...args);
},
send(...args) {
let model = modelFor(this);
if (model.destroyed && args[0] !== 'detach')
throw new Error(ERR_DESTROYED);
if (!model.attached) {
model.earlyEvents.push(args);
return;
}
processes.port.emit('sdk/worker/message', model.id, JSON.stringify(args));
},
// properties
get url() {
let { url } = modelFor(this);
return url;
},
get contentURL() {
return this.url;
},
get tab() {
require('sdk/tabs');
let { frame } = modelFor(this);
if (!frame)
return null;
let rawTab = getTabForBrowser(frame.frameElement);
return rawTab && tabFor(rawTab);
},
toString: () => '[object Worker]',
detach: method(detach),
destroy: method(destroy),
})
exports.Worker = Worker;
attach.define(Worker, function(worker, window) {
let model = modelFor(worker);
if (model.attached)
detach(worker);
let childOptions = makeChildOptions(model.options);
processes.port.emitCPOW('sdk/worker/create', [childOptions], { window });
let listener = (frame, id, url) => {
if (id != childOptions.id)
return;
frames.port.off('sdk/worker/connect', listener);
connect(worker, frame, { id, url });
};
frames.port.on('sdk/worker/connect', listener);
});
connect.define(Worker, function(worker, frame, { id, url }) {
let model = modelFor(worker);
if (model.attached)
detach(worker);
model.id = id;
model.frame = frame;
model.url = url;
// Messages from content -> chrome come through the process message manager
// since that lives longer than the frame message manager
processes.port.on('sdk/worker/event', worker.receive);
model.attached = true;
model.destroyed = false;
model.frozen = false;
model.earlyEvents.forEach(args => worker.send(...args));
model.earlyEvents = [];
emit(worker, 'attach');
});
// unload and release the child worker, release window reference
detach.define(Worker, function(worker) {
let model = modelFor(worker);
if (!model.attached)
return;
processes.port.off('sdk/worker/event', worker.receive);
model.attached = false;
model.destroyed = true;
emit(worker, 'detach');
});
isPrivate.define(Worker, ({ tab }) => isPrivate(tab));
// Something in the parent side has destroyed the worker, tell the child to
// detach, the child will respond when it has detached
destroy.define(Worker, function(worker, reason) {
let model = modelFor(worker);
model.destroyed = true;
if (!model.attached)
return;
worker.send('detach', reason);
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,147 @@
/* 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 { Class } = require("../core/heritage");
const { extend } = require("../util/object");
const { MatchPattern } = require("../util/match-pattern");
const readers = require("./readers");
// Context class is required to implement a single `isCurrent(target)` method
// that must return boolean value indicating weather given target matches a
// context or not. Most context implementations below will have an associated
// reader that way context implementation can setup a reader to extract necessary
// information to make decision if target is matching a context.
const Context = Class({
isRequired: false,
isCurrent(target) {
throw Error("Context class must implement isCurrent(target) method");
},
get required() {
Object.defineProperty(this, "required", {
value: Object.assign(Object.create(Object.getPrototypeOf(this)),
this,
{isRequired: true})
});
return this.required;
}
});
Context.required = function(...params) {
return Object.assign(new this(...params), {isRequired: true});
};
exports.Context = Context;
// Next few context implementations use an associated reader to extract info
// from the context target and story it to a private symbol associtaed with
// a context implementation. That way name collisions are avoided while required
// information is still carried along.
const isPage = Symbol("context/page?")
const PageContext = Class({
extends: Context,
read: {[isPage]: new readers.isPage()},
isCurrent: target => target[isPage]
});
exports.Page = PageContext;
const isFrame = Symbol("context/frame?");
const FrameContext = Class({
extends: Context,
read: {[isFrame]: new readers.isFrame()},
isCurrent: target => target[isFrame]
});
exports.Frame = FrameContext;
const selection = Symbol("context/selection")
const SelectionContext = Class({
read: {[selection]: new readers.Selection()},
isCurrent: target => !!target[selection]
});
exports.Selection = SelectionContext;
const link = Symbol("context/link");
const LinkContext = Class({
extends: Context,
read: {[link]: new readers.LinkURL()},
isCurrent: target => !!target[link]
});
exports.Link = LinkContext;
const isEditable = Symbol("context/editable?")
const EditableContext = Class({
extends: Context,
read: {[isEditable]: new readers.isEditable()},
isCurrent: target => target[isEditable]
});
exports.Editable = EditableContext;
const mediaType = Symbol("context/mediaType")
const ImageContext = Class({
extends: Context,
read: {[mediaType]: new readers.MediaType()},
isCurrent: target => target[mediaType] === "image"
});
exports.Image = ImageContext;
const VideoContext = Class({
extends: Context,
read: {[mediaType]: new readers.MediaType()},
isCurrent: target => target[mediaType] === "video"
});
exports.Video = VideoContext;
const AudioContext = Class({
extends: Context,
read: {[mediaType]: new readers.MediaType()},
isCurrent: target => target[mediaType] === "audio"
});
exports.Audio = AudioContext;
const isSelectorMatch = Symbol("context/selector/mathches?")
const SelectorContext = Class({
extends: Context,
initialize(selector) {
this.selector = selector;
// Each instance of selector context will need to store read
// data into different field, so that case with multilpe selector
// contexts won't cause a conflicts.
this[isSelectorMatch] = Symbol(selector);
this.read = {[this[isSelectorMatch]]: new readers.SelectorMatch(selector)};
},
isCurrent(target) {
return target[this[isSelectorMatch]];
}
});
exports.Selector = SelectorContext;
const url = Symbol("context/url");
const URLContext = Class({
extends: Context,
initialize(pattern) {
this.pattern = new MatchPattern(pattern);
},
read: {[url]: new readers.PageURL()},
isCurrent(target) {
return this.pattern.test(target[url]);
}
});
exports.URL = URLContext;
var PredicateContext = Class({
extends: Context,
initialize(isMatch) {
if (typeof(isMatch) !== "function") {
throw TypeError("Predicate context mus be passed a function");
}
this.isMatch = isMatch
},
isCurrent(target) {
return this.isMatch(target);
}
});
exports.Predicate = PredicateContext;

View file

@ -0,0 +1,384 @@
/* 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 Contexts = require("./context");
const Readers = require("./readers");
const Component = require("../ui/component");
const { Class } = require("../core/heritage");
const { map, filter, object, reduce, keys, symbols,
pairs, values, each, some, isEvery, count } = require("../util/sequence");
const { loadModule } = require("framescript/manager");
const { Cu, Cc, Ci } = require("chrome");
const prefs = require("sdk/preferences/service");
const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
const preferencesService = Cc["@mozilla.org/preferences-service;1"].
getService(Ci.nsIPrefService).
getBranch(null);
const readTable = Symbol("context-menu/read-table");
const nameTable = Symbol("context-menu/name-table");
const onContext = Symbol("context-menu/on-context");
const isMatching = Symbol("context-menu/matching-handler?");
exports.onContext = onContext;
exports.readTable = readTable;
exports.nameTable = nameTable;
const propagateOnContext = (item, data) =>
each(child => child[onContext](data), item.state.children);
const isContextMatch = item => !item[isMatching] || item[isMatching]();
// For whatever reason addWeakMessageListener does not seems to work as our
// instance seems to dropped even though it's alive. This is simple workaround
// to avoid dead object excetptions.
const WeakMessageListener = function(receiver, handler="receiveMessage") {
this.receiver = receiver
this.handler = handler
};
WeakMessageListener.prototype = {
constructor: WeakMessageListener,
receiveMessage(message) {
if (Cu.isDeadWrapper(this.receiver)) {
message.target.messageManager.removeMessageListener(message.name, this);
}
else {
this.receiver[this.handler](message);
}
}
};
const OVERFLOW_THRESH = "extensions.addon-sdk.context-menu.overflowThreshold";
const onMessage = Symbol("context-menu/message-listener");
const onPreferceChange = Symbol("context-menu/preference-change");
const ContextMenuExtension = Class({
extends: Component,
initialize: Component,
setup() {
const messageListener = new WeakMessageListener(this, onMessage);
loadModule(globalMessageManager, "framescript/context-menu", true, "onContentFrame");
globalMessageManager.addMessageListener("sdk/context-menu/read", messageListener);
globalMessageManager.addMessageListener("sdk/context-menu/readers?", messageListener);
preferencesService.addObserver(OVERFLOW_THRESH, this, false);
},
observe(_, __, name) {
if (name === OVERFLOW_THRESH) {
const overflowThreshold = prefs.get(OVERFLOW_THRESH, 10);
this[Component.patch]({overflowThreshold});
}
},
[onMessage]({name, data, target}) {
if (name === "sdk/context-menu/read")
this[onContext]({target, data});
if (name === "sdk/context-menu/readers?")
target.messageManager.sendAsyncMessage("sdk/context-menu/readers",
JSON.parse(JSON.stringify(this.state.readers)));
},
[Component.initial](options={}, children) {
const element = options.element || null;
const target = options.target || null;
const readers = Object.create(null);
const users = Object.create(null);
const registry = new WeakSet();
const overflowThreshold = prefs.get(OVERFLOW_THRESH, 10);
return { target, children: [], readers, users, element,
registry, overflowThreshold };
},
[Component.isUpdated](before, after) {
// Update only if target changed, since there is no point in re-rendering
// when children are. Also new items added won't be in sync with a latest
// context target so we should really just render before drawing context
// menu.
return before.target !== after.target;
},
[Component.render]({element, children, overflowThreshold}) {
if (!element) return null;
const items = children.filter(isContextMatch);
const body = items.length === 0 ? items :
items.length < overflowThreshold ? [new Separator(),
...items] :
[{tagName: "menu",
className: "sdk-context-menu-overflow-menu",
label: "Add-ons",
accesskey: "A",
children: [{tagName: "menupopup",
children: items}]}];
return {
element: element,
tagName: "menugroup",
style: "-moz-box-orient: vertical;",
className: "sdk-context-menu-extension",
children: body
}
},
// Adds / remove child to it's own list.
add(item) {
this[Component.patch]({children: this.state.children.concat(item)});
},
remove(item) {
this[Component.patch]({
children: this.state.children.filter(x => x !== item)
});
},
register(item) {
const { users, registry } = this.state;
if (registry.has(item)) return;
registry.add(item);
// Each (ContextHandler) item has a readTable that is a
// map of keys to readers extracting them from the content.
// During the registraction we update intrnal record of unique
// readers and users per reader. Most context will have a reader
// shared across all instances there for map of users per reader
// is stored separately from the reader so that removing reader
// will occur only when no users remain.
const table = item[readTable];
// Context readers store data in private symbols so we need to
// collect both table keys and private symbols.
const names = [...keys(table), ...symbols(table)];
const readers = map(name => table[name], names);
// Create delta for registered readers that will be merged into
// internal readers table.
const added = filter(x => !users[x.id], readers);
const delta = object(...map(x => [x.id, x], added));
const update = reduce((update, reader) => {
const n = update[reader.id] || 0;
update[reader.id] = n + 1;
return update;
}, Object.assign({}, users), readers);
// Patch current state with a changes that registered item caused.
this[Component.patch]({users: update,
readers: Object.assign(this.state.readers, delta)});
if (count(added)) {
globalMessageManager.broadcastAsyncMessage("sdk/context-menu/readers",
JSON.parse(JSON.stringify(delta)));
}
},
unregister(item) {
const { users, registry } = this.state;
if (!registry.has(item)) return;
registry.delete(item);
const table = item[readTable];
const names = [...keys(table), ...symbols(table)];
const readers = map(name => table[name], names);
const update = reduce((update, reader) => {
update[reader.id] = update[reader.id] - 1;
return update;
}, Object.assign({}, users), readers);
const removed = filter(id => !update[id], keys(update));
const delta = object(...map(x => [x, null], removed));
this[Component.patch]({users: update,
readers: Object.assign(this.state.readers, delta)});
if (count(removed)) {
globalMessageManager.broadcastAsyncMessage("sdk/context-menu/readers",
JSON.parse(JSON.stringify(delta)));
}
},
[onContext]({data, target}) {
propagateOnContext(this, data);
const document = target.ownerDocument;
const element = document.getElementById("contentAreaContextMenu");
this[Component.patch]({target: data, element: element});
}
});this,
exports.ContextMenuExtension = ContextMenuExtension;
// Takes an item options and
const makeReadTable = ({context, read}) => {
// Result of this function is a tuple of all readers &
// name, reader id pairs.
// Filter down to contexts that have a reader associated.
const contexts = filter(context => context.read, context);
// Merge all contexts read maps to a single hash, note that there should be
// no name collisions as context implementations expect to use private
// symbols for storing it's read data.
return Object.assign({}, ...map(({read}) => read, contexts), read);
}
const readTarget = (nameTable, data) =>
object(...map(([name, id]) => [name, data[id]], nameTable))
const ContextHandler = Class({
extends: Component,
initialize: Component,
get context() {
return this.state.options.context;
},
get read() {
return this.state.options.read;
},
[Component.initial](options) {
return {
table: makeReadTable(options),
requiredContext: filter(context => context.isRequired, options.context),
optionalContext: filter(context => !context.isRequired, options.context)
}
},
[isMatching]() {
const {target, requiredContext, optionalContext} = this.state;
return isEvery(context => context.isCurrent(target), requiredContext) &&
(count(optionalContext) === 0 ||
some(context => context.isCurrent(target), optionalContext));
},
setup() {
const table = makeReadTable(this.state.options);
this[readTable] = table;
this[nameTable] = [...map(symbol => [symbol, table[symbol].id], symbols(table)),
...map(name => [name, table[name].id], keys(table))];
contextMenu.register(this);
each(child => contextMenu.remove(child), this.state.children);
contextMenu.add(this);
},
dispose() {
contextMenu.remove(this);
each(child => contextMenu.unregister(child), this.state.children);
contextMenu.unregister(this);
},
// Internal `Symbol("onContext")` method is invoked when "contextmenu" event
// occurs in content process. Context handles with children delegate to each
// child and patch it's internal state to reflect new contextmenu target.
[onContext](data) {
propagateOnContext(this, data);
this[Component.patch]({target: readTarget(this[nameTable], data)});
}
});
const isContextHandler = item => item instanceof ContextHandler;
exports.ContextHandler = ContextHandler;
const Menu = Class({
extends: ContextHandler,
[isMatching]() {
return ContextHandler.prototype[isMatching].call(this) &&
this.state.children.filter(isContextHandler)
.some(isContextMatch);
},
[Component.render]({children, options}) {
const items = children.filter(isContextMatch);
return {tagName: "menu",
className: "sdk-context-menu menu-iconic",
label: options.label,
accesskey: options.accesskey,
image: options.icon,
children: [{tagName: "menupopup",
children: items}]};
}
});
exports.Menu = Menu;
const onCommand = Symbol("context-menu/item/onCommand");
const Item = Class({
extends: ContextHandler,
get onClick() {
return this.state.options.onClick;
},
[Component.render]({options}) {
const {label, icon, accesskey} = options;
return {tagName: "menuitem",
className: "sdk-context-menu-item menuitem-iconic",
label,
accesskey,
image: icon,
oncommand: this};
},
handleEvent(event) {
if (this.onClick)
this.onClick(this.state.target);
}
});
exports.Item = Item;
var Separator = Class({
extends: Component,
initialize: Component,
[Component.render]() {
return {tagName: "menuseparator",
className: "sdk-context-menu-separator"}
},
[onContext]() {
}
});
exports.Separator = Separator;
exports.Contexts = Contexts;
exports.Readers = Readers;
const createElement = (vnode, {document}) => {
const node = vnode.namespace ?
document.createElementNS(vnode.namespace, vnode.tagName) :
document.createElement(vnode.tagName);
node.setAttribute("data-component-path", vnode[Component.path]);
each(([key, value]) => {
if (key === "tagName") {
return;
}
if (key === "children") {
return;
}
if (key.startsWith("on")) {
node.addEventListener(key.substr(2), value)
return;
}
if (typeof(value) !== "object" &&
typeof(value) !== "function" &&
value !== void(0) &&
value !== null)
{
if (key === "className") {
node[key] = value;
}
else {
node.setAttribute(key, value);
}
return;
}
}, pairs(vnode));
each(child => node.appendChild(createElement(child, {document})), vnode.children);
return node;
};
const htmlWriter = tree => {
if (tree !== null) {
const root = tree.element;
const node = createElement(tree, {document: root.ownerDocument});
const before = root.querySelector("[data-component-path='/']");
if (before) {
root.replaceChild(node, before);
} else {
root.appendChild(node);
}
}
};
const contextMenu = ContextMenuExtension();
exports.contextMenu = contextMenu;
Component.mount(contextMenu, htmlWriter);

View file

@ -0,0 +1,112 @@
/* 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 { Class } = require("../core/heritage");
const { extend } = require("../util/object");
const { memoize, method, identity } = require("../lang/functional");
const serializeCategory = ({type}) => ({ category: `reader/${type}()` });
const Reader = Class({
initialize() {
this.id = `reader/${this.type}()`
},
toJSON() {
return serializeCategory(this);
}
});
const MediaTypeReader = Class({ extends: Reader, type: "MediaType" });
exports.MediaType = MediaTypeReader;
const LinkURLReader = Class({ extends: Reader, type: "LinkURL" });
exports.LinkURL = LinkURLReader;
const SelectionReader = Class({ extends: Reader, type: "Selection" });
exports.Selection = SelectionReader;
const isPageReader = Class({ extends: Reader, type: "isPage" });
exports.isPage = isPageReader;
const isFrameReader = Class({ extends: Reader, type: "isFrame" });
exports.isFrame = isFrameReader;
const isEditable = Class({ extends: Reader, type: "isEditable"});
exports.isEditable = isEditable;
const ParameterizedReader = Class({
extends: Reader,
readParameter: function(value) {
return value;
},
toJSON: function() {
var json = serializeCategory(this);
json[this.parameter] = this[this.parameter];
return json;
},
initialize(...params) {
if (params.length) {
this[this.parameter] = this.readParameter(...params);
}
this.id = `reader/${this.type}(${JSON.stringify(this[this.parameter])})`;
}
});
exports.ParameterizedReader = ParameterizedReader;
const QueryReader = Class({
extends: ParameterizedReader,
type: "Query",
parameter: "path"
});
exports.Query = QueryReader;
const AttributeReader = Class({
extends: ParameterizedReader,
type: "Attribute",
parameter: "name"
});
exports.Attribute = AttributeReader;
const SrcURLReader = Class({
extends: AttributeReader,
name: "src",
});
exports.SrcURL = SrcURLReader;
const PageURLReader = Class({
extends: QueryReader,
path: "ownerDocument.URL",
});
exports.PageURL = PageURLReader;
const SelectorMatchReader = Class({
extends: ParameterizedReader,
type: "SelectorMatch",
parameter: "selector"
});
exports.SelectorMatch = SelectorMatchReader;
const extractors = new WeakMap();
extractors.id = 0;
var Extractor = Class({
extends: ParameterizedReader,
type: "Extractor",
parameter: "source",
initialize: function(f) {
this[this.parameter] = String(f);
if (!extractors.has(f)) {
extractors.id = extractors.id + 1;
extractors.set(f, extractors.id);
}
this.id = `reader/${this.type}.for(${extractors.get(f)})`
}
});
exports.Extractor = Extractor;

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";
const shared = require("toolkit/require");
const { Item, Separator, Menu, Contexts, Readers } = shared.require("sdk/context-menu/core");
const { setupDisposable, disposeDisposable, Disposable } = require("sdk/core/disposable")
const { Class } = require("sdk/core/heritage")
const makeDisposable = Type => Class({
extends: Type,
implements: [Disposable],
initialize: Type.prototype.initialize,
setup(...params) {
Type.prototype.setup.call(this, ...params);
setupDisposable(this);
},
dispose(...params) {
disposeDisposable(this);
Type.prototype.dispose.call(this, ...params);
}
});
exports.Separator = Separator;
exports.Contexts = Contexts;
exports.Readers = Readers;
// Subclass Item & Menu shared classes so their items
// will be unloaded when add-on is unloaded.
exports.Item = makeDisposable(Item);
exports.Menu = makeDisposable(Menu);

View file

@ -0,0 +1,186 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Class } = require("./heritage");
const { Observer, subscribe, unsubscribe, observe } = require("./observer");
const { isWeak } = require("./reference");
const SDKWeakSet = require("../lang/weak-set");
const method = require("../../method/core");
const unloadSubject = require('@loader/unload');
const addonUnloadTopic = "sdk:loader:destroy";
const uninstall = method("disposable/uninstall");
exports.uninstall = uninstall;
const shutdown = method("disposable/shutdown");
exports.shutdown = shutdown;
const disable = method("disposable/disable");
exports.disable = disable;
const upgrade = method("disposable/upgrade");
exports.upgrade = upgrade;
const downgrade = method("disposable/downgrade");
exports.downgrade = downgrade;
const unload = method("disposable/unload");
exports.unload = unload;
const dispose = method("disposable/dispose");
exports.dispose = dispose;
dispose.define(Object, object => object.dispose());
const setup = method("disposable/setup");
exports.setup = setup;
setup.define(Object, (object, ...args) => object.setup(...args));
// DisposablesUnloadObserver is the class which subscribe the
// Observer Service to be notified when the add-on loader is
// unloading to be able to dispose all the existent disposables.
const DisposablesUnloadObserver = Class({
implements: [Observer],
initialize: function(...args) {
// Set of the non-weak disposables registered to be disposed.
this.disposables = new Set();
// Target of the weak disposables registered to be disposed
// (and tracked on this target using the SDK weak-set module).
this.weakDisposables = {};
},
subscribe(disposable) {
if (isWeak(disposable)) {
SDKWeakSet.add(this.weakDisposables, disposable);
} else {
this.disposables.add(disposable);
}
},
unsubscribe(disposable) {
if (isWeak(disposable)) {
SDKWeakSet.remove(this.weakDisposables, disposable);
} else {
this.disposables.delete(disposable);
}
},
tryUnloadDisposable(disposable) {
try {
if (disposable) {
unload(disposable);
}
} catch(e) {
console.error("Error unloading a",
isWeak(disposable) ? "weak disposable" : "disposable",
disposable, e);
}
},
unloadAll() {
// Remove all the subscribed disposables.
for (let disposable of this.disposables) {
this.tryUnloadDisposable(disposable);
}
this.disposables.clear();
// Remove all the subscribed weak disposables.
for (let disposable of SDKWeakSet.iterator(this.weakDisposables)) {
this.tryUnloadDisposable(disposable);
}
SDKWeakSet.clear(this.weakDisposables);
}
});
const disposablesUnloadObserver = new DisposablesUnloadObserver();
// The DisposablesUnloadObserver instance is the only object which subscribes
// the Observer Service directly, it observes add-on unload notifications in
// order to trigger `unload` on all its subscribed disposables.
observe.define(DisposablesUnloadObserver, (obj, subject, topic, data) => {
const isUnloadTopic = topic === addonUnloadTopic;
const isUnloadSubject = subject.wrappedJSObject === unloadSubject;
if (isUnloadTopic && isUnloadSubject) {
unsubscribe(disposablesUnloadObserver, addonUnloadTopic);
disposablesUnloadObserver.unloadAll();
}
});
subscribe(disposablesUnloadObserver, addonUnloadTopic, false);
// Set's up disposable instance.
const setupDisposable = disposable => {
disposablesUnloadObserver.subscribe(disposable);
};
exports.setupDisposable = setupDisposable;
// Tears down disposable instance.
const disposeDisposable = disposable => {
disposablesUnloadObserver.unsubscribe(disposable);
};
exports.disposeDisposable = disposeDisposable;
// Base type that takes care of disposing it's instances on add-on unload.
// Also makes sure to remove unload listener if it's already being disposed.
const Disposable = Class({
initialize: function(...args) {
// First setup instance before initializing it's disposal. If instance
// fails to initialize then there is no instance to be disposed at the
// unload.
setup(this, ...args);
setupDisposable(this);
},
destroy: function(reason) {
// Destroying disposable removes unload handler so that attempt to dispose
// won't be made at unload & delegates to dispose.
disposeDisposable(this);
unload(this, reason);
},
setup: function() {
// Implement your initialize logic here.
},
dispose: function() {
// Implement your cleanup logic here.
}
});
exports.Disposable = Disposable;
const unloaders = {
destroy: dispose,
uninstall: uninstall,
shutdown: shutdown,
disable: disable,
upgrade: upgrade,
downgrade: downgrade
};
const unloaded = new WeakMap();
unload.define(Disposable, (disposable, reason) => {
if (!unloaded.get(disposable)) {
unloaded.set(disposable, true);
// Pick an unload handler associated with an unload
// reason (falling back to destroy if not found) and
// delegate unloading to it.
const unload = unloaders[reason] || unloaders.destroy;
unload(disposable);
}
});
// If add-on is disabled manually, it's being upgraded, downgraded
// or uninstalled `dispose` is invoked to undo any changes that
// has being done by it in this session.
disable.define(Disposable, dispose);
downgrade.define(Disposable, dispose);
upgrade.define(Disposable, dispose);
uninstall.define(Disposable, dispose);
// If application is shut down no dispose is invoked as undo-ing
// changes made by instance is likely to just waste of resources &
// increase shutdown time. Although specefic components may choose
// to implement shutdown handler that does something better.
shutdown.define(Disposable, disposable => {});

View file

@ -0,0 +1,184 @@
/* 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"
};
var getPrototypeOf = Object.getPrototypeOf;
var getNames = x => [...Object.getOwnPropertyNames(x),
...Object.getOwnPropertySymbols(x)];
var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
var create = Object.create;
var freeze = Object.freeze;
var unbind = Function.call.bind(Function.bind, Function.call);
// This shortcut makes sure that we do perform desired operations, even if
// associated methods have being overridden on the used object.
var owns = unbind(Object.prototype.hasOwnProperty);
var apply = unbind(Function.prototype.apply);
var slice = Array.slice || unbind(Array.prototype.slice);
var reduce = Array.reduce || unbind(Array.prototype.reduce);
var map = Array.map || unbind(Array.prototype.map);
var concat = Array.concat || unbind(Array.prototype.concat);
// Utility function to get own properties descriptor map.
function getOwnPropertyDescriptors(object) {
return reduce(getNames(object), function(descriptor, name) {
descriptor[name] = getOwnPropertyDescriptor(object, name);
return descriptor;
}, {});
}
function isDataProperty(property) {
var value = property.value;
var type = typeof(property.value);
return "value" in property &&
(type !== "object" || value === null) &&
type !== "function";
}
function getDataProperties(object) {
var properties = getOwnPropertyDescriptors(object);
return getNames(properties).reduce(function(result, name) {
var property = properties[name];
if (isDataProperty(property)) {
result[name] = {
value: property.value,
writable: true,
configurable: true,
enumerable: false
};
}
return result;
}, {})
}
/**
* Takes `source` object as an argument and returns identical object
* with the difference that all own properties will be non-enumerable
*/
function obscure(source) {
var descriptor = reduce(getNames(source), function(descriptor, name) {
var property = getOwnPropertyDescriptor(source, name);
property.enumerable = false;
descriptor[name] = property;
return descriptor;
}, {});
return create(getPrototypeOf(source), descriptor);
}
exports.obscure = obscure;
/**
* Takes arbitrary number of source objects and returns fresh one, that
* inherits from the same prototype as a first argument and implements all
* own properties of all argument objects. If two or more argument objects
* have own properties with the same name, the property is overridden, with
* precedence from right to left, implying, that properties of the object on
* the left are overridden by a same named property of the object on the right.
*/
var mix = function(source) {
var descriptor = reduce(slice(arguments), function(descriptor, source) {
return reduce(getNames(source), function(descriptor, name) {
descriptor[name] = getOwnPropertyDescriptor(source, name);
return descriptor;
}, descriptor);
}, {});
return create(getPrototypeOf(source), descriptor);
};
exports.mix = mix;
/**
* Returns a frozen object with that inherits from the given `prototype` and
* implements all own properties of the given `properties` object.
*/
function extend(prototype, properties) {
return create(prototype, getOwnPropertyDescriptors(properties));
}
exports.extend = extend;
/**
* Returns a constructor function with a proper `prototype` setup. Returned
* constructor's `prototype` inherits from a given `options.extends` or
* `Class.prototype` if omitted and implements all the properties of the
* given `option`. If `options.implemens` array is passed, it's elements
* will be mixed into prototype as well. Also, `options.extends` can be
* a function or a prototype. If function than it's prototype is used as
* an ancestor of the prototype, if it's an object that it's used directly.
* Also `options.implements` may contain functions or objects, in case of
* functions their prototypes are used for mixing.
*/
var Class = new function() {
function prototypeOf(input) {
return typeof(input) === 'function' ? input.prototype : input;
}
var none = freeze([]);
return function Class(options) {
// Create descriptor with normalized `options.extends` and
// `options.implements`.
var descriptor = {
// Normalize extends property of `options.extends` to a prototype object
// in case it's constructor. If property is missing that fallback to
// `Type.prototype`.
extends: owns(options, 'extends') ?
prototypeOf(options.extends) : Class.prototype,
// Normalize `options.implements` to make sure that it's array of
// prototype objects instead of constructor functions.
implements: owns(options, 'implements') ?
freeze(map(options.implements, prototypeOf)) : none
};
// Create array of property descriptors who's properties will be defined
// on the resulting prototype. Note: Using reflection `concat` instead of
// method as it may be overridden.
var descriptors = concat(descriptor.implements, options, descriptor, {
constructor: constructor
});
// Note: we use reflection `apply` in the constructor instead of method
// call since later may be overridden.
function constructor() {
var instance = create(prototype, attributes);
if (initialize) apply(initialize, instance, arguments);
return instance;
}
// Create `prototype` that inherits from given ancestor passed as
// `options.extends`, falling back to `Type.prototype`, implementing all
// properties of given `options.implements` and `options` itself.
var prototype = extend(descriptor.extends, mix.apply(mix, descriptors));
var initialize = prototype.initialize;
// Combine ancestor attributes with prototype's attributes so that
// ancestors attributes also become initializeable.
var attributes = mix(descriptor.extends.constructor.attributes || {},
getDataProperties(prototype));
constructor.attributes = attributes;
Object.defineProperty(constructor, 'prototype', {
configurable: false,
writable: false,
value: prototype
});
return constructor;
};
}
Class.prototype = extend(null, obscure({
constructor: function constructor() {
this.initialize.apply(this, arguments);
return this;
},
initialize: function initialize() {
// Do your initialization logic here
},
// Copy useful properties from `Object.prototype`.
toString: Object.prototype.toString,
toLocaleString: Object.prototype.toLocaleString,
toSource: Object.prototype.toSource,
valueOf: Object.prototype.valueOf,
isPrototypeOf: Object.prototype.isPrototypeOf
}));
exports.Class = freeze(Class);

View file

@ -0,0 +1,43 @@
/* 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 create = Object.create;
const prototypeOf = Object.getPrototypeOf;
/**
* Returns a new namespace, function that may can be used to access an
* namespaced object of the argument argument. Namespaced object are associated
* with owner objects via weak references. Namespaced objects inherit from the
* owners ancestor namespaced object. If owner's ancestor is `null` then
* namespaced object inherits from given `prototype`. Namespaces can be used
* to define internal APIs that can be shared via enclosing `namespace`
* function.
* @examples
* const internals = ns();
* internals(object).secret = secret;
*/
function ns() {
const map = new WeakMap();
return function namespace(target) {
if (!target) // If `target` is not an object return `target` itself.
return target;
// If target has no namespaced object yet, create one that inherits from
// the target prototype's namespaced object.
if (!map.has(target))
map.set(target, create(namespace(prototypeOf(target) || null)));
return map.get(target);
};
};
// `Namespace` is a e4x function in the scope, so we export the function also as
// `ns` as alias to avoid clashing.
exports.ns = ns;
exports.Namespace = ns;

View file

@ -0,0 +1,89 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Cc, Ci, Cr, Cu } = require("chrome");
const { Class } = require("./heritage");
const { isWeak } = require("./reference");
const method = require("../../method/core");
const observerService = Cc['@mozilla.org/observer-service;1'].
getService(Ci.nsIObserverService);
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
const addObserver = ShimWaiver.getProperty(observerService, "addObserver");
const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver");
// This is a method that will be invoked when notification observer
// subscribed to occurs.
const observe = method("observer/observe");
exports.observe = observe;
// Method to subscribe to the observer notification.
const subscribe = method("observe/subscribe");
exports.subscribe = subscribe;
// Method to unsubscribe from the observer notifications.
const unsubscribe = method("observer/unsubscribe");
exports.unsubscribe = unsubscribe;
// This is wrapper class that takes a `delegate` and produces
// instance of `nsIObserver` which will delegate to a given
// object when observer notification occurs.
const ObserverDelegee = Class({
initialize: function(delegate) {
this.delegate = delegate;
},
QueryInterface: function(iid) {
if (!iid.equals(Ci.nsIObserver) &&
!iid.equals(Ci.nsISupportsWeakReference) &&
!iid.equals(Ci.nsISupports))
throw Cr.NS_ERROR_NO_INTERFACE;
return this;
},
observe: function(subject, topic, data) {
observe(this.delegate, subject, topic, data);
}
});
// Class that can be either mixed in or inherited from in
// order to subscribe / unsubscribe for observer notifications.
const Observer = Class({});
exports.Observer = Observer;
// Weak maps that associates instance of `ObserverDelegee` with
// an actual observer. It ensures that `ObserverDelegee` instance
// won't be GC-ed until given `observer` is.
const subscribers = new WeakMap();
// Implementation of `subscribe` for `Observer` type just registers
// observer for an observer service. If `isWeak(observer)` is `true`
// observer service won't hold strong reference to a given `observer`.
subscribe.define(Observer, (observer, topic) => {
if (!subscribers.has(observer)) {
const delegee = new ObserverDelegee(observer);
subscribers.set(observer, delegee);
addObserver(delegee, topic, isWeak(observer));
}
});
// Unsubscribes `observer` from observer notifications for the
// given `topic`.
unsubscribe.define(Observer, (observer, topic) => {
const delegee = subscribers.get(observer);
if (delegee) {
subscribers.delete(observer);
removeObserver(delegee, topic);
}
});

View file

@ -0,0 +1,118 @@
/* 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';
/*
* Uses `Promise.jsm` as a core implementation, with additional sugar
* from previous implementation, with inspiration from `Q` and `when`
*
* https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm
* https://github.com/cujojs/when
* https://github.com/kriskowal/q
*/
const PROMISE_URI = 'resource://gre/modules/Promise.jsm';
getEnvironment.call(this, function ({ require, exports, module, Cu }) {
const Promise = Cu.import(PROMISE_URI, {}).Promise;
const { Debugging, defer, resolve, all, reject, race } = Promise;
module.metadata = {
'stability': 'unstable'
};
var promised = (function() {
// Note: Define shortcuts and utility functions here in order to avoid
// slower property accesses and unnecessary closure creations on each
// call of this popular function.
var call = Function.call;
var concat = Array.prototype.concat;
// Utility function that does following:
// execute([ f, self, args...]) => f.apply(self, args)
function execute (args) {
return call.apply(call, args);
}
// Utility function that takes promise of `a` array and maybe promise `b`
// as arguments and returns promise for `a.concat(b)`.
function promisedConcat(promises, unknown) {
return promises.then(function (values) {
return resolve(unknown)
.then(value => values.concat([value]));
});
}
return function promised(f, prototype) {
/**
Returns a wrapped `f`, which when called returns a promise that resolves to
`f(...)` passing all the given arguments to it, which by the way may be
promises. Optionally second `prototype` argument may be provided to be used
a prototype for a returned promise.
## Example
var promise = promised(Array)(1, promise(2), promise(3))
promise.then(console.log) // => [ 1, 2, 3 ]
**/
return function promised(...args) {
// create array of [ f, this, args... ]
return [f, this, ...args].
// reduce it via `promisedConcat` to get promised array of fulfillments
reduce(promisedConcat, resolve([], prototype)).
// finally map that to promise of `f.apply(this, args...)`
then(execute);
};
};
})();
exports.promised = promised;
exports.all = all;
exports.defer = defer;
exports.resolve = resolve;
exports.reject = reject;
exports.race = race;
exports.Promise = Promise;
exports.Debugging = Debugging;
});
function getEnvironment (callback) {
let Cu, _exports, _module, _require;
// CommonJS / SDK
if (typeof(require) === 'function') {
Cu = require('chrome').Cu;
_exports = exports;
_module = module;
_require = require;
}
// JSM
else if (String(this).indexOf('BackstagePass') >= 0) {
Cu = this['Components'].utils;
_exports = this.Promise = {};
_module = { uri: __URI__, id: 'promise/core' };
_require = uri => {
let imports = {};
Cu.import(uri, imports);
return imports;
};
this.EXPORTED_SYMBOLS = ['Promise'];
// mozIJSSubScriptLoader.loadSubscript
} else if (~String(this).indexOf('Sandbox')) {
Cu = this['Components'].utils;
_exports = this;
_module = { id: 'promise/core' };
_require = uri => {};
}
callback({
Cu: Cu,
exports: _exports,
module: _module,
require: _require
});
}

View file

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const method = require("../../method/core");
const { Class } = require("./heritage");
// Object that inherit or mix WeakRefence inn will register
// weak observes for system notifications.
const WeakReference = Class({});
exports.WeakReference = WeakReference;
// If `isWeak(object)` is `true` observer installed
// for such `object` will be weak, meaning that it will
// be GC-ed if nothing else but observer is observing it.
// By default everything except `WeakReference` will return
// `false`.
const isWeak = method("reference/weak?");
exports.isWeak = isWeak;
isWeak.define(Object, _ => false);
isWeak.define(WeakReference, _ => true);

View file

@ -0,0 +1,197 @@
/* 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 { merge } = require("../util/object");
const { union } = require("../util/array");
const { isNil, isRegExp } = require("../lang/type");
// The possible return values of getTypeOf.
const VALID_TYPES = [
"array",
"boolean",
"function",
"null",
"number",
"object",
"string",
"undefined",
"regexp"
];
const { isArray } = Array;
/**
* Returns a validated options dictionary given some requirements. If any of
* the requirements are not met, an exception is thrown.
*
* @param options
* An object, the options dictionary to validate. It's not modified.
* If it's null or otherwise falsey, an empty object is assumed.
* @param requirements
* An object whose keys are the expected keys in options. Any key in
* options that is not present in requirements is ignored. Each value
* in requirements is itself an object describing the requirements of
* its key. There are four optional keys in this object:
* map: A function that's passed the value of the key in options.
* map's return value is taken as the key's value in the final
* validated options, is, and ok. If map throws an exception
* it's caught and discarded, and the key's value is its value in
* options.
* is: An array containing any number of the typeof type names. If
* the key's value is none of these types, it fails validation.
* Arrays, null and regexps are identified by the special type names
* "array", "null", "regexp"; "object" will not match either. No type
* coercion is done.
* ok: A function that's passed the key's value. If it returns
* false, the value fails validation.
* msg: If the key's value fails validation, an exception is thrown.
* This string will be used as its message. If undefined, a
* generic message is used, unless is is defined, in which case
* the message will state that the value needs to be one of the
* given types.
* @return An object whose keys are those keys in requirements that are also in
* options and whose values are the corresponding return values of map
* or the corresponding values in options. Note that any keys not
* shared by both requirements and options are not in the returned
* object.
*/
exports.validateOptions = function validateOptions(options, requirements) {
options = options || {};
let validatedOptions = {};
for (let key in requirements) {
let isOptional = false;
let mapThrew = false;
let req = requirements[key];
let [optsVal, keyInOpts] = (key in options) ?
[options[key], true] :
[undefined, false];
if (req.map) {
try {
optsVal = req.map(optsVal);
}
catch (err) {
if (err instanceof RequirementError)
throw err;
mapThrew = true;
}
}
if (req.is) {
let types = req.is;
if (!isArray(types) && isArray(types.is))
types = types.is;
if (isArray(types)) {
isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v));
// Sanity check the caller's type names.
types.forEach(function (typ) {
if (VALID_TYPES.indexOf(typ) < 0) {
let msg = 'Internal error: invalid requirement type "' + typ + '".';
throw new Error(msg);
}
});
if (types.indexOf(getTypeOf(optsVal)) < 0)
throw new RequirementError(key, req);
}
}
if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal)))
throw new RequirementError(key, req);
if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined))
validatedOptions[key] = optsVal;
}
return validatedOptions;
};
exports.addIterator = function addIterator(obj, keysValsGenerator) {
obj.__iterator__ = function(keysOnly, keysVals) {
let keysValsIterator = keysValsGenerator.call(this);
// "for (.. in ..)" gets only keys, "for each (.. in ..)" gets values,
// and "for (.. in Iterator(..))" gets [key, value] pairs.
let index = keysOnly ? 0 : 1;
while (true)
yield keysVals ? keysValsIterator.next() : keysValsIterator.next()[index];
};
};
// Similar to typeof, except arrays, null and regexps are identified by "array" and
// "null" and "regexp", not "object".
var getTypeOf = exports.getTypeOf = function getTypeOf(val) {
let typ = typeof(val);
if (typ === "object") {
if (!val)
return "null";
if (isArray(val))
return "array";
if (isRegExp(val))
return "regexp";
}
return typ;
}
function RequirementError(key, requirement) {
Error.call(this);
this.name = "RequirementError";
let msg = requirement.msg;
if (!msg) {
msg = 'The option "' + key + '" ';
msg += requirement.is ?
"must be one of the following types: " + requirement.is.join(", ") :
"is invalid.";
}
this.message = msg;
}
RequirementError.prototype = Object.create(Error.prototype);
var string = { is: ['string', 'undefined', 'null'] };
exports.string = string;
var number = { is: ['number', 'undefined', 'null'] };
exports.number = number;
var boolean = { is: ['boolean', 'undefined', 'null'] };
exports.boolean = boolean;
var object = { is: ['object', 'undefined', 'null'] };
exports.object = object;
var array = { is: ['array', 'undefined', 'null'] };
exports.array = array;
var isTruthyType = type => !(type === 'undefined' || type === 'null');
var findTypes = v => { while (!isArray(v) && v.is) v = v.is; return v };
function required(req) {
let types = (findTypes(req) || VALID_TYPES).filter(isTruthyType);
return merge({}, req, {is: types});
}
exports.required = required;
function optional(req) {
req = merge({is: []}, req);
req.is = findTypes(req).filter(isTruthyType).concat('undefined', 'null');
return req;
}
exports.optional = optional;
function either(...types) {
return union.apply(null, types.map(findTypes));
}
exports.either = either;

View file

@ -0,0 +1,54 @@
/* 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 { Class } = require("../../core/heritage");
const { removeListener, on } = require("../../dom/events");
/**
* Event targets
* can be added / removed by calling `observe / ignore` methods. Composer should
* provide array of event types it wishes to handle as property
* `supportedEventsTypes` and function for handling all those events as
* `handleEvent` property.
*/
exports.DOMEventAssembler = Class({
/**
* Function that is supposed to handle all the supported events (that are
* present in the `supportedEventsTypes`) from all the observed
* `eventTargets`.
* @param {Event} event
* Event being dispatched.
*/
handleEvent() {
throw new TypeError("Instance of DOMEventAssembler must implement `handleEvent` method");
},
/**
* Array of supported event names.
* @type {String[]}
*/
get supportedEventsTypes() {
throw new TypeError("Instance of DOMEventAssembler must implement `handleEvent` field");
},
/**
* Adds `eventTarget` to the list of observed `eventTarget`s. Listeners for
* supported events will be registered on the given `eventTarget`.
* @param {EventTarget} eventTarget
*/
observe: function observe(eventTarget) {
this.supportedEventsTypes.forEach(function(eventType) {
on(eventTarget, eventType, this);
}, this);
},
/**
* Removes `eventTarget` from the list of observed `eventTarget`s. Listeners
* for all supported events will be unregistered from the given `eventTarget`.
* @param {EventTarget} eventTarget
*/
ignore: function ignore(eventTarget) {
this.supportedEventsTypes.forEach(function(eventType) {
removeListener(eventTarget, eventType, this);
}, this);
}
});

View file

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

View file

@ -0,0 +1,199 @@
/* 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 file = require("../io/file");
const { Loader } = require("../test/loader");
const { isNative } = require('@loader/options');
const cuddlefish = isNative ? require("toolkit/loader") : require("../loader/cuddlefish");
const { defer, resolve } = require("../core/promise");
const { getAddon } = require("../addon/installer");
const { id } = require("sdk/self");
const { newURI } = require('sdk/url/utils');
const { getZipReader } = require("../zip/utils");
const { Cc, Ci, Cu } = require("chrome");
const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
var ios = Cc['@mozilla.org/network/io-service;1']
.getService(Ci.nsIIOService);
const CFX_TEST_REGEX = /(([^\/]+\/)(?:lib\/)?)?(tests?\/test-[^\.\/]+)\.js$/;
const JPM_TEST_REGEX = /^()(tests?\/test-[^\.\/]+)\.js$/;
const { mapcat, map, filter, fromEnumerator } = require("sdk/util/sequence");
const toFile = x => x.QueryInterface(Ci.nsIFile);
const isTestFile = ({leafName}) => leafName.substr(0, 5) == "test-" && leafName.substr(-3, 3) == ".js";
const getFileURI = x => ios.newFileURI(x).spec;
const getDirectoryEntries = file => map(toFile, fromEnumerator(_ => file.directoryEntries));
const getTestFiles = directory => filter(isTestFile, getDirectoryEntries(directory));
const getTestURIs = directory => map(getFileURI, getTestFiles(directory));
const isDirectory = x => x.isDirectory();
const getTestEntries = directory => mapcat(entry =>
/^tests?$/.test(entry.leafName) ? getTestURIs(entry) : getTestEntries(entry),
filter(isDirectory, getDirectoryEntries(directory)));
const removeDups = (array) => array.reduce((result, value) => {
if (value != result[result.length - 1]) {
result.push(value);
}
return result;
}, []);
const getSuites = function getSuites({ id, filter }) {
const TEST_REGEX = isNative ? JPM_TEST_REGEX : CFX_TEST_REGEX;
return getAddon(id).then(addon => {
let fileURI = addon.getResourceURI("tests/");
let isPacked = fileURI.scheme == "jar";
let xpiURI = addon.getResourceURI();
let file = xpiURI.QueryInterface(Ci.nsIFileURL).file;
let suites = [];
let addEntry = (entry) => {
if (filter(entry) && TEST_REGEX.test(entry)) {
let suite = (isNative ? "./" : "") + (RegExp.$2 || "") + RegExp.$3;
suites.push(suite);
}
}
if (isPacked) {
return getZipReader(file).then(zip => {
let entries = zip.findEntries(null);
while (entries.hasMore()) {
let entry = entries.getNext();
addEntry(entry);
}
zip.close();
// sort and remove dups
suites = removeDups(suites.sort());
return suites;
})
}
else {
let tests = [...getTestEntries(file)];
let rootURI = addon.getResourceURI("/");
tests.forEach((entry) => {
addEntry(entry.replace(rootURI.spec, ""));
});
}
// sort and remove dups
suites = removeDups(suites.sort());
return suites;
});
}
exports.getSuites = getSuites;
const makeFilters = function makeFilters(options) {
options = options || {};
// A filter string is {fileNameRegex}[:{testNameRegex}] - ie, a colon
// optionally separates a regex for the test fileName from a regex for the
// testName.
if (options.filter) {
let colonPos = options.filter.indexOf(':');
let filterFileRegex, filterNameRegex;
if (colonPos === -1) {
filterFileRegex = new RegExp(options.filter);
filterNameRegex = { test: () => true }
}
else {
filterFileRegex = new RegExp(options.filter.substr(0, colonPos));
filterNameRegex = new RegExp(options.filter.substr(colonPos + 1));
}
return {
fileFilter: (name) => filterFileRegex.test(name),
testFilter: (name) => filterNameRegex.test(name)
}
}
return {
fileFilter: () => true,
testFilter: () => true
};
}
exports.makeFilters = makeFilters;
var loader = Loader(module);
const NOT_TESTS = ['setup', 'teardown'];
var TestFinder = exports.TestFinder = function TestFinder(options) {
this.filter = options.filter;
this.testInProcess = options.testInProcess === false ? false : true;
this.testOutOfProcess = options.testOutOfProcess === true ? true : false;
};
TestFinder.prototype = {
findTests: function findTests() {
let { fileFilter, testFilter } = makeFilters({ filter: this.filter });
return getSuites({ id: id, filter: fileFilter }).then(suites => {
let testsRemaining = [];
let getNextTest = () => {
if (testsRemaining.length) {
return testsRemaining.shift();
}
if (!suites.length) {
return null;
}
let suite = suites.shift();
// Load each test file as a main module in its own loader instance
// `suite` is defined by cuddlefish/manifest.py:ManifestBuilder.build
let suiteModule;
try {
suiteModule = cuddlefish.main(loader, suite);
}
catch (e) {
if (/Unsupported Application/i.test(e.message)) {
// If `Unsupported Application` error thrown during test,
// skip the test suite
suiteModule = {
'test suite skipped': assert => assert.pass(e.message)
};
}
else {
console.exception(e);
throw e;
}
}
if (this.testInProcess) {
for (let name of Object.keys(suiteModule).sort()) {
if (NOT_TESTS.indexOf(name) === -1 && testFilter(name)) {
testsRemaining.push({
setup: suiteModule.setup,
teardown: suiteModule.teardown,
testFunction: suiteModule[name],
name: suite + "." + name
});
}
}
}
return getNextTest();
};
return {
getNext: () => resolve(getNextTest())
};
});
}
};

View file

@ -0,0 +1,584 @@
/* 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 timer = require("../timers");
const cfxArgs = require("../test/options");
const { getTabs, closeTab, getURI, getTabId, getSelectedTab } = require("../tabs/utils");
const { windows, isBrowser, getMostRecentBrowserWindow } = require("../window/utils");
const { defer, all, Debugging: PromiseDebugging, resolve } = require("../core/promise");
const { getInnerId } = require("../window/utils");
const { cleanUI } = require("../test/utils");
const findAndRunTests = function findAndRunTests(options) {
var TestFinder = require("./unit-test-finder").TestFinder;
var finder = new TestFinder({
filter: options.filter,
testInProcess: options.testInProcess,
testOutOfProcess: options.testOutOfProcess
});
var runner = new TestRunner({fs: options.fs});
finder.findTests().then(tests => {
runner.startMany({
tests: tests,
stopOnError: options.stopOnError,
onDone: options.onDone
});
});
};
exports.findAndRunTests = findAndRunTests;
var runnerWindows = new WeakMap();
var runnerTabs = new WeakMap();
const TestRunner = function TestRunner(options) {
options = options || {};
// remember the id's for the open window and tab
let window = getMostRecentBrowserWindow();
runnerWindows.set(this, getInnerId(window));
runnerTabs.set(this, getTabId(getSelectedTab(window)));
this.fs = options.fs;
this.console = options.console || console;
this.passed = 0;
this.failed = 0;
this.testRunSummary = [];
this.expectFailNesting = 0;
this.done = TestRunner.prototype.done.bind(this);
};
TestRunner.prototype = {
toString: function toString() {
return "[object TestRunner]";
},
DEFAULT_PAUSE_TIMEOUT: (cfxArgs.parseable ? 300000 : 15000), //Five minutes (5*60*1000ms)
PAUSE_DELAY: 500,
_logTestFailed: function _logTestFailed(why) {
if (!(why in this.test.errors))
this.test.errors[why] = 0;
this.test.errors[why]++;
},
_uncaughtErrorObserver: function({message, date, fileName, stack, lineNumber}) {
this.fail("There was an uncaught Promise rejection: " + message + " @ " +
fileName + ":" + lineNumber + "\n" + stack);
},
pass: function pass(message) {
if(!this.expectFailure) {
if ("testMessage" in this.console)
this.console.testMessage(true, true, this.test.name, message);
else
this.console.info("pass:", message);
this.passed++;
this.test.passed++;
this.test.last = message;
}
else {
this.expectFailure = false;
this._logTestFailed("failure");
if ("testMessage" in this.console) {
this.console.testMessage(true, false, this.test.name, message);
}
else {
this.console.error("fail:", 'Failure Expected: ' + message)
this.console.trace();
}
this.failed++;
this.test.failed++;
}
},
fail: function fail(message) {
if(!this.expectFailure) {
this._logTestFailed("failure");
if ("testMessage" in this.console) {
this.console.testMessage(false, false, this.test.name, message);
}
else {
this.console.error("fail:", message)
this.console.trace();
}
this.failed++;
this.test.failed++;
}
else {
this.expectFailure = false;
if ("testMessage" in this.console)
this.console.testMessage(false, true, this.test.name, message);
else
this.console.info("pass:", message);
this.passed++;
this.test.passed++;
this.test.last = message;
}
},
expectFail: function(callback) {
this.expectFailure = true;
callback();
this.expectFailure = false;
},
exception: function exception(e) {
this._logTestFailed("exception");
if (cfxArgs.parseable)
this.console.print("TEST-UNEXPECTED-FAIL | " + this.test.name + " | " + e + "\n");
this.console.exception(e);
this.failed++;
this.test.failed++;
},
assertMatches: function assertMatches(string, regexp, message) {
if (regexp.test(string)) {
if (!message)
message = uneval(string) + " matches " + uneval(regexp);
this.pass(message);
} else {
var no = uneval(string) + " doesn't match " + uneval(regexp);
if (!message)
message = no;
else
message = message + " (" + no + ")";
this.fail(message);
}
},
assertRaises: function assertRaises(func, predicate, message) {
try {
func();
if (message)
this.fail(message + " (no exception thrown)");
else
this.fail("function failed to throw exception");
} catch (e) {
var errorMessage;
if (typeof(e) == "string")
errorMessage = e;
else
errorMessage = e.message;
if (typeof(predicate) == "string")
this.assertEqual(errorMessage, predicate, message);
else
this.assertMatches(errorMessage, predicate, message);
}
},
assert: function assert(a, message) {
if (!a) {
if (!message)
message = "assertion failed, value is " + a;
this.fail(message);
} else
this.pass(message || "assertion successful");
},
assertNotEqual: function assertNotEqual(a, b, message) {
if (a != b) {
if (!message)
message = "a != b != " + uneval(a);
this.pass(message);
} else {
var equality = uneval(a) + " == " + uneval(b);
if (!message)
message = equality;
else
message += " (" + equality + ")";
this.fail(message);
}
},
assertEqual: function assertEqual(a, b, message) {
if (a == b) {
if (!message)
message = "a == b == " + uneval(a);
this.pass(message);
} else {
var inequality = uneval(a) + " != " + uneval(b);
if (!message)
message = inequality;
else
message += " (" + inequality + ")";
this.fail(message);
}
},
assertNotStrictEqual: function assertNotStrictEqual(a, b, message) {
if (a !== b) {
if (!message)
message = "a !== b !== " + uneval(a);
this.pass(message);
} else {
var equality = uneval(a) + " === " + uneval(b);
if (!message)
message = equality;
else
message += " (" + equality + ")";
this.fail(message);
}
},
assertStrictEqual: function assertStrictEqual(a, b, message) {
if (a === b) {
if (!message)
message = "a === b === " + uneval(a);
this.pass(message);
} else {
var inequality = uneval(a) + " !== " + uneval(b);
if (!message)
message = inequality;
else
message += " (" + inequality + ")";
this.fail(message);
}
},
assertFunction: function assertFunction(a, message) {
this.assertStrictEqual('function', typeof a, message);
},
assertUndefined: function(a, message) {
this.assertStrictEqual('undefined', typeof a, message);
},
assertNotUndefined: function(a, message) {
this.assertNotStrictEqual('undefined', typeof a, message);
},
assertNull: function(a, message) {
this.assertStrictEqual(null, a, message);
},
assertNotNull: function(a, message) {
this.assertNotStrictEqual(null, a, message);
},
assertObject: function(a, message) {
this.assertStrictEqual('[object Object]', Object.prototype.toString.apply(a), message);
},
assertString: function(a, message) {
this.assertStrictEqual('[object String]', Object.prototype.toString.apply(a), message);
},
assertArray: function(a, message) {
this.assertStrictEqual('[object Array]', Object.prototype.toString.apply(a), message);
},
assertNumber: function(a, message) {
this.assertStrictEqual('[object Number]', Object.prototype.toString.apply(a), message);
},
done: function done() {
if (this.isDone) {
return resolve();
}
this.isDone = true;
this.pass("This test is done.");
if (this.test.teardown) {
this.test.teardown(this);
}
if (this.waitTimeout !== null) {
timer.clearTimeout(this.waitTimeout);
this.waitTimeout = null;
}
// Do not leave any callback set when calling to `waitUntil`
this.waitUntilCallback = null;
if (this.test.passed == 0 && this.test.failed == 0) {
this._logTestFailed("empty test");
if ("testMessage" in this.console) {
this.console.testMessage(false, false, this.test.name, "Empty test");
}
else {
this.console.error("fail:", "Empty test")
}
this.failed++;
this.test.failed++;
}
let wins = windows(null, { includePrivate: true });
let winPromises = wins.map(win => {
return new Promise(resolve => {
if (["interactive", "complete"].indexOf(win.document.readyState) >= 0) {
resolve()
}
else {
win.addEventListener("DOMContentLoaded", function onLoad() {
win.removeEventListener("DOMContentLoaded", onLoad, false);
resolve();
}, false);
}
});
});
PromiseDebugging.flushUncaughtErrors();
PromiseDebugging.removeUncaughtErrorObserver(this._uncaughtErrorObserver);
return all(winPromises).then(() => {
let browserWins = wins.filter(isBrowser);
let tabs = browserWins.reduce((tabs, window) => tabs.concat(getTabs(window)), []);
let newTabID = getTabId(getSelectedTab(wins[0]));
let oldTabID = runnerTabs.get(this);
let hasMoreTabsOpen = browserWins.length && tabs.length != 1;
let failure = false;
if (wins.length != 1 || getInnerId(wins[0]) !== runnerWindows.get(this)) {
failure = true;
this.fail("Should not be any unexpected windows open");
}
else if (hasMoreTabsOpen) {
failure = true;
this.fail("Should not be any unexpected tabs open");
}
else if (oldTabID != newTabID) {
failure = true;
runnerTabs.set(this, newTabID);
this.fail("Should not be any new tabs left open, old id: " + oldTabID + " new id: " + newTabID);
}
if (failure) {
console.log("Windows open:");
for (let win of wins) {
if (isBrowser(win)) {
tabs = getTabs(win);
console.log(win.location + " - " + tabs.map(getURI).join(", "));
}
else {
console.log(win.location);
}
}
}
return failure;
}).
then(failure => {
if (!failure) {
this.pass("There was a clean UI.");
return null;
}
return cleanUI().then(() => {
this.pass("There is a clean UI.");
});
}).
then(() => {
this.testRunSummary.push({
name: this.test.name,
passed: this.test.passed,
failed: this.test.failed,
errors: Object.keys(this.test.errors).join(", ")
});
if (this.onDone !== null) {
let onDone = this.onDone;
this.onDone = null;
timer.setTimeout(_ => onDone(this));
}
}).
catch(console.exception);
},
// Set of assertion functions to wait for an assertion to become true
// These functions take the same arguments as the TestRunner.assert* methods.
waitUntil: function waitUntil() {
return this._waitUntil(this.assert, arguments);
},
waitUntilNotEqual: function waitUntilNotEqual() {
return this._waitUntil(this.assertNotEqual, arguments);
},
waitUntilEqual: function waitUntilEqual() {
return this._waitUntil(this.assertEqual, arguments);
},
waitUntilMatches: function waitUntilMatches() {
return this._waitUntil(this.assertMatches, arguments);
},
/**
* Internal function that waits for an assertion to become true.
* @param {Function} assertionMethod
* Reference to a TestRunner assertion method like test.assert,
* test.assertEqual, ...
* @param {Array} args
* List of arguments to give to the previous assertion method.
* All functions in this list are going to be called to retrieve current
* assertion values.
*/
_waitUntil: function waitUntil(assertionMethod, args) {
let { promise, resolve } = defer();
let count = 0;
let maxCount = this.DEFAULT_PAUSE_TIMEOUT / this.PAUSE_DELAY;
// We need to ensure that test is asynchronous
if (!this.waitTimeout)
this.waitUntilDone(this.DEFAULT_PAUSE_TIMEOUT);
let finished = false;
let test = this;
// capture a traceback before we go async.
let traceback = require("../console/traceback");
let stack = traceback.get();
stack.splice(-2, 2);
let currentWaitStack = traceback.format(stack);
let timeout = null;
function loop(stopIt) {
timeout = null;
// Build a mockup object to fake TestRunner API and intercept calls to
// pass and fail methods, in order to retrieve nice error messages
// and assertion result
let mock = {
pass: function (msg) {
test.pass(msg);
test.waitUntilCallback = null;
if (!stopIt)
resolve();
},
fail: function (msg) {
// If we are called on test timeout, we stop the loop
// and print which test keeps failing:
if (stopIt) {
test.console.error("test assertion never became true:\n",
msg + "\n",
currentWaitStack);
if (timeout)
timer.clearTimeout(timeout);
return;
}
timeout = timer.setTimeout(loop, test.PAUSE_DELAY);
}
};
// Automatically call args closures in order to build arguments for
// assertion function
let appliedArgs = [];
for (let i = 0, l = args.length; i < l; i++) {
let a = args[i];
if (typeof a == "function") {
try {
a = a();
}
catch(e) {
test.fail("Exception when calling asynchronous assertion: " + e +
"\n" + e.stack);
return resolve();
}
}
appliedArgs.push(a);
}
// Finally call assertion function with current assertion values
assertionMethod.apply(mock, appliedArgs);
}
loop();
this.waitUntilCallback = loop;
return promise;
},
waitUntilDone: function waitUntilDone(ms) {
if (ms === undefined)
ms = this.DEFAULT_PAUSE_TIMEOUT;
var self = this;
function tiredOfWaiting() {
self._logTestFailed("timed out");
if ("testMessage" in self.console) {
self.console.testMessage(false, false, self.test.name,
`Test timed out (after: ${self.test.last})`);
}
else {
self.console.error("fail:", `Timed out (after: ${self.test.last})`)
}
if (self.waitUntilCallback) {
self.waitUntilCallback(true);
self.waitUntilCallback = null;
}
self.failed++;
self.test.failed++;
self.done();
}
// We may already have registered a timeout callback
if (this.waitTimeout)
timer.clearTimeout(this.waitTimeout);
this.waitTimeout = timer.setTimeout(tiredOfWaiting, ms);
},
startMany: function startMany(options) {
function runNextTest(self) {
let { tests, onDone } = options;
return tests.getNext().then((test) => {
if (options.stopOnError && self.test && self.test.failed) {
self.console.error("aborted: test failed and --stop-on-error was specified");
onDone(self);
}
else if (test) {
self.start({test: test, onDone: runNextTest});
}
else {
onDone(self);
}
});
}
return runNextTest(this).catch(console.exception);
},
start: function start(options) {
this.test = options.test;
this.test.passed = 0;
this.test.failed = 0;
this.test.errors = {};
this.test.last = 'START';
PromiseDebugging.clearUncaughtErrorObservers();
this._uncaughtErrorObserver = this._uncaughtErrorObserver.bind(this);
PromiseDebugging.addUncaughtErrorObserver(this._uncaughtErrorObserver);
this.isDone = false;
this.onDone = function(self) {
if (cfxArgs.parseable)
self.console.print("TEST-END | " + self.test.name + "\n");
options.onDone(self);
}
this.waitTimeout = null;
try {
if (cfxArgs.parseable)
this.console.print("TEST-START | " + this.test.name + "\n");
else
this.console.info("executing '" + this.test.name + "'");
if(this.test.setup) {
this.test.setup(this);
}
this.test.testFunction(this);
} catch (e) {
this.exception(e);
}
if (this.waitTimeout === null)
this.done();
}
};
exports.TestRunner = TestRunner;

View file

@ -0,0 +1,193 @@
/* 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 events = require('../system/events');
const { getInnerId, getOuterId, windows, isDocumentLoaded, isBrowser,
getMostRecentBrowserWindow, getToplevelWindow, getMostRecentWindow } = require('../window/utils');
const { deprecateFunction } = require('../util/deprecate');
const { ignoreWindow } = require('sdk/private-browsing/utils');
const { isPrivateBrowsingSupported } = require('../self');
const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1'].
getService(Ci.nsIWindowWatcher);
const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
getService(Ci.nsIAppShellService);
// Bug 834961: ignore private windows when they are not supported
function getWindows() {
return windows(null, { includePrivate: isPrivateBrowsingSupported });
}
/**
* An iterator for XUL windows currently in the application.
*
* @return A generator that yields XUL windows exposing the
* nsIDOMWindow interface.
*/
function windowIterator() {
// Bug 752631: We only pass already loaded window in order to avoid
// breaking XUL windows DOM. DOM is broken when some JS code try
// to access DOM during "uninitialized" state of the related document.
let list = getWindows().filter(isDocumentLoaded);
for (let i = 0, l = list.length; i < l; i++) {
yield list[i];
}
};
exports.windowIterator = windowIterator;
/**
* An iterator for browser windows currently open in the application.
* @returns {Function}
* A generator that yields browser windows exposing the `nsIDOMWindow`
* interface.
*/
function browserWindowIterator() {
for (let window of windowIterator()) {
if (isBrowser(window))
yield window;
}
}
exports.browserWindowIterator = browserWindowIterator;
function WindowTracker(delegate) {
if (!(this instanceof WindowTracker)) {
return new WindowTracker(delegate);
}
this._delegate = delegate;
for (let window of getWindows())
this._regWindow(window);
windowWatcher.registerNotification(this);
this._onToplevelWindowReady = this._onToplevelWindowReady.bind(this);
events.on('toplevel-window-ready', this._onToplevelWindowReady);
require('../system/unload').ensure(this);
return this;
};
WindowTracker.prototype = {
_regLoadingWindow: function _regLoadingWindow(window) {
// Bug 834961: ignore private windows when they are not supported
if (ignoreWindow(window))
return;
window.addEventListener('load', this, true);
},
_unregLoadingWindow: function _unregLoadingWindow(window) {
// This may have no effect if we ignored the window in _regLoadingWindow().
window.removeEventListener('load', this, true);
},
_regWindow: function _regWindow(window) {
// Bug 834961: ignore private windows when they are not supported
if (ignoreWindow(window))
return;
if (window.document.readyState == 'complete') {
this._unregLoadingWindow(window);
this._delegate.onTrack(window);
} else
this._regLoadingWindow(window);
},
_unregWindow: function _unregWindow(window) {
if (window.document.readyState == 'complete') {
if (this._delegate.onUntrack)
this._delegate.onUntrack(window);
} else {
this._unregLoadingWindow(window);
}
},
unload: function unload() {
windowWatcher.unregisterNotification(this);
events.off('toplevel-window-ready', this._onToplevelWindowReady);
for (let window of getWindows())
this._unregWindow(window);
},
handleEvent: function handleEvent(event) {
try {
if (event.type == 'load' && event.target) {
var window = event.target.defaultView;
if (window)
this._regWindow(getToplevelWindow(window));
}
}
catch(e) {
console.exception(e);
}
},
_onToplevelWindowReady: function _onToplevelWindowReady({subject}) {
let window = getToplevelWindow(subject);
// ignore private windows if they are not supported
if (ignoreWindow(window))
return;
this._regWindow(window);
},
observe: function observe(subject, topic, data) {
try {
var window = subject.QueryInterface(Ci.nsIDOMWindow);
// ignore private windows if they are not supported
if (ignoreWindow(window))
return;
if (topic == 'domwindowclosed')
this._unregWindow(window);
}
catch(e) {
console.exception(e);
}
}
};
exports.WindowTracker = WindowTracker;
Object.defineProperties(exports, {
activeWindow: {
enumerable: true,
get: function() {
return getMostRecentWindow(null);
},
set: function(window) {
try {
window.focus();
} catch (e) {}
}
},
activeBrowserWindow: {
enumerable: true,
get: getMostRecentBrowserWindow
}
});
/**
* Returns the ID of the window's current inner window.
*/
exports.getInnerId = deprecateFunction(getInnerId,
'require("window-utils").getInnerId is deprecated, ' +
'please use require("sdk/window/utils").getInnerId instead'
);
exports.getOuterId = deprecateFunction(getOuterId,
'require("window-utils").getOuterId is deprecated, ' +
'please use require("sdk/window/utils").getOuterId instead'
);
exports.isBrowser = deprecateFunction(isBrowser,
'require("window-utils").isBrowser is deprecated, ' +
'please use require("sdk/window/utils").isBrowser instead'
);
exports.hiddenWindow = appShellService.hiddenDOMWindow;

View file

@ -0,0 +1,18 @@
/* 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 events = require('./events.js');
exports.emit = (element, type, obj) => events.emit(element, type, obj, true);
exports.on = (element, type, listener, capture) => events.on(element, type, listener, capture, true);
exports.once = (element, type, listener, capture) => events.once(element, type, listener, capture, true);
exports.removeListener = (element, type, listener, capture) => events.removeListener(element, type, listener, capture, true);
exports.removed = events.removed;
exports.when = (element, eventName, capture) => events.when(element, eventName, capture ? capture : false, true);

View file

@ -0,0 +1,192 @@
/* 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 { Cu } = require("chrome");
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
// Utility function that returns copy of the given `text` with last character
// removed if it is `"s"`.
function singularify(text) {
return text[text.length - 1] === "s" ? text.substr(0, text.length - 1) : text;
}
// Utility function that takes event type, argument is passed to
// `document.createEvent` and returns name of the initializer method of the
// given event. Please note that there are some event types whose initializer
// methods can't be guessed by this function. For more details see following
// link: https://developer.mozilla.org/En/DOM/Document.createEvent
function getInitializerName(category) {
return "init" + singularify(category);
}
/**
* Registers an event `listener` on a given `element`, that will be called
* when events of specified `type` is dispatched on the `element`.
* @param {Element} element
* Dom element to register listener on.
* @param {String} type
* A string representing the
* [event type](https://developer.mozilla.org/en/DOM/event.type) to
* listen for.
* @param {Function} listener
* Function that is called whenever an event of the specified `type`
* occurs.
* @param {Boolean} capture
* If true, indicates that the user wishes to initiate capture. After
* initiating capture, all events of the specified type will be dispatched
* to the registered listener before being dispatched to any `EventTarget`s
* beneath it in the DOM tree. Events which are bubbling upward through
* the tree will not trigger a listener designated to use capture.
* See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow)
* for a detailed explanation.
*/
function on(element, type, listener, capture, shimmed = false) {
// `capture` defaults to `false`.
capture = capture || false;
if (shimmed) {
element.addEventListener(type, listener, capture);
} else {
ShimWaiver.getProperty(element, "addEventListener")(type, listener, capture);
}
}
exports.on = on;
/**
* Registers an event `listener` on a given `element`, that will be called
* only once, next time event of specified `type` is dispatched on the
* `element`.
* @param {Element} element
* Dom element to register listener on.
* @param {String} type
* A string representing the
* [event type](https://developer.mozilla.org/en/DOM/event.type) to
* listen for.
* @param {Function} listener
* Function that is called whenever an event of the specified `type`
* occurs.
* @param {Boolean} capture
* If true, indicates that the user wishes to initiate capture. After
* initiating capture, all events of the specified type will be dispatched
* to the registered listener before being dispatched to any `EventTarget`s
* beneath it in the DOM tree. Events which are bubbling upward through
* the tree will not trigger a listener designated to use capture.
* See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow)
* for a detailed explanation.
*/
function once(element, type, listener, capture, shimmed = false) {
on(element, type, function selfRemovableListener(event) {
removeListener(element, type, selfRemovableListener, capture, shimmed);
listener.apply(this, arguments);
}, capture, shimmed);
}
exports.once = once;
/**
* Unregisters an event `listener` on a given `element` for the events of the
* specified `type`.
*
* @param {Element} element
* Dom element to unregister listener from.
* @param {String} type
* A string representing the
* [event type](https://developer.mozilla.org/en/DOM/event.type) to
* listen for.
* @param {Function} listener
* Function that is called whenever an event of the specified `type`
* occurs.
* @param {Boolean} capture
* If true, indicates that the user wishes to initiate capture. After
* initiating capture, all events of the specified type will be dispatched
* to the registered listener before being dispatched to any `EventTarget`s
* beneath it in the DOM tree. Events which are bubbling upward through
* the tree will not trigger a listener designated to use capture.
* See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow)
* for a detailed explanation.
*/
function removeListener(element, type, listener, capture, shimmed = false) {
if (shimmed) {
element.removeEventListener(type, listener, capture);
} else {
ShimWaiver.getProperty(element, "removeEventListener")(type, listener, capture);
}
}
exports.removeListener = removeListener;
/**
* Emits event of the specified `type` and `category` on the given `element`.
* Specified `settings` are used to initialize event before dispatching it.
* @param {Element} element
* Dom element to dispatch event on.
* @param {String} type
* A string representing the
* [event type](https://developer.mozilla.org/en/DOM/event.type).
* @param {Object} options
* Options object containing following properties:
* - `category`: String passed to the `document.createEvent`. Option is
* optional and defaults to "UIEvents".
* - `initializer`: If passed it will be used as name of the method used
* to initialize event. If omitted name will be generated from the
* `category` field by prefixing it with `"init"` and removing last
* character if it matches `"s"`.
* - `settings`: Array of settings that are forwarded to the event
* initializer after firs `type` argument.
* @see https://developer.mozilla.org/En/DOM/Document.createEvent
*/
function emit(element, type, { category, initializer, settings }, shimmed = false) {
category = category || "UIEvents";
initializer = initializer || getInitializerName(category);
let document = element.ownerDocument;
let event = document.createEvent(category);
event[initializer].apply(event, [type].concat(settings));
if (shimmed) {
element.dispatchEvent(event);
} else {
ShimWaiver.getProperty(element, "dispatchEvent")(event);
}
};
exports.emit = emit;
// Takes DOM `element` and returns promise which is resolved
// when given element is removed from it's parent node.
const removed = element => {
return new Promise(resolve => {
const { MutationObserver } = element.ownerDocument.defaultView;
const observer = new MutationObserver(mutations => {
for (let mutation of mutations) {
for (let node of mutation.removedNodes || []) {
if (node === element) {
observer.disconnect();
resolve(element);
}
}
}
});
observer.observe(element.parentNode, {childList: true});
});
};
exports.removed = removed;
const when = (element, eventName, capture=false, shimmed=false) => new Promise(resolve => {
const listener = event => {
if (shimmed) {
element.removeEventListener(eventName, listener, capture);
} else {
ShimWaiver.getProperty(element, "removeEventListener")(eventName, listener, capture);
}
resolve(event);
};
if (shimmed) {
element.addEventListener(eventName, listener, capture);
} else {
ShimWaiver.getProperty(element, "addEventListener")(eventName, listener, capture);
}
});
exports.when = when;

View file

@ -0,0 +1,63 @@
/* 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 { emit } = require("../events");
const { getCodeForKey, toJSON } = require("../../keyboard/utils");
const { has } = require("../../util/array");
const { isString } = require("../../lang/type");
const INITIALIZER = "initKeyEvent";
const CATEGORY = "KeyboardEvent";
function Options(options) {
if (!isString(options))
return options;
var { key, modifiers } = toJSON(options);
return {
key: key,
control: has(modifiers, "control"),
alt: has(modifiers, "alt"),
shift: has(modifiers, "shift"),
meta: has(modifiers, "meta")
};
}
var keyEvent = exports.keyEvent = function keyEvent(element, type, options) {
emit(element, type, {
initializer: INITIALIZER,
category: CATEGORY,
settings: [
!("bubbles" in options) || options.bubbles !== false,
!("cancelable" in options) || options.cancelable !== false,
"window" in options && options.window ? options.window : null,
"control" in options && !!options.control,
"alt" in options && !!options.alt,
"shift" in options && !!options.shift,
"meta" in options && !!options.meta,
getCodeForKey(options.key) || 0,
options.key.length === 1 ? options.key.charCodeAt(0) : 0
]
});
}
exports.keyDown = function keyDown(element, options) {
keyEvent(element, "keydown", Options(options));
};
exports.keyUp = function keyUp(element, options) {
keyEvent(element, "keyup", Options(options));
};
exports.keyPress = function keyPress(element, options) {
keyEvent(element, "keypress", Options(options));
};

View file

@ -0,0 +1,65 @@
/* 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 { Cc, Ci, Cr, Cu } = require("chrome");
const { emit, on, off } = require("./core");
var observerService = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
const addObserver = ShimWaiver.getProperty(observerService, "addObserver");
const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver");
const { when: unload } = require("../system/unload");
// Simple class that can be used to instantiate event channel that
// implements `nsIObserver` interface. It's will is used by `observe`
// function as observer + event target. It basically proxies observer
// notifications as to it's registered listeners.
function ObserverChannel() {}
Object.freeze(Object.defineProperties(ObserverChannel.prototype, {
QueryInterface: {
value: function(iid) {
if (!iid.equals(Ci.nsIObserver) &&
!iid.equals(Ci.nsISupportsWeakReference) &&
!iid.equals(Ci.nsISupports))
throw Cr.NS_ERROR_NO_INTERFACE;
return this;
}
},
observe: {
value: function(subject, topic, data) {
emit(this, "data", {
type: topic,
target: subject,
data: data
});
}
}
}));
function observe(topic) {
let observerChannel = new ObserverChannel();
// Note: `nsIObserverService` will not hold a weak reference to a
// observerChannel (since third argument is `true`). There for if it
// will be GC-ed with all it's event listeners once no other references
// will be held.
addObserver(observerChannel, topic, true);
// We need to remove any observer added once the add-on is unloaded;
// otherwise we'll get a "dead object" exception.
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1001833
unload(() => removeObserver(observerChannel, topic));
return observerChannel;
}
exports.observe = observe;

View file

@ -0,0 +1,193 @@
/* 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 UNCAUGHT_ERROR = 'An error event was emitted for which there was no listener.';
const BAD_LISTENER = 'The event listener must be a function.';
const { ns } = require('../core/namespace');
const event = ns();
const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/;
exports.EVENT_TYPE_PATTERN = EVENT_TYPE_PATTERN;
// Utility function to access given event `target` object's event listeners for
// the specific event `type`. If listeners for this type does not exists they
// will be created.
const observers = function observers(target, type) {
if (!target) throw TypeError("Event target must be an object");
let listeners = event(target);
return type in listeners ? listeners[type] : listeners[type] = [];
};
/**
* Registers an event `listener` that is called every time events of
* specified `type` is emitted on the given event `target`.
* @param {Object} target
* Event target object.
* @param {String} type
* The type of event.
* @param {Function} listener
* The listener function that processes the event.
*/
function on(target, type, listener) {
if (typeof(listener) !== 'function')
throw new Error(BAD_LISTENER);
let listeners = observers(target, type);
if (!~listeners.indexOf(listener))
listeners.push(listener);
}
exports.on = on;
var onceWeakMap = new WeakMap();
/**
* Registers an event `listener` that is called only the next time an event
* of the specified `type` is emitted on the given event `target`.
* @param {Object} target
* Event target object.
* @param {String} type
* The type of the event.
* @param {Function} listener
* The listener function that processes the event.
*/
function once(target, type, listener) {
let replacement = function observer(...args) {
off(target, type, observer);
onceWeakMap.delete(listener);
listener.apply(target, args);
};
onceWeakMap.set(listener, replacement);
on(target, type, replacement);
}
exports.once = once;
/**
* Execute each of the listeners in order with the supplied arguments.
* All the exceptions that are thrown by listeners during the emit
* are caught and can be handled by listeners of 'error' event. Thrown
* exceptions are passed as an argument to an 'error' event listener.
* If no 'error' listener is registered exception will be logged into an
* error console.
* @param {Object} target
* Event target object.
* @param {String} type
* The type of event.
* @params {Object|Number|String|Boolean} args
* Arguments that will be passed to listeners.
*/
function emit (target, type, ...args) {
emitOnObject(target, type, target, ...args);
}
exports.emit = emit;
/**
* A variant of emit that allows setting the this property for event listeners
*/
function emitOnObject(target, type, thisArg, ...args) {
let all = observers(target, '*').length;
let state = observers(target, type);
let listeners = state.slice();
let count = listeners.length;
let index = 0;
// If error event and there are no handlers (explicit or catch-all)
// then print error message to the console.
if (count === 0 && type === 'error' && all === 0)
console.exception(args[0]);
while (index < count) {
try {
let listener = listeners[index];
// Dispatch only if listener is still registered.
if (~state.indexOf(listener))
listener.apply(thisArg, args);
}
catch (error) {
// If exception is not thrown by a error listener and error listener is
// registered emit `error` event. Otherwise dump exception to the console.
if (type !== 'error') emit(target, 'error', error);
else console.exception(error);
}
index++;
}
// Also emit on `"*"` so that one could listen for all events.
if (type !== '*') emit(target, '*', type, ...args);
}
exports.emitOnObject = emitOnObject;
/**
* Removes an event `listener` for the given event `type` on the given event
* `target`. If no `listener` is passed removes all listeners of the given
* `type`. If `type` is not passed removes all the listeners of the given
* event `target`.
* @param {Object} target
* The event target object.
* @param {String} type
* The type of event.
* @param {Function} listener
* The listener function that processes the event.
*/
function off(target, type, listener) {
let length = arguments.length;
if (length === 3) {
if (onceWeakMap.has(listener)) {
listener = onceWeakMap.get(listener);
onceWeakMap.delete(listener);
}
let listeners = observers(target, type);
let index = listeners.indexOf(listener);
if (~index)
listeners.splice(index, 1);
}
else if (length === 2) {
observers(target, type).splice(0);
}
else if (length === 1) {
let listeners = event(target);
Object.keys(listeners).forEach(type => delete listeners[type]);
}
}
exports.off = off;
/**
* Returns a number of event listeners registered for the given event `type`
* on the given event `target`.
*/
function count(target, type) {
return observers(target, type).length;
}
exports.count = count;
/**
* Registers listeners on the given event `target` from the given `listeners`
* dictionary. Iterates over the listeners and if property name matches name
* pattern `onEventType` and property is a function, then registers it as
* an `eventType` listener on `target`.
*
* @param {Object} target
* The type of event.
* @param {Object} listeners
* Dictionary of listeners.
*/
function setListeners(target, listeners) {
Object.keys(listeners || {}).forEach(key => {
let match = EVENT_TYPE_PATTERN.exec(key);
let type = match && match[1].toLowerCase();
if (!type) return;
let listener = listeners[key];
if (typeof(listener) === 'function')
on(target, type, listener);
});
}
exports.setListeners = setListeners;

Some files were not shown because too many files have changed in this diff Show more