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

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;

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 { Ci } = require("chrome");
var { emit } = require("./core");
var { when: unload } = require("../system/unload");
var listeners = new WeakMap();
const { Cu } = require("chrome");
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
const { ThreadSafeChromeUtils } = Cu.import("resource://gre/modules/Services.jsm", {});
var getWindowFrom = x =>
x instanceof Ci.nsIDOMWindow ? x :
x instanceof Ci.nsIDOMDocument ? x.defaultView :
x instanceof Ci.nsIDOMNode ? x.ownerDocument.defaultView :
null;
function removeFromListeners() {
ShimWaiver.getProperty(this, "removeEventListener")("DOMWindowClose", removeFromListeners);
for (let cleaner of listeners.get(this))
cleaner();
listeners.delete(this);
}
// Simple utility function takes event target, event type and optional
// `options.capture` and returns node style event stream that emits "data"
// events every time event of that type occurs on the given `target`.
function open(target, type, options) {
let output = {};
let capture = options && options.capture ? true : false;
let listener = (event) => emit(output, "data", event);
// `open` is currently used only on DOM Window objects, however it was made
// to be used to any kind of `target` that supports `addEventListener`,
// therefore is safer get the `window` from the `target` instead assuming
// that `target` is the `window`.
let window = getWindowFrom(target);
// If we're not able to get a `window` from `target`, there is something
// wrong. We cannot add listeners that can leak later, or results in
// "dead object" exception.
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1001833
if (!window)
throw new Error("Unable to obtain the owner window from the target given.");
let cleaners = listeners.get(window);
if (!cleaners) {
cleaners = [];
listeners.set(window, cleaners);
// We need to remove from our map the `window` once is closed, to prevent
// memory leak
ShimWaiver.getProperty(window, "addEventListener")("DOMWindowClose", removeFromListeners);
}
cleaners.push(() => ShimWaiver.getProperty(target, "removeEventListener")(type, listener, capture));
ShimWaiver.getProperty(target, "addEventListener")(type, listener, capture);
return output;
}
unload(() => {
let keys = ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(listeners)
for (let window of keys)
removeFromListeners.call(window);
});
exports.open = open;

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": "stable"
};
const { on, once, off, setListeners } = require('./core');
const { method, chainable } = require('../lang/functional/core');
const { Class } = require('../core/heritage');
/**
* `EventTarget` is an exemplar for creating an objects that can be used to
* add / remove event listeners on them. Events on these objects may be emitted
* via `emit` function exported by 'event/core' module.
*/
const EventTarget = Class({
/**
* Method initializes `this` event source. It goes through properties of a
* given `options` and registers listeners for the ones that look like an
* event listeners.
*/
/**
* Method initializes `this` event source. It goes through properties of a
* given `options` and registers listeners for the ones that look like an
* event listeners.
*/
initialize: function initialize(options) {
setListeners(this, options);
},
/**
* Registers an event `listener` that is called every time events of
* specified `type` are emitted.
* @param {String} type
* The type of event.
* @param {Function} listener
* The listener function that processes the event.
* @example
* worker.on('message', function (data) {
* console.log('data received: ' + data)
* })
*/
on: chainable(method(on)),
/**
* Registers an event `listener` that is called once the next time an event
* of the specified `type` is emitted.
* @param {String} type
* The type of the event.
* @param {Function} listener
* The listener function that processes the event.
*/
once: chainable(method(once)),
/**
* Removes an event `listener` for the given event `type`.
* @param {String} type
* The type of event.
* @param {Function} listener
* The listener function that processes the event.
*/
removeListener: function removeListener(type, listener) {
// Note: We can't just wrap `off` in `method` as we do it for other methods
// cause skipping a second or third argument will behave very differently
// than intended. This way we make sure all arguments are passed and only
// one listener is removed at most.
off(this, type, listener);
return this;
},
// but we can wrap `off` here, as the semantics are the same
off: chainable(method(off))
});
exports.EventTarget = EventTarget;

View file

@ -0,0 +1,328 @@
/* 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 { emit, on, once, off, EVENT_TYPE_PATTERN } = require("./core");
const { Cu } = require("chrome");
// This module provides set of high order function for working with event
// streams (streams in a NodeJS style that dispatch data, end and error
// events).
// Function takes a `target` object and returns set of implicit references
// (non property references) it keeps. This basically allows defining
// references between objects without storing the explicitly. See transform for
// more details.
var refs = (function() {
let refSets = new WeakMap();
return function refs(target) {
if (!refSets.has(target)) refSets.set(target, new Set());
return refSets.get(target);
};
})();
function transform(input, f) {
let output = new Output();
// Since event listeners don't prevent `input` to be GC-ed we wanna presrve
// it until `output` can be GC-ed. There for we add implicit reference which
// is removed once `input` ends.
refs(output).add(input);
const next = data => receive(output, data);
once(output, "start", () => start(input));
on(input, "error", error => emit(output, "error", error));
on(input, "end", function() {
refs(output).delete(input);
end(output);
});
on(input, "data", data => f(data, next));
return output;
}
// High order event transformation function that takes `input` event channel
// and returns transformation containing only events on which `p` predicate
// returns `true`.
function filter(input, predicate) {
return transform(input, function(data, next) {
if (predicate(data))
next(data);
});
}
exports.filter = filter;
// High order function that takes `input` and returns input of it's values
// mapped via given `f` function.
const map = (input, f) => transform(input, (data, next) => next(f(data)));
exports.map = map;
// High order function that takes `input` stream of streams and merges them
// into single event stream. Like flatten but time based rather than order
// based.
function merge(inputs) {
let output = new Output();
let open = 1;
let state = [];
output.state = state;
refs(output).add(inputs);
function end(input) {
open = open - 1;
refs(output).delete(input);
if (open === 0) emit(output, "end");
}
const error = e => emit(output, "error", e);
function forward(input) {
state.push(input);
open = open + 1;
on(input, "end", () => end(input));
on(input, "error", error);
on(input, "data", data => emit(output, "data", data));
}
// If `inputs` is an array treat it as a stream.
if (Array.isArray(inputs)) {
inputs.forEach(forward);
end(inputs);
}
else {
on(inputs, "end", () => end(inputs));
on(inputs, "error", error);
on(inputs, "data", forward);
}
return output;
}
exports.merge = merge;
const expand = (inputs, f) => merge(map(inputs, f));
exports.expand = expand;
const pipe = (from, to) => on(from, "*", emit.bind(emit, to));
exports.pipe = pipe;
// Shim signal APIs so other modules can be used as is.
const receive = (input, message) => {
if (input[receive])
input[receive](input, message);
else
emit(input, "data", message);
// Ideally our input will extend Input and already provide a weak value
// getter. If not, opportunistically shim the weak value getter on
// other types passed as the input.
if (!("value" in input)) {
Object.defineProperty(input, "value", WeakValueGetterSetter);
}
input.value = message;
};
receive.toString = () => "@@receive";
exports.receive = receive;
exports.send = receive;
const end = input => {
if (input[end])
input[end](input);
else
emit(input, "end", input);
};
end.toString = () => "@@end";
exports.end = end;
const stop = input => {
if (input[stop])
input[stop](input);
else
emit(input, "stop", input);
};
stop.toString = () => "@@stop";
exports.stop = stop;
const start = input => {
if (input[start])
input[start](input);
else
emit(input, "start", input);
};
start.toString = () => "@@start";
exports.start = start;
const lift = (step, ...inputs) => {
let args = null;
let opened = inputs.length;
let started = false;
const output = new Output();
const init = () => {
args = [...inputs.map(input => input.value)];
output.value = step(...args);
};
inputs.forEach((input, index) => {
on(input, "data", data => {
args[index] = data;
receive(output, step(...args));
});
on(input, "end", () => {
opened = opened - 1;
if (opened <= 0)
end(output);
});
});
once(output, "start", () => {
inputs.forEach(start);
init();
});
init();
return output;
};
exports.lift = lift;
const merges = inputs => {
let opened = inputs.length;
let output = new Output();
output.value = inputs[0].value;
inputs.forEach((input, index) => {
on(input, "data", data => receive(output, data));
on(input, "end", () => {
opened = opened - 1;
if (opened <= 0)
end(output);
});
});
once(output, "start", () => {
inputs.forEach(start);
output.value = inputs[0].value;
});
return output;
};
exports.merges = merges;
const foldp = (step, initial, input) => {
let output = map(input, x => step(output.value, x));
output.value = initial;
return output;
};
exports.foldp = foldp;
const keepIf = (p, base, input) => {
let output = filter(input, p);
output.value = base;
return output;
};
exports.keepIf = keepIf;
function Input() {}
Input.start = input => emit(input, "start", input);
Input.prototype.start = Input.start;
Input.end = input => {
emit(input, "end", input);
stop(input);
};
Input.prototype[end] = Input.end;
// The event channel system caches the last event seen as input.value.
// Unfortunately, if the last event is a DOM object this is a great way
// leak windows. Mitigate this by storing input.value using a weak
// reference. This allows the system to work for normal event processing
// while also allowing the objects to be reclaimed. It means, however,
// input.value cannot be accessed long after the event was dispatched.
const WeakValueGetterSetter = {
get: function() {
return this._weakValue ? this._weakValue.get() : this._simpleValue
},
set: function(v) {
if (v && typeof v === "object") {
try {
// Try to set a weak reference. This can throw for some values.
// For example, if the value is a native object that does not
// implement nsISupportsWeakReference.
this._weakValue = Cu.getWeakReference(v)
this._simpleValue = undefined;
return;
} catch (e) {
// Do nothing. Fall through to setting _simpleValue below.
}
}
this._simpleValue = v;
this._weakValue = undefined;
},
}
Object.defineProperty(Input.prototype, "value", WeakValueGetterSetter);
exports.Input = Input;
// Define an Output type with a weak value getter for the transformation
// functions that produce new channels.
function Output() { }
Object.defineProperty(Output.prototype, "value", WeakValueGetterSetter);
exports.Output = Output;
const $source = "@@source";
const $outputs = "@@outputs";
exports.outputs = $outputs;
// NOTE: Passing DOM objects through a Reactor can cause them to leak
// when they get cached in this.value. We cannot use a weak reference
// in this case because the Reactor design expects to always have both the
// past and present value. If we allow past values to be collected the
// system breaks.
function Reactor(options={}) {
const {onStep, onStart, onEnd} = options;
if (onStep)
this.onStep = onStep;
if (onStart)
this.onStart = onStart;
if (onEnd)
this.onEnd = onEnd;
}
Reactor.prototype.onStep = _ => void(0);
Reactor.prototype.onStart = _ => void(0);
Reactor.prototype.onEnd = _ => void(0);
Reactor.prototype.onNext = function(present, past) {
this.value = present;
this.onStep(present, past);
};
Reactor.prototype.run = function(input) {
on(input, "data", message => this.onNext(message, input.value));
on(input, "end", () => this.onEnd(input.value));
start(input);
this.value = input.value;
this.onStart(input.value);
};
exports.Reactor = Reactor;
/**
* Takes an object used as options with potential keys like 'onMessage',
* used to be called `require('sdk/event/core').setListeners` on.
* This strips all keys that would trigger a listener to be set.
*
* @params {Object} object
* @return {Object}
*/
function stripListeners (object) {
return Object.keys(object || {}).reduce((agg, key) => {
if (!EVENT_TYPE_PATTERN.test(key))
agg[key] = object[key];
return agg;
}, {});
}
exports.stripListeners = stripListeners;
const when = (target, type) => new Promise(resolve => {
once(target, type, resolve);
});
exports.when = when;

View file

@ -0,0 +1,115 @@
/* 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 } = require("chrome");
const { Class } = require("../core/heritage");
const { List, addListItem, removeListItem } = require("../util/list");
const { EventTarget } = require("../event/target");
const { emit } = require("../event/core");
const { create: makeFrame } = require("./utils");
const { defer } = require("../core/promise");
const { when: unload } = require("../system/unload");
const { validateOptions, getTypeOf } = require("../deprecated/api-utils");
const { window } = require("../addon/window");
const { fromIterator } = require("../util/array");
// This cache is used to access friend properties between functions
// without exposing them on the public API.
var cache = new Set();
var elements = new WeakMap();
function contentLoaded(target) {
var deferred = defer();
target.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) {
// "DOMContentLoaded" events from nested frames propagate up to target,
// ignore events unless it's DOMContentLoaded for the given target.
if (event.target === target || event.target === target.contentDocument) {
target.removeEventListener("DOMContentLoaded", DOMContentLoaded, false);
deferred.resolve(target);
}
}, false);
return deferred.promise;
}
function FrameOptions(options) {
options = options || {}
return validateOptions(options, FrameOptions.validator);
}
FrameOptions.validator = {
onReady: {
is: ["undefined", "function", "array"],
ok: function(v) {
if (getTypeOf(v) === "array") {
// make sure every item is a function
return v.every(item => typeof(item) === "function")
}
return true;
}
},
onUnload: {
is: ["undefined", "function"]
}
};
var HiddenFrame = Class({
extends: EventTarget,
initialize: function initialize(options) {
options = FrameOptions(options);
EventTarget.prototype.initialize.call(this, options);
},
get element() {
return elements.get(this);
},
toString: function toString() {
return "[object Frame]"
}
});
exports.HiddenFrame = HiddenFrame
function addHidenFrame(frame) {
if (!(frame instanceof HiddenFrame))
throw Error("The object to be added must be a HiddenFrame.");
// This instance was already added.
if (cache.has(frame)) return frame;
else cache.add(frame);
let element = makeFrame(window.document, {
nodeName: "iframe",
type: "content",
allowJavascript: true,
allowPlugins: true,
allowAuth: true,
});
elements.set(frame, element);
contentLoaded(element).then(function onFrameReady(element) {
emit(frame, "ready");
}, console.exception);
return frame;
}
exports.add = addHidenFrame
function removeHiddenFrame(frame) {
if (!(frame instanceof HiddenFrame))
throw Error("The object to be removed must be a HiddenFrame.");
if (!cache.has(frame)) return;
// Remove from cache before calling in order to avoid loop
cache.delete(frame);
emit(frame, "unload")
let element = frame.element
if (element) element.parentNode.removeChild(element)
}
exports.remove = removeHiddenFrame;
unload(() => fromIterator(cache).forEach(removeHiddenFrame));

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';
module.metadata = {
"stability": "experimental"
};
const { Ci } = require("chrome");
const XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
function eventTarget(frame) {
return getDocShell(frame).chromeEventHandler;
}
exports.eventTarget = eventTarget;
function getDocShell(frame) {
let { frameLoader } = frame.QueryInterface(Ci.nsIFrameLoaderOwner);
return frameLoader && frameLoader.docShell;
}
exports.getDocShell = getDocShell;
/**
* Creates a XUL `browser` element in a privileged document.
* @params {nsIDOMDocument} document
* @params {String} options.type
* By default is 'content' for possible values see:
* https://developer.mozilla.org/en/XUL/iframe#a-browser.type
* @params {String} options.uri
* URI of the document to be loaded into created frame.
* @params {Boolean} options.remote
* If `true` separate process will be used for this frame, also in such
* case all the following options are ignored.
* @params {Boolean} options.allowAuth
* Whether to allow auth dialogs. Defaults to `false`.
* @params {Boolean} options.allowJavascript
* Whether to allow Javascript execution. Defaults to `false`.
* @params {Boolean} options.allowPlugins
* Whether to allow plugin execution. Defaults to `false`.
*/
function create(target, options) {
target = target instanceof Ci.nsIDOMDocument ? target.documentElement :
target instanceof Ci.nsIDOMWindow ? target.document.documentElement :
target;
options = options || {};
let remote = options.remote || false;
let namespaceURI = options.namespaceURI || XUL;
let isXUL = namespaceURI === XUL;
let nodeName = isXUL && options.browser ? 'browser' : 'iframe';
let document = target.ownerDocument;
let frame = document.createElementNS(namespaceURI, nodeName);
// Type="content" is mandatory to enable stuff here:
// http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1776
frame.setAttribute('type', options.type || 'content');
frame.setAttribute('src', options.uri || 'about:blank');
// Must set the remote attribute before attaching the frame to the document
if (remote && isXUL) {
// We remove XBL binding to avoid execution of code that is not going to
// work because browser has no docShell attribute in remote mode
// (for example)
frame.setAttribute('style', '-moz-binding: none;');
frame.setAttribute('remote', 'true');
}
target.appendChild(frame);
// Load in separate process if `options.remote` is `true`.
// http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1347
if (remote && !isXUL) {
frame.QueryInterface(Ci.nsIMozBrowserFrame);
frame.createRemoteFrameLoader(null);
}
// If browser is remote it won't have a `docShell`.
if (!remote) {
let docShell = getDocShell(frame);
docShell.allowAuth = options.allowAuth || false;
docShell.allowJavascript = options.allowJavascript || false;
docShell.allowPlugins = options.allowPlugins || false;
docShell.allowWindowControl = options.allowWindowControl || false;
}
return frame;
}
exports.create = create;
function swapFrameLoaders(from, to) {
return from.QueryInterface(Ci.nsIFrameLoaderOwner).swapFrameLoaders(to);
}
exports.swapFrameLoaders = swapFrameLoaders;

View file

@ -0,0 +1,500 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// 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.
// Adapted version of:
// https://github.com/joyent/node/blob/v0.11.3/lib/path.js
// Shim process global from node.
var process = Object.create(require('../system'));
process.cwd = process.pathFor.bind(process, 'CurProcD');
// Update original check in node `process.platform === 'win32'` since in SDK it's `winnt`.
var isWindows = process.platform.indexOf('win') === 0;
// resolves . and .. elements in a path array with directory names there
// must be no slashes, empty elements, or device names (c:\) in the array
// (so also no leading and trailing slashes - it does not distinguish
// relative and absolute paths)
function normalizeArray(parts, allowAboveRoot) {
// if the path tries to go above the root, `up` ends up > 0
var up = 0;
for (var i = parts.length - 1; i >= 0; i--) {
var last = parts[i];
if (last === '.') {
parts.splice(i, 1);
} else if (last === '..') {
parts.splice(i, 1);
up++;
} else if (up) {
parts.splice(i, 1);
up--;
}
}
// if the path is allowed to go above the root, restore leading ..s
if (allowAboveRoot) {
for (; up--; up) {
parts.unshift('..');
}
}
return parts;
}
if (isWindows) {
// Regex to split a windows path into three parts: [*, device, slash,
// tail] windows-only
var splitDeviceRe =
/^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/;
// Regex to split the tail part of the above into [*, dir, basename, ext]
var splitTailRe =
/^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/;
// Function to split a filename into [root, dir, basename, ext]
// windows version
var splitPath = function(filename) {
// Separate device+slash from tail
var result = splitDeviceRe.exec(filename),
device = (result[1] || '') + (result[2] || ''),
tail = result[3] || '';
// Split the tail into dir, basename and extension
var result2 = splitTailRe.exec(tail),
dir = result2[1],
basename = result2[2],
ext = result2[3];
return [device, dir, basename, ext];
};
var normalizeUNCRoot = function(device) {
return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\');
};
// path.resolve([from ...], to)
// windows version
exports.resolve = function() {
var resolvedDevice = '',
resolvedTail = '',
resolvedAbsolute = false;
for (var i = arguments.length - 1; i >= -1; i--) {
var path;
if (i >= 0) {
path = arguments[i];
} else if (!resolvedDevice) {
path = process.cwd();
} else {
// Windows has the concept of drive-specific current working
// directories. If we've resolved a drive letter but not yet an
// absolute path, get cwd for that drive. We're sure the device is not
// an unc path at this points, because unc paths are always absolute.
path = process.env['=' + resolvedDevice];
// Verify that a drive-local cwd was found and that it actually points
// to our drive. If not, default to the drive's root.
if (!path || path.substr(0, 3).toLowerCase() !==
resolvedDevice.toLowerCase() + '\\') {
path = resolvedDevice + '\\';
}
}
// Skip empty and invalid entries
if (typeof path !== 'string') {
throw new TypeError('Arguments to path.resolve must be strings');
} else if (!path) {
continue;
}
var result = splitDeviceRe.exec(path),
device = result[1] || '',
isUnc = device && device.charAt(1) !== ':',
isAbsolute = exports.isAbsolute(path),
tail = result[3];
if (device &&
resolvedDevice &&
device.toLowerCase() !== resolvedDevice.toLowerCase()) {
// This path points to another device so it is not applicable
continue;
}
if (!resolvedDevice) {
resolvedDevice = device;
}
if (!resolvedAbsolute) {
resolvedTail = tail + '\\' + resolvedTail;
resolvedAbsolute = isAbsolute;
}
if (resolvedDevice && resolvedAbsolute) {
break;
}
}
// Convert slashes to backslashes when `resolvedDevice` points to an UNC
// root. Also squash multiple slashes into a single one where appropriate.
if (isUnc) {
resolvedDevice = normalizeUNCRoot(resolvedDevice);
}
// At this point the path should be resolved to a full absolute path,
// but handle relative paths to be safe (might happen when process.cwd()
// fails)
// Normalize the tail path
function f(p) {
return !!p;
}
resolvedTail = normalizeArray(resolvedTail.split(/[\\\/]+/).filter(f),
!resolvedAbsolute).join('\\');
return (resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail) ||
'.';
};
// windows version
exports.normalize = function(path) {
var result = splitDeviceRe.exec(path),
device = result[1] || '',
isUnc = device && device.charAt(1) !== ':',
isAbsolute = exports.isAbsolute(path),
tail = result[3],
trailingSlash = /[\\\/]$/.test(tail);
// If device is a drive letter, we'll normalize to lower case.
if (device && device.charAt(1) === ':') {
device = device[0].toLowerCase() + device.substr(1);
}
// Normalize the tail path
tail = normalizeArray(tail.split(/[\\\/]+/).filter(function(p) {
return !!p;
}), !isAbsolute).join('\\');
if (!tail && !isAbsolute) {
tail = '.';
}
if (tail && trailingSlash) {
tail += '\\';
}
// Convert slashes to backslashes when `device` points to an UNC root.
// Also squash multiple slashes into a single one where appropriate.
if (isUnc) {
device = normalizeUNCRoot(device);
}
return device + (isAbsolute ? '\\' : '') + tail;
};
// windows version
exports.isAbsolute = function(path) {
var result = splitDeviceRe.exec(path),
device = result[1] || '',
isUnc = device && device.charAt(1) !== ':';
// UNC paths are always absolute
return !!result[2] || isUnc;
};
// windows version
exports.join = function() {
function f(p) {
if (typeof p !== 'string') {
throw new TypeError('Arguments to path.join must be strings');
}
return p;
}
var paths = Array.prototype.filter.call(arguments, f);
var joined = paths.join('\\');
// Make sure that the joined path doesn't start with two slashes, because
// normalize() will mistake it for an UNC path then.
//
// This step is skipped when it is very clear that the user actually
// intended to point at an UNC path. This is assumed when the first
// non-empty string arguments starts with exactly two slashes followed by
// at least one more non-slash character.
//
// Note that for normalize() to treat a path as an UNC path it needs to
// have at least 2 components, so we don't filter for that here.
// This means that the user can use join to construct UNC paths from
// a server name and a share name; for example:
// path.join('//server', 'share') -> '\\\\server\\share\')
if (!/^[\\\/]{2}[^\\\/]/.test(paths[0])) {
joined = joined.replace(/^[\\\/]{2,}/, '\\');
}
return exports.normalize(joined);
};
// path.relative(from, to)
// it will solve the relative path from 'from' to 'to', for instance:
// from = 'C:\\orandea\\test\\aaa'
// to = 'C:\\orandea\\impl\\bbb'
// The output of the function should be: '..\\..\\impl\\bbb'
// windows version
exports.relative = function(from, to) {
from = exports.resolve(from);
to = exports.resolve(to);
// windows is not case sensitive
var lowerFrom = from.toLowerCase();
var lowerTo = to.toLowerCase();
function trim(arr) {
var start = 0;
for (; start < arr.length; start++) {
if (arr[start] !== '') break;
}
var end = arr.length - 1;
for (; end >= 0; end--) {
if (arr[end] !== '') break;
}
if (start > end) return [];
return arr.slice(start, end - start + 1);
}
var toParts = trim(to.split('\\'));
var lowerFromParts = trim(lowerFrom.split('\\'));
var lowerToParts = trim(lowerTo.split('\\'));
var length = Math.min(lowerFromParts.length, lowerToParts.length);
var samePartsLength = length;
for (var i = 0; i < length; i++) {
if (lowerFromParts[i] !== lowerToParts[i]) {
samePartsLength = i;
break;
}
}
if (samePartsLength == 0) {
return to;
}
var outputParts = [];
for (var i = samePartsLength; i < lowerFromParts.length; i++) {
outputParts.push('..');
}
outputParts = outputParts.concat(toParts.slice(samePartsLength));
return outputParts.join('\\');
};
exports.sep = '\\';
exports.delimiter = ';';
} else /* posix */ {
// Split a filename into [root, dir, basename, ext], unix version
// 'root' is just a slash, or nothing.
var splitPathRe =
/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
var splitPath = function(filename) {
return splitPathRe.exec(filename).slice(1);
};
// path.resolve([from ...], to)
// posix version
exports.resolve = function() {
var resolvedPath = '',
resolvedAbsolute = false;
for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
var path = (i >= 0) ? arguments[i] : process.cwd();
// Skip empty and invalid entries
if (typeof path !== 'string') {
throw new TypeError('Arguments to path.resolve must be strings');
} else if (!path) {
continue;
}
resolvedPath = path + '/' + resolvedPath;
resolvedAbsolute = path.charAt(0) === '/';
}
// At this point the path should be resolved to a full absolute path, but
// handle relative paths to be safe (might happen when process.cwd() fails)
// Normalize the path
resolvedPath = normalizeArray(resolvedPath.split('/').filter(function(p) {
return !!p;
}), !resolvedAbsolute).join('/');
return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
};
// path.normalize(path)
// posix version
exports.normalize = function(path) {
var isAbsolute = exports.isAbsolute(path),
trailingSlash = path.substr(-1) === '/';
// Normalize the path
path = normalizeArray(path.split('/').filter(function(p) {
return !!p;
}), !isAbsolute).join('/');
if (!path && !isAbsolute) {
path = '.';
}
if (path && trailingSlash) {
path += '/';
}
return (isAbsolute ? '/' : '') + path;
};
// posix version
exports.isAbsolute = function(path) {
return path.charAt(0) === '/';
};
// posix version
exports.join = function() {
var paths = Array.prototype.slice.call(arguments, 0);
return exports.normalize(paths.filter(function(p, index) {
if (typeof p !== 'string') {
throw new TypeError('Arguments to path.join must be strings');
}
return p;
}).join('/'));
};
// path.relative(from, to)
// posix version
exports.relative = function(from, to) {
from = exports.resolve(from).substr(1);
to = exports.resolve(to).substr(1);
function trim(arr) {
var start = 0;
for (; start < arr.length; start++) {
if (arr[start] !== '') break;
}
var end = arr.length - 1;
for (; end >= 0; end--) {
if (arr[end] !== '') break;
}
if (start > end) return [];
return arr.slice(start, end - start + 1);
}
var fromParts = trim(from.split('/'));
var toParts = trim(to.split('/'));
var length = Math.min(fromParts.length, toParts.length);
var samePartsLength = length;
for (var i = 0; i < length; i++) {
if (fromParts[i] !== toParts[i]) {
samePartsLength = i;
break;
}
}
var outputParts = [];
for (var i = samePartsLength; i < fromParts.length; i++) {
outputParts.push('..');
}
outputParts = outputParts.concat(toParts.slice(samePartsLength));
return outputParts.join('/');
};
exports.sep = '/';
exports.delimiter = ':';
}
exports.dirname = function(path) {
var result = splitPath(path),
root = result[0],
dir = result[1];
if (!root && !dir) {
// No dirname whatsoever
return '.';
}
if (dir) {
// It has a dirname, strip trailing slash
dir = dir.substr(0, dir.length - 1);
}
return root + dir;
};
exports.basename = function(path, ext) {
var f = splitPath(path)[2];
// TODO: make this comparison case-insensitive on windows?
if (ext && f.substr(-1 * ext.length) === ext) {
f = f.substr(0, f.length - ext.length);
}
return f;
};
exports.extname = function(path) {
return splitPath(path)[3];
};
if (isWindows) {
exports._makeLong = function(path) {
// Note: this will *probably* throw somewhere.
if (typeof path !== 'string')
return path;
if (!path) {
return '';
}
var resolvedPath = exports.resolve(path);
if (/^[a-zA-Z]\:\\/.test(resolvedPath)) {
// path is local filesystem path, which needs to be converted
// to long UNC path.
return '\\\\?\\' + resolvedPath;
} else if (/^\\\\[^?.]/.test(resolvedPath)) {
// path is network UNC path, which needs to be converted
// to long UNC path.
return '\\\\?\\UNC\\' + resolvedPath.substring(2);
}
return path;
};
} else {
exports._makeLong = function(path) {
return path;
};
}

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";
module.metadata = {
"stability": "stable"
};
const INVALID_HOTKEY = "Hotkey must have at least one modifier.";
const { toJSON: jsonify, toString: stringify,
isFunctionKey } = require("./keyboard/utils");
const { register, unregister } = require("./keyboard/hotkeys");
const Hotkey = exports.Hotkey = function Hotkey(options) {
if (!(this instanceof Hotkey))
return new Hotkey(options);
// Parsing key combination string.
let hotkey = jsonify(options.combo);
if (!isFunctionKey(hotkey.key) && !hotkey.modifiers.length) {
throw new TypeError(INVALID_HOTKEY);
}
this.onPress = options.onPress && options.onPress.bind(this);
this.toString = stringify.bind(null, hotkey);
// Registering listener on keyboard combination enclosed by this hotkey.
// Please note that `this.toString()` is a normalized version of
// `options.combination` where order of modifiers is sorted and `accel` is
// replaced with platform specific key.
register(this.toString(), this.onPress);
// We freeze instance before returning it in order to make it's properties
// read-only.
return Object.freeze(this);
};
Hotkey.prototype.destroy = function destroy() {
unregister(this.toString(), this.onPress);
};

View file

@ -0,0 +1,79 @@
/* 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 } = require("chrome");
const { id } = require("./self");
// placeholder, copied from bootstrap.js
var sanitizeId = function(id){
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");
return domain
};
const PSEUDOURI = "indexeddb://" + sanitizeId(id) // https://bugzilla.mozilla.org/show_bug.cgi?id=779197
// Use XPCOM because `require("./url").URL` doesn't expose the raw uri object.
var principaluri = Cc["@mozilla.org/network/io-service;1"].
getService(Ci.nsIIOService).
newURI(PSEUDOURI, null, null);
var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"]
.getService(Ci.nsIScriptSecurityManager);
var principal = ssm.createCodebasePrincipal(principaluri, {});
function toArray(args) {
return Array.prototype.slice.call(args);
}
function openInternal(args, forPrincipal, deleting) {
if (forPrincipal) {
args = toArray(args);
} else {
args = [principal].concat(toArray(args));
}
if (args.length == 2) {
args.push({ storage: "persistent" });
} else if (!deleting && args.length >= 3 && typeof args[2] === "number") {
args[2] = { version: args[2], storage: "persistent" };
}
if (deleting) {
return indexedDB.deleteForPrincipal.apply(indexedDB, args);
}
return indexedDB.openForPrincipal.apply(indexedDB, args);
}
exports.indexedDB = Object.freeze({
open: function () {
return openInternal(arguments, false, false);
},
deleteDatabase: function () {
return openInternal(arguments, false, true);
},
openForPrincipal: function () {
return openInternal(arguments, true, false);
},
deleteForPrincipal: function () {
return openInternal(arguments, true, true);
},
cmp: indexedDB.cmp.bind(indexedDB)
});
exports.IDBKeyRange = IDBKeyRange;
exports.DOMException = Ci.nsIDOMDOMException;

View file

@ -0,0 +1,73 @@
/* 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 { windows, isBrowser, isInteractive, isDocumentLoaded,
getOuterId } = require("../window/utils");
const { InputPort } = require("./system");
const { lift, merges, foldp, keepIf, start, Input } = require("../event/utils");
const { patch } = require("diffpatcher/index");
const { Sequence, seq, filter, object, pairs } = require("../util/sequence");
// Create lazy iterators from the regular arrays, although
// once https://github.com/mozilla/addon-sdk/pull/1314 lands
// `windows` will be transforme to lazy iterators.
// When iterated over belowe sequences items will represent
// state of windows at the time of iteration.
const opened = seq(function*() {
const items = windows("navigator:browser", {includePrivate: true});
for (let item of items) {
yield [getOuterId(item), item];
}
});
const interactive = filter(([_, window]) => isInteractive(window), opened);
const loaded = filter(([_, window]) => isDocumentLoaded(window), opened);
// Helper function that converts given argument to a delta.
const Update = window => window && object([getOuterId(window), window]);
const Delete = window => window && object([getOuterId(window), null]);
// Signal represents delta for last top level window close.
const LastClosed = lift(Delete,
keepIf(isBrowser, null,
new InputPort({topic: "domwindowclosed"})));
exports.LastClosed = LastClosed;
const windowFor = document => document && document.defaultView;
// Signal represent delta for last top level window document becoming interactive.
const InteractiveDoc = new InputPort({topic: "chrome-document-interactive"});
const InteractiveWin = lift(windowFor, InteractiveDoc);
const LastInteractive = lift(Update, keepIf(isBrowser, null, InteractiveWin));
exports.LastInteractive = LastInteractive;
// Signal represent delta for last top level window loaded.
const LoadedDoc = new InputPort({topic: "chrome-document-loaded"});
const LoadedWin = lift(windowFor, LoadedDoc);
const LastLoaded = lift(Update, keepIf(isBrowser, null, LoadedWin));
exports.LastLoaded = LastLoaded;
const initialize = input => {
if (!input.initialized) {
input.value = object(...input.value);
Input.start(input);
input.initialized = true;
}
};
// Signal represents set of top level interactive windows, updated any
// time new window becomes interactive or one get's closed.
const Interactive = foldp(patch, interactive, merges([LastInteractive,
LastClosed]));
Interactive[start] = initialize;
exports.Interactive = Interactive;
// Signal represents set of top level loaded window, updated any time
// new window becomes interactive or one get's closed.
const Loaded = foldp(patch, loaded, merges([LastLoaded, LastClosed]));
Loaded[start] = initialize;
exports.Loaded = Loaded;

View file

@ -0,0 +1,28 @@
/* 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 { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
const { receive } = require("../event/utils");
const { InputPort } = require("./system");
const { object} = require("../util/sequence");
const { getOuterId } = require("../window/utils");
const Input = function() {};
Input.prototype = Object.create(InputPort.prototype);
Input.prototype.onCustomizeStart = function (window) {
receive(this, object([getOuterId(window), true]));
}
Input.prototype.onCustomizeEnd = function (window) {
receive(this, object([getOuterId(window), null]));
}
Input.prototype.addListener = input => CustomizableUI.addListener(input);
Input.prototype.removeListener = input => CustomizableUI.removeListener(input);
exports.CustomizationInput = Input;

View file

@ -0,0 +1,85 @@
/* 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 { InputPort } = require("./system");
const { getFrameElement, getOuterId,
getOwnerBrowserWindow } = require("../window/utils");
const { isnt } = require("../lang/functional");
const { foldp, lift, merges, keepIf } = require("../event/utils");
const { object } = require("../util/sequence");
const { compose } = require("../lang/functional");
const { LastClosed } = require("./browser");
const { patch } = require("diffpatcher/index");
const Document = Ci.nsIDOMDocument;
const isntNull = isnt(null);
const frameID = frame => frame.id;
const browserID = compose(getOuterId, getOwnerBrowserWindow);
const isInnerFrame = frame =>
frame && frame.hasAttribute("data-is-sdk-inner-frame");
// Utility function that given content window loaded in our frame views returns
// an actual frame. This basically takes care of fact that actual frame document
// is loaded in the nested iframe. If content window is not loaded in the nested
// frame of the frame view it returs null.
const getFrame = document =>
document && document.defaultView && getFrameElement(document.defaultView);
const FrameInput = function(options) {
const input = keepIf(isInnerFrame, null,
lift(getFrame, new InputPort(options)));
return lift(frame => {
if (!frame) return frame;
const [id, owner] = [frameID(frame), browserID(frame)];
return object([id, {owners: object([owner, options.update])}]);
}, input);
};
const LastLoading = new FrameInput({topic: "document-element-inserted",
update: {readyState: "loading"}});
exports.LastLoading = LastLoading;
const LastInteractive = new FrameInput({topic: "content-document-interactive",
update: {readyState: "interactive"}});
exports.LastInteractive = LastInteractive;
const LastLoaded = new FrameInput({topic: "content-document-loaded",
update: {readyState: "complete"}});
exports.LastLoaded = LastLoaded;
const LastUnloaded = new FrameInput({topic: "content-page-hidden",
update: null});
exports.LastUnloaded = LastUnloaded;
// Represents state of SDK frames in form of data structure:
// {"frame#1": {"id": "frame#1",
// "inbox": {"data": "ping",
// "target": {"id": "frame#1", "owner": "outerWindowID#2"},
// "source": {"id": "frame#1"}}
// "url": "resource://addon-1/data/index.html",
// "owners": {"outerWindowID#1": {"readyState": "loading"},
// "outerWindowID#2": {"readyState": "complete"}}
//
//
// frame#2: {"id": "frame#2",
// "url": "resource://addon-1/data/main.html",
// "outbox": {"data": "pong",
// "source": {"id": "frame#2", "owner": "outerWindowID#1"}
// "target": {"id": "frame#2"}}
// "owners": {outerWindowID#1: {readyState: "interacitve"}}}}
const Frames = foldp(patch, {}, merges([
LastLoading,
LastInteractive,
LastLoaded,
LastUnloaded,
new InputPort({ id: "frame-mailbox" }),
new InputPort({ id: "frame-change" }),
new InputPort({ id: "frame-changed" })
]));
exports.Frames = Frames;

View file

@ -0,0 +1,113 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cc, Ci, Cr, Cu } = require("chrome");
const { Input, start, stop, end, receive, outputs } = require("../event/utils");
const { once, off } = require("../event/core");
const { id: addonID } = require("../self");
const unloadMessage = require("@loader/unload");
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");
const addonUnloadTopic = "sdk:loader:destroy";
const isXrayWrapper = Cu.isXrayWrapper;
// In the past SDK used to double-wrap notifications dispatched, which
// made them awkward to use outside of SDK. At present they no longer
// do that, although we still supported for legacy reasons.
const isLegacyWrapper = x =>
x && x.wrappedJSObject &&
"observersModuleSubjectWrapper" in x.wrappedJSObject;
const unwrapLegacy = x => x.wrappedJSObject.object;
// `InputPort` provides a way to create a signal out of the observer
// notification subject's for the given `topic`. If `options.initial`
// is provided it is used as initial value otherwise `null` is used.
// Constructor can be given `options.id` that will be used to create
// a `topic` which is namespaced to an add-on (this avoids conflicts
// when multiple add-on are used, although in a future host probably
// should just be shared across add-ons). It is also possible to
// specify a specific `topic` via `options.topic` which is used as
// without namespacing. Created signal ends whenever add-on is
// unloaded.
const InputPort = function InputPort({id, topic, initial}) {
this.id = id || topic;
this.topic = topic || "sdk:" + addonID + ":" + id;
this.value = initial === void(0) ? null : initial;
this.observing = false;
this[outputs] = [];
};
// InputPort type implements `Input` signal interface.
InputPort.prototype = new Input();
InputPort.prototype.constructor = InputPort;
// When port is started (which is when it's subgraph get's
// first subscriber) actual observer is registered.
InputPort.start = input => {
input.addListener(input);
// Also register add-on unload observer to end this signal
// when that happens.
addObserver(input, addonUnloadTopic, false);
};
InputPort.prototype[start] = InputPort.start;
InputPort.addListener = input => addObserver(input, input.topic, false);
InputPort.prototype.addListener = InputPort.addListener;
// When port is stopped (which is when it's subgraph has no
// no subcribers left) an actual observer unregistered.
// Note that port stopped once it ends as well (which is when
// add-on is unloaded).
InputPort.stop = input => {
input.removeListener(input);
removeObserver(input, addonUnloadTopic);
};
InputPort.prototype[stop] = InputPort.stop;
InputPort.removeListener = input => removeObserver(input, input.topic);
InputPort.prototype.removeListener = InputPort.removeListener;
// `InputPort` also implements `nsIObserver` interface and
// `nsISupportsWeakReference` interfaces as it's going to be used as such.
InputPort.prototype.QueryInterface = function(iid) {
if (!iid.equals(Ci.nsIObserver) && !iid.equals(Ci.nsISupportsWeakReference))
throw Cr.NS_ERROR_NO_INTERFACE;
return this;
};
// `InputPort` instances implement `observe` method, which is invoked when
// observer notifications are dispatched. The `subject` of that notification
// are received on this signal.
InputPort.prototype.observe = function(subject, topic, data) {
// Unwrap message from the subject. SDK used to have it's own version of
// wrappedJSObjects which take precedence, if subject has `wrappedJSObject`
// and it's not an XrayWrapper use it as message. Otherwise use subject as
// is.
const message = subject === null ? null :
isLegacyWrapper(subject) ? unwrapLegacy(subject) :
isXrayWrapper(subject) ? subject :
subject.wrappedJSObject ? subject.wrappedJSObject :
subject;
// If observer topic matches topic of the input port receive a message.
if (topic === this.topic) {
receive(this, message);
}
// If observe topic is add-on unload topic we create an end message.
if (topic === addonUnloadTopic && message === unloadMessage) {
end(this);
}
};
exports.InputPort = InputPort;

View file

@ -0,0 +1,351 @@
/* 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'
};
/*
* Encodings supported by TextEncoder/Decoder:
* utf-8, utf-16le, utf-16be
* http://encoding.spec.whatwg.org/#interface-textencoder
*
* Node however supports the following encodings:
* ascii, utf-8, utf-16le, usc2, base64, hex
*/
const { Cu } = require('chrome');
const { isNumber } = require('sdk/lang/type');
const { TextEncoder, TextDecoder } = Cu.import('resource://gre/modules/commonjs/toolkit/loader.js', {});
exports.TextEncoder = TextEncoder;
exports.TextDecoder = TextDecoder;
/**
* Use WeakMaps to work around Bug 929146, which prevents us from adding
* getters or values to typed arrays
* https://bugzilla.mozilla.org/show_bug.cgi?id=929146
*/
const parents = new WeakMap();
const views = new WeakMap();
function Buffer(subject, encoding /*, bufferLength */) {
// Allow invocation without `new` constructor
if (!(this instanceof Buffer))
return new Buffer(subject, encoding, arguments[2]);
var type = typeof(subject);
switch (type) {
case 'number':
// Create typed array of the given size if number.
try {
let buffer = new Uint8Array(subject > 0 ? Math.floor(subject) : 0);
return buffer;
} catch (e) {
if (/size and count too large/.test(e.message) ||
/invalid arguments/.test(e.message))
throw new RangeError('Could not instantiate buffer: size of buffer may be too large');
else
throw new Error('Could not instantiate buffer');
}
break;
case 'string':
// If string encode it and use buffer for the returned Uint8Array
// to create a local patched version that acts like node buffer.
encoding = encoding || 'utf8';
return new Uint8Array(new TextEncoder(encoding).encode(subject).buffer);
case 'object':
// This form of the constructor uses the form of
// new Uint8Array(buffer, offset, length);
// So we can instantiate a typed array within the constructor
// to inherit the appropriate properties, where both the
// `subject` and newly instantiated buffer share the same underlying
// data structure.
if (arguments.length === 3)
return new Uint8Array(subject, encoding, arguments[2]);
// If array or alike just make a copy with a local patched prototype.
else
return new Uint8Array(subject);
default:
throw new TypeError('must start with number, buffer, array or string');
}
}
exports.Buffer = Buffer;
// Tests if `value` is a Buffer.
Buffer.isBuffer = value => value instanceof Buffer
// Returns true if the encoding is a valid encoding argument & false otherwise
Buffer.isEncoding = function (encoding) {
if (!encoding) return false;
try {
new TextDecoder(encoding);
} catch(e) {
return false;
}
return true;
}
// Gives the actual byte length of a string. encoding defaults to 'utf8'.
// This is not the same as String.prototype.length since that returns the
// number of characters in a string.
Buffer.byteLength = (value, encoding = 'utf8') =>
new TextEncoder(encoding).encode(value).byteLength
// Direct copy of the nodejs's buffer implementation:
// https://github.com/joyent/node/blob/b255f4c10a80343f9ce1cee56d0288361429e214/lib/buffer.js#L146-L177
Buffer.concat = function(list, length) {
if (!Array.isArray(list))
throw new TypeError('Usage: Buffer.concat(list[, length])');
if (typeof length === 'undefined') {
length = 0;
for (var i = 0; i < list.length; i++)
length += list[i].length;
} else {
length = ~~length;
}
if (length < 0)
length = 0;
if (list.length === 0)
return new Buffer(0);
else if (list.length === 1)
return list[0];
if (length < 0)
throw new RangeError('length is not a positive number');
var buffer = new Buffer(length);
var pos = 0;
for (var i = 0; i < list.length; i++) {
var buf = list[i];
buf.copy(buffer, pos);
pos += buf.length;
}
return buffer;
};
// Node buffer is very much like Uint8Array although it has bunch of methods
// that typically can be used in combination with `DataView` while preserving
// access by index. Since in SDK each module has it's own set of bult-ins it
// ok to patch ours to make it nodejs Buffer compatible.
const Uint8ArraySet = Uint8Array.prototype.set
Buffer.prototype = Uint8Array.prototype;
Object.defineProperties(Buffer.prototype, {
parent: {
get: function() { return parents.get(this, undefined); }
},
view: {
get: function () {
let view = views.get(this, undefined);
if (view) return view;
view = new DataView(this.buffer);
views.set(this, view);
return view;
}
},
toString: {
value: function(encoding, start, end) {
encoding = !!encoding ? (encoding + '').toLowerCase() : 'utf8';
start = Math.max(0, ~~start);
end = Math.min(this.length, end === void(0) ? this.length : ~~end);
return new TextDecoder(encoding).decode(this.subarray(start, end));
}
},
toJSON: {
value: function() {
return { type: 'Buffer', data: Array.slice(this, 0) };
}
},
get: {
value: function(offset) {
return this[offset];
}
},
set: {
value: function(offset, value) { this[offset] = value; }
},
copy: {
value: function(target, offset, start, end) {
let length = this.length;
let targetLength = target.length;
offset = isNumber(offset) ? offset : 0;
start = isNumber(start) ? start : 0;
if (start < 0)
throw new RangeError('sourceStart is outside of valid range');
if (end < 0)
throw new RangeError('sourceEnd is outside of valid range');
// If sourceStart > sourceEnd, or targetStart > targetLength,
// zero bytes copied
if (start > end ||
offset > targetLength
)
return 0;
// If `end` is not defined, or if it is defined
// but would overflow `target`, redefine `end`
// so we can copy as much as we can
if (end - start > targetLength - offset ||
end == null) {
let remainingTarget = targetLength - offset;
let remainingSource = length - start;
if (remainingSource <= remainingTarget)
end = length;
else
end = start + remainingTarget;
}
Uint8ArraySet.call(target, this.subarray(start, end), offset);
return end - start;
}
},
slice: {
value: function(start, end) {
let length = this.length;
start = ~~start;
end = end != null ? end : length;
if (start < 0) {
start += length;
if (start < 0) start = 0;
} else if (start > length)
start = length;
if (end < 0) {
end += length;
if (end < 0) end = 0;
} else if (end > length)
end = length;
if (end < start)
end = start;
// This instantiation uses the new Uint8Array(buffer, offset, length) version
// of construction to share the same underling data structure
let buffer = new Buffer(this.buffer, start, end - start);
// If buffer has a value, assign its parent value to the
// buffer it shares its underlying structure with. If a slice of
// a slice, then use the root structure
if (buffer.length > 0)
parents.set(buffer, this.parent || this);
return buffer;
}
},
write: {
value: function(string, offset, length, encoding = 'utf8') {
// write(string, encoding);
if (typeof(offset) === 'string' && Number.isNaN(parseInt(offset))) {
[offset, length, encoding] = [0, null, offset];
}
// write(string, offset, encoding);
else if (typeof(length) === 'string')
[length, encoding] = [null, length];
if (offset < 0 || offset > this.length)
throw new RangeError('offset is outside of valid range');
offset = ~~offset;
// Clamp length if it would overflow buffer, or if its
// undefined
if (length == null || length + offset > this.length)
length = this.length - offset;
let buffer = new TextEncoder(encoding).encode(string);
let result = Math.min(buffer.length, length);
if (buffer.length !== length)
buffer = buffer.subarray(0, length);
Uint8ArraySet.call(this, buffer, offset);
return result;
}
},
fill: {
value: function fill(value, start, end) {
let length = this.length;
value = value || 0;
start = start || 0;
end = end || length;
if (typeof(value) === 'string')
value = value.charCodeAt(0);
if (typeof(value) !== 'number' || isNaN(value))
throw TypeError('value is not a number');
if (end < start)
throw new RangeError('end < start');
// Fill 0 bytes; we're done
if (end === start)
return 0;
if (length == 0)
return 0;
if (start < 0 || start >= length)
throw RangeError('start out of bounds');
if (end < 0 || end > length)
throw RangeError('end out of bounds');
let index = start;
while (index < end) this[index++] = value;
}
}
});
// Define nodejs Buffer's getter and setter functions that just proxy
// to internal DataView's equivalent methods.
// TODO do we need to check architecture to see if it's default big/little endian?
[['readUInt16LE', 'getUint16', true],
['readUInt16BE', 'getUint16', false],
['readInt16LE', 'getInt16', true],
['readInt16BE', 'getInt16', false],
['readUInt32LE', 'getUint32', true],
['readUInt32BE', 'getUint32', false],
['readInt32LE', 'getInt32', true],
['readInt32BE', 'getInt32', false],
['readFloatLE', 'getFloat32', true],
['readFloatBE', 'getFloat32', false],
['readDoubleLE', 'getFloat64', true],
['readDoubleBE', 'getFloat64', false],
['readUInt8', 'getUint8'],
['readInt8', 'getInt8']].forEach(([alias, name, littleEndian]) => {
Object.defineProperty(Buffer.prototype, alias, {
value: function(offset) {
return this.view[name](offset, littleEndian);
}
});
});
[['writeUInt16LE', 'setUint16', true],
['writeUInt16BE', 'setUint16', false],
['writeInt16LE', 'setInt16', true],
['writeInt16BE', 'setInt16', false],
['writeUInt32LE', 'setUint32', true],
['writeUInt32BE', 'setUint32', false],
['writeInt32LE', 'setInt32', true],
['writeInt32BE', 'setInt32', false],
['writeFloatLE', 'setFloat32', true],
['writeFloatBE', 'setFloat32', false],
['writeDoubleLE', 'setFloat64', true],
['writeDoubleBE', 'setFloat64', false],
['writeUInt8', 'setUint8'],
['writeInt8', 'setInt8']].forEach(([alias, name, littleEndian]) => {
Object.defineProperty(Buffer.prototype, alias, {
value: function(value, offset) {
return this.view[name](offset, value, littleEndian);
}
});
});

View file

@ -0,0 +1,104 @@
/* 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.ByteReader = ByteReader;
exports.ByteWriter = ByteWriter;
const {Cc, Ci} = require("chrome");
// This just controls the maximum number of bytes we read in at one time.
const BUFFER_BYTE_LEN = 0x8000;
function ByteReader(inputStream) {
const self = this;
let stream = Cc["@mozilla.org/binaryinputstream;1"].
createInstance(Ci.nsIBinaryInputStream);
stream.setInputStream(inputStream);
let manager = new StreamManager(this, stream);
this.read = function ByteReader_read(numBytes) {
manager.ensureOpened();
if (typeof(numBytes) !== "number")
numBytes = Infinity;
let data = "";
let read = 0;
try {
while (true) {
let avail = stream.available();
let toRead = Math.min(numBytes - read, avail, BUFFER_BYTE_LEN);
if (toRead <= 0)
break;
data += stream.readBytes(toRead);
read += toRead;
}
}
catch (err) {
throw new Error("Error reading from stream: " + err);
}
return data;
};
}
function ByteWriter(outputStream) {
const self = this;
let stream = Cc["@mozilla.org/binaryoutputstream;1"].
createInstance(Ci.nsIBinaryOutputStream);
stream.setOutputStream(outputStream);
let manager = new StreamManager(this, stream);
this.write = function ByteWriter_write(str) {
manager.ensureOpened();
try {
stream.writeBytes(str, str.length);
}
catch (err) {
throw new Error("Error writing to stream: " + err);
}
};
}
// This manages the lifetime of stream, a ByteReader or ByteWriter. It defines
// closed and close() on stream and registers an unload listener that closes
// rawStream if it's still opened. It also provides ensureOpened(), which
// throws an exception if the stream is closed.
function StreamManager(stream, rawStream) {
const self = this;
this.rawStream = rawStream;
this.opened = true;
stream.__defineGetter__("closed", function stream_closed() {
return !self.opened;
});
stream.close = function stream_close() {
self.ensureOpened();
self.unload();
};
require("../system/unload").ensure(this);
}
StreamManager.prototype = {
ensureOpened: function StreamManager_ensureOpened() {
if (!this.opened)
throw new Error("The stream is closed and cannot be used.");
},
unload: function StreamManager_unload() {
this.rawStream.close();
this.opened = false;
}
};

View file

@ -0,0 +1,196 @@
/* 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,Cr} = require("chrome");
const byteStreams = require("./byte-streams");
const textStreams = require("./text-streams");
// Flags passed when opening a file. See nsprpub/pr/include/prio.h.
const OPEN_FLAGS = {
RDONLY: parseInt("0x01"),
WRONLY: parseInt("0x02"),
CREATE_FILE: parseInt("0x08"),
APPEND: parseInt("0x10"),
TRUNCATE: parseInt("0x20"),
EXCL: parseInt("0x80")
};
var dirsvc = Cc["@mozilla.org/file/directory_service;1"]
.getService(Ci.nsIProperties);
function MozFile(path) {
var file = Cc['@mozilla.org/file/local;1']
.createInstance(Ci.nsILocalFile);
file.initWithPath(path);
return file;
}
function ensureReadable(file) {
if (!file.isReadable())
throw new Error("path is not readable: " + file.path);
}
function ensureDir(file) {
ensureExists(file);
if (!file.isDirectory())
throw new Error("path is not a directory: " + file.path);
}
function ensureFile(file) {
ensureExists(file);
if (!file.isFile())
throw new Error("path is not a file: " + file.path);
}
function ensureExists(file) {
if (!file.exists())
throw friendlyError(Cr.NS_ERROR_FILE_NOT_FOUND, file.path);
}
function friendlyError(errOrResult, filename) {
var isResult = typeof(errOrResult) === "number";
var result = isResult ? errOrResult : errOrResult.result;
switch (result) {
case Cr.NS_ERROR_FILE_NOT_FOUND:
return new Error("path does not exist: " + filename);
}
return isResult ? new Error("XPCOM error code: " + errOrResult) : errOrResult;
}
exports.exists = function exists(filename) {
return MozFile(filename).exists();
};
exports.isFile = function isFile(filename) {
return MozFile(filename).isFile();
};
exports.read = function read(filename, mode) {
if (typeof(mode) !== "string")
mode = "";
// Ensure mode is read-only.
mode = /b/.test(mode) ? "b" : "";
var stream = exports.open(filename, mode);
try {
var str = stream.read();
}
finally {
stream.close();
}
return str;
};
exports.join = function join(base) {
if (arguments.length < 2)
throw new Error("need at least 2 args");
base = MozFile(base);
for (var i = 1; i < arguments.length; i++)
base.append(arguments[i]);
return base.path;
};
exports.dirname = function dirname(path) {
var parent = MozFile(path).parent;
return parent ? parent.path : "";
};
exports.basename = function basename(path) {
var leafName = MozFile(path).leafName;
// On Windows, leafName when the path is a volume letter and colon ("c:") is
// the path itself. But such a path has no basename, so we want the empty
// string.
return leafName == path ? "" : leafName;
};
exports.list = function list(path) {
var file = MozFile(path);
ensureDir(file);
ensureReadable(file);
var entries = file.directoryEntries;
var entryNames = [];
while(entries.hasMoreElements()) {
var entry = entries.getNext();
entry.QueryInterface(Ci.nsIFile);
entryNames.push(entry.leafName);
}
return entryNames;
};
exports.open = function open(filename, mode) {
var file = MozFile(filename);
if (typeof(mode) !== "string")
mode = "";
// File opened for write only.
if (/w/.test(mode)) {
if (file.exists())
ensureFile(file);
var stream = Cc['@mozilla.org/network/file-output-stream;1'].
createInstance(Ci.nsIFileOutputStream);
var openFlags = OPEN_FLAGS.WRONLY |
OPEN_FLAGS.CREATE_FILE |
OPEN_FLAGS.TRUNCATE;
var permFlags = 0o644; // u+rw go+r
try {
stream.init(file, openFlags, permFlags, 0);
}
catch (err) {
throw friendlyError(err, filename);
}
return /b/.test(mode) ?
new byteStreams.ByteWriter(stream) :
new textStreams.TextWriter(stream);
}
// File opened for read only, the default.
ensureFile(file);
stream = Cc['@mozilla.org/network/file-input-stream;1'].
createInstance(Ci.nsIFileInputStream);
try {
stream.init(file, OPEN_FLAGS.RDONLY, 0, 0);
}
catch (err) {
throw friendlyError(err, filename);
}
return /b/.test(mode) ?
new byteStreams.ByteReader(stream) :
new textStreams.TextReader(stream);
};
exports.remove = function remove(path) {
var file = MozFile(path);
ensureFile(file);
file.remove(false);
};
exports.mkpath = function mkpath(path) {
var file = MozFile(path);
if (!file.exists())
file.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); // u+rwx go+rx
else if (!file.isDirectory())
throw new Error("The path already exists and is not a directory: " + path);
};
exports.rmdir = function rmdir(path) {
var file = MozFile(path);
ensureDir(file);
try {
file.remove(false);
}
catch (err) {
// Bug 566950 explains why we're not catching a specific exception here.
throw new Error("The directory is not empty: " + path);
}
};

View file

@ -0,0 +1,984 @@
/* 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, CC } = require("chrome");
const { setTimeout } = require("../timers");
const { Stream, InputStream, OutputStream } = require("./stream");
const { emit, on } = require("../event/core");
const { Buffer } = require("./buffer");
const { ns } = require("../core/namespace");
const { Class } = require("../core/heritage");
const nsILocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile",
"initWithPath");
const FileOutputStream = CC("@mozilla.org/network/file-output-stream;1",
"nsIFileOutputStream", "init");
const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
"nsIFileInputStream", "init");
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
"nsIBinaryOutputStream", "setOutputStream");
const StreamPump = CC("@mozilla.org/network/input-stream-pump;1",
"nsIInputStreamPump", "init");
const { createOutputTransport, createInputTransport } =
Cc["@mozilla.org/network/stream-transport-service;1"].
getService(Ci.nsIStreamTransportService);
const { OPEN_UNBUFFERED } = Ci.nsITransport;
const { REOPEN_ON_REWIND, DEFER_OPEN } = Ci.nsIFileInputStream;
const { DIRECTORY_TYPE, NORMAL_FILE_TYPE } = Ci.nsIFile;
const { NS_SEEK_SET, NS_SEEK_CUR, NS_SEEK_END } = Ci.nsISeekableStream;
const FILE_PERMISSION = 0o666;
const PR_UINT32_MAX = 0xfffffff;
// Values taken from:
// http://mxr.mozilla.org/mozilla-central/source/nsprpub/pr/include/prio.h#615
const PR_RDONLY = 0x01;
const PR_WRONLY = 0x02;
const PR_RDWR = 0x04;
const PR_CREATE_FILE = 0x08;
const PR_APPEND = 0x10;
const PR_TRUNCATE = 0x20;
const PR_SYNC = 0x40;
const PR_EXCL = 0x80;
const FLAGS = {
"r": PR_RDONLY,
"r+": PR_RDWR,
"w": PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY,
"w+": PR_CREATE_FILE | PR_TRUNCATE | PR_RDWR,
"a": PR_APPEND | PR_CREATE_FILE | PR_WRONLY,
"a+": PR_APPEND | PR_CREATE_FILE | PR_RDWR
};
function accessor() {
let map = new WeakMap();
return function(fd, value) {
if (value === null) map.delete(fd);
if (value !== undefined) map.set(fd, value);
return map.get(fd);
}
}
var nsIFile = accessor();
var nsIFileInputStream = accessor();
var nsIFileOutputStream = accessor();
var nsIBinaryInputStream = accessor();
var nsIBinaryOutputStream = accessor();
// Just a contstant object used to signal that all of the file
// needs to be read.
const ALL = new String("Read all of the file");
function isWritable(mode) {
return !!(mode & PR_WRONLY || mode & PR_RDWR);
}
function isReadable(mode) {
return !!(mode & PR_RDONLY || mode & PR_RDWR);
}
function isString(value) {
return typeof(value) === "string";
}
function isFunction(value) {
return typeof(value) === "function";
}
function toArray(enumerator) {
let value = [];
while(enumerator.hasMoreElements())
value.push(enumerator.getNext())
return value
}
function getFileName(file) {
return file.QueryInterface(Ci.nsIFile).leafName;
}
function remove(path, recursive) {
let fd = new nsILocalFile(path)
if (fd.exists()) {
fd.remove(recursive || false);
}
else {
throw FSError("remove", "ENOENT", 34, path);
}
}
/**
* Utility function to convert either an octal number or string
* into an octal number
* 0777 => 0o777
* "0644" => 0o644
*/
function Mode(mode, fallback) {
return isString(mode) ? parseInt(mode, 8) : mode || fallback;
}
function Flags(flag) {
return !isString(flag) ? flag :
FLAGS[flag] || Error("Unknown file open flag: " + flag);
}
function FSError(op, code, errno, path, file, line) {
let error = Error(code + ", " + op + " " + path, file, line);
error.code = code;
error.path = path;
error.errno = errno;
return error;
}
const ReadStream = Class({
extends: InputStream,
initialize: function initialize(path, options) {
this.position = -1;
this.length = -1;
this.flags = "r";
this.mode = FILE_PERMISSION;
this.bufferSize = 64 * 1024;
options = options || {};
if ("flags" in options && options.flags)
this.flags = options.flags;
if ("bufferSize" in options && options.bufferSize)
this.bufferSize = options.bufferSize;
if ("length" in options && options.length)
this.length = options.length;
if ("position" in options && options.position !== undefined)
this.position = options.position;
let { flags, mode, position, length } = this;
let fd = isString(path) ? openSync(path, flags, mode) : path;
this.fd = fd;
let input = nsIFileInputStream(fd);
// Setting a stream position, unless it"s `-1` which means current position.
if (position >= 0)
input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position);
// We use `nsIStreamTransportService` service to transform blocking
// file input stream into a fully asynchronous stream that can be written
// without blocking the main thread.
let transport = createInputTransport(input, position, length, false);
// Open an input stream on a transport. We don"t pass flags to guarantee
// non-blocking stream semantics. Also we use defaults for segment size &
// count.
InputStream.prototype.initialize.call(this, {
asyncInputStream: transport.openInputStream(null, 0, 0)
});
// Close file descriptor on end and destroy the stream.
on(this, "end", _ => {
this.destroy();
emit(this, "close");
});
this.read();
},
destroy: function() {
closeSync(this.fd);
InputStream.prototype.destroy.call(this);
}
});
exports.ReadStream = ReadStream;
exports.createReadStream = function createReadStream(path, options) {
return new ReadStream(path, options);
};
const WriteStream = Class({
extends: OutputStream,
initialize: function initialize(path, options) {
this.drainable = true;
this.flags = "w";
this.position = -1;
this.mode = FILE_PERMISSION;
options = options || {};
if ("flags" in options && options.flags)
this.flags = options.flags;
if ("mode" in options && options.mode)
this.mode = options.mode;
if ("position" in options && options.position !== undefined)
this.position = options.position;
let { position, flags, mode } = this;
// If pass was passed we create a file descriptor out of it. Otherwise
// we just use given file descriptor.
let fd = isString(path) ? openSync(path, flags, mode) : path;
this.fd = fd;
let output = nsIFileOutputStream(fd);
// Setting a stream position, unless it"s `-1` which means current position.
if (position >= 0)
output.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position);
// We use `nsIStreamTransportService` service to transform blocking
// file output stream into a fully asynchronous stream that can be written
// without blocking the main thread.
let transport = createOutputTransport(output, position, -1, false);
// Open an output stream on a transport. We don"t pass flags to guarantee
// non-blocking stream semantics. Also we use defaults for segment size &
// count.
OutputStream.prototype.initialize.call(this, {
asyncOutputStream: transport.openOutputStream(OPEN_UNBUFFERED, 0, 0),
output: output
});
// For write streams "finish" basically means close.
on(this, "finish", _ => {
this.destroy();
emit(this, "close");
});
},
destroy: function() {
OutputStream.prototype.destroy.call(this);
closeSync(this.fd);
}
});
exports.WriteStream = WriteStream;
exports.createWriteStream = function createWriteStream(path, options) {
return new WriteStream(path, options);
};
const Stats = Class({
initialize: function initialize(path) {
let file = new nsILocalFile(path);
if (!file.exists()) throw FSError("stat", "ENOENT", 34, path);
nsIFile(this, file);
},
isDirectory: function() {
return nsIFile(this).isDirectory();
},
isFile: function() {
return nsIFile(this).isFile();
},
isSymbolicLink: function() {
return nsIFile(this).isSymlink();
},
get mode() {
return nsIFile(this).permissions;
},
get size() {
return nsIFile(this).fileSize;
},
get mtime() {
return nsIFile(this).lastModifiedTime;
},
isBlockDevice: function() {
return nsIFile(this).isSpecial();
},
isCharacterDevice: function() {
return nsIFile(this).isSpecial();
},
isFIFO: function() {
return nsIFile(this).isSpecial();
},
isSocket: function() {
return nsIFile(this).isSpecial();
},
// non standard
get exists() {
return nsIFile(this).exists();
},
get hidden() {
return nsIFile(this).isHidden();
},
get writable() {
return nsIFile(this).isWritable();
},
get readable() {
return nsIFile(this).isReadable();
}
});
exports.Stats = Stats;
const LStats = Class({
extends: Stats,
get size() {
return this.isSymbolicLink() ? nsIFile(this).fileSizeOfLink :
nsIFile(this).fileSize;
},
get mtime() {
return this.isSymbolicLink() ? nsIFile(this).lastModifiedTimeOfLink :
nsIFile(this).lastModifiedTime;
},
// non standard
get permissions() {
return this.isSymbolicLink() ? nsIFile(this).permissionsOfLink :
nsIFile(this).permissions;
}
});
const FStat = Class({
extends: Stats,
initialize: function initialize(fd) {
nsIFile(this, nsIFile(fd));
}
});
function noop() {}
function Async(wrapped) {
return function (path, callback) {
let args = Array.slice(arguments);
callback = args.pop();
// If node is not given a callback argument
// it just does not calls it.
if (typeof(callback) !== "function") {
args.push(callback);
callback = noop;
}
setTimeout(function() {
try {
var result = wrapped.apply(this, args);
if (result === undefined) callback(null);
else callback(null, result);
} catch (error) {
callback(error);
}
}, 0);
}
}
/**
* Synchronous rename(2)
*/
function renameSync(oldPath, newPath) {
let source = new nsILocalFile(oldPath);
let target = new nsILocalFile(newPath);
if (!source.exists()) throw FSError("rename", "ENOENT", 34, oldPath);
return source.moveTo(target.parent, target.leafName);
};
exports.renameSync = renameSync;
/**
* Asynchronous rename(2). No arguments other than a possible exception are
* given to the completion callback.
*/
var rename = Async(renameSync);
exports.rename = rename;
/**
* Test whether or not the given path exists by checking with the file system.
*/
function existsSync(path) {
return new nsILocalFile(path).exists();
}
exports.existsSync = existsSync;
var exists = Async(existsSync);
exports.exists = exists;
/**
* Synchronous ftruncate(2).
*/
function truncateSync(path, length) {
let fd = openSync(path, "w");
ftruncateSync(fd, length);
closeSync(fd);
}
exports.truncateSync = truncateSync;
/**
* Asynchronous ftruncate(2). No arguments other than a possible exception are
* given to the completion callback.
*/
function truncate(path, length, callback) {
open(path, "w", function(error, fd) {
if (error) return callback(error);
ftruncate(fd, length, function(error) {
if (error) {
closeSync(fd);
callback(error);
}
else {
close(fd, callback);
}
});
});
}
exports.truncate = truncate;
function ftruncate(fd, length, callback) {
write(fd, new Buffer(length), 0, length, 0, function(error) {
callback(error);
});
}
exports.ftruncate = ftruncate;
function ftruncateSync(fd, length = 0) {
writeSync(fd, new Buffer(length), 0, length, 0);
}
exports.ftruncateSync = ftruncateSync;
function chownSync(path, uid, gid) {
throw Error("Not implemented yet!!");
}
exports.chownSync = chownSync;
var chown = Async(chownSync);
exports.chown = chown;
function lchownSync(path, uid, gid) {
throw Error("Not implemented yet!!");
}
exports.lchownSync = chownSync;
var lchown = Async(lchown);
exports.lchown = lchown;
/**
* Synchronous chmod(2).
*/
function chmodSync (path, mode) {
let file;
try {
file = new nsILocalFile(path);
} catch(e) {
throw FSError("chmod", "ENOENT", 34, path);
}
file.permissions = Mode(mode);
}
exports.chmodSync = chmodSync;
/**
* Asynchronous chmod(2). No arguments other than a possible exception are
* given to the completion callback.
*/
var chmod = Async(chmodSync);
exports.chmod = chmod;
/**
* Synchronous chmod(2).
*/
function fchmodSync(fd, mode) {
throw Error("Not implemented yet!!");
};
exports.fchmodSync = fchmodSync;
/**
* Asynchronous chmod(2). No arguments other than a possible exception are
* given to the completion callback.
*/
var fchmod = Async(fchmodSync);
exports.fchmod = fchmod;
/**
* Synchronous stat(2). Returns an instance of `fs.Stats`
*/
function statSync(path) {
return new Stats(path);
};
exports.statSync = statSync;
/**
* Asynchronous stat(2). The callback gets two arguments (err, stats) where
* stats is a `fs.Stats` object. It looks like this:
*/
var stat = Async(statSync);
exports.stat = stat;
/**
* Synchronous lstat(2). Returns an instance of `fs.Stats`.
*/
function lstatSync(path) {
return new LStats(path);
};
exports.lstatSync = lstatSync;
/**
* Asynchronous lstat(2). The callback gets two arguments (err, stats) where
* stats is a fs.Stats object. lstat() is identical to stat(), except that if
* path is a symbolic link, then the link itself is stat-ed, not the file that
* it refers to.
*/
var lstat = Async(lstatSync);
exports.lstat = lstat;
/**
* Synchronous fstat(2). Returns an instance of `fs.Stats`.
*/
function fstatSync(fd) {
return new FStat(fd);
};
exports.fstatSync = fstatSync;
/**
* Asynchronous fstat(2). The callback gets two arguments (err, stats) where
* stats is a fs.Stats object.
*/
var fstat = Async(fstatSync);
exports.fstat = fstat;
/**
* Synchronous link(2).
*/
function linkSync(source, target) {
throw Error("Not implemented yet!!");
};
exports.linkSync = linkSync;
/**
* Asynchronous link(2). No arguments other than a possible exception are given
* to the completion callback.
*/
var link = Async(linkSync);
exports.link = link;
/**
* Synchronous symlink(2).
*/
function symlinkSync(source, target) {
throw Error("Not implemented yet!!");
};
exports.symlinkSync = symlinkSync;
/**
* Asynchronous symlink(2). No arguments other than a possible exception are
* given to the completion callback.
*/
var symlink = Async(symlinkSync);
exports.symlink = symlink;
/**
* Synchronous readlink(2). Returns the resolved path.
*/
function readlinkSync(path) {
return new nsILocalFile(path).target;
};
exports.readlinkSync = readlinkSync;
/**
* Asynchronous readlink(2). The callback gets two arguments
* `(error, resolvedPath)`.
*/
var readlink = Async(readlinkSync);
exports.readlink = readlink;
/**
* Synchronous realpath(2). Returns the resolved path.
*/
function realpathSync(path) {
return new nsILocalFile(path).path;
};
exports.realpathSync = realpathSync;
/**
* Asynchronous realpath(2). The callback gets two arguments
* `(err, resolvedPath)`.
*/
var realpath = Async(realpathSync);
exports.realpath = realpath;
/**
* Synchronous unlink(2).
*/
var unlinkSync = remove;
exports.unlinkSync = unlinkSync;
/**
* Asynchronous unlink(2). No arguments other than a possible exception are
* given to the completion callback.
*/
var unlink = Async(remove);
exports.unlink = unlink;
/**
* Synchronous rmdir(2).
*/
var rmdirSync = remove;
exports.rmdirSync = rmdirSync;
/**
* Asynchronous rmdir(2). No arguments other than a possible exception are
* given to the completion callback.
*/
var rmdir = Async(rmdirSync);
exports.rmdir = rmdir;
/**
* Synchronous mkdir(2).
*/
function mkdirSync(path, mode) {
try {
return nsILocalFile(path).create(DIRECTORY_TYPE, Mode(mode));
} catch (error) {
// Adjust exception thorw to match ones thrown by node.
if (error.name === "NS_ERROR_FILE_ALREADY_EXISTS") {
let { fileName, lineNumber } = error;
error = FSError("mkdir", "EEXIST", 47, path, fileName, lineNumber);
}
throw error;
}
};
exports.mkdirSync = mkdirSync;
/**
* Asynchronous mkdir(2). No arguments other than a possible exception are
* given to the completion callback.
*/
var mkdir = Async(mkdirSync);
exports.mkdir = mkdir;
/**
* Synchronous readdir(3). Returns an array of filenames excluding `"."` and
* `".."`.
*/
function readdirSync(path) {
try {
return toArray(new nsILocalFile(path).directoryEntries).map(getFileName);
}
catch (error) {
// Adjust exception thorw to match ones thrown by node.
if (error.name === "NS_ERROR_FILE_TARGET_DOES_NOT_EXIST" ||
error.name === "NS_ERROR_FILE_NOT_FOUND")
{
let { fileName, lineNumber } = error;
error = FSError("readdir", "ENOENT", 34, path, fileName, lineNumber);
}
throw error;
}
};
exports.readdirSync = readdirSync;
/**
* Asynchronous readdir(3). Reads the contents of a directory. The callback
* gets two arguments `(error, files)` where `files` is an array of the names
* of the files in the directory excluding `"."` and `".."`.
*/
var readdir = Async(readdirSync);
exports.readdir = readdir;
/**
* Synchronous close(2).
*/
function closeSync(fd) {
let input = nsIFileInputStream(fd);
let output = nsIFileOutputStream(fd);
// Closing input stream and removing reference.
if (input) input.close();
// Closing output stream and removing reference.
if (output) output.close();
nsIFile(fd, null);
nsIFileInputStream(fd, null);
nsIFileOutputStream(fd, null);
nsIBinaryInputStream(fd, null);
nsIBinaryOutputStream(fd, null);
};
exports.closeSync = closeSync;
/**
* Asynchronous close(2). No arguments other than a possible exception are
* given to the completion callback.
*/
var close = Async(closeSync);
exports.close = close;
/**
* Synchronous open(2).
*/
function openSync(aPath, aFlag, aMode) {
let [ fd, flags, mode, file ] =
[ { path: aPath }, Flags(aFlag), Mode(aMode), nsILocalFile(aPath) ];
nsIFile(fd, file);
// If trying to open file for just read that does not exists
// need to throw exception as node does.
if (!file.exists() && !isWritable(flags))
throw FSError("open", "ENOENT", 34, aPath);
// If we want to open file in read mode we initialize input stream.
if (isReadable(flags)) {
let input = FileInputStream(file, flags, mode, DEFER_OPEN);
nsIFileInputStream(fd, input);
}
// If we want to open file in write mode we initialize output stream for it.
if (isWritable(flags)) {
let output = FileOutputStream(file, flags, mode, DEFER_OPEN);
nsIFileOutputStream(fd, output);
}
return fd;
}
exports.openSync = openSync;
/**
* Asynchronous file open. See open(2). Flags can be
* `"r", "r+", "w", "w+", "a"`, or `"a+"`. mode defaults to `0666`.
* The callback gets two arguments `(error, fd).
*/
var open = Async(openSync);
exports.open = open;
/**
* Synchronous version of buffer-based fs.write(). Returns the number of bytes
* written.
*/
function writeSync(fd, buffer, offset, length, position) {
if (length + offset > buffer.length) {
throw Error("Length is extends beyond buffer");
}
else if (length + offset !== buffer.length) {
buffer = buffer.slice(offset, offset + length);
}
let output = BinaryOutputStream(nsIFileOutputStream(fd));
nsIBinaryOutputStream(fd, output);
// We write content as a byte array as this will avoid any transcoding
// if content was a buffer.
output.writeByteArray(buffer.valueOf(), buffer.length);
output.flush();
};
exports.writeSync = writeSync;
/**
* Write buffer to the file specified by fd.
*
* `offset` and `length` determine the part of the buffer to be written.
*
* `position` refers to the offset from the beginning of the file where this
* data should be written. If `position` is `null`, the data will be written
* at the current position. See pwrite(2).
*
* The callback will be given three arguments `(error, written, buffer)` where
* written specifies how many bytes were written into buffer.
*
* Note that it is unsafe to use `fs.write` multiple times on the same file
* without waiting for the callback.
*/
function write(fd, buffer, offset, length, position, callback) {
if (!Buffer.isBuffer(buffer)) {
// (fd, data, position, encoding, callback)
let encoding = null;
[ position, encoding, callback ] = Array.slice(arguments, 1);
buffer = new Buffer(String(buffer), encoding);
offset = 0;
} else if (length + offset > buffer.length) {
throw Error("Length is extends beyond buffer");
} else if (length + offset !== buffer.length) {
buffer = buffer.slice(offset, offset + length);
}
let writeStream = new WriteStream(fd, { position: position,
length: length });
writeStream.on("error", callback);
writeStream.write(buffer, function onEnd() {
writeStream.destroy();
if (callback)
callback(null, buffer.length, buffer);
});
};
exports.write = write;
/**
* Synchronous version of string-based fs.read. Returns the number of
* bytes read.
*/
function readSync(fd, buffer, offset, length, position) {
let input = nsIFileInputStream(fd);
// Setting a stream position, unless it"s `-1` which means current position.
if (position >= 0)
input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position);
// We use `nsIStreamTransportService` service to transform blocking
// file input stream into a fully asynchronous stream that can be written
// without blocking the main thread.
let binaryInputStream = BinaryInputStream(input);
let count = length === ALL ? binaryInputStream.available() : length;
if (offset === 0) binaryInputStream.readArrayBuffer(count, buffer.buffer);
else {
let chunk = new Buffer(count);
binaryInputStream.readArrayBuffer(count, chunk.buffer);
chunk.copy(buffer, offset);
}
return buffer.slice(offset, offset + count);
};
exports.readSync = readSync;
/**
* Read data from the file specified by `fd`.
*
* `buffer` is the buffer that the data will be written to.
* `offset` is offset within the buffer where writing will start.
*
* `length` is an integer specifying the number of bytes to read.
*
* `position` is an integer specifying where to begin reading from in the file.
* If `position` is `null`, data will be read from the current file position.
*
* The callback is given the three arguments, `(error, bytesRead, buffer)`.
*/
function read(fd, buffer, offset, length, position, callback) {
let bytesRead = 0;
let readStream = new ReadStream(fd, { position: position, length: length });
readStream.on("data", function onData(data) {
data.copy(buffer, offset + bytesRead);
bytesRead += data.length;
});
readStream.on("end", function onEnd() {
callback(null, bytesRead, buffer);
readStream.destroy();
});
};
exports.read = read;
/**
* Asynchronously reads the entire contents of a file.
* The callback is passed two arguments `(error, data)`, where data is the
* contents of the file.
*/
function readFile(path, encoding, callback) {
if (isFunction(encoding)) {
callback = encoding
encoding = null
}
let buffer = null;
try {
let readStream = new ReadStream(path);
readStream.on("data", function(data) {
if (!buffer) buffer = data;
else buffer = Buffer.concat([buffer, data], 2);
});
readStream.on("error", function onError(error) {
callback(error);
});
readStream.on("end", function onEnd() {
// Note: Need to destroy before invoking a callback
// so that file descriptor is released.
readStream.destroy();
callback(null, buffer);
});
}
catch (error) {
setTimeout(callback, 0, error);
}
};
exports.readFile = readFile;
/**
* Synchronous version of `fs.readFile`. Returns the contents of the path.
* If encoding is specified then this function returns a string.
* Otherwise it returns a buffer.
*/
function readFileSync(path, encoding) {
let fd = openSync(path, "r");
let size = fstatSync(fd).size;
let buffer = new Buffer(size);
try {
readSync(fd, buffer, 0, ALL, 0);
}
finally {
closeSync(fd);
}
return buffer;
};
exports.readFileSync = readFileSync;
/**
* Asynchronously writes data to a file, replacing the file if it already
* exists. data can be a string or a buffer.
*/
function writeFile(path, content, encoding, callback) {
if (!isString(path))
throw new TypeError('path must be a string');
try {
if (isFunction(encoding)) {
callback = encoding
encoding = null
}
if (isString(content))
content = new Buffer(content, encoding);
let writeStream = new WriteStream(path);
let error = null;
writeStream.end(content, function() {
writeStream.destroy();
callback(error);
});
writeStream.on("error", function onError(reason) {
error = reason;
writeStream.destroy();
});
} catch (error) {
callback(error);
}
};
exports.writeFile = writeFile;
/**
* The synchronous version of `fs.writeFile`.
*/
function writeFileSync(filename, data, encoding) {
// TODO: Implement this in bug 1148209 https://bugzilla.mozilla.org/show_bug.cgi?id=1148209
throw Error("Not implemented");
};
exports.writeFileSync = writeFileSync;
function utimesSync(path, atime, mtime) {
throw Error("Not implemented");
}
exports.utimesSync = utimesSync;
var utimes = Async(utimesSync);
exports.utimes = utimes;
function futimesSync(fd, atime, mtime, callback) {
throw Error("Not implemented");
}
exports.futimesSync = futimesSync;
var futimes = Async(futimesSync);
exports.futimes = futimes;
function fsyncSync(fd, atime, mtime, callback) {
throw Error("Not implemented");
}
exports.fsyncSync = fsyncSync;
var fsync = Async(fsyncSync);
exports.fsync = fsync;
/**
* Watch for changes on filename. The callback listener will be called each
* time the file is accessed.
*
* The second argument is optional. The options if provided should be an object
* containing two members a boolean, persistent, and interval, a polling value
* in milliseconds. The default is { persistent: true, interval: 0 }.
*/
function watchFile(path, options, listener) {
throw Error("Not implemented");
};
exports.watchFile = watchFile;
function unwatchFile(path, listener) {
throw Error("Not implemented");
}
exports.unwatchFile = unwatchFile;
function watch(path, options, listener) {
throw Error("Not implemented");
}
exports.watch = watch;

View file

@ -0,0 +1,440 @@
/* 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, Cc, Ci, Cu, Cr, components } = require("chrome");
const { EventTarget } = require("../event/target");
const { emit } = require("../event/core");
const { Buffer } = require("./buffer");
const { Class } = require("../core/heritage");
const { setTimeout } = require("../timers");
const MultiplexInputStream = CC("@mozilla.org/io/multiplex-input-stream;1",
"nsIMultiplexInputStream");
const AsyncStreamCopier = CC("@mozilla.org/network/async-stream-copier;1",
"nsIAsyncStreamCopier", "init");
const StringInputStream = CC("@mozilla.org/io/string-input-stream;1",
"nsIStringInputStream");
const ArrayBufferInputStream = CC("@mozilla.org/io/arraybuffer-input-stream;1",
"nsIArrayBufferInputStream");
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
const InputStreamPump = CC("@mozilla.org/network/input-stream-pump;1",
"nsIInputStreamPump", "init");
const threadManager = Cc["@mozilla.org/thread-manager;1"].
getService(Ci.nsIThreadManager);
const eventTarget = Cc["@mozilla.org/network/stream-transport-service;1"].
getService(Ci.nsIEventTarget);
var isFunction = value => typeof(value) === "function"
function accessor() {
let map = new WeakMap();
return function(target, value) {
if (value)
map.set(target, value);
return map.get(target);
}
}
const Stream = Class({
extends: EventTarget,
initialize: function() {
this.readable = false;
this.writable = false;
this.encoding = null;
},
setEncoding: function setEncoding(encoding) {
this.encoding = String(encoding).toUpperCase();
},
pipe: function pipe(target, options) {
let source = this;
function onData(chunk) {
if (target.writable) {
if (false === target.write(chunk))
source.pause();
}
}
function onDrain() {
if (source.readable)
source.resume();
}
function onEnd() {
target.end();
}
function onPause() {
source.pause();
}
function onResume() {
if (source.readable)
source.resume();
}
function cleanup() {
source.removeListener("data", onData);
target.removeListener("drain", onDrain);
source.removeListener("end", onEnd);
target.removeListener("pause", onPause);
target.removeListener("resume", onResume);
source.removeListener("end", cleanup);
source.removeListener("close", cleanup);
target.removeListener("end", cleanup);
target.removeListener("close", cleanup);
}
if (!options || options.end !== false)
target.on("end", onEnd);
source.on("data", onData);
target.on("drain", onDrain);
target.on("resume", onResume);
target.on("pause", onPause);
source.on("end", cleanup);
source.on("close", cleanup);
target.on("end", cleanup);
target.on("close", cleanup);
emit(target, "pipe", source);
},
pause: function pause() {
emit(this, "pause");
},
resume: function resume() {
emit(this, "resume");
},
destroySoon: function destroySoon() {
this.destroy();
}
});
exports.Stream = Stream;
var nsIStreamListener = accessor();
var nsIInputStreamPump = accessor();
var nsIAsyncInputStream = accessor();
var nsIBinaryInputStream = accessor();
const StreamListener = Class({
initialize: function(stream) {
this.stream = stream;
},
// Next three methods are part of `nsIStreamListener` interface and are
// invoked by `nsIInputStreamPump.asyncRead`.
onDataAvailable: function(request, context, input, offset, count) {
let stream = this.stream;
let buffer = new ArrayBuffer(count);
nsIBinaryInputStream(stream).readArrayBuffer(count, buffer);
emit(stream, "data", new Buffer(buffer));
},
// Next two methods implement `nsIRequestObserver` interface and are invoked
// by `nsIInputStreamPump.asyncRead`.
onStartRequest: function() {},
// Called to signify the end of an asynchronous request. We only care to
// discover errors.
onStopRequest: function(request, context, status) {
let stream = this.stream;
stream.readable = false;
if (!components.isSuccessCode(status))
emit(stream, "error", status);
else
emit(stream, "end");
}
});
const InputStream = Class({
extends: Stream,
readable: false,
paused: false,
initialize: function initialize(options) {
let { asyncInputStream } = options;
this.readable = true;
let binaryInputStream = new BinaryInputStream(asyncInputStream);
let inputStreamPump = new InputStreamPump(asyncInputStream,
-1, -1, 0, 0, false);
let streamListener = new StreamListener(this);
nsIAsyncInputStream(this, asyncInputStream);
nsIInputStreamPump(this, inputStreamPump);
nsIBinaryInputStream(this, binaryInputStream);
nsIStreamListener(this, streamListener);
this.asyncInputStream = asyncInputStream;
this.inputStreamPump = inputStreamPump;
this.binaryInputStream = binaryInputStream;
},
get status() {
return nsIInputStreamPump(this).status;
},
read: function() {
nsIInputStreamPump(this).asyncRead(nsIStreamListener(this), null);
},
pause: function pause() {
this.paused = true;
nsIInputStreamPump(this).suspend();
emit(this, "paused");
},
resume: function resume() {
this.paused = false;
if (nsIInputStreamPump(this).isPending()) {
nsIInputStreamPump(this).resume();
emit(this, "resume");
}
},
close: function close() {
this.readable = false;
nsIInputStreamPump(this).cancel(Cr.NS_OK);
nsIBinaryInputStream(this).close();
nsIAsyncInputStream(this).close();
},
destroy: function destroy() {
this.close();
nsIInputStreamPump(this);
nsIAsyncInputStream(this);
nsIBinaryInputStream(this);
nsIStreamListener(this);
}
});
exports.InputStream = InputStream;
var nsIRequestObserver = accessor();
var nsIAsyncOutputStream = accessor();
var nsIAsyncStreamCopier = accessor();
var nsIMultiplexInputStream = accessor();
const RequestObserver = Class({
initialize: function(stream) {
this.stream = stream;
},
// Method is part of `nsIRequestObserver` interface that is
// invoked by `nsIAsyncStreamCopier.asyncCopy`.
onStartRequest: function() {},
// Method is part of `nsIRequestObserver` interface that is
// invoked by `nsIAsyncStreamCopier.asyncCopy`.
onStopRequest: function(request, context, status) {
let stream = this.stream;
stream.drained = true;
// Remove copied chunk.
let multiplexInputStream = nsIMultiplexInputStream(stream);
multiplexInputStream.removeStream(0);
// If there was an error report.
if (!components.isSuccessCode(status))
emit(stream, "error", status);
// If there more chunks in queue then flush them.
else if (multiplexInputStream.count)
stream.flush();
// If stream is still writable notify that queue has drained.
else if (stream.writable)
emit(stream, "drain");
// If stream is no longer writable close it.
else {
nsIAsyncStreamCopier(stream).cancel(Cr.NS_OK);
nsIMultiplexInputStream(stream).close();
nsIAsyncOutputStream(stream).close();
nsIAsyncOutputStream(stream).flush();
}
}
});
const OutputStreamCallback = Class({
initialize: function(stream) {
this.stream = stream;
},
// Method is part of `nsIOutputStreamCallback` interface that
// is invoked by `nsIAsyncOutputStream.asyncWait`. It is registered
// with `WAIT_CLOSURE_ONLY` flag that overrides the default behavior,
// causing the `onOutputStreamReady` notification to be suppressed until
// the stream becomes closed.
onOutputStreamReady: function(nsIAsyncOutputStream) {
emit(this.stream, "finish");
}
});
const OutputStream = Class({
extends: Stream,
writable: false,
drained: true,
get bufferSize() {
let multiplexInputStream = nsIMultiplexInputStream(this);
return multiplexInputStream && multiplexInputStream.available();
},
initialize: function initialize(options) {
let { asyncOutputStream, output } = options;
this.writable = true;
// Ensure that `nsIAsyncOutputStream` was provided.
asyncOutputStream.QueryInterface(Ci.nsIAsyncOutputStream);
// Create a `nsIMultiplexInputStream` and `nsIAsyncStreamCopier`. Former
// is used to queue written data chunks that `asyncStreamCopier` will
// asynchronously drain into `asyncOutputStream`.
let multiplexInputStream = MultiplexInputStream();
let asyncStreamCopier = AsyncStreamCopier(multiplexInputStream,
output || asyncOutputStream,
eventTarget,
// nsIMultiplexInputStream
// implemnts .readSegments()
true,
// nsIOutputStream may or
// may not implemnet
// .writeSegments().
false,
// Use default buffer size.
null,
// Should not close an input.
false,
// Should not close an output.
false);
// Create `requestObserver` implementing `nsIRequestObserver` interface
// in the constructor that's gonna be reused across several flushes.
let requestObserver = RequestObserver(this);
// Create observer that implements `nsIOutputStreamCallback` and register
// using `WAIT_CLOSURE_ONLY` flag. That way it will be notfied once
// `nsIAsyncOutputStream` is closed.
asyncOutputStream.asyncWait(OutputStreamCallback(this),
asyncOutputStream.WAIT_CLOSURE_ONLY,
0,
threadManager.currentThread);
nsIRequestObserver(this, requestObserver);
nsIAsyncOutputStream(this, asyncOutputStream);
nsIMultiplexInputStream(this, multiplexInputStream);
nsIAsyncStreamCopier(this, asyncStreamCopier);
this.asyncOutputStream = asyncOutputStream;
this.multiplexInputStream = multiplexInputStream;
this.asyncStreamCopier = asyncStreamCopier;
},
write: function write(content, encoding, callback) {
if (isFunction(encoding)) {
callback = encoding;
encoding = callback;
}
// If stream is not writable we throw an error.
if (!this.writable) throw Error("stream is not writable");
let chunk = null;
// If content is not a buffer then we create one out of it.
if (Buffer.isBuffer(content)) {
chunk = new ArrayBufferInputStream();
chunk.setData(content.buffer, 0, content.length);
}
else {
chunk = new StringInputStream();
chunk.setData(content, content.length);
}
if (callback)
this.once("drain", callback);
// Queue up chunk to be copied to output sync.
nsIMultiplexInputStream(this).appendStream(chunk);
this.flush();
return this.drained;
},
flush: function() {
if (this.drained) {
this.drained = false;
nsIAsyncStreamCopier(this).asyncCopy(nsIRequestObserver(this), null);
}
},
end: function end(content, encoding, callback) {
if (isFunction(content)) {
callback = content
content = callback
}
if (isFunction(encoding)) {
callback = encoding
encoding = callback
}
// Setting a listener to "finish" event if passed.
if (isFunction(callback))
this.once("finish", callback);
if (content)
this.write(content, encoding);
this.writable = false;
// Close `asyncOutputStream` only if output has drained. If it's
// not drained than `asyncStreamCopier` is busy writing, so let
// it finish. Note that since `this.writable` is false copier will
// close `asyncOutputStream` once output drains.
if (this.drained)
nsIAsyncOutputStream(this).close();
},
destroy: function destroy() {
nsIAsyncOutputStream(this).close();
nsIAsyncOutputStream(this);
nsIMultiplexInputStream(this);
nsIAsyncStreamCopier(this);
nsIRequestObserver(this);
}
});
exports.OutputStream = OutputStream;
const DuplexStream = Class({
extends: Stream,
implements: [InputStream, OutputStream],
allowHalfOpen: true,
initialize: function initialize(options) {
options = options || {};
let { readable, writable, allowHalfOpen } = options;
InputStream.prototype.initialize.call(this, options);
OutputStream.prototype.initialize.call(this, options);
if (readable === false)
this.readable = false;
if (writable === false)
this.writable = false;
if (allowHalfOpen === false)
this.allowHalfOpen = false;
// If in a half open state and it's disabled enforce end.
this.once("end", () => {
if (!this.allowHalfOpen && (!this.readable || !this.writable))
this.end();
});
},
destroy: function destroy(error) {
InputStream.prototype.destroy.call(this);
OutputStream.prototype.destroy.call(this);
}
});
exports.DuplexStream = DuplexStream;

View file

@ -0,0 +1,235 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { Cc, Ci, Cu, components } = require("chrome");
const { ensure } = require("../system/unload");
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
// NetUtil.asyncCopy() uses this buffer length, and since we call it, for best
// performance we use it, too.
const BUFFER_BYTE_LEN = 0x8000;
const PR_UINT32_MAX = 0xffffffff;
const DEFAULT_CHARSET = "UTF-8";
/**
* An input stream that reads text from a backing stream using a given text
* encoding.
*
* @param inputStream
* The stream is backed by this nsIInputStream. It must already be
* opened.
* @param charset
* Text in inputStream is expected to be in this character encoding. If
* not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl for
* documentation on how to determine other valid values for this.
*/
function TextReader(inputStream, charset) {
charset = checkCharset(charset);
let stream = Cc["@mozilla.org/intl/converter-input-stream;1"].
createInstance(Ci.nsIConverterInputStream);
stream.init(inputStream, charset, BUFFER_BYTE_LEN,
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
let manager = new StreamManager(this, stream);
/**
* Reads a string from the stream. If the stream is closed, an exception is
* thrown.
*
* @param numChars
* The number of characters to read. If not given, the remainder of
* the stream is read.
* @return The string read. If the stream is already at EOS, returns the
* empty string.
*/
this.read = function TextReader_read(numChars) {
manager.ensureOpened();
let readAll = false;
if (typeof(numChars) === "number")
numChars = Math.max(numChars, 0);
else
readAll = true;
let str = "";
let totalRead = 0;
let chunkRead = 1;
// Read in numChars or until EOS, whichever comes first. Note that the
// units here are characters, not bytes.
while (true) {
let chunk = {};
let toRead = readAll ?
PR_UINT32_MAX :
Math.min(numChars - totalRead, PR_UINT32_MAX);
if (toRead <= 0 || chunkRead <= 0)
break;
// The converter stream reads in at most BUFFER_BYTE_LEN bytes in a call
// to readString, enough to fill its byte buffer. chunkRead will be the
// number of characters encoded by the bytes in that buffer.
chunkRead = stream.readString(toRead, chunk);
str += chunk.value;
totalRead += chunkRead;
}
return str;
};
}
exports.TextReader = TextReader;
/**
* A buffered output stream that writes text to a backing stream using a given
* text encoding.
*
* @param outputStream
* The stream is backed by this nsIOutputStream. It must already be
* opened.
* @param charset
* Text will be written to outputStream using this character encoding.
* If not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl
* for documentation on how to determine other valid values for this.
*/
function TextWriter(outputStream, charset) {
charset = checkCharset(charset);
let stream = outputStream;
// Buffer outputStream if it's not already.
let ioUtils = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil);
if (!ioUtils.outputStreamIsBuffered(outputStream)) {
stream = Cc["@mozilla.org/network/buffered-output-stream;1"].
createInstance(Ci.nsIBufferedOutputStream);
stream.init(outputStream, BUFFER_BYTE_LEN);
}
// I'd like to use nsIConverterOutputStream. But NetUtil.asyncCopy(), which
// we use below in writeAsync(), naturally expects its sink to be an instance
// of nsIOutputStream, which nsIConverterOutputStream's only implementation is
// not. So we use uconv and manually convert all strings before writing to
// outputStream.
let uconv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Ci.nsIScriptableUnicodeConverter);
uconv.charset = charset;
let manager = new StreamManager(this, stream);
/**
* Flushes the backing stream's buffer.
*/
this.flush = function TextWriter_flush() {
manager.ensureOpened();
stream.flush();
};
/**
* Writes a string to the stream. If the stream is closed, an exception is
* thrown.
*
* @param str
* The string to write.
*/
this.write = function TextWriter_write(str) {
manager.ensureOpened();
let istream = uconv.convertToInputStream(str);
let len = istream.available();
while (len > 0) {
stream.writeFrom(istream, len);
len = istream.available();
}
istream.close();
};
/**
* Writes a string on a background thread. After the write completes, the
* backing stream's buffer is flushed, and both the stream and the backing
* stream are closed, also on the background thread. If the stream is already
* closed, an exception is thrown immediately.
*
* @param str
* The string to write.
* @param callback
* An optional function. If given, it's called as callback(error) when
* the write completes. error is an Error object or undefined if there
* was no error. Inside callback, |this| is the stream object.
*/
this.writeAsync = function TextWriter_writeAsync(str, callback) {
manager.ensureOpened();
let istream = uconv.convertToInputStream(str);
NetUtil.asyncCopy(istream, stream, (result) => {
let err = components.isSuccessCode(result) ? undefined :
new Error("An error occured while writing to the stream: " + result);
if (err)
console.error(err);
// asyncCopy() closes its output (and input) stream.
manager.opened = false;
if (typeof(callback) === "function") {
try {
callback.call(this, err);
}
catch (exc) {
console.exception(exc);
}
}
});
};
}
exports.TextWriter = TextWriter;
// This manages the lifetime of stream, a TextReader or TextWriter. It defines
// closed and close() on stream and registers an unload listener that closes
// rawStream if it's still opened. It also provides ensureOpened(), which
// throws an exception if the stream is closed.
function StreamManager(stream, rawStream) {
this.rawStream = rawStream;
this.opened = true;
/**
* True iff the stream is closed.
*/
stream.__defineGetter__("closed", () => !this.opened);
/**
* Closes both the stream and its backing stream. If the stream is already
* closed, an exception is thrown. For TextWriters, this first flushes the
* backing stream's buffer.
*/
stream.close = () => {
this.ensureOpened();
this.unload();
};
ensure(this);
}
StreamManager.prototype = {
ensureOpened: function StreamManager_ensureOpened() {
if (!this.opened)
throw new Error("The stream is closed and cannot be used.");
},
unload: function StreamManager_unload() {
// TextWriter.writeAsync() causes rawStream to close and therefore sets
// opened to false, so check that we're still opened.
if (this.opened) {
// Calling close() on both an nsIUnicharInputStream and
// nsIBufferedOutputStream closes their backing streams. It also forces
// nsIOutputStreams to flush first.
this.rawStream.close();
this.opened = false;
}
}
};
function checkCharset(charset) {
return typeof(charset) === "string" ? charset : DEFAULT_CHARSET;
}

View file

@ -0,0 +1,110 @@
/* 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 { observer: keyboardObserver } = require("./observer");
const { getKeyForCode, normalize, isFunctionKey,
MODIFIERS } = require("./utils");
/**
* Register a global `hotkey` that executes `listener` when the key combination
* in `hotkey` is pressed. If more then one `listener` is registered on the same
* key combination only last one will be executed.
*
* @param {string} hotkey
* Key combination in the format of 'modifier key'.
*
* Examples:
*
* "accel s"
* "meta shift i"
* "control alt d"
*
* Modifier keynames:
*
* - **shift**: The Shift key.
* - **alt**: The Alt key. On the Macintosh, this is the Option key. On
* Macintosh this can only be used in conjunction with another modifier,
* since `Alt+Letter` combinations are reserved for entering special
* characters in text.
* - **meta**: The Meta key. On the Macintosh, this is the Command key.
* - **control**: The Control key.
* - **accel**: The key used for keyboard shortcuts on the user's platform,
* which is Control on Windows and Linux, and Command on Mac. Usually, this
* would be the value you would use.
*
* @param {function} listener
* Function to execute when the `hotkey` is executed.
*/
exports.register = function register(hotkey, listener) {
hotkey = normalize(hotkey);
hotkeys[hotkey] = listener;
};
/**
* Unregister a global `hotkey`. If passed `listener` is not the one registered
* for the given `hotkey`, the call to this function will be ignored.
*
* @param {string} hotkey
* Key combination in the format of 'modifier key'.
* @param {function} listener
* Function that will be invoked when the `hotkey` is pressed.
*/
exports.unregister = function unregister(hotkey, listener) {
hotkey = normalize(hotkey);
if (hotkeys[hotkey] === listener)
delete hotkeys[hotkey];
};
/**
* Map of hotkeys and associated functions.
*/
const hotkeys = exports.hotkeys = {};
keyboardObserver.on("keydown", function onKeypress(event, window) {
let key, modifiers = [];
let isChar = "isChar" in event && event.isChar;
let which = "which" in event ? event.which : null;
let keyCode = "keyCode" in event ? event.keyCode : null;
if ("shiftKey" in event && event.shiftKey)
modifiers.push("shift");
if ("altKey" in event && event.altKey)
modifiers.push("alt");
if ("ctrlKey" in event && event.ctrlKey)
modifiers.push("control");
if ("metaKey" in event && event.metaKey)
modifiers.push("meta");
// If it's not a printable character then we fall back to a human readable
// equivalent of one of the following constants.
// http://dxr.mozilla.org/mozilla-central/source/dom/interfaces/events/nsIDOMKeyEvent.idl
key = getKeyForCode(keyCode);
// If only non-function (f1 - f24) key or only modifiers are pressed we don't
// have a valid combination so we return immediately (Also, sometimes
// `keyCode` may be one for the modifier which means we do not have a
// modifier).
if (!key || (!isFunctionKey(key) && !modifiers.length) || key in MODIFIERS)
return;
let combination = normalize({ key: key, modifiers: modifiers });
let hotkey = hotkeys[combination];
if (hotkey) {
try {
hotkey();
} catch (exception) {
console.exception(exception);
} finally {
// Work around bug 582052 by preventing the (nonexistent) default action.
event.preventDefault();
}
}
});

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";
module.metadata = {
"stability": "unstable"
};
const { Class } = require("../core/heritage");
const { EventTarget } = require("../event/target");
const { emit } = require("../event/core");
const { DOMEventAssembler } = require("../deprecated/events/assembler");
const { browserWindowIterator } = require('../deprecated/window-utils');
const { isBrowser } = require('../window/utils');
const { observer: windowObserver } = require("../windows/observer");
// Event emitter objects used to register listeners and emit events on them
// when they occur.
const Observer = Class({
implements: [DOMEventAssembler, EventTarget],
initialize() {
// Adding each opened window to a list of observed windows.
windowObserver.on("open", window => {
if (isBrowser(window))
this.observe(window);
});
// Removing each closed window form the list of observed windows.
windowObserver.on("close", window => {
if (isBrowser(window))
this.ignore(window);
});
// Making observer aware of already opened windows.
for (let window of browserWindowIterator()) {
this.observe(window);
}
},
/**
* Events that are supported and emitted by the module.
*/
supportedEventsTypes: [ "keydown", "keyup", "keypress" ],
/**
* Function handles all the supported events on all the windows that are
* observed. Method is used to proxy events to the listeners registered on
* this event emitter.
* @param {Event} event
* Keyboard event being emitted.
*/
handleEvent(event) {
emit(this, event.type, event, event.target.ownerDocument ? event.target.ownerDocument.defaultView
: undefined);
}
});
exports.observer = new Observer();

View file

@ -0,0 +1,189 @@
/* 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 runtime = require("../system/runtime");
const { isString } = require("../lang/type");
const array = require("../util/array");
const SWP = "{{SEPARATOR}}";
const SEPARATOR = "-"
const INVALID_COMBINATION = "Hotkey key combination must contain one or more " +
"modifiers and only one key";
// Map of modifier key mappings.
const MODIFIERS = exports.MODIFIERS = {
'accel': runtime.OS === "Darwin" ? 'meta' : 'control',
'meta': 'meta',
'control': 'control',
'ctrl': 'control',
'option': 'alt',
'command': 'meta',
'alt': 'alt',
'shift': 'shift'
};
// Hash of key:code pairs for all the chars supported by `nsIDOMKeyEvent`.
// This is just a copy of the `nsIDOMKeyEvent` hash with normalized names.
// @See: http://dxr.mozilla.org/mozilla-central/source/dom/interfaces/events/nsIDOMKeyEvent.idl
const CODES = exports.CODES = new function Codes() {
let nsIDOMKeyEvent = Ci.nsIDOMKeyEvent;
// Names that will be substituted with a shorter analogs.
let aliases = {
'subtract': '-',
'add': '+',
'equals': '=',
'slash': '/',
'backslash': '\\',
'openbracket': '[',
'closebracket': ']',
'quote': '\'',
'backquote': '`',
'period': '.',
'semicolon': ';',
'comma': ','
};
// Normalizing keys and copying values to `this` object.
Object.keys(nsIDOMKeyEvent).filter(function(key) {
// Filter out only key codes.
return key.indexOf('DOM_VK') === 0;
}).map(function(key) {
// Map to key:values
return [ key, nsIDOMKeyEvent[key] ];
}).map(function([key, value]) {
return [ key.replace('DOM_VK_', '').replace('_', '').toLowerCase(), value ];
}).forEach(function ([ key, value ]) {
this[aliases[key] || key] = value;
}, this);
};
// Inverted `CODES` hash of `code:key`.
const KEYS = exports.KEYS = new function Keys() {
Object.keys(CODES).forEach(function(key) {
this[CODES[key]] = key;
}, this)
}
exports.getKeyForCode = function getKeyForCode(code) {
return (code in KEYS) && KEYS[code];
};
exports.getCodeForKey = function getCodeForKey(key) {
return (key in CODES) && CODES[key];
};
/**
* Utility function that takes string or JSON that defines a `hotkey` and
* returns normalized string version of it.
* @param {JSON|String} hotkey
* @param {String} [separator=" "]
* Optional string that represents separator used to concatenate keys in the
* given `hotkey`.
* @returns {String}
* @examples
*
* require("keyboard/hotkeys").normalize("b Shift accel");
* // 'control shift b' -> on windows & linux
* // 'meta shift b' -> on mac
* require("keyboard/hotkeys").normalize("alt-d-shift", "-");
* // 'alt shift d'
*/
var normalize = exports.normalize = function normalize(hotkey, separator) {
if (!isString(hotkey))
hotkey = toString(hotkey, separator);
return toString(toJSON(hotkey, separator), separator);
};
/*
* Utility function that splits a string of characters that defines a `hotkey`
* into modifier keys and the defining key.
* @param {String} hotkey
* @param {String} [separator=" "]
* Optional string that represents separator used to concatenate keys in the
* given `hotkey`.
* @returns {JSON}
* @examples
*
* require("keyboard/hotkeys").toJSON("accel shift b");
* // { key: 'b', modifiers: [ 'control', 'shift' ] } -> on windows & linux
* // { key: 'b', modifiers: [ 'meta', 'shift' ] } -> on mac
*
* require("keyboard/hotkeys").normalize("alt-d-shift", "-");
* // { key: 'd', modifiers: [ 'alt', 'shift' ] }
*/
var toJSON = exports.toJSON = function toJSON(hotkey, separator) {
separator = separator || SEPARATOR;
// Since default separator is `-`, combination may take form of `alt--`. To
// avoid misbehavior we replace `--` with `-{{SEPARATOR}}` where
// `{{SEPARATOR}}` can be swapped later.
hotkey = hotkey.toLowerCase().replace(separator + separator, separator + SWP);
let value = {};
let modifiers = [];
let keys = hotkey.split(separator);
keys.forEach(function(name) {
// If name is `SEPARATOR` than we swap it back.
if (name === SWP)
name = separator;
if (name in MODIFIERS) {
array.add(modifiers, MODIFIERS[name]);
} else {
if (!value.key)
value.key = name;
else
throw new TypeError(INVALID_COMBINATION);
}
});
if (!value.key)
throw new TypeError(INVALID_COMBINATION);
value.modifiers = modifiers.sort();
return value;
};
/**
* Utility function that takes object that defines a `hotkey` and returns
* string representation of it.
*
* _Please note that this function does not validates data neither it normalizes
* it, if you are unsure that data is well formed use `normalize` function
* instead.
*
* @param {JSON} hotkey
* @param {String} [separator=" "]
* Optional string that represents separator used to concatenate keys in the
* given `hotkey`.
* @returns {String}
* @examples
*
* require("keyboard/hotkeys").toString({
* key: 'b',
* modifiers: [ 'control', 'shift' ]
* }, '+');
* // 'control+shift+b
*
*/
var toString = exports.toString = function toString(hotkey, separator) {
let keys = hotkey.modifiers.slice();
keys.push(hotkey.key);
return keys.join(separator || SEPARATOR);
};
/**
* Utility function takes `key` name and returns `true` if it's function key
* (F1, ..., F24) and `false` if it's not.
*/
var isFunctionKey = exports.isFunctionKey = function isFunctionKey(key) {
var $
return key[0].toLowerCase() === 'f' &&
($ = parseInt(key.substr(1)), 0 < $ && $ < 25);
};

View file

@ -0,0 +1,91 @@
/* 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 json = require("./l10n/json/core");
const { get: getKey } = require("./l10n/core");
const properties = require("./l10n/properties/core");
const { getRulesForLocale } = require("./l10n/plural-rules");
// Retrieve the plural mapping function
var pluralMappingFunction = getRulesForLocale(json.language()) ||
getRulesForLocale("en");
exports.get = function get(k) {
// For now, we only accept a "string" as first argument
// TODO: handle plural forms in gettext pattern
if (typeof k !== "string")
throw new Error("First argument of localization method should be a string");
let n = arguments[1];
// Get translation from big hashmap or default to hard coded string:
let localized = getKey(k, n) || k;
// # Simplest usecase:
// // String hard coded in source code:
// _("Hello world")
// // Identifier of a key stored in properties file
// _("helloString")
if (arguments.length <= 1)
return localized;
let args = Array.slice(arguments);
let placeholders = [null, ...args.slice(typeof(n) === "number" ? 2 : 1)];
if (typeof localized == "object" && "other" in localized) {
// # Plural form:
// // Strings hard coded in source code:
// _(["One download", "%d downloads"], 10);
// // Identifier of a key stored in properties file
// _("downloadNumber", 0);
let n = arguments[1];
// First handle simple universal forms that may not be mandatory
// for each language, (i.e. not different than 'other' form,
// but still usefull for better phrasing)
// For example 0 in english is the same form than 'other'
// but we accept 'zero' form if specified in localization file
if (n === 0 && "zero" in localized)
localized = localized["zero"];
else if (n === 1 && "one" in localized)
localized = localized["one"];
else if (n === 2 && "two" in localized)
localized = localized["two"];
else {
let pluralForm = pluralMappingFunction(n);
if (pluralForm in localized)
localized = localized[pluralForm];
else // Fallback in case of error: missing plural form
localized = localized["other"];
}
// Simulate a string with one placeholder:
args = [null, n];
}
// # String with placeholders:
// // Strings hard coded in source code:
// _("Hello %s", username)
// // Identifier of a key stored in properties file
// _("helloString", username)
// * We supports `%1s`, `%2s`, ... pattern in order to change arguments order
// in translation.
// * In case of plural form, we has `%d` instead of `%s`.
let offset = 1;
if (placeholders.length > 1) {
args = placeholders;
}
localized = localized.replace(/%(\d*)[sd]/g, (v, n) => {
let rv = args[n != "" ? n : offset];
offset++;
return rv;
});
return localized;
}

View file

@ -0,0 +1,9 @@
/* 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 json = require("./json/core");
const properties = require("./properties/core");
exports.get = json.usingJSON ? json.get : properties.get;

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";
module.metadata = {
"stability": "unstable"
};
const { processes, remoteRequire } = require("../remote/parent");
remoteRequire("sdk/content/l10n-html");
var enabled = false;
function enable() {
if (!enabled) {
processes.port.emit("sdk/l10n/html/enable");
enabled = true;
}
}
exports.enable = enable;
function disable() {
if (enabled) {
processes.port.emit("sdk/l10n/html/disable");
enabled = false;
}
}
exports.disable = disable;
processes.forEvery(process => {
process.port.emit(enabled ? "sdk/l10n/html/enable" : "sdk/l10n/html/disable");
});

View file

@ -0,0 +1,36 @@
/* 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 usingJSON = false;
var hash = {}, bestMatchingLocale = null;
try {
let data = require("@l10n/data");
hash = data.hash;
bestMatchingLocale = data.bestMatchingLocale;
usingJSON = true;
}
catch(e) {}
exports.usingJSON = usingJSON;
// Returns the translation for a given key, if available.
exports.get = function get(k) {
return k in hash ? hash[k] : null;
}
// Returns the full length locale code: ja-JP-mac, en-US or fr
exports.locale = function locale() {
return bestMatchingLocale;
}
// Returns the short locale code: ja, en, fr
exports.language = function language() {
return bestMatchingLocale ? bestMatchingLocale.split("-")[0].toLowerCase()
: "en";
}

View file

@ -0,0 +1,70 @@
/* 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 { getPreferedLocales, findClosestLocale } = require("./locale");
const { readURI } = require("../net/url");
const { resolve } = require("../core/promise");
function parseJsonURI(uri) {
return readURI(uri).
then(JSON.parse).
then(null, function (error) {
throw Error("Failed to parse locale file:\n" + uri + "\n" + error);
});
}
// Returns the array stored in `locales.json` manifest that list available
// locales files
function getAvailableLocales(rootURI) {
let uri = rootURI + "locales.json";
return parseJsonURI(uri).then(function (manifest) {
return "locales" in manifest &&
Array.isArray(manifest.locales) ?
manifest.locales : [];
});
}
// Returns URI of the best locales file to use from the XPI
function getBestLocale(rootURI) {
// Read localization manifest file that contains list of available languages
return getAvailableLocales(rootURI).then(function (availableLocales) {
// Retrieve list of prefered locales to use
let preferedLocales = getPreferedLocales();
// Compute the most preferable locale to use by using these two lists
return findClosestLocale(availableLocales, preferedLocales);
});
}
/**
* Read localization files and returns a promise of data to put in `@l10n/data`
* pseudo module, in order to allow l10n/json/core to fetch it.
*/
exports.load = function load(rootURI) {
// First, search for a locale file:
return getBestLocale(rootURI).then(function (bestMatchingLocale) {
// It may be null if the addon doesn't have any locale file
if (!bestMatchingLocale)
return resolve(null);
let localeURI = rootURI + "locale/" + bestMatchingLocale + ".json";
// Locale files only contains one big JSON object that is used as
// an hashtable of: "key to translate" => "translated key"
// TODO: We are likely to change this in order to be able to overload
// a specific key translation. For a specific package, module or line?
return parseJsonURI(localeURI).then(function (json) {
return {
hash: json,
bestMatchingLocale: bestMatchingLocale
};
});
});
}

View file

@ -0,0 +1,127 @@
/* 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 prefs = require("../preferences/service");
const { Cu, Cc, Ci } = require("chrome");
const { Services } = Cu.import("resource://gre/modules/Services.jsm");
/**
* Gets the currently selected locale for display.
* Gets all usable locale that we can use sorted by priority of relevance
* @return Array of locales, begins with highest priority
*/
const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
const PREF_SELECTED_LOCALE = "general.useragent.locale";
const PREF_ACCEPT_LANGUAGES = "intl.accept_languages";
function getPreferedLocales(caseSensitve) {
let locales = [];
function addLocale(locale) {
locale = locale.trim();
if (!caseSensitve)
locale = locale.toLowerCase();
if (locales.indexOf(locale) === -1)
locales.push(locale);
}
// Most important locale is OS one. But we use it, only if
// "intl.locale.matchOS" pref is set to `true`.
// Currently only used for multi-locales mobile builds.
// http://mxr.mozilla.org/mozilla-central/source/mobile/android/installer/Makefile.in#46
if (prefs.get(PREF_MATCH_OS_LOCALE, false)) {
let localeService = Cc["@mozilla.org/intl/nslocaleservice;1"].
getService(Ci.nsILocaleService);
let osLocale = localeService.getLocaleComponentForUserAgent();
addLocale(osLocale);
}
// In some cases, mainly on Fennec and on Linux version,
// `general.useragent.locale` is a special 'localized' value, like:
// "chrome://global/locale/intl.properties"
let browserUiLocale = prefs.getLocalized(PREF_SELECTED_LOCALE, "") ||
prefs.get(PREF_SELECTED_LOCALE, "");
if (browserUiLocale)
addLocale(browserUiLocale);
// Third priority is the list of locales used for web content
let contentLocales = prefs.getLocalized(PREF_ACCEPT_LANGUAGES, "") ||
prefs.get(PREF_ACCEPT_LANGUAGES, "");
if (contentLocales) {
// This list is a string of locales seperated by commas.
// There is spaces after commas, so strip each item
for (let locale of contentLocales.split(","))
addLocale(locale.replace(/(^\s+)|(\s+$)/g, ""));
}
// Finally, we ensure that en-US is the final fallback if it wasn't added
addLocale("en-US");
return locales;
}
exports.getPreferedLocales = getPreferedLocales;
/**
* Selects the closest matching locale from a list of locales.
*
* @param aLocales
* An array of available locales
* @param aMatchLocales
* An array of prefered locales, ordered by priority. Most wanted first.
* Locales have to be in lowercase.
* If null, uses getPreferedLocales() results
* @return the best match for the currently selected locale
*
* Stolen from http://dxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm
*/
exports.findClosestLocale = function findClosestLocale(aLocales, aMatchLocales) {
aMatchLocales = aMatchLocales || getPreferedLocales();
// Holds the best matching localized resource
let bestmatch = null;
// The number of locale parts it matched with
let bestmatchcount = 0;
// The number of locale parts in the match
let bestpartcount = 0;
for (let locale of aMatchLocales) {
let lparts = locale.split("-");
for (let localized of aLocales) {
let found = localized.toLowerCase();
// Exact match is returned immediately
if (locale == found)
return localized;
let fparts = found.split("-");
/* If we have found a possible match and this one isn't any longer
then we dont need to check further. */
if (bestmatch && fparts.length < bestmatchcount)
continue;
// Count the number of parts that match
let maxmatchcount = Math.min(fparts.length, lparts.length);
let matchcount = 0;
while (matchcount < maxmatchcount &&
fparts[matchcount] == lparts[matchcount])
matchcount++;
/* If we matched more than the last best match or matched the same and
this locale is less specific than the last best match. */
if (matchcount > bestmatchcount ||
(matchcount == bestmatchcount && fparts.length < bestpartcount)) {
bestmatch = localized;
bestmatchcount = matchcount;
bestpartcount = fparts.length;
}
}
// If we found a valid match for this locale return it
if (bestmatch)
return bestmatch;
}
return null;
}

View file

@ -0,0 +1,407 @@
/* 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/. */
// This file is automatically generated with /python-lib/plural-rules-generator.py
// Fetching data from: http://unicode.org/repos/cldr/trunk/common/supplemental/plurals.xml
// Mapping of short locale name == to == > rule index in following list
module.metadata = {
"stability": "unstable"
};
const LOCALES_TO_RULES = {
"af": 3,
"ak": 4,
"am": 4,
"ar": 1,
"asa": 3,
"az": 0,
"be": 11,
"bem": 3,
"bez": 3,
"bg": 3,
"bh": 4,
"bm": 0,
"bn": 3,
"bo": 0,
"br": 20,
"brx": 3,
"bs": 11,
"ca": 3,
"cgg": 3,
"chr": 3,
"cs": 12,
"cy": 17,
"da": 3,
"de": 3,
"dv": 3,
"dz": 0,
"ee": 3,
"el": 3,
"en": 3,
"eo": 3,
"es": 3,
"et": 3,
"eu": 3,
"fa": 0,
"ff": 5,
"fi": 3,
"fil": 4,
"fo": 3,
"fr": 5,
"fur": 3,
"fy": 3,
"ga": 8,
"gd": 24,
"gl": 3,
"gsw": 3,
"gu": 3,
"guw": 4,
"gv": 23,
"ha": 3,
"haw": 3,
"he": 2,
"hi": 4,
"hr": 11,
"hu": 0,
"id": 0,
"ig": 0,
"ii": 0,
"is": 3,
"it": 3,
"iu": 7,
"ja": 0,
"jmc": 3,
"jv": 0,
"ka": 0,
"kab": 5,
"kaj": 3,
"kcg": 3,
"kde": 0,
"kea": 0,
"kk": 3,
"kl": 3,
"km": 0,
"kn": 0,
"ko": 0,
"ksb": 3,
"ksh": 21,
"ku": 3,
"kw": 7,
"lag": 18,
"lb": 3,
"lg": 3,
"ln": 4,
"lo": 0,
"lt": 10,
"lv": 6,
"mas": 3,
"mg": 4,
"mk": 16,
"ml": 3,
"mn": 3,
"mo": 9,
"mr": 3,
"ms": 0,
"mt": 15,
"my": 0,
"nah": 3,
"naq": 7,
"nb": 3,
"nd": 3,
"ne": 3,
"nl": 3,
"nn": 3,
"no": 3,
"nr": 3,
"nso": 4,
"ny": 3,
"nyn": 3,
"om": 3,
"or": 3,
"pa": 3,
"pap": 3,
"pl": 13,
"ps": 3,
"pt": 3,
"rm": 3,
"ro": 9,
"rof": 3,
"ru": 11,
"rwk": 3,
"sah": 0,
"saq": 3,
"se": 7,
"seh": 3,
"ses": 0,
"sg": 0,
"sh": 11,
"shi": 19,
"sk": 12,
"sl": 14,
"sma": 7,
"smi": 7,
"smj": 7,
"smn": 7,
"sms": 7,
"sn": 3,
"so": 3,
"sq": 3,
"sr": 11,
"ss": 3,
"ssy": 3,
"st": 3,
"sv": 3,
"sw": 3,
"syr": 3,
"ta": 3,
"te": 3,
"teo": 3,
"th": 0,
"ti": 4,
"tig": 3,
"tk": 3,
"tl": 4,
"tn": 3,
"to": 0,
"tr": 0,
"ts": 3,
"tzm": 22,
"uk": 11,
"ur": 3,
"ve": 3,
"vi": 0,
"vun": 3,
"wa": 4,
"wae": 3,
"wo": 0,
"xh": 3,
"xog": 3,
"yo": 0,
"zh": 0,
"zu": 3
};
// Utility functions for plural rules methods
function isIn(n, list) {
return list.indexOf(n) !== -1;
}
function isBetween(n, start, end) {
return start <= n && n <= end;
}
// List of all plural rules methods, that maps an integer to the plural form name to use
const RULES = {
"0": function (n) {
return "other"
},
"1": function (n) {
if ((isBetween((n % 100), 3, 10)))
return "few";
if (n == 0)
return "zero";
if ((isBetween((n % 100), 11, 99)))
return "many";
if (n == 2)
return "two";
if (n == 1)
return "one";
return "other"
},
"2": function (n) {
if (n != 0 && (n % 10) == 0)
return "many";
if (n == 2)
return "two";
if (n == 1)
return "one";
return "other"
},
"3": function (n) {
if (n == 1)
return "one";
return "other"
},
"4": function (n) {
if ((isBetween(n, 0, 1)))
return "one";
return "other"
},
"5": function (n) {
if ((isBetween(n, 0, 2)) && n != 2)
return "one";
return "other"
},
"6": function (n) {
if (n == 0)
return "zero";
if ((n % 10) == 1 && (n % 100) != 11)
return "one";
return "other"
},
"7": function (n) {
if (n == 2)
return "two";
if (n == 1)
return "one";
return "other"
},
"8": function (n) {
if ((isBetween(n, 3, 6)))
return "few";
if ((isBetween(n, 7, 10)))
return "many";
if (n == 2)
return "two";
if (n == 1)
return "one";
return "other"
},
"9": function (n) {
if (n == 0 || n != 1 && (isBetween((n % 100), 1, 19)))
return "few";
if (n == 1)
return "one";
return "other"
},
"10": function (n) {
if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
return "few";
if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
return "one";
return "other"
},
"11": function (n) {
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
return "few";
if ((n % 10) == 0 || (isBetween((n % 10), 5, 9)) || (isBetween((n % 100), 11, 14)))
return "many";
if ((n % 10) == 1 && (n % 100) != 11)
return "one";
return "other"
},
"12": function (n) {
if ((isBetween(n, 2, 4)))
return "few";
if (n == 1)
return "one";
return "other"
},
"13": function (n) {
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
return "few";
if (n != 1 && (isBetween((n % 10), 0, 1)) || (isBetween((n % 10), 5, 9)) || (isBetween((n % 100), 12, 14)))
return "many";
if (n == 1)
return "one";
return "other"
},
"14": function (n) {
if ((isBetween((n % 100), 3, 4)))
return "few";
if ((n % 100) == 2)
return "two";
if ((n % 100) == 1)
return "one";
return "other"
},
"15": function (n) {
if (n == 0 || (isBetween((n % 100), 2, 10)))
return "few";
if ((isBetween((n % 100), 11, 19)))
return "many";
if (n == 1)
return "one";
return "other"
},
"16": function (n) {
if ((n % 10) == 1 && n != 11)
return "one";
return "other"
},
"17": function (n) {
if (n == 3)
return "few";
if (n == 0)
return "zero";
if (n == 6)
return "many";
if (n == 2)
return "two";
if (n == 1)
return "one";
return "other"
},
"18": function (n) {
if (n == 0)
return "zero";
if ((isBetween(n, 0, 2)) && n != 0 && n != 2)
return "one";
return "other"
},
"19": function (n) {
if ((isBetween(n, 2, 10)))
return "few";
if ((isBetween(n, 0, 1)))
return "one";
return "other"
},
"20": function (n) {
if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(isBetween((n % 100), 10, 19) || isBetween((n % 100), 70, 79) || isBetween((n % 100), 90, 99)))
return "few";
if ((n % 1000000) == 0 && n != 0)
return "many";
if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
return "two";
if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
return "one";
return "other"
},
"21": function (n) {
if (n == 0)
return "zero";
if (n == 1)
return "one";
return "other"
},
"22": function (n) {
if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
return "one";
return "other"
},
"23": function (n) {
if ((isBetween((n % 10), 1, 2)) || (n % 20) == 0)
return "one";
return "other"
},
"24": function (n) {
if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
return "few";
if (isIn(n, [2, 12]))
return "two";
if (isIn(n, [1, 11]))
return "one";
return "other"
},
};
/**
* Return a function that gives the plural form name for a given integer
* for the specified `locale`
* let fun = getRulesForLocale('en');
* fun(1) -> 'one'
* fun(0) -> 'other'
* fun(1000) -> 'other'
*/
exports.getRulesForLocale = function getRulesForLocale(locale) {
let index = LOCALES_TO_RULES[locale];
if (!(index in RULES)) {
console.warn('Plural form unknown for locale "' + locale + '"');
return function () { return "other"; };
}
return RULES[index];
}

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";
const { on } = require("../system/events");
const core = require("./core");
const { id: jetpackId } = require('../self');
const OPTIONS_DISPLAYED = "addon-options-displayed";
function enable() {
on(OPTIONS_DISPLAYED, onOptionsDisplayed);
}
exports.enable = enable;
function onOptionsDisplayed({ subject: document, data: addonId }) {
if (addonId !== jetpackId)
return;
localizeInlineOptions(document);
}
function localizeInlineOptions(document) {
let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' +
'button[data-jetpack-id="' + jetpackId + '"][pref-name]';
let nodes = document.querySelectorAll(query);
for (let node of nodes) {
let name = node.getAttribute("pref-name");
if (node.tagName == "setting") {
let desc = core.get(name + "_description");
if (desc)
node.setAttribute("desc", desc);
let title = core.get(name + "_title");
if (title)
node.setAttribute("title", title);
for (let item of node.querySelectorAll("menuitem, radio")) {
let key = name + "_options." + item.getAttribute("label");
let label = core.get(key);
if (label)
item.setAttribute("label", label);
}
}
else if (node.tagName == "button") {
let label = core.get(name + "_label");
if (label)
node.setAttribute("label", label);
}
}
}
exports.localizeInlineOptions = localizeInlineOptions;

View file

@ -0,0 +1,87 @@
/* 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 { newURI } = require('../../url/utils')
const { getRulesForLocale } = require("../plural-rules");
const { getPreferedLocales } = require('../locale');
const { rootURI } = require("@loader/options");
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const baseURI = rootURI + "locale/";
const preferedLocales = getPreferedLocales(true);
// Make sure we don't get stale data after an update
// (See Bug 1300735 for rationale).
Services.strings.flushBundles();
function getLocaleURL(locale) {
// if the locale is a valid chrome URI, return it
try {
let uri = newURI(locale);
if (uri.scheme == 'chrome')
return uri.spec;
}
catch(_) {}
// otherwise try to construct the url
return baseURI + locale + ".properties";
}
function getKey(locale, key) {
let bundle = Services.strings.createBundle(getLocaleURL(locale));
try {
return bundle.GetStringFromName(key) + "";
}
catch (_) {}
return undefined;
}
function get(key, n, locales) {
// try this locale
let locale = locales.shift();
let localized;
if (typeof n == 'number') {
if (n == 0) {
localized = getKey(locale, key + '[zero]');
}
else if (n == 1) {
localized = getKey(locale, key + '[one]');
}
else if (n == 2) {
localized = getKey(locale, key + '[two]');
}
if (!localized) {
// Retrieve the plural mapping function
let pluralForm = (getRulesForLocale(locale.split("-")[0].toLowerCase()) ||
getRulesForLocale("en"))(n);
localized = getKey(locale, key + '[' + pluralForm + ']');
}
if (!localized) {
localized = getKey(locale, key + '[other]');
}
}
if (!localized) {
localized = getKey(locale, key);
}
if (!localized) {
localized = getKey(locale, key + '[other]');
}
if (localized) {
return localized;
}
// try next locale
if (locales.length)
return get(key, n, locales);
return undefined;
}
exports.get = (k, n) => get(k, n, Array.slice(preferedLocales));

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/. */
// Disclaimer: Some of the functions in this module implement APIs from
// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for
// those goes to him.
"use strict";
module.metadata = {
"stability": "unstable"
};
const { defer, remit, delay, debounce,
throttle } = require("./functional/concurrent");
const { method, invoke, partial, curry, compose, wrap, identity, memoize, once,
cache, complement, constant, when, apply, flip, field, query,
isInstance, chainable, is, isnt } = require("./functional/core");
exports.defer = defer;
exports.remit = remit;
exports.delay = delay;
exports.debounce = debounce;
exports.throttle = throttle;
exports.method = method;
exports.invoke = invoke;
exports.partial = partial;
exports.curry = curry;
exports.compose = compose;
exports.wrap = wrap;
exports.identity = identity;
exports.memoize = memoize;
exports.once = once;
exports.cache = cache;
exports.complement = complement;
exports.constant = constant;
exports.when = when;
exports.apply = apply;
exports.flip = flip;
exports.field = field;
exports.query = query;
exports.isInstance = isInstance;
exports.chainable = chainable;
exports.is = is;
exports.isnt = isnt;

View file

@ -0,0 +1,110 @@
/* 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/. */
// Disclaimer: Some of the functions in this module implement APIs from
// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for
// those goes to him.
"use strict";
module.metadata = {
"stability": "unstable"
};
const { arity, name, derive, invoke } = require("./helpers");
const { setTimeout, clearTimeout, setImmediate } = require("../../timers");
/**
* Takes a function and returns a wrapped one instead, calling which will call
* original function in the next turn of event loop. This is basically utility
* to do `setImmediate(function() { ... })`, with a difference that returned
* function is reused, instead of creating a new one each time. This also allows
* to use this functions as event listeners.
*/
const defer = f => derive(function(...args) {
setImmediate(invoke, f, args, this);
}, f);
exports.defer = defer;
// Exporting `remit` alias as `defer` may conflict with promises.
exports.remit = defer;
/**
* Much like setTimeout, invokes function after wait milliseconds. If you pass
* the optional arguments, they will be forwarded on to the function when it is
* invoked.
*/
const delay = function delay(f, ms, ...args) {
setTimeout(() => f.apply(this, args), ms);
};
exports.delay = delay;
/**
* From underscore's `_.debounce`
* http://underscorejs.org
* (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
* Underscore may be freely distributed under the MIT license.
*/
const debounce = function debounce (fn, wait) {
let timeout, args, context, timestamp, result;
let later = function () {
let last = Date.now() - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
result = fn.apply(context, args);
context = args = null;
}
};
return function (...aArgs) {
context = this;
args = aArgs;
timestamp = Date.now();
if (!timeout) {
timeout = setTimeout(later, wait);
}
return result;
};
};
exports.debounce = debounce;
/**
* From underscore's `_.throttle`
* http://underscorejs.org
* (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
* Underscore may be freely distributed under the MIT license.
*/
const throttle = function throttle (func, wait, options) {
let context, args, result;
let timeout = null;
let previous = 0;
options || (options = {});
let later = function() {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
result = func.apply(context, args);
context = args = null;
};
return function() {
let now = Date.now();
if (!previous && options.leading === false) previous = now;
let remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
exports.throttle = throttle;

View file

@ -0,0 +1,290 @@
/* 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/. */
// Disclaimer: Some of the functions in this module implement APIs from
// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for
// those goes to him.
"use strict";
module.metadata = {
"stability": "unstable"
}
const { arity, name, derive, invoke } = require("./helpers");
/**
* Takes variadic numeber of functions and returns composed one.
* Returned function pushes `this` pseudo-variable to the head
* of the passed arguments and invokes all the functions from
* left to right passing same arguments to them. Composite function
* returns return value of the right most funciton.
*/
const method = (...lambdas) => {
return function method(...args) {
args.unshift(this);
return lambdas.reduce((_, lambda) => lambda.apply(this, args),
void(0));
};
};
exports.method = method;
/**
* Invokes `callee` by passing `params` as an arguments and `self` as `this`
* pseudo-variable. Returns value that is returned by a callee.
* @param {Function} callee
* Function to invoke.
* @param {Array} params
* Arguments to invoke function with.
* @param {Object} self
* Object to be passed as a `this` pseudo variable.
*/
exports.invoke = invoke;
/**
* Takes a function and bind values to one or more arguments, returning a new
* function of smaller arity.
*
* @param {Function} fn
* The function to partial
*
* @returns The new function with binded values
*/
const partial = (f, ...curried) => {
if (typeof(f) !== "function")
throw new TypeError(String(f) + " is not a function");
let fn = derive(function(...args) {
return f.apply(this, curried.concat(args));
}, f);
fn.arity = arity(f) - curried.length;
return fn;
};
exports.partial = partial;
/**
* Returns function with implicit currying, which will continue currying until
* expected number of argument is collected. Expected number of arguments is
* determined by `fn.length`. Using this with variadic functions is stupid,
* so don't do it.
*
* @examples
*
* var sum = curry(function(a, b) {
* return a + b
* })
* console.log(sum(2, 2)) // 4
* console.log(sum(2)(4)) // 6
*/
const curry = new function() {
const currier = (fn, arity, params) => {
// Function either continues to curry arguments or executes function
// if desired arguments have being collected.
const curried = function(...input) {
// Prepend all curried arguments to the given arguments.
if (params) input.unshift.apply(input, params);
// If expected number of arguments has being collected invoke fn,
// othrewise return curried version Otherwise continue curried.
return (input.length >= arity) ? fn.apply(this, input) :
currier(fn, arity, input);
};
curried.arity = arity - (params ? params.length : 0);
return curried;
};
return fn => currier(fn, arity(fn));
};
exports.curry = curry;
/**
* Returns the composition of a list of functions, where each function consumes
* the return value of the function that follows. In math terms, composing the
* functions `f()`, `g()`, and `h()` produces `f(g(h()))`.
* @example
*
* var greet = function(name) { return "hi: " + name; };
* var exclaim = function(statement) { return statement + "!"; };
* var welcome = compose(exclaim, greet);
*
* welcome('moe'); // => 'hi: moe!'
*/
function compose(...lambdas) {
return function composed(...args) {
let index = lambdas.length;
while (0 <= --index)
args = [lambdas[index].apply(this, args)];
return args[0];
};
}
exports.compose = compose;
/*
* Returns the first function passed as an argument to the second,
* allowing you to adjust arguments, run code before and after, and
* conditionally execute the original function.
* @example
*
* var hello = function(name) { return "hello: " + name; };
* hello = wrap(hello, function(f) {
* return "before, " + f("moe") + ", after";
* });
*
* hello(); // => 'before, hello: moe, after'
*/
const wrap = (f, wrapper) => derive(function wrapped(...args) {
return wrapper.apply(this, [f].concat(args));
}, f);
exports.wrap = wrap;
/**
* Returns the same value that is used as the argument. In math: f(x) = x
*/
const identity = value => value;
exports.identity = identity;
/**
* Memoizes a given function by caching the computed result. Useful for
* speeding up slow-running computations. If passed an optional hashFunction,
* it will be used to compute the hash key for storing the result, based on
* the arguments to the original function. The default hashFunction just uses
* the first argument to the memoized function as the key.
*/
const memoize = (f, hasher) => {
let memo = Object.create(null);
let cache = new WeakMap();
hasher = hasher || identity;
return derive(function memoizer(...args) {
const key = hasher.apply(this, args);
const type = typeof(key);
if (key && (type === "object" || type === "function")) {
if (!cache.has(key))
cache.set(key, f.apply(this, args));
return cache.get(key);
}
else {
if (!(key in memo))
memo[key] = f.apply(this, args);
return memo[key];
}
}, f);
};
exports.memoize = memoize;
/*
* Creates a version of the function that can only be called one time. Repeated
* calls to the modified function will have no effect, returning the value from
* the original call. Useful for initialization functions, instead of having to
* set a boolean flag and then check it later.
*/
const once = f => {
let ran = false, cache;
return derive(function(...args) {
return ran ? cache : (ran = true, cache = f.apply(this, args));
}, f);
};
exports.once = once;
// export cache as once will may be conflicting with event once a lot.
exports.cache = once;
// Takes a `f` function and returns a function that takes the same
// arguments as `f`, has the same effects, if any, and returns the
// opposite truth value.
const complement = f => derive(function(...args) {
return args.length < arity(f) ? complement(partial(f, ...args)) :
!f.apply(this, args);
}, f);
exports.complement = complement;
// Constructs function that returns `x` no matter what is it
// invoked with.
const constant = x => _ => x;
exports.constant = constant;
// Takes `p` predicate, `consequent` function and an optional
// `alternate` function and composes function that returns
// application of arguments over `consequent` if application over
// `p` is `true` otherwise returns application over `alternate`.
// If `alternate` is not a function returns `undefined`.
const when = (p, consequent, alternate) => {
if (typeof(alternate) !== "function" && alternate !== void(0))
throw TypeError("alternate must be a function");
if (typeof(consequent) !== "function")
throw TypeError("consequent must be a function");
return function(...args) {
return p.apply(this, args) ?
consequent.apply(this, args) :
alternate && alternate.apply(this, args);
};
};
exports.when = when;
// Apply function that behaves as `apply` does in lisp:
// apply(f, x, [y, z]) => f.apply(f, [x, y, z])
// apply(f, x) => f.apply(f, [x])
const apply = (f, ...rest) => f.apply(f, rest.concat(rest.pop()));
exports.apply = apply;
// Returns function identical to given `f` but with flipped order
// of arguments.
const flip = f => derive(function(...args) {
return f.apply(this, args.reverse());
}, f);
exports.flip = flip;
// Takes field `name` and `target` and returns value of that field.
// If `target` is `null` or `undefined` it would be returned back
// instead of attempt to access it's field. Function is implicitly
// curried, this allows accessor function generation by calling it
// with only `name` argument.
const field = curry((name, target) =>
// Note: Permisive `==` is intentional.
target == null ? target : target[name]);
exports.field = field;
// Takes `.` delimited string representing `path` to a nested field
// and a `target` to get it from. For convinience function is
// implicitly curried, there for accessors can be created by invoking
// it with just a `path` argument.
const query = curry((path, target) => {
const names = path.split(".");
const count = names.length;
let index = 0;
let result = target;
// Note: Permisive `!=` is intentional.
while (result != null && index < count) {
result = result[names[index]];
index = index + 1;
}
return result;
});
exports.query = query;
// Takes `Type` (constructor function) and a `value` and returns
// `true` if `value` is instance of the given `Type`. Function is
// implicitly curried this allows predicate generation by calling
// function with just first argument.
const isInstance = curry((Type, value) => value instanceof Type);
exports.isInstance = isInstance;
/*
* Takes a funtion and returns a wrapped function that returns `this`
*/
const chainable = f => derive(function(...args) {
f.apply(this, args);
return this;
}, f);
exports.chainable = chainable;
// Functions takes `expected` and `actual` values and returns `true` if
// `expected === actual`. Returns curried function if called with less then
// two arguments.
//
// [ 1, 0, 1, 0, 1 ].map(is(1)) // => [ true, false, true, false, true ]
const is = curry((expected, actual) => actual === expected);
exports.is = is;
const isnt = complement(is);
exports.isnt = isnt;

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/. */
// Disclaimer: Some of the functions in this module implement APIs from
// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for
// those goes to him.
"use strict";
module.metadata = {
"stability": "unstable"
}
const arity = f => f.arity || f.length;
exports.arity = arity;
const name = f => f.displayName || f.name;
exports.name = name;
const derive = (f, source) => {
f.displayName = name(source);
f.arity = arity(source);
return f;
};
exports.derive = derive;
const invoke = (callee, params, self) => callee.apply(self, params);
exports.invoke = invoke;

View file

@ -0,0 +1,388 @@
/* 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"
};
/**
* Returns `true` if `value` is `undefined`.
* @examples
* var foo; isUndefined(foo); // true
* isUndefined(0); // false
*/
function isUndefined(value) {
return value === undefined;
}
exports.isUndefined = isUndefined;
/**
* Returns `true` if value is `null`.
* @examples
* isNull(null); // true
* isNull(undefined); // false
*/
function isNull(value) {
return value === null;
}
exports.isNull = isNull;
/**
* Returns `true` if value is `null` or `undefined`.
* It's equivalent to `== null`, but resolve the ambiguity of the writer
* intention, makes clear that he's clearly checking both `null` and `undefined`
* values, and it's not a typo for `=== null`.
*/
function isNil(value) {
return value === null || value === undefined;
}
exports.isNil = isNil;
function isBoolean(value) {
return typeof value === "boolean";
}
exports.isBoolean = isBoolean;
/**
* Returns `true` if value is a string.
* @examples
* isString("moe"); // true
*/
function isString(value) {
return typeof value === "string";
}
exports.isString = isString;
/**
* Returns `true` if `value` is a number.
* @examples
* isNumber(8.4 * 5); // true
*/
function isNumber(value) {
return typeof value === "number";
}
exports.isNumber = isNumber;
/**
* Returns `true` if `value` is a `RegExp`.
* @examples
* isRegExp(/moe/); // true
*/
function isRegExp(value) {
return isObject(value) && instanceOf(value, RegExp);
}
exports.isRegExp = isRegExp;
/**
* Returns true if `value` is a `Date`.
* @examples
* isDate(new Date()); // true
*/
function isDate(value) {
return isObject(value) && instanceOf(value, Date);
}
exports.isDate = isDate;
/**
* Returns true if object is a Function.
* @examples
* isFunction(function foo(){}) // true
*/
function isFunction(value) {
return typeof value === "function";
}
exports.isFunction = isFunction;
/**
* Returns `true` if `value` is an object (please note that `null` is considered
* to be an atom and not an object).
* @examples
* isObject({}) // true
* isObject(null) // false
*/
function isObject(value) {
return typeof value === "object" && value !== null;
}
exports.isObject = isObject;
/**
* Detect whether a value is a generator.
*
* @param aValue
* The value to identify.
* @return A boolean indicating whether the value is a generator.
*/
function isGenerator(aValue) {
return !!(aValue && aValue.isGenerator && aValue.isGenerator());
}
exports.isGenerator = isGenerator;
/**
* Returns true if `value` is an Array.
* @examples
* isArray([1, 2, 3]) // true
* isArray({ 0: 'foo', length: 1 }) // false
*/
var isArray = Array.isArray;
exports.isArray = isArray;
/**
* Returns `true` if `value` is an Arguments object.
* @examples
* (function(){ return isArguments(arguments); })(1, 2, 3); // true
* isArguments([1,2,3]); // false
*/
function isArguments(value) {
return Object.prototype.toString.call(value) === "[object Arguments]";
}
exports.isArguments = isArguments;
var isMap = value => Object.prototype.toString.call(value) === "[object Map]"
exports.isMap = isMap;
var isSet = value => Object.prototype.toString.call(value) === "[object Set]"
exports.isSet = isSet;
/**
* Returns true if it is a primitive `value`. (null, undefined, number,
* boolean, string)
* @examples
* isPrimitive(3) // true
* isPrimitive('foo') // true
* isPrimitive({ bar: 3 }) // false
*/
function isPrimitive(value) {
return !isFunction(value) && !isObject(value);
}
exports.isPrimitive = isPrimitive;
/**
* Returns `true` if given `object` is flat (it is direct decedent of
* `Object.prototype` or `null`).
* @examples
* isFlat({}) // true
* isFlat(new Type()) // false
*/
function isFlat(object) {
return isObject(object) && (isNull(Object.getPrototypeOf(object)) ||
isNull(Object.getPrototypeOf(
Object.getPrototypeOf(object))));
}
exports.isFlat = isFlat;
/**
* Returns `true` if object contains no values.
*/
function isEmpty(object) {
if (isObject(object)) {
for (var key in object)
return false;
return true;
}
return false;
}
exports.isEmpty = isEmpty;
/**
* Returns `true` if `value` is an array / flat object containing only atomic
* values and other flat objects.
*/
function isJSON(value, visited) {
// Adding value to array of visited values.
(visited || (visited = [])).push(value);
// If `value` is an atom return `true` cause it's valid JSON.
return isPrimitive(value) ||
// If `value` is an array of JSON values that has not been visited
// yet.
(isArray(value) && value.every(function(element) {
return isJSON(element, visited);
})) ||
// If `value` is a plain object containing properties with a JSON
// values it's a valid JSON.
(isFlat(value) && Object.keys(value).every(function(key) {
var $ = Object.getOwnPropertyDescriptor(value, key);
// Check every proprety of a plain object to verify that
// it's neither getter nor setter, but a JSON value, that
// has not been visited yet.
return ((!isObject($.value) || !~visited.indexOf($.value)) &&
!('get' in $) && !('set' in $) &&
isJSON($.value, visited));
}));
}
exports.isJSON = function (value) {
return isJSON(value);
};
/**
* Returns `true` if `value` is JSONable
*/
const isJSONable = (value) => {
try {
JSON.parse(JSON.stringify(value));
}
catch (e) {
return false;
}
return true;
};
exports.isJSONable = isJSONable;
/**
* Returns if `value` is an instance of a given `Type`. This is exactly same as
* `value instanceof Type` with a difference that `Type` can be from a scope
* that has a different top level object. (Like in case where `Type` is a
* function from different iframe / jetpack module / sandbox).
*/
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;
}
exports.instanceOf = instanceOf;
/**
* Function returns textual representation of a value passed to it. Function
* takes additional `indent` argument that is used for indentation. Also
* optional `limit` argument may be passed to limit amount of detail returned.
* @param {Object} value
* @param {String} [indent=" "]
* @param {Number} [limit]
*/
function source(value, indent, limit, offset, visited) {
var result;
var names;
var nestingIndex;
var isCompact = !isUndefined(limit);
indent = indent || " ";
offset = (offset || "");
result = "";
visited = visited || [];
if (isUndefined(value)) {
result += "undefined";
}
else if (isNull(value)) {
result += "null";
}
else if (isString(value)) {
result += '"' + value + '"';
}
else if (isFunction(value)) {
value = String(value).split("\n");
if (isCompact && value.length > 2) {
value = value.splice(0, 2);
value.push("...}");
}
result += value.join("\n" + offset);
}
else if (isArray(value)) {
if ((nestingIndex = (visited.indexOf(value) + 1))) {
result = "#" + nestingIndex + "#";
}
else {
visited.push(value);
if (isCompact)
value = value.slice(0, limit);
result += "[\n";
result += value.map(function(value) {
return offset + indent + source(value, indent, limit, offset + indent,
visited);
}).join(",\n");
result += isCompact && value.length > limit ?
",\n" + offset + "...]" : "\n" + offset + "]";
}
}
else if (isObject(value)) {
if ((nestingIndex = (visited.indexOf(value) + 1))) {
result = "#" + nestingIndex + "#"
}
else {
visited.push(value)
names = Object.keys(value);
result += "{ // " + value + "\n";
result += (isCompact ? names.slice(0, limit) : names).map(function(name) {
var _limit = isCompact ? limit - 1 : limit;
var descriptor = Object.getOwnPropertyDescriptor(value, name);
var result = offset + indent + "// ";
var accessor;
if (0 <= name.indexOf(" "))
name = '"' + name + '"';
if (descriptor.writable)
result += "writable ";
if (descriptor.configurable)
result += "configurable ";
if (descriptor.enumerable)
result += "enumerable ";
result += "\n";
if ("value" in descriptor) {
result += offset + indent + name + ": ";
result += source(descriptor.value, indent, _limit, indent + offset,
visited);
}
else {
if (descriptor.get) {
result += offset + indent + "get " + name + " ";
accessor = source(descriptor.get, indent, _limit, indent + offset,
visited);
result += accessor.substr(accessor.indexOf("{"));
}
if (descriptor.set) {
result += offset + indent + "set " + name + " ";
accessor = source(descriptor.set, indent, _limit, indent + offset,
visited);
result += accessor.substr(accessor.indexOf("{"));
}
}
return result;
}).join(",\n");
if (isCompact) {
if (names.length > limit && limit > 0) {
result += ",\n" + offset + indent + "//...";
}
}
else {
if (names.length)
result += ",";
result += "\n" + offset + indent + '"__proto__": ';
result += source(Object.getPrototypeOf(value), indent, 0,
offset + indent);
}
result += "\n" + offset + "}";
}
}
else {
result += String(value);
}
return result;
}
exports.source = function (value, indentation, limit) {
return source(value, indentation, limit);
};

View file

@ -0,0 +1,75 @@
/* 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"
};
"use strict";
const { Cu } = require("chrome");
function makeGetterFor(Type) {
let cache = new WeakMap();
return {
getFor(target) {
if (!cache.has(target))
cache.set(target, new Type());
return cache.get(target);
},
clearFor(target) {
return cache.delete(target)
}
}
}
var {getFor: getLookupFor, clearFor: clearLookupFor} = makeGetterFor(WeakMap);
var {getFor: getRefsFor, clearFor: clearRefsFor} = makeGetterFor(Set);
function add(target, value) {
if (has(target, value))
return;
getLookupFor(target).set(value, true);
getRefsFor(target).add(Cu.getWeakReference(value));
}
exports.add = add;
function remove(target, value) {
getLookupFor(target).delete(value);
}
exports.remove = remove;
function has(target, value) {
return getLookupFor(target).has(value);
}
exports.has = has;
function clear(target) {
clearLookupFor(target);
clearRefsFor(target);
}
exports.clear = clear;
function iterator(target) {
let refs = getRefsFor(target);
for (let ref of refs) {
let value = ref.get();
// If `value` is already gc'ed, it would be `null`.
// The `has` function is using a WeakMap as lookup table, so passing `null`
// would raise an exception because WeakMap accepts as value only non-null
// object.
// Plus, if `value` is already gc'ed, we do not have to take it in account
// during the iteration, and remove it from the references.
if (value !== null && has(target, value))
yield value;
else
refs.delete(ref);
}
}
exports.iterator = iterator;

View file

@ -0,0 +1,102 @@
/* 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"
};
// This module is manually loaded by bootstrap.js in a sandbox and immediatly
// put in module cache so that it is never loaded in any other way.
/* Workarounds to include dependencies in the manifest
require('chrome') // Otherwise CFX will complain about Components
require('toolkit/loader') // Otherwise CFX will stip out loader.js
require('sdk/addon/runner') // Otherwise CFX will stip out addon/runner.js
*/
const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
// `loadSandbox` is exposed by bootstrap.js
const loaderURI = module.uri.replace("sdk/loader/cuddlefish.js",
"toolkit/loader.js");
const xulappURI = module.uri.replace("loader/cuddlefish.js",
"system/xul-app.jsm");
// We need to keep a reference to the sandbox in order to unload it in
// bootstrap.js
var loaderSandbox = loadSandbox(loaderURI);
const loaderModule = loaderSandbox.exports;
const { incompatibility } = Cu.import(xulappURI, {}).XulApp;
const { override, load } = loaderModule;
function CuddlefishLoader(options) {
let { manifest } = options;
options = override(options, {
// Put `api-utils/loader` and `api-utils/cuddlefish` loaded as JSM to module
// cache to avoid subsequent loads via `require`.
modules: override({
'toolkit/loader': loaderModule,
'sdk/loader/cuddlefish': exports
}, options.modules),
resolve: function resolve(id, requirer) {
let entry = requirer && requirer in manifest && manifest[requirer];
let uri = null;
// If manifest entry for this requirement is present we follow manifest.
// Note: Standard library modules like 'panel' will be present in
// manifest unless they were moved to platform.
if (entry) {
let requirement = entry.requirements[id];
// If requirer entry is in manifest and it's requirement is not, than
// it has no authority to load since linker was not able to find it.
if (!requirement)
throw Error('Module: ' + requirer + ' has no authority to load: '
+ id, requirer);
uri = requirement;
} else {
// If requirer is off manifest than it's a system module and we allow it
// to go off manifest by resolving a relative path.
uri = loaderModule.resolve(id, requirer);
}
return uri;
},
load: function(loader, module) {
let result;
let error;
// In order to get the module's metadata, we need to load the module.
// if an exception is raised here, it could be that is due to application
// incompatibility. Therefore the exception is stored, and thrown again
// only if the module seems be compatible with the application currently
// running. Otherwise the incompatibility message takes the precedence.
try {
result = load(loader, module);
}
catch (e) {
error = e;
}
error = incompatibility(module) || error;
if (error)
throw error;
return result;
}
});
let loader = loaderModule.Loader(options);
// Hack to allow loading from `toolkit/loader`.
loader.modules[loaderURI] = loaderSandbox;
return loader;
}
exports = override(loaderModule, {
Loader: CuddlefishLoader
});

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": "experimental"
};
const { Cc, Ci, CC, Cu } = require('chrome');
const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1'].
getService(Ci.mozIJSSubScriptLoader);
const self = require('sdk/self');
const { getTabId } = require('../tabs/utils');
const { getInnerId } = require('../window/utils');
const { devtools } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const { require: devtoolsRequire } = devtools;
const { addContentGlobal, removeContentGlobal } = devtoolsRequire("devtools/server/content-globals");
/**
* Make a new sandbox that inherits given `source`'s principals. Source can be
* URI string, DOMWindow or `null` for system principals.
*/
function sandbox(target, options) {
options = options || {};
options.metadata = options.metadata ? options.metadata : {};
options.metadata.addonID = options.metadata.addonID ?
options.metadata.addonID : self.id;
let sandbox = Cu.Sandbox(target || systemPrincipal, options);
Cu.setSandboxMetadata(sandbox, options.metadata);
let innerWindowID = options.metadata['inner-window-id']
if (innerWindowID) {
addContentGlobal({
global: sandbox,
'inner-window-id': innerWindowID
});
}
return sandbox;
}
exports.sandbox = sandbox;
/**
* Evaluates given `source` in a given `sandbox` and returns result.
*/
function evaluate(sandbox, code, uri, line, version) {
return Cu.evalInSandbox(code, sandbox, version || '1.8', uri || '', line || 1);
}
exports.evaluate = evaluate;
/**
* Evaluates code under the given `uri` in the given `sandbox`.
*
* @param {String} uri
* The URL pointing to the script to load.
* It must be a local chrome:, resource:, file: or data: URL.
*/
function load(sandbox, uri) {
if (uri.indexOf('data:') === 0) {
let source = uri.substr(uri.indexOf(',') + 1);
return evaluate(sandbox, decodeURIComponent(source), '1.8', uri, 0);
} else {
return scriptLoader.loadSubScript(uri, sandbox, 'UTF-8');
}
}
exports.load = load;
/**
* Forces the given `sandbox` to be freed immediately.
*/
exports.nuke = Cu.nukeSandbox

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": "unstable"
};
const { window } = require("sdk/addon/window");
exports.MessageChannel = window.MessageChannel;
exports.MessagePort = window.MessagePort;

View file

@ -0,0 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "unstable"
};
const { dispatcher } = require("../util/dispatcher");
// Define `modelFor` accessor function that can be implemented
// for different types of views. Since view's we'll be dealing
// with types that don't really play well with `instanceof`
// operator we're gonig to use `dispatcher` that is slight
// extension over polymorphic dispatch provided by method.
// This allows models to extend implementations of this by
// providing predicates:
//
// modelFor.when($ => $ && $.nodeName === "tab", findTabById($.id))
const modelFor = dispatcher("modelFor");
exports.modelFor = modelFor;

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";
module.metadata = {
"stability": "experimental"
};
const { Ci, Cu, components } = require("chrome");
const { defer } = require("../core/promise");
const { merge } = require("../util/object");
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
/**
* Reads a URI and returns a promise.
*
* @param uri {string} The URI to read
* @param [options] {object} This parameter can have any or all of the following
* fields: `charset`. By default the `charset` is set to 'UTF-8'.
*
* @returns {promise} The promise that will be resolved with the content of the
* URL given.
*
* @example
* let promise = readURI('resource://gre/modules/NetUtil.jsm', {
* charset: 'US-ASCII'
* });
*/
function readURI(uri, options) {
options = options || {};
let charset = options.charset || 'UTF-8';
let channel = NetUtil.newChannel({
uri: NetUtil.newURI(uri, charset),
loadUsingSystemPrincipal: true});
let { promise, resolve, reject } = defer();
try {
NetUtil.asyncFetch(channel, function (stream, result) {
if (components.isSuccessCode(result)) {
let count = stream.available();
let data = NetUtil.readInputStreamToString(stream, count, { charset : charset });
resolve(data);
} else {
reject("Failed to read: '" + uri + "' (Error Code: " + result + ")");
}
});
}
catch (e) {
reject("Failed to read: '" + uri + "' (Error: " + e.message + ")");
}
return promise;
}
exports.readURI = readURI;
/**
* Reads a URI synchronously.
* This function is intentionally undocumented to favorites the `readURI` usage.
*
* @param uri {string} The URI to read
* @param [charset] {string} The character set to use when read the content of
* the `uri` given. By default is set to 'UTF-8'.
*
* @returns {string} The content of the URI given.
*
* @example
* let data = readURISync('resource://gre/modules/NetUtil.jsm');
*/
function readURISync(uri, charset) {
charset = typeof charset === "string" ? charset : "UTF-8";
let channel = NetUtil.newChannel({
uri: NetUtil.newURI(uri, charset),
loadUsingSystemPrincipal: true});
let stream = channel.open2();
let count = stream.available();
let data = NetUtil.readInputStreamToString(stream, count, { charset : charset });
stream.close();
return data;
}
exports.readURISync = readURISync;

View file

@ -0,0 +1,36 @@
/* 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 { deprecateFunction } = require("../util/deprecate");
const { Cc, Ci } = require("chrome");
const XMLHttpRequest = require("../addon/window").window.XMLHttpRequest;
Object.defineProperties(XMLHttpRequest.prototype, {
mozBackgroundRequest: {
value: true,
},
forceAllowThirdPartyCookie: {
configurable: true,
value: deprecateFunction(function() {
forceAllowThirdPartyCookie(this);
}, "`xhr.forceAllowThirdPartyCookie()` is deprecated, please use" +
"`require('sdk/net/xhr').forceAllowThirdPartyCookie(request)` instead")
}
});
exports.XMLHttpRequest = XMLHttpRequest;
function forceAllowThirdPartyCookie(xhr) {
if (xhr.channel instanceof Ci.nsIHttpChannelInternal)
xhr.channel.forceAllowThirdPartyCookie = true;
}
exports.forceAllowThirdPartyCookie = forceAllowThirdPartyCookie;
// No need to handle add-on unloads as addon/window is closed at unload
// and it will take down all the associated requests.

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/. */
"use strict";
module.metadata = {
"stability": "stable"
};
const { Cc, Ci, Cr } = require("chrome");
const apiUtils = require("./deprecated/api-utils");
const { isString, isUndefined, instanceOf } = require('./lang/type');
const { URL, isLocalURL } = require('./url');
const { data } = require('./self');
const NOTIFICATION_DIRECTIONS = ["auto", "ltr", "rtl"];
try {
let alertServ = Cc["@mozilla.org/alerts-service;1"].
getService(Ci.nsIAlertsService);
// The unit test sets this to a mock notification function.
var notify = alertServ.showAlertNotification.bind(alertServ);
}
catch (err) {
// An exception will be thrown if the platform doesn't provide an alert
// service, e.g., if Growl is not installed on OS X. In that case, use a
// mock notification function that just logs to the console.
notify = notifyUsingConsole;
}
exports.notify = function notifications_notify(options) {
let valOpts = validateOptions(options);
let clickObserver = !valOpts.onClick ? null : {
observe: (subject, topic, data) => {
if (topic === "alertclickcallback") {
try {
valOpts.onClick.call(exports, valOpts.data);
}
catch(e) {
console.exception(e);
}
}
}
};
function notifyWithOpts(notifyFn) {
let { iconURL } = valOpts;
iconURL = iconURL && isLocalURL(iconURL) ? data.url(iconURL) : iconURL;
notifyFn(iconURL, valOpts.title, valOpts.text, !!clickObserver,
valOpts.data, clickObserver, valOpts.tag, valOpts.dir, valOpts.lang);
}
try {
notifyWithOpts(notify);
}
catch (err) {
if (err instanceof Ci.nsIException && err.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
console.warn("The notification icon named by " + iconURL +
" does not exist. A default icon will be used instead.");
delete valOpts.iconURL;
notifyWithOpts(notify);
}
else {
notifyWithOpts(notifyUsingConsole);
}
}
};
function notifyUsingConsole(iconURL, title, text) {
title = title ? "[" + title + "]" : "";
text = text || "";
let str = [title, text].filter(s => s).join(" ");
console.log(str);
}
function validateOptions(options) {
return apiUtils.validateOptions(options, {
data: {
is: ["string", "undefined"]
},
iconURL: {
is: ["string", "undefined", "object"],
ok: function(value) {
return isUndefined(value) || isString(value) || (value instanceof URL);
},
msg: "`iconURL` must be a string or an URL instance."
},
onClick: {
is: ["function", "undefined"]
},
text: {
is: ["string", "undefined", "number"]
},
title: {
is: ["string", "undefined", "number"]
},
tag: {
is: ["string", "undefined", "number"]
},
dir: {
is: ["string", "undefined"],
ok: function(value) {
return isUndefined(value) || ~NOTIFICATION_DIRECTIONS.indexOf(value);
},
msg: '`dir` option must be one of: "auto", "ltr" or "rtl".'
},
lang: {
is: ["string", "undefined"]
}
});
}

View file

@ -0,0 +1,71 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cc, Ci, Cr } = require("chrome");
const { Input, start, stop, receive, outputs } = require("../event/utils");
const { id: addonID } = require("../self");
const { setImmediate } = require("../timers");
const { notifyObservers } = Cc['@mozilla.org/observer-service;1'].
getService(Ci.nsIObserverService);
const NOT_AN_INPUT = "OutputPort can be used only for sending messages";
// `OutputPort` creates a port to which messages can be send. Those
// messages are actually disptached as `subject`'s of the observer
// notifications. This is handy for communicating between different
// components of the SDK. By default messages are dispatched
// asynchronously, although `options.sync` can be used to make them
// synchronous. If `options.id` is given `topic` for observer
// notifications is generated by namespacing it, to avoid spamming
// other SDK add-ons. It's also possible to provide `options.topic`
// to use excat `topic` without namespacing it.
//
// Note: Symmetric `new InputPort({ id: "x" })` instances can be used to
// receive messages send to the instances of `new OutputPort({ id: "x" })`.
const OutputPort = function({id, topic, sync}) {
this.id = id || topic;
this.sync = !!sync;
this.topic = topic || "sdk:" + addonID + ":" + id;
};
// OutputPort extends base signal type to implement same message
// receiving interface.
OutputPort.prototype = new Input();
OutputPort.constructor = OutputPort;
// OutputPort can not be consumed there for starting or stopping it
// is not supported.
OutputPort.prototype[start] = _ => { throw TypeError(NOT_AN_INPUT); };
OutputPort.prototype[stop] = _ => { throw TypeError(NOT_AN_INPUT); };
// Port reecives message send to it, which will be dispatched via
// observer notification service.
OutputPort.receive = ({topic, sync}, message) => {
const type = typeof(message);
const supported = message === null ||
type === "object" ||
type === "function";
// There is no sensible way to wrap JS primitives that would make sense
// for general observer notification users. It's also probably not very
// useful to dispatch JS primitives as subject of observer service, there
// for we do not support those use cases.
if (!supported)
throw new TypeError("Unsupproted message type: `" + type + "`");
// Normalize `message` to create a valid observer notification `subject`.
// If `message` is `null`, implements `nsISupports` interface or already
// represents wrapped JS object use it as is. Otherwise create a wrapped
// object so that observers could receive it.
const subject = message === null ? null :
message instanceof Ci.nsISupports ? message :
message.wrappedJSObject ? message :
{wrappedJSObject: message};
if (sync)
notifyObservers(subject, topic, null);
else
setImmediate(notifyObservers, subject, topic, null);
};
OutputPort.prototype[receive] = OutputPort.receive;
exports.OutputPort = OutputPort;

View file

@ -0,0 +1,190 @@
/* 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 { contract: loaderContract } = require('./content/loader');
const { contract } = require('./util/contract');
const { WorkerHost, connect } = require('./content/utils');
const { Class } = require('./core/heritage');
const { Disposable } = require('./core/disposable');
const { Worker } = require('./content/worker');
const { EventTarget } = require('./event/target');
const { on, emit, once, setListeners } = require('./event/core');
const { isRegExp, isUndefined } = require('./lang/type');
const { merge, omit } = require('./util/object');
const { remove, has, hasAny } = require("./util/array");
const { Rules } = require("./util/rules");
const { processes, frames, remoteRequire } = require('./remote/parent');
remoteRequire('sdk/content/page-mod');
const pagemods = new Map();
const workers = new Map();
const models = new WeakMap();
var modelFor = (mod) => models.get(mod);
var workerFor = (mod) => workers.get(mod)[0];
// Helper functions
var isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string';
var PAGEMOD_ID = 0;
// Validation Contracts
const modOptions = {
// contentStyle* / contentScript* are sharing the same validation constraints,
// so they can be mostly reused, except for the messages.
contentStyle: merge(Object.create(loaderContract.rules.contentScript), {
msg: 'The `contentStyle` option must be a string or an array of strings.'
}),
contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), {
msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
}),
include: {
is: ['string', 'array', 'regexp'],
ok: (rule) => {
if (isRegExpOrString(rule))
return true;
if (Array.isArray(rule) && rule.length > 0)
return rule.every(isRegExpOrString);
return false;
},
msg: 'The `include` option must always contain atleast one rule as a string, regular expression, or an array of strings and regular expressions.'
},
exclude: {
is: ['string', 'array', 'regexp', 'undefined'],
ok: (rule) => {
if (isRegExpOrString(rule) || isUndefined(rule))
return true;
if (Array.isArray(rule) && rule.length > 0)
return rule.every(isRegExpOrString);
return false;
},
msg: 'If set, the `exclude` option must always contain at least one ' +
'rule as a string, regular expression, or an array of strings and ' +
'regular expressions.'
},
attachTo: {
is: ['string', 'array', 'undefined'],
map: function (attachTo) {
if (!attachTo) return ['top', 'frame'];
if (typeof attachTo === 'string') return [attachTo];
return attachTo;
},
ok: function (attachTo) {
return hasAny(attachTo, ['top', 'frame']) &&
attachTo.every(has.bind(null, ['top', 'frame', 'existing']));
},
msg: 'The `attachTo` option must be a string or an array of strings. ' +
'The only valid options are "existing", "top" and "frame", and must ' +
'contain at least "top" or "frame" values.'
},
};
const modContract = contract(merge({}, loaderContract.rules, modOptions));
/**
* PageMod constructor (exported below).
* @constructor
*/
const PageMod = Class({
implements: [
modContract.properties(modelFor),
EventTarget,
Disposable,
],
extends: WorkerHost(workerFor),
setup: function PageMod(options) {
let mod = this;
let model = modContract(options);
models.set(this, model);
model.id = PAGEMOD_ID++;
let include = model.include;
model.include = Rules();
model.include.add.apply(model.include, [].concat(include));
let exclude = isUndefined(model.exclude) ? [] : model.exclude;
model.exclude = Rules();
model.exclude.add.apply(model.exclude, [].concat(exclude));
// Set listeners on {PageMod} itself, not the underlying worker,
// like `onMessage`, as it'll get piped.
setListeners(this, options);
pagemods.set(model.id, this);
workers.set(this, []);
function serializeRules(rules) {
for (let rule of rules) {
yield isRegExp(rule) ? { type: "regexp", pattern: rule.source, flags: rule.flags }
: { type: "string", value: rule };
}
}
model.childOptions = omit(model, ["include", "exclude", "contentScriptOptions"]);
model.childOptions.include = [...serializeRules(model.include)];
model.childOptions.exclude = [...serializeRules(model.exclude)];
model.childOptions.contentScriptOptions = model.contentScriptOptions ?
JSON.stringify(model.contentScriptOptions) :
null;
processes.port.emit('sdk/page-mod/create', model.childOptions);
},
dispose: function(reason) {
processes.port.emit('sdk/page-mod/destroy', modelFor(this).id);
pagemods.delete(modelFor(this).id);
workers.delete(this);
},
destroy: function(reason) {
// Explicit destroy call, i.e. not via unload so destroy the workers
let list = workers.get(this);
if (!list)
return;
// Triggers dispose which will cause the child page-mod to be destroyed
Disposable.prototype.destroy.call(this, reason);
// Destroy any active workers
for (let worker of list)
worker.destroy(reason);
}
});
exports.PageMod = PageMod;
// Whenever a new process starts send over the list of page-mods
processes.forEvery(process => {
for (let mod of pagemods.values())
process.port.emit('sdk/page-mod/create', modelFor(mod).childOptions);
});
frames.port.on('sdk/page-mod/worker-create', (frame, modId, workerOptions) => {
let mod = pagemods.get(modId);
if (!mod)
return;
// Attach the parent side of the worker to the child
let worker = Worker();
workers.get(mod).unshift(worker);
worker.on('*', (event, ...args) => {
// page-mod's "attach" event needs to be passed a worker
if (event === 'attach')
emit(mod, event, worker)
else
emit(mod, event, ...args);
});
worker.on('detach', () => {
let array = workers.get(mod);
if (array)
remove(array, worker);
});
connect(worker, frame, workerOptions);
});

View file

@ -0,0 +1,10 @@
/* 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 { deprecateUsage } = require("../util/deprecate");
deprecateUsage("Module 'sdk/page-mod/match-pattern' is deprecated use 'sdk/util/match-pattern' instead");
module.exports = require("../util/match-pattern");

View file

@ -0,0 +1,194 @@
/* 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 { Class } = require('./core/heritage');
const { ns } = require('./core/namespace');
const { pipe, stripListeners } = require('./event/utils');
const { connect, destroy, WorkerHost } = require('./content/utils');
const { Worker } = require('./content/worker');
const { Disposable } = require('./core/disposable');
const { EventTarget } = require('./event/target');
const { setListeners } = require('./event/core');
const { window } = require('./addon/window');
const { create: makeFrame, getDocShell } = require('./frame/utils');
const { contract } = require('./util/contract');
const { contract: loaderContract } = require('./content/loader');
const { Rules } = require('./util/rules');
const { merge } = require('./util/object');
const { uuid } = require('./util/uuid');
const { useRemoteProcesses, remoteRequire, frames } = require("./remote/parent");
remoteRequire("sdk/content/page-worker");
const workers = new WeakMap();
const pages = new Map();
const internal = ns();
let workerFor = (page) => workers.get(page);
let isDisposed = (page) => !pages.has(internal(page).id);
// The frame is used to ensure we have a remote process to load workers in
let remoteFrame = null;
let framePromise = null;
function getFrame() {
if (framePromise)
return framePromise;
framePromise = new Promise(resolve => {
let view = makeFrame(window.document, {
namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
nodeName: "iframe",
type: "content",
remote: useRemoteProcesses,
uri: "about:blank"
});
// Wait for the remote side to connect
let listener = (frame) => {
if (frame.frameElement != view)
return;
frames.off("attach", listener);
remoteFrame = frame;
resolve(frame);
}
frames.on("attach", listener);
});
return framePromise;
}
var pageContract = contract(merge({
allow: {
is: ['object', 'undefined', 'null'],
map: function (allow) { return { script: !allow || allow.script !== false }}
},
onMessage: {
is: ['function', 'undefined']
},
include: {
is: ['string', 'array', 'regexp', 'undefined']
},
contentScriptWhen: {
is: ['string', 'undefined'],
map: (when) => when || "end"
}
}, loaderContract.rules));
function enableScript (page) {
getDocShell(viewFor(page)).allowJavascript = true;
}
function disableScript (page) {
getDocShell(viewFor(page)).allowJavascript = false;
}
function Allow (page) {
return {
get script() {
return internal(page).options.allow.script;
},
set script(value) {
internal(page).options.allow.script = value;
if (isDisposed(page))
return;
remoteFrame.port.emit("sdk/frame/set", internal(page).id, { allowScript: value });
}
};
}
function isValidURL(page, url) {
return !page.rules || page.rules.matchesAny(url);
}
const Page = Class({
implements: [
EventTarget,
Disposable
],
extends: WorkerHost(workerFor),
setup: function Page(options) {
options = pageContract(options);
// Sanitize the options
if ("contentScriptOptions" in options)
options.contentScriptOptions = JSON.stringify(options.contentScriptOptions);
internal(this).id = uuid().toString();
internal(this).options = options;
for (let prop of ['contentScriptFile', 'contentScript', 'contentScriptWhen']) {
this[prop] = options[prop];
}
pages.set(internal(this).id, this);
// Set listeners on the {Page} object itself, not the underlying worker,
// like `onMessage`, as it gets piped
setListeners(this, options);
let worker = new Worker(stripListeners(options));
workers.set(this, worker);
pipe(worker, this);
if (options.include) {
this.rules = Rules();
this.rules.add.apply(this.rules, [].concat(options.include));
}
getFrame().then(frame => {
if (isDisposed(this))
return;
frame.port.emit("sdk/frame/create", internal(this).id, stripListeners(options));
});
},
get allow() { return Allow(this); },
set allow(value) {
if (isDisposed(this))
return;
this.allow.script = pageContract({ allow: value }).allow.script;
},
get contentURL() {
return internal(this).options.contentURL;
},
set contentURL(value) {
if (!isValidURL(this, value))
return;
internal(this).options.contentURL = value;
if (isDisposed(this))
return;
remoteFrame.port.emit("sdk/frame/set", internal(this).id, { contentURL: value });
},
dispose: function () {
if (isDisposed(this))
return;
pages.delete(internal(this).id);
let worker = workerFor(this);
if (worker)
destroy(worker);
remoteFrame.port.emit("sdk/frame/destroy", internal(this).id);
// Destroy the remote frame if all the pages have been destroyed
if (pages.size == 0) {
framePromise = null;
remoteFrame.frameElement.remove();
remoteFrame = null;
}
},
toString: function () { return '[object Page]' }
});
exports.Page = Page;
frames.port.on("sdk/frame/connect", (frame, id, params) => {
let page = pages.get(id);
if (!page)
return;
connect(workerFor(page), frame, params);
});

View file

@ -0,0 +1,427 @@
/* 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";
// The panel module currently supports only Firefox and SeaMonkey.
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps
module.metadata = {
"stability": "stable",
"engines": {
"Firefox": "*",
"SeaMonkey": "*"
}
};
const { Cu, Ci } = require("chrome");
const { setTimeout } = require('./timers');
const { Class } = require("./core/heritage");
const { merge } = require("./util/object");
const { WorkerHost } = require("./content/utils");
const { Worker } = require("./deprecated/sync-worker");
const { Disposable } = require("./core/disposable");
const { WeakReference } = require('./core/reference');
const { contract: loaderContract } = require("./content/loader");
const { contract } = require("./util/contract");
const { on, off, emit, setListeners } = require("./event/core");
const { EventTarget } = require("./event/target");
const domPanel = require("./panel/utils");
const { getDocShell } = require('./frame/utils');
const { events } = require("./panel/events");
const systemEvents = require("./system/events");
const { filter, pipe, stripListeners } = require("./event/utils");
const { getNodeView, getActiveView } = require("./view/core");
const { isNil, isObject, isNumber } = require("./lang/type");
const { getAttachEventType } = require("./content/utils");
const { number, boolean, object } = require('./deprecated/api-utils');
const { Style } = require("./stylesheet/style");
const { attach, detach } = require("./content/mod");
var isRect = ({top, right, bottom, left}) => [top, right, bottom, left].
some(value => isNumber(value) && !isNaN(value));
var isSDKObj = obj => obj instanceof Class;
var rectContract = contract({
top: number,
right: number,
bottom: number,
left: number
});
var position = {
is: object,
map: v => (isNil(v) || isSDKObj(v) || !isObject(v)) ? v : rectContract(v),
ok: v => isNil(v) || isSDKObj(v) || (isObject(v) && isRect(v)),
msg: 'The option "position" must be a SDK object registered as anchor; ' +
'or an object with one or more of the following keys set to numeric ' +
'values: top, right, bottom, left.'
}
var displayContract = contract({
width: number,
height: number,
focus: boolean,
position: position
});
var panelContract = contract(merge({
// contentStyle* / contentScript* are sharing the same validation constraints,
// so they can be mostly reused, except for the messages.
contentStyle: merge(Object.create(loaderContract.rules.contentScript), {
msg: 'The `contentStyle` option must be a string or an array of strings.'
}),
contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), {
msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
}),
contextMenu: boolean,
allow: {
is: ['object', 'undefined', 'null'],
map: function (allow) { return { script: !allow || allow.script !== false }}
},
}, displayContract.rules, loaderContract.rules));
function Allow(panel) {
return {
get script() { return getDocShell(viewFor(panel).backgroundFrame).allowJavascript; },
set script(value) { return setScriptState(panel, value); },
};
}
function setScriptState(panel, value) {
let view = viewFor(panel);
getDocShell(view.backgroundFrame).allowJavascript = value;
getDocShell(view.viewFrame).allowJavascript = value;
view.setAttribute("sdkscriptenabled", "" + value);
}
function isDisposed(panel) {
return !views.has(panel);
}
var panels = new WeakMap();
var models = new WeakMap();
var views = new WeakMap();
var workers = new WeakMap();
var styles = new WeakMap();
const viewFor = (panel) => views.get(panel);
const modelFor = (panel) => models.get(panel);
const panelFor = (view) => panels.get(view);
const workerFor = (panel) => workers.get(panel);
const styleFor = (panel) => styles.get(panel);
function getPanelFromWeakRef(weakRef) {
if (!weakRef) {
return null;
}
let panel = weakRef.get();
if (!panel) {
return null;
}
if (isDisposed(panel)) {
return null;
}
return panel;
}
var SinglePanelManager = {
visiblePanel: null,
enqueuedPanel: null,
enqueuedPanelCallback: null,
// Calls |callback| with no arguments when the panel may be shown.
requestOpen: function(panelToOpen, callback) {
let currentPanel = getPanelFromWeakRef(SinglePanelManager.visiblePanel);
if (currentPanel || SinglePanelManager.enqueuedPanel) {
SinglePanelManager.enqueuedPanel = Cu.getWeakReference(panelToOpen);
SinglePanelManager.enqueuedPanelCallback = callback;
if (currentPanel && currentPanel.isShowing) {
currentPanel.hide();
}
} else {
SinglePanelManager.notifyPanelCanOpen(panelToOpen, callback);
}
},
notifyPanelCanOpen: function(panel, callback) {
let view = viewFor(panel);
// Can't pass an arrow function as the event handler because we need to be
// able to call |removeEventListener| later.
view.addEventListener("popuphidden", SinglePanelManager.onVisiblePanelHidden, true);
view.addEventListener("popupshown", SinglePanelManager.onVisiblePanelShown, false);
SinglePanelManager.enqueuedPanel = null;
SinglePanelManager.enqueuedPanelCallback = null;
SinglePanelManager.visiblePanel = Cu.getWeakReference(panel);
callback();
},
onVisiblePanelShown: function(event) {
let panel = panelFor(event.target);
if (SinglePanelManager.enqueuedPanel) {
// Another panel started waiting for |panel| to close before |panel| was
// even done opening.
panel.hide();
}
},
onVisiblePanelHidden: function(event) {
let view = event.target;
let panel = panelFor(view);
let currentPanel = getPanelFromWeakRef(SinglePanelManager.visiblePanel);
if (currentPanel && currentPanel != panel) {
return;
}
SinglePanelManager.visiblePanel = null;
view.removeEventListener("popuphidden", SinglePanelManager.onVisiblePanelHidden, true);
view.removeEventListener("popupshown", SinglePanelManager.onVisiblePanelShown, false);
let nextPanel = getPanelFromWeakRef(SinglePanelManager.enqueuedPanel);
let nextPanelCallback = SinglePanelManager.enqueuedPanelCallback;
if (nextPanel) {
SinglePanelManager.notifyPanelCanOpen(nextPanel, nextPanelCallback);
}
}
};
const Panel = Class({
implements: [
// Generate accessors for the validated properties that update model on
// set and return values from model on get.
panelContract.properties(modelFor),
EventTarget,
Disposable,
WeakReference
],
extends: WorkerHost(workerFor),
setup: function setup(options) {
let model = merge({
defaultWidth: 320,
defaultHeight: 240,
focus: true,
position: Object.freeze({}),
contextMenu: false
}, panelContract(options));
model.ready = false;
models.set(this, model);
if (model.contentStyle || model.contentStyleFile) {
styles.set(this, Style({
uri: model.contentStyleFile,
source: model.contentStyle
}));
}
// Setup view
let viewOptions = {allowJavascript: !model.allow || (model.allow.script !== false)};
let view = domPanel.make(null, viewOptions);
panels.set(view, this);
views.set(this, view);
// Load panel content.
domPanel.setURL(view, model.contentURL);
// Allow context menu
domPanel.allowContextMenu(view, model.contextMenu);
// Setup listeners.
setListeners(this, options);
let worker = new Worker(stripListeners(options));
workers.set(this, worker);
// pipe events from worker to a panel.
pipe(worker, this);
},
dispose: function dispose() {
this.hide();
off(this);
workerFor(this).destroy();
detach(styleFor(this));
domPanel.dispose(viewFor(this));
// Release circular reference between view and panel instance. This
// way view will be GC-ed. And panel as well once all the other refs
// will be removed from it.
views.delete(this);
},
/* Public API: Panel.width */
get width() {
return modelFor(this).width;
},
set width(value) {
this.resize(value, this.height);
},
/* Public API: Panel.height */
get height() {
return modelFor(this).height;
},
set height(value) {
this.resize(this.width, value);
},
/* Public API: Panel.focus */
get focus() {
return modelFor(this).focus;
},
/* Public API: Panel.position */
get position() {
return modelFor(this).position;
},
/* Public API: Panel.contextMenu */
get contextMenu() {
return modelFor(this).contextMenu;
},
set contextMenu(allow) {
let model = modelFor(this);
model.contextMenu = panelContract({ contextMenu: allow }).contextMenu;
domPanel.allowContextMenu(viewFor(this), model.contextMenu);
},
get contentURL() {
return modelFor(this).contentURL;
},
set contentURL(value) {
let model = modelFor(this);
model.contentURL = panelContract({ contentURL: value }).contentURL;
domPanel.setURL(viewFor(this), model.contentURL);
// Detach worker so that messages send will be queued until it's
// reatached once panel content is ready.
workerFor(this).detach();
},
get allow() { return Allow(this); },
set allow(value) {
let allowJavascript = panelContract({ allow: value }).allow.script;
return setScriptState(this, value);
},
/* Public API: Panel.isShowing */
get isShowing() {
return !isDisposed(this) && domPanel.isOpen(viewFor(this));
},
/* Public API: Panel.show */
show: function show(options={}, anchor) {
SinglePanelManager.requestOpen(this, () => {
if (options instanceof Ci.nsIDOMElement) {
[anchor, options] = [options, null];
}
if (anchor instanceof Ci.nsIDOMElement) {
console.warn(
"Passing a DOM node to Panel.show() method is an unsupported " +
"feature that will be soon replaced. " +
"See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877"
);
}
let model = modelFor(this);
let view = viewFor(this);
let anchorView = getNodeView(anchor || options.position || model.position);
options = merge({
position: model.position,
width: model.width,
height: model.height,
defaultWidth: model.defaultWidth,
defaultHeight: model.defaultHeight,
focus: model.focus,
contextMenu: model.contextMenu
}, displayContract(options));
if (!isDisposed(this)) {
domPanel.show(view, options, anchorView);
}
});
return this;
},
/* Public API: Panel.hide */
hide: function hide() {
// Quit immediately if panel is disposed or there is no state change.
domPanel.close(viewFor(this));
return this;
},
/* Public API: Panel.resize */
resize: function resize(width, height) {
let model = modelFor(this);
let view = viewFor(this);
let change = panelContract({
width: width || model.width || model.defaultWidth,
height: height || model.height || model.defaultHeight
});
model.width = change.width
model.height = change.height
domPanel.resize(view, model.width, model.height);
return this;
}
});
exports.Panel = Panel;
// Note must be defined only after value to `Panel` is assigned.
getActiveView.define(Panel, viewFor);
// Filter panel events to only panels that are create by this module.
var panelEvents = filter(events, ({target}) => panelFor(target));
// Panel events emitted after panel has being shown.
var shows = filter(panelEvents, ({type}) => type === "popupshown");
// Panel events emitted after panel became hidden.
var hides = filter(panelEvents, ({type}) => type === "popuphidden");
// Panel events emitted after content inside panel is ready. For different
// panels ready may mean different state based on `contentScriptWhen` attribute.
// Weather given event represents readyness is detected by `getAttachEventType`
// helper function.
var ready = filter(panelEvents, ({type, target}) =>
getAttachEventType(modelFor(panelFor(target))) === type);
// Panel event emitted when the contents of the panel has been loaded.
var readyToShow = filter(panelEvents, ({type}) => type === "DOMContentLoaded");
// Styles should be always added as soon as possible, and doesn't makes them
// depends on `contentScriptWhen`
var start = filter(panelEvents, ({type}) => type === "document-element-inserted");
// Forward panel show / hide events to panel's own event listeners.
on(shows, "data", ({target}) => {
let panel = panelFor(target);
if (modelFor(panel).ready)
emit(panel, "show");
});
on(hides, "data", ({target}) => {
let panel = panelFor(target);
if (modelFor(panel).ready)
emit(panel, "hide");
});
on(ready, "data", ({target}) => {
let panel = panelFor(target);
let window = domPanel.getContentDocument(target).defaultView;
workerFor(panel).attach(window);
});
on(readyToShow, "data", ({target}) => {
let panel = panelFor(target);
if (!modelFor(panel).ready) {
modelFor(panel).ready = true;
if (viewFor(panel).state == "open")
emit(panel, "show");
}
});
on(start, "data", ({target}) => {
let panel = panelFor(target);
let window = domPanel.getContentDocument(target).defaultView;
attach(styleFor(panel), window);
});

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";
// This module basically translates system/events to a SDK standard events
// so that `map`, `filter` and other utilities could be used with them.
module.metadata = {
"stability": "experimental"
};
const events = require("../system/events");
const { emit } = require("../event/core");
var channel = {};
function forward({ subject, type, data }) {
return emit(channel, "data", { target: subject, type: type, data: data });
}
["popupshowing", "popuphiding", "popupshown", "popuphidden",
"document-element-inserted", "DOMContentLoaded", "load"
].forEach(type => events.on(type, forward));
exports.events = channel;

View file

@ -0,0 +1,451 @@
/* 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 { Services } = require("resource://gre/modules/Services.jsm");
const { setTimeout } = require("../timers");
const { platform } = require("../system");
const { getMostRecentBrowserWindow, getOwnerBrowserWindow,
getHiddenWindow, getScreenPixelsPerCSSPixel } = require("../window/utils");
const { create: createFrame, swapFrameLoaders, getDocShell } = require("../frame/utils");
const { window: addonWindow } = require("../addon/window");
const { isNil } = require("../lang/type");
const { data } = require('../self');
const events = require("../system/events");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
function calculateRegion({ position, width, height, defaultWidth, defaultHeight }, rect) {
position = position || {};
let x, y;
let hasTop = !isNil(position.top);
let hasRight = !isNil(position.right);
let hasBottom = !isNil(position.bottom);
let hasLeft = !isNil(position.left);
let hasWidth = !isNil(width);
let hasHeight = !isNil(height);
// if width is not specified by constructor or show's options, then get
// the default width
if (!hasWidth)
width = defaultWidth;
// if height is not specified by constructor or show's options, then get
// the default height
if (!hasHeight)
height = defaultHeight;
// default position is centered
x = (rect.right - width) / 2;
y = (rect.top + rect.bottom - height) / 2;
if (hasTop) {
y = rect.top + position.top;
if (hasBottom && !hasHeight)
height = rect.bottom - position.bottom - y;
}
else if (hasBottom) {
y = rect.bottom - position.bottom - height;
}
if (hasLeft) {
x = position.left;
if (hasRight && !hasWidth)
width = rect.right - position.right - x;
}
else if (hasRight) {
x = rect.right - width - position.right;
}
return {x: x, y: y, width: width, height: height};
}
function open(panel, options, anchor) {
// Wait for the XBL binding to be constructed
if (!panel.openPopup) setTimeout(open, 50, panel, options, anchor);
else display(panel, options, anchor);
}
exports.open = open;
function isOpen(panel) {
return panel.state === "open"
}
exports.isOpen = isOpen;
function isOpening(panel) {
return panel.state === "showing"
}
exports.isOpening = isOpening
function close(panel) {
// Sometimes "TypeError: panel.hidePopup is not a function" is thrown
// when quitting the host application while a panel is visible. To suppress
// these errors, check for "hidePopup" in panel before calling it.
// It's not clear if there's an issue or it's expected behavior.
// See Bug 1151796.
return panel.hidePopup && panel.hidePopup();
}
exports.close = close
function resize(panel, width, height) {
// Resize the iframe instead of using panel.sizeTo
// because sizeTo doesn't work with arrow panels
if (panel.firstChild) {
panel.firstChild.style.width = width + "px";
panel.firstChild.style.height = height + "px";
}
}
exports.resize = resize
function display(panel, options, anchor) {
let document = panel.ownerDocument;
let x, y;
let { width, height, defaultWidth, defaultHeight } = options;
let popupPosition = null;
// Panel XBL has some SDK incompatible styling decisions. We shim panel
// instances until proper fix for Bug 859504 is shipped.
shimDefaultStyle(panel);
if (!anchor) {
// The XUL Panel doesn't have an arrow, so the margin needs to be reset
// in order to, be positioned properly
panel.style.margin = "0";
let viewportRect = document.defaultView.gBrowser.getBoundingClientRect();
({x, y, width, height} = calculateRegion(options, viewportRect));
}
else {
// The XUL Panel has an arrow, so the margin needs to be reset
// to the default value.
panel.style.margin = "";
let { CustomizableUI, window } = anchor.ownerDocument.defaultView;
// In Australis, widgets may be positioned in an overflow panel or the
// menu panel.
// In such cases clicking this widget will hide the overflow/menu panel,
// and the widget's panel will show instead.
// If `CustomizableUI` is not available, it means the anchor is not in a
// chrome browser window, and therefore there is no need for this check.
if (CustomizableUI) {
let node = anchor;
({anchor} = CustomizableUI.getWidget(anchor.id).forWindow(window));
// if `node` is not the `anchor` itself, it means the widget is
// positioned in a panel, therefore we have to hide it before show
// the widget's panel in the same anchor
if (node !== anchor)
CustomizableUI.hidePanelForNode(anchor);
}
width = width || defaultWidth;
height = height || defaultHeight;
// Open the popup by the anchor.
let rect = anchor.getBoundingClientRect();
let zoom = getScreenPixelsPerCSSPixel(window);
let screenX = rect.left + window.mozInnerScreenX * zoom;
let screenY = rect.top + window.mozInnerScreenY * zoom;
// Set up the vertical position of the popup relative to the anchor
// (always display the arrow on anchor center)
let horizontal, vertical;
if (screenY > window.screen.availHeight / 2 + height)
vertical = "top";
else
vertical = "bottom";
if (screenY > window.screen.availWidth / 2 + width)
horizontal = "left";
else
horizontal = "right";
let verticalInverse = vertical == "top" ? "bottom" : "top";
popupPosition = vertical + "center " + verticalInverse + horizontal;
// Allow panel to flip itself if the panel can't be displayed at the
// specified position (useful if we compute a bad position or if the
// user moves the window and panel remains visible)
panel.setAttribute("flip", "both");
}
if (!panel.viewFrame) {
panel.viewFrame = document.importNode(panel.backgroundFrame, false);
panel.appendChild(panel.viewFrame);
let {privateBrowsingId} = getDocShell(panel.viewFrame).getOriginAttributes();
let principal = Services.scriptSecurityManager.createNullPrincipal({privateBrowsingId});
getDocShell(panel.viewFrame).createAboutBlankContentViewer(principal);
}
// Resize the iframe instead of using panel.sizeTo
// because sizeTo doesn't work with arrow panels
panel.firstChild.style.width = width + "px";
panel.firstChild.style.height = height + "px";
panel.openPopup(anchor, popupPosition, x, y);
}
exports.display = display;
// This utility function is just a workaround until Bug 859504 has shipped.
function shimDefaultStyle(panel) {
let document = panel.ownerDocument;
// Please note that `panel` needs to be part of document in order to reach
// it's anonymous nodes. One of the anonymous node has a big padding which
// doesn't work well since panel frame needs to fill all of the panel.
// XBL binding is a not the best option as it's applied asynchronously, and
// makes injected frames behave in strange way. Also this feels a lot
// cheaper to do.
["panel-inner-arrowcontent", "panel-arrowcontent"].forEach(function(value) {
let node = document.getAnonymousElementByAttribute(panel, "class", value);
if (node) node.style.padding = 0;
});
}
function show(panel, options, anchor) {
// Prevent the panel from getting focus when showing up
// if focus is set to false
panel.setAttribute("noautofocus", !options.focus);
let window = anchor && getOwnerBrowserWindow(anchor);
let { document } = window ? window : getMostRecentBrowserWindow();
attach(panel, document);
open(panel, options, anchor);
}
exports.show = show
function onPanelClick(event) {
let { target, metaKey, ctrlKey, shiftKey, button } = event;
let accel = platform === "darwin" ? metaKey : ctrlKey;
let isLeftClick = button === 0;
let isMiddleClick = button === 1;
if ((isLeftClick && (accel || shiftKey)) || isMiddleClick) {
let link = target.closest('a');
if (link && link.href)
getMostRecentBrowserWindow().openUILink(link.href, event)
}
}
function setupPanelFrame(frame) {
frame.setAttribute("flex", 1);
frame.setAttribute("transparent", "transparent");
frame.setAttribute("autocompleteenabled", true);
frame.setAttribute("tooltip", "aHTMLTooltip");
if (platform === "darwin") {
frame.style.borderRadius = "var(--arrowpanel-border-radius, 3.5px)";
frame.style.padding = "1px";
}
}
function make(document, options) {
document = document || getMostRecentBrowserWindow().document;
let panel = document.createElementNS(XUL_NS, "panel");
panel.setAttribute("type", "arrow");
panel.setAttribute("sdkscriptenabled", options.allowJavascript);
// The panel needs to be attached to a browser window in order for us
// to copy browser styles to the content document when it loads.
attach(panel, document);
let frameOptions = {
allowJavascript: options.allowJavascript,
allowPlugins: true,
allowAuth: true,
allowWindowControl: false,
// Need to override `nodeName` to use `iframe` as `browsers` save session
// history and in consequence do not dispatch "inner-window-destroyed"
// notifications.
browser: false,
};
let backgroundFrame = createFrame(addonWindow, frameOptions);
setupPanelFrame(backgroundFrame);
getDocShell(backgroundFrame).inheritPrivateBrowsingId = false;
function onPopupShowing({type, target}) {
if (target === this) {
let attrs = getDocShell(backgroundFrame).getOriginAttributes();
getDocShell(panel.viewFrame).setOriginAttributes(attrs);
swapFrameLoaders(backgroundFrame, panel.viewFrame);
}
}
function onPopupHiding({type, target}) {
if (target === this) {
swapFrameLoaders(backgroundFrame, panel.viewFrame);
panel.viewFrame.remove();
panel.viewFrame = null;
}
}
function onContentReady({target, type}) {
if (target === getContentDocument(panel)) {
style(panel);
events.emit(type, { subject: panel });
}
}
function onContentLoad({target, type}) {
if (target === getContentDocument(panel))
events.emit(type, { subject: panel });
}
function onContentChange({subject: document, type}) {
if (document === getContentDocument(panel) && document.defaultView)
events.emit(type, { subject: panel });
}
function onPanelStateChange({target, type}) {
if (target === this)
events.emit(type, { subject: panel })
}
panel.addEventListener("popupshowing", onPopupShowing);
panel.addEventListener("popuphiding", onPopupHiding);
for (let event of ["popupshowing", "popuphiding", "popupshown", "popuphidden"])
panel.addEventListener(event, onPanelStateChange);
panel.addEventListener("click", onPanelClick, false);
// Panel content document can be either in panel `viewFrame` or in
// a `backgroundFrame` depending on panel state. Listeners are set
// on both to avoid setting and removing listeners on panel state changes.
panel.addEventListener("DOMContentLoaded", onContentReady, true);
backgroundFrame.addEventListener("DOMContentLoaded", onContentReady, true);
panel.addEventListener("load", onContentLoad, true);
backgroundFrame.addEventListener("load", onContentLoad, true);
events.on("document-element-inserted", onContentChange);
panel.backgroundFrame = backgroundFrame;
panel.viewFrame = null;
// Store event listener on the panel instance so that it won't be GC-ed
// while panel is alive.
panel.onContentChange = onContentChange;
return panel;
}
exports.make = make;
function attach(panel, document) {
document = document || getMostRecentBrowserWindow().document;
let container = document.getElementById("mainPopupSet");
if (container !== panel.parentNode) {
detach(panel);
document.getElementById("mainPopupSet").appendChild(panel);
}
}
exports.attach = attach;
function detach(panel) {
if (panel.parentNode) panel.parentNode.removeChild(panel);
}
exports.detach = detach;
function dispose(panel) {
panel.backgroundFrame.remove();
panel.backgroundFrame = null;
events.off("document-element-inserted", panel.onContentChange);
panel.onContentChange = null;
detach(panel);
}
exports.dispose = dispose;
function style(panel) {
/**
Injects default OS specific panel styles into content document that is loaded
into given panel. Optionally `document` of the browser window can be
given to inherit styles from it, by default it will use either panel owner
document or an active browser's document. It should not matter though unless
Firefox decides to style windows differently base on profile or mode like
chrome for example.
**/
try {
let document = panel.ownerDocument;
let contentDocument = getContentDocument(panel);
let window = document.defaultView;
let node = document.getAnonymousElementByAttribute(panel, "class",
"panel-arrowcontent");
let { color, fontFamily, fontSize, fontWeight } = window.getComputedStyle(node);
let style = contentDocument.createElement("style");
style.id = "sdk-panel-style";
style.textContent = "body { " +
"color: " + color + ";" +
"font-family: " + fontFamily + ";" +
"font-weight: " + fontWeight + ";" +
"font-size: " + fontSize + ";" +
"}";
let container = contentDocument.head ? contentDocument.head :
contentDocument.documentElement;
if (container.firstChild)
container.insertBefore(style, container.firstChild);
else
container.appendChild(style);
}
catch (error) {
console.error("Unable to apply panel style");
console.exception(error);
}
}
exports.style = style;
var getContentFrame = panel => panel.viewFrame || panel.backgroundFrame;
exports.getContentFrame = getContentFrame;
function getContentDocument(panel) {
return getContentFrame(panel).contentDocument;
}
exports.getContentDocument = getContentDocument;
function setURL(panel, url) {
let frame = getContentFrame(panel);
let webNav = getDocShell(frame).QueryInterface(Ci.nsIWebNavigation);
webNav.loadURI(url ? data.url(url) : "about:blank", 0, null, null, null);
}
exports.setURL = setURL;
function allowContextMenu(panel, allow) {
if (allow) {
panel.setAttribute("context", "contentAreaContextMenu");
}
else {
panel.removeAttribute("context");
}
}
exports.allowContextMenu = allowContextMenu;

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