summaryrefslogtreecommitdiffstats
path: root/addon-sdk/source/lib/sdk
diff options
context:
space:
mode:
Diffstat (limited to 'addon-sdk/source/lib/sdk')
-rw-r--r--addon-sdk/source/lib/sdk/addon/bootstrap.js182
-rw-r--r--addon-sdk/source/lib/sdk/addon/events.js56
-rw-r--r--addon-sdk/source/lib/sdk/addon/host.js12
-rw-r--r--addon-sdk/source/lib/sdk/addon/installer.js121
-rw-r--r--addon-sdk/source/lib/sdk/addon/manager.js18
-rw-r--r--addon-sdk/source/lib/sdk/addon/runner.js180
-rw-r--r--addon-sdk/source/lib/sdk/addon/window.js66
-rw-r--r--addon-sdk/source/lib/sdk/base64.js47
-rw-r--r--addon-sdk/source/lib/sdk/browser/events.js20
-rw-r--r--addon-sdk/source/lib/sdk/clipboard.js337
-rw-r--r--addon-sdk/source/lib/sdk/console/plain-text.js78
-rw-r--r--addon-sdk/source/lib/sdk/console/traceback.js86
-rw-r--r--addon-sdk/source/lib/sdk/content/content-worker.js305
-rw-r--r--addon-sdk/source/lib/sdk/content/content.js17
-rw-r--r--addon-sdk/source/lib/sdk/content/context-menu.js408
-rw-r--r--addon-sdk/source/lib/sdk/content/events.js57
-rw-r--r--addon-sdk/source/lib/sdk/content/l10n-html.js133
-rw-r--r--addon-sdk/source/lib/sdk/content/loader.js74
-rw-r--r--addon-sdk/source/lib/sdk/content/mod.js68
-rw-r--r--addon-sdk/source/lib/sdk/content/page-mod.js236
-rw-r--r--addon-sdk/source/lib/sdk/content/page-worker.js154
-rw-r--r--addon-sdk/source/lib/sdk/content/sandbox.js426
-rw-r--r--addon-sdk/source/lib/sdk/content/sandbox/events.js12
-rw-r--r--addon-sdk/source/lib/sdk/content/tab-events.js58
-rw-r--r--addon-sdk/source/lib/sdk/content/thumbnail.js51
-rw-r--r--addon-sdk/source/lib/sdk/content/utils.js105
-rw-r--r--addon-sdk/source/lib/sdk/content/worker-child.js158
-rw-r--r--addon-sdk/source/lib/sdk/content/worker.js180
-rw-r--r--addon-sdk/source/lib/sdk/context-menu.js1188
-rw-r--r--addon-sdk/source/lib/sdk/context-menu/context.js147
-rw-r--r--addon-sdk/source/lib/sdk/context-menu/core.js384
-rw-r--r--addon-sdk/source/lib/sdk/context-menu/readers.js112
-rw-r--r--addon-sdk/source/lib/sdk/context-menu@2.js32
-rw-r--r--addon-sdk/source/lib/sdk/core/disposable.js186
-rw-r--r--addon-sdk/source/lib/sdk/core/heritage.js184
-rw-r--r--addon-sdk/source/lib/sdk/core/namespace.js43
-rw-r--r--addon-sdk/source/lib/sdk/core/observer.js89
-rw-r--r--addon-sdk/source/lib/sdk/core/promise.js118
-rw-r--r--addon-sdk/source/lib/sdk/core/reference.js29
-rw-r--r--addon-sdk/source/lib/sdk/deprecated/api-utils.js197
-rw-r--r--addon-sdk/source/lib/sdk/deprecated/events/assembler.js54
-rw-r--r--addon-sdk/source/lib/sdk/deprecated/sync-worker.js288
-rw-r--r--addon-sdk/source/lib/sdk/deprecated/unit-test-finder.js199
-rw-r--r--addon-sdk/source/lib/sdk/deprecated/unit-test.js584
-rw-r--r--addon-sdk/source/lib/sdk/deprecated/window-utils.js193
-rw-r--r--addon-sdk/source/lib/sdk/dom/events-shimmed.js18
-rw-r--r--addon-sdk/source/lib/sdk/dom/events.js192
-rw-r--r--addon-sdk/source/lib/sdk/dom/events/keys.js63
-rw-r--r--addon-sdk/source/lib/sdk/event/chrome.js65
-rw-r--r--addon-sdk/source/lib/sdk/event/core.js193
-rw-r--r--addon-sdk/source/lib/sdk/event/dom.js78
-rw-r--r--addon-sdk/source/lib/sdk/event/target.js74
-rw-r--r--addon-sdk/source/lib/sdk/event/utils.js328
-rw-r--r--addon-sdk/source/lib/sdk/frame/hidden-frame.js115
-rw-r--r--addon-sdk/source/lib/sdk/frame/utils.js94
-rw-r--r--addon-sdk/source/lib/sdk/fs/path.js500
-rw-r--r--addon-sdk/source/lib/sdk/hotkeys.js40
-rw-r--r--addon-sdk/source/lib/sdk/indexed-db.js79
-rw-r--r--addon-sdk/source/lib/sdk/input/browser.js73
-rw-r--r--addon-sdk/source/lib/sdk/input/customizable-ui.js28
-rw-r--r--addon-sdk/source/lib/sdk/input/frame.js85
-rw-r--r--addon-sdk/source/lib/sdk/input/system.js113
-rw-r--r--addon-sdk/source/lib/sdk/io/buffer.js351
-rw-r--r--addon-sdk/source/lib/sdk/io/byte-streams.js104
-rw-r--r--addon-sdk/source/lib/sdk/io/file.js196
-rw-r--r--addon-sdk/source/lib/sdk/io/fs.js984
-rw-r--r--addon-sdk/source/lib/sdk/io/stream.js440
-rw-r--r--addon-sdk/source/lib/sdk/io/text-streams.js235
-rw-r--r--addon-sdk/source/lib/sdk/keyboard/hotkeys.js110
-rw-r--r--addon-sdk/source/lib/sdk/keyboard/observer.js58
-rw-r--r--addon-sdk/source/lib/sdk/keyboard/utils.js189
-rw-r--r--addon-sdk/source/lib/sdk/l10n.js91
-rw-r--r--addon-sdk/source/lib/sdk/l10n/core.js9
-rw-r--r--addon-sdk/source/lib/sdk/l10n/html.js32
-rw-r--r--addon-sdk/source/lib/sdk/l10n/json/core.js36
-rw-r--r--addon-sdk/source/lib/sdk/l10n/loader.js70
-rw-r--r--addon-sdk/source/lib/sdk/l10n/locale.js127
-rw-r--r--addon-sdk/source/lib/sdk/l10n/plural-rules.js407
-rw-r--r--addon-sdk/source/lib/sdk/l10n/prefs.js51
-rw-r--r--addon-sdk/source/lib/sdk/l10n/properties/core.js87
-rw-r--r--addon-sdk/source/lib/sdk/lang/functional.js47
-rw-r--r--addon-sdk/source/lib/sdk/lang/functional/concurrent.js110
-rw-r--r--addon-sdk/source/lib/sdk/lang/functional/core.js290
-rw-r--r--addon-sdk/source/lib/sdk/lang/functional/helpers.js29
-rw-r--r--addon-sdk/source/lib/sdk/lang/type.js388
-rw-r--r--addon-sdk/source/lib/sdk/lang/weak-set.js75
-rw-r--r--addon-sdk/source/lib/sdk/loader/cuddlefish.js102
-rw-r--r--addon-sdk/source/lib/sdk/loader/sandbox.js74
-rw-r--r--addon-sdk/source/lib/sdk/messaging.js12
-rw-r--r--addon-sdk/source/lib/sdk/model/core.js23
-rw-r--r--addon-sdk/source/lib/sdk/net/url.js94
-rw-r--r--addon-sdk/source/lib/sdk/net/xhr.js36
-rw-r--r--addon-sdk/source/lib/sdk/notifications.js112
-rw-r--r--addon-sdk/source/lib/sdk/output/system.js71
-rw-r--r--addon-sdk/source/lib/sdk/page-mod.js190
-rw-r--r--addon-sdk/source/lib/sdk/page-mod/match-pattern.js10
-rw-r--r--addon-sdk/source/lib/sdk/page-worker.js194
-rw-r--r--addon-sdk/source/lib/sdk/panel.js427
-rw-r--r--addon-sdk/source/lib/sdk/panel/events.js27
-rw-r--r--addon-sdk/source/lib/sdk/panel/utils.js451
-rw-r--r--addon-sdk/source/lib/sdk/passwords.js61
-rw-r--r--addon-sdk/source/lib/sdk/passwords/utils.js107
-rw-r--r--addon-sdk/source/lib/sdk/places/bookmarks.js395
-rw-r--r--addon-sdk/source/lib/sdk/places/contract.js73
-rw-r--r--addon-sdk/source/lib/sdk/places/events.js128
-rw-r--r--addon-sdk/source/lib/sdk/places/favicon.js49
-rw-r--r--addon-sdk/source/lib/sdk/places/history.js65
-rw-r--r--addon-sdk/source/lib/sdk/places/host/host-bookmarks.js238
-rw-r--r--addon-sdk/source/lib/sdk/places/host/host-query.js179
-rw-r--r--addon-sdk/source/lib/sdk/places/host/host-tags.js92
-rw-r--r--addon-sdk/source/lib/sdk/places/utils.js268
-rw-r--r--addon-sdk/source/lib/sdk/platform/xpcom.js241
-rw-r--r--addon-sdk/source/lib/sdk/preferences/event-target.js61
-rw-r--r--addon-sdk/source/lib/sdk/preferences/native-options.js193
-rw-r--r--addon-sdk/source/lib/sdk/preferences/service.js137
-rw-r--r--addon-sdk/source/lib/sdk/preferences/utils.js42
-rw-r--r--addon-sdk/source/lib/sdk/private-browsing.js12
-rw-r--r--addon-sdk/source/lib/sdk/private-browsing/utils.js54
-rw-r--r--addon-sdk/source/lib/sdk/querystring.js121
-rw-r--r--addon-sdk/source/lib/sdk/remote/child.js284
-rw-r--r--addon-sdk/source/lib/sdk/remote/core.js8
-rw-r--r--addon-sdk/source/lib/sdk/remote/parent.js338
-rw-r--r--addon-sdk/source/lib/sdk/remote/utils.js39
-rw-r--r--addon-sdk/source/lib/sdk/request.js248
-rw-r--r--addon-sdk/source/lib/sdk/selection.js470
-rw-r--r--addon-sdk/source/lib/sdk/self.js61
-rw-r--r--addon-sdk/source/lib/sdk/simple-prefs.js26
-rw-r--r--addon-sdk/source/lib/sdk/simple-storage.js235
-rw-r--r--addon-sdk/source/lib/sdk/stylesheet/style.js71
-rw-r--r--addon-sdk/source/lib/sdk/stylesheet/utils.js75
-rw-r--r--addon-sdk/source/lib/sdk/system.js172
-rw-r--r--addon-sdk/source/lib/sdk/system/child_process.js332
-rw-r--r--addon-sdk/source/lib/sdk/system/child_process/subprocess.js186
-rw-r--r--addon-sdk/source/lib/sdk/system/environment.js33
-rw-r--r--addon-sdk/source/lib/sdk/system/events-shimmed.js16
-rw-r--r--addon-sdk/source/lib/sdk/system/events.js181
-rw-r--r--addon-sdk/source/lib/sdk/system/globals.js46
-rw-r--r--addon-sdk/source/lib/sdk/system/process.js62
-rw-r--r--addon-sdk/source/lib/sdk/system/runtime.js28
-rw-r--r--addon-sdk/source/lib/sdk/system/unload.js104
-rw-r--r--addon-sdk/source/lib/sdk/system/xul-app.js12
-rw-r--r--addon-sdk/source/lib/sdk/system/xul-app.jsm242
-rw-r--r--addon-sdk/source/lib/sdk/tab/events.js74
-rw-r--r--addon-sdk/source/lib/sdk/tabs.js17
-rw-r--r--addon-sdk/source/lib/sdk/tabs/common.js34
-rw-r--r--addon-sdk/source/lib/sdk/tabs/events.js39
-rw-r--r--addon-sdk/source/lib/sdk/tabs/helpers.js22
-rw-r--r--addon-sdk/source/lib/sdk/tabs/namespace.js10
-rw-r--r--addon-sdk/source/lib/sdk/tabs/observer.js113
-rw-r--r--addon-sdk/source/lib/sdk/tabs/tab-fennec.js249
-rw-r--r--addon-sdk/source/lib/sdk/tabs/tab-firefox.js353
-rw-r--r--addon-sdk/source/lib/sdk/tabs/tab.js24
-rw-r--r--addon-sdk/source/lib/sdk/tabs/tabs-firefox.js135
-rw-r--r--addon-sdk/source/lib/sdk/tabs/utils.js370
-rw-r--r--addon-sdk/source/lib/sdk/tabs/worker.js17
-rw-r--r--addon-sdk/source/lib/sdk/test.js114
-rw-r--r--addon-sdk/source/lib/sdk/test/assert.js366
-rw-r--r--addon-sdk/source/lib/sdk/test/harness.js645
-rw-r--r--addon-sdk/source/lib/sdk/test/httpd.js6
-rw-r--r--addon-sdk/source/lib/sdk/test/loader.js123
-rw-r--r--addon-sdk/source/lib/sdk/test/memory.js11
-rw-r--r--addon-sdk/source/lib/sdk/test/options.js23
-rw-r--r--addon-sdk/source/lib/sdk/test/runner.js131
-rw-r--r--addon-sdk/source/lib/sdk/test/utils.js199
-rw-r--r--addon-sdk/source/lib/sdk/timers.js105
-rw-r--r--addon-sdk/source/lib/sdk/ui.js17
-rw-r--r--addon-sdk/source/lib/sdk/ui/button/action.js114
-rw-r--r--addon-sdk/source/lib/sdk/ui/button/contract.js73
-rw-r--r--addon-sdk/source/lib/sdk/ui/button/toggle.js127
-rw-r--r--addon-sdk/source/lib/sdk/ui/button/view.js243
-rw-r--r--addon-sdk/source/lib/sdk/ui/button/view/events.js18
-rw-r--r--addon-sdk/source/lib/sdk/ui/component.js182
-rw-r--r--addon-sdk/source/lib/sdk/ui/frame.js16
-rw-r--r--addon-sdk/source/lib/sdk/ui/frame/model.js154
-rw-r--r--addon-sdk/source/lib/sdk/ui/frame/view.html18
-rw-r--r--addon-sdk/source/lib/sdk/ui/frame/view.js150
-rw-r--r--addon-sdk/source/lib/sdk/ui/id.js27
-rw-r--r--addon-sdk/source/lib/sdk/ui/sidebar.js311
-rw-r--r--addon-sdk/source/lib/sdk/ui/sidebar/actions.js10
-rw-r--r--addon-sdk/source/lib/sdk/ui/sidebar/contract.js27
-rw-r--r--addon-sdk/source/lib/sdk/ui/sidebar/namespace.js15
-rw-r--r--addon-sdk/source/lib/sdk/ui/sidebar/utils.js8
-rw-r--r--addon-sdk/source/lib/sdk/ui/sidebar/view.js214
-rw-r--r--addon-sdk/source/lib/sdk/ui/state.js239
-rw-r--r--addon-sdk/source/lib/sdk/ui/state/events.js18
-rw-r--r--addon-sdk/source/lib/sdk/ui/toolbar.js16
-rw-r--r--addon-sdk/source/lib/sdk/ui/toolbar/model.js151
-rw-r--r--addon-sdk/source/lib/sdk/ui/toolbar/view.js248
-rw-r--r--addon-sdk/source/lib/sdk/uri/resource.js37
-rw-r--r--addon-sdk/source/lib/sdk/url.js349
-rw-r--r--addon-sdk/source/lib/sdk/url/utils.js29
-rw-r--r--addon-sdk/source/lib/sdk/util/array.js123
-rw-r--r--addon-sdk/source/lib/sdk/util/collection.js115
-rw-r--r--addon-sdk/source/lib/sdk/util/contract.js55
-rw-r--r--addon-sdk/source/lib/sdk/util/deprecate.js40
-rw-r--r--addon-sdk/source/lib/sdk/util/dispatcher.js54
-rw-r--r--addon-sdk/source/lib/sdk/util/list.js90
-rw-r--r--addon-sdk/source/lib/sdk/util/match-pattern.js113
-rw-r--r--addon-sdk/source/lib/sdk/util/object.js104
-rw-r--r--addon-sdk/source/lib/sdk/util/rules.js53
-rw-r--r--addon-sdk/source/lib/sdk/util/sequence.js593
-rw-r--r--addon-sdk/source/lib/sdk/util/uuid.js19
-rw-r--r--addon-sdk/source/lib/sdk/view/core.js26
-rw-r--r--addon-sdk/source/lib/sdk/webextension.js43
-rw-r--r--addon-sdk/source/lib/sdk/window/browser.js54
-rw-r--r--addon-sdk/source/lib/sdk/window/events.js68
-rw-r--r--addon-sdk/source/lib/sdk/window/helpers.js81
-rw-r--r--addon-sdk/source/lib/sdk/window/namespace.js6
-rw-r--r--addon-sdk/source/lib/sdk/window/utils.js460
-rw-r--r--addon-sdk/source/lib/sdk/windows.js32
-rw-r--r--addon-sdk/source/lib/sdk/windows/fennec.js83
-rw-r--r--addon-sdk/source/lib/sdk/windows/firefox.js224
-rw-r--r--addon-sdk/source/lib/sdk/windows/observer.js53
-rw-r--r--addon-sdk/source/lib/sdk/windows/tabs-fennec.js172
-rw-r--r--addon-sdk/source/lib/sdk/worker/utils.js19
-rw-r--r--addon-sdk/source/lib/sdk/zip/utils.js16
216 files changed, 30621 insertions, 0 deletions
diff --git a/addon-sdk/source/lib/sdk/addon/bootstrap.js b/addon-sdk/source/lib/sdk/addon/bootstrap.js
new file mode 100644
index 000000000..0397d91e5
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/addon/bootstrap.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/addon/events.js b/addon-sdk/source/lib/sdk/addon/events.js
new file mode 100644
index 000000000..45bada6e1
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/addon/events.js
@@ -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 || {}));
+}
diff --git a/addon-sdk/source/lib/sdk/addon/host.js b/addon-sdk/source/lib/sdk/addon/host.js
new file mode 100644
index 000000000..91aa0e869
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/addon/host.js
@@ -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 = {};
diff --git a/addon-sdk/source/lib/sdk/addon/installer.js b/addon-sdk/source/lib/sdk/addon/installer.js
new file mode 100644
index 000000000..bb8cf8d16
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/addon/installer.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/addon/manager.js b/addon-sdk/source/lib/sdk/addon/manager.js
new file mode 100644
index 000000000..7ac0a7d6e
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/addon/manager.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/addon/runner.js b/addon-sdk/source/lib/sdk/addon/runner.js
new file mode 100644
index 000000000..3977a04e4
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/addon/runner.js
@@ -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);
+ });
+}
diff --git a/addon-sdk/source/lib/sdk/addon/window.js b/addon-sdk/source/lib/sdk/addon/window.js
new file mode 100644
index 000000000..93ed1d8dc
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/addon/window.js
@@ -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);
+});
diff --git a/addon-sdk/source/lib/sdk/base64.js b/addon-sdk/source/lib/sdk/base64.js
new file mode 100644
index 000000000..a07b302e0
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/base64.js
@@ -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);
+}
diff --git a/addon-sdk/source/lib/sdk/browser/events.js b/addon-sdk/source/lib/sdk/browser/events.js
new file mode 100644
index 000000000..f91119031
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/browser/events.js
@@ -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));
diff --git a/addon-sdk/source/lib/sdk/clipboard.js b/addon-sdk/source/lib/sdk/clipboard.js
new file mode 100644
index 000000000..048d5f2f1
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/clipboard.js
@@ -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;
+}
diff --git a/addon-sdk/source/lib/sdk/console/plain-text.js b/addon-sdk/source/lib/sdk/console/plain-text.js
new file mode 100644
index 000000000..0e44cf106
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/console/plain-text.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/console/traceback.js b/addon-sdk/source/lib/sdk/console/traceback.js
new file mode 100644
index 000000000..be0fb7b94
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/console/traceback.js
@@ -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");
+};
diff --git a/addon-sdk/source/lib/sdk/content/content-worker.js b/addon-sdk/source/lib/sdk/content/content-worker.js
new file mode 100644
index 000000000..0a8225733
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/content-worker.js
@@ -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;
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/content/content.js b/addon-sdk/source/lib/sdk/content/content.js
new file mode 100644
index 000000000..9655223a3
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/content.js
@@ -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;
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/content/context-menu.js b/addon-sdk/source/lib/sdk/content/context-menu.js
new file mode 100644
index 000000000..2955e2f09
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/context-menu.js
@@ -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);
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/content/events.js b/addon-sdk/source/lib/sdk/content/events.js
new file mode 100644
index 000000000..c085b6179
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/events.js
@@ -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]);
diff --git a/addon-sdk/source/lib/sdk/content/l10n-html.js b/addon-sdk/source/lib/sdk/content/l10n-html.js
new file mode 100644
index 000000000..f324623dc
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/l10n-html.js
@@ -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);
diff --git a/addon-sdk/source/lib/sdk/content/loader.js b/addon-sdk/source/lib/sdk/content/loader.js
new file mode 100644
index 000000000..e4f0dd2aa
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/loader.js
@@ -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);
diff --git a/addon-sdk/source/lib/sdk/content/mod.js b/addon-sdk/source/lib/sdk/content/mod.js
new file mode 100644
index 000000000..81fe9ee42
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/mod.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/content/page-mod.js b/addon-sdk/source/lib/sdk/content/page-mod.js
new file mode 100644
index 000000000..8ff9b1e7b
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/page-mod.js
@@ -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();
+});
diff --git a/addon-sdk/source/lib/sdk/content/page-worker.js b/addon-sdk/source/lib/sdk/content/page-worker.js
new file mode 100644
index 000000000..e9e741120
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/page-worker.js
@@ -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();
+});
diff --git a/addon-sdk/source/lib/sdk/content/sandbox.js b/addon-sdk/source/lib/sdk/content/sandbox.js
new file mode 100644
index 000000000..096ba5c87
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/sandbox.js
@@ -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});
+}
diff --git a/addon-sdk/source/lib/sdk/content/sandbox/events.js b/addon-sdk/source/lib/sdk/content/sandbox/events.js
new file mode 100644
index 000000000..d6f7eb004
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/sandbox/events.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/content/tab-events.js b/addon-sdk/source/lib/sdk/content/tab-events.js
new file mode 100644
index 000000000..9e244a853
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/tab-events.js
@@ -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 });
+}
diff --git a/addon-sdk/source/lib/sdk/content/thumbnail.js b/addon-sdk/source/lib/sdk/content/thumbnail.js
new file mode 100644
index 000000000..783615fc6
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/thumbnail.js
@@ -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';
diff --git a/addon-sdk/source/lib/sdk/content/utils.js b/addon-sdk/source/lib/sdk/content/utils.js
new file mode 100644
index 000000000..90995a614
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/utils.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/content/worker-child.js b/addon-sdk/source/lib/sdk/content/worker-child.js
new file mode 100644
index 000000000..dbf65a933
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/worker-child.js
@@ -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);
+});
diff --git a/addon-sdk/source/lib/sdk/content/worker.js b/addon-sdk/source/lib/sdk/content/worker.js
new file mode 100644
index 000000000..39b940a88
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/worker.js
@@ -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);
+});
diff --git a/addon-sdk/source/lib/sdk/context-menu.js b/addon-sdk/source/lib/sdk/context-menu.js
new file mode 100644
index 000000000..004c642d4
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/context-menu.js
@@ -0,0 +1,1188 @@
+/* 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 Bug 788334
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Class, mix } = require("./core/heritage");
+const { addCollectionProperty } = require("./util/collection");
+const { ns } = require("./core/namespace");
+const { validateOptions, getTypeOf } = require("./deprecated/api-utils");
+const { URL, isValidURI } = require("./url");
+const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils");
+const { isBrowser, getInnerId } = require("./window/utils");
+const { MatchPattern } = require("./util/match-pattern");
+const { EventTarget } = require("./event/target");
+const { emit } = require('./event/core');
+const { when } = require('./system/unload');
+const { contract: loaderContract } = require('./content/loader');
+const { omit } = require('./util/object');
+const self = require('./self')
+const { remoteRequire, processes } = require('./remote/parent');
+remoteRequire('sdk/content/context-menu');
+
+// All user items we add have this class.
+const ITEM_CLASS = "addon-context-menu-item";
+
+// Items in the top-level context menu also have this class.
+const TOPLEVEL_ITEM_CLASS = "addon-context-menu-item-toplevel";
+
+// Items in the overflow submenu also have this class.
+const OVERFLOW_ITEM_CLASS = "addon-context-menu-item-overflow";
+
+// The class of the menu separator that separates standard context menu items
+// from our user items.
+const SEPARATOR_CLASS = "addon-context-menu-separator";
+
+// If more than this number of items are added to the context menu, all items
+// overflow into a "Jetpack" submenu.
+const OVERFLOW_THRESH_DEFAULT = 10;
+const OVERFLOW_THRESH_PREF =
+ "extensions.addon-sdk.context-menu.overflowThreshold";
+
+// The label of the overflow sub-xul:menu.
+//
+// TODO: Localize these.
+const OVERFLOW_MENU_LABEL = "Add-ons";
+const OVERFLOW_MENU_ACCESSKEY = "A";
+
+// The class of the overflow sub-xul:menu.
+const OVERFLOW_MENU_CLASS = "addon-content-menu-overflow-menu";
+
+// The class of the overflow submenu's xul:menupopup.
+const OVERFLOW_POPUP_CLASS = "addon-content-menu-overflow-popup";
+
+// Holds private properties for API objects
+var internal = ns();
+
+// A little hacky but this is the last process ID that last opened the context
+// menu
+var lastContextProcessId = null;
+
+var uuidModule = require('./util/uuid');
+function uuid() {
+ return uuidModule.uuid().toString();
+}
+
+function getScheme(spec) {
+ try {
+ return URL(spec).scheme;
+ }
+ catch(e) {
+ return null;
+ }
+}
+
+var Context = Class({
+ initialize: function() {
+ internal(this).id = uuid();
+ },
+
+ // Returns the node that made this context current
+ adjustPopupNode: function adjustPopupNode(popupNode) {
+ return popupNode;
+ },
+
+ // Returns whether this context is current for the current node
+ isCurrent: function isCurrent(state) {
+ return state;
+ }
+});
+
+// Matches when the context-clicked node doesn't have any of
+// NON_PAGE_CONTEXT_ELTS in its ancestors
+var PageContext = Class({
+ extends: Context,
+
+ serialize: function() {
+ return {
+ id: internal(this).id,
+ type: "PageContext",
+ args: []
+ }
+ }
+});
+exports.PageContext = PageContext;
+
+// Matches when there is an active selection in the window
+var SelectionContext = Class({
+ extends: Context,
+
+ serialize: function() {
+ return {
+ id: internal(this).id,
+ type: "SelectionContext",
+ args: []
+ }
+ }
+});
+exports.SelectionContext = SelectionContext;
+
+// Matches when the context-clicked node or any of its ancestors matches the
+// selector given
+var SelectorContext = Class({
+ extends: Context,
+
+ initialize: function initialize(selector) {
+ Context.prototype.initialize.call(this);
+ let options = validateOptions({ selector: selector }, {
+ selector: {
+ is: ["string"],
+ msg: "selector must be a string."
+ }
+ });
+ internal(this).selector = options.selector;
+ },
+
+ serialize: function() {
+ return {
+ id: internal(this).id,
+ type: "SelectorContext",
+ args: [internal(this).selector]
+ }
+ }
+});
+exports.SelectorContext = SelectorContext;
+
+// Matches when the page url matches any of the patterns given
+var URLContext = Class({
+ extends: Context,
+
+ initialize: function initialize(patterns) {
+ Context.prototype.initialize.call(this);
+ patterns = Array.isArray(patterns) ? patterns : [patterns];
+
+ try {
+ internal(this).patterns = patterns.map(p => new MatchPattern(p));
+ }
+ catch (err) {
+ throw new Error("Patterns must be a string, regexp or an array of " +
+ "strings or regexps: " + err);
+ }
+ },
+
+ isCurrent: function isCurrent(url) {
+ return internal(this).patterns.some(p => p.test(url));
+ },
+
+ serialize: function() {
+ return {
+ id: internal(this).id,
+ type: "URLContext",
+ args: []
+ }
+ }
+});
+exports.URLContext = URLContext;
+
+// Matches when the user-supplied predicate returns true
+var PredicateContext = Class({
+ extends: Context,
+
+ initialize: function initialize(predicate) {
+ Context.prototype.initialize.call(this);
+ let options = validateOptions({ predicate: predicate }, {
+ predicate: {
+ is: ["function"],
+ msg: "predicate must be a function."
+ }
+ });
+ internal(this).predicate = options.predicate;
+ },
+
+ isCurrent: function isCurrent(state) {
+ return internal(this).predicate(state);
+ },
+
+ serialize: function() {
+ return {
+ id: internal(this).id,
+ type: "PredicateContext",
+ args: []
+ }
+ }
+});
+exports.PredicateContext = PredicateContext;
+
+function removeItemFromArray(array, item) {
+ return array.filter(i => i !== item);
+}
+
+// Converts anything that isn't false, null or undefined into a string
+function stringOrNull(val) {
+ return val ? String(val) : val;
+}
+
+// Shared option validation rules for Item, Menu, and Separator
+var baseItemRules = {
+ parentMenu: {
+ is: ["object", "undefined"],
+ ok: function (v) {
+ if (!v)
+ return true;
+ return (v instanceof ItemContainer) || (v instanceof Menu);
+ },
+ msg: "parentMenu must be a Menu or not specified."
+ },
+ context: {
+ is: ["undefined", "object", "array"],
+ ok: function (v) {
+ if (!v)
+ return true;
+ let arr = Array.isArray(v) ? v : [v];
+ return arr.every(o => o instanceof Context);
+ },
+ msg: "The 'context' option must be a Context object or an array of " +
+ "Context objects."
+ },
+ onMessage: {
+ is: ["function", "undefined"]
+ },
+ contentScript: loaderContract.rules.contentScript,
+ contentScriptFile: loaderContract.rules.contentScriptFile
+};
+
+var labelledItemRules = mix(baseItemRules, {
+ label: {
+ map: stringOrNull,
+ is: ["string"],
+ ok: v => !!v,
+ msg: "The item must have a non-empty string label."
+ },
+ accesskey: {
+ map: stringOrNull,
+ is: ["string", "undefined", "null"],
+ ok: (v) => {
+ if (!v) {
+ return true;
+ }
+ return typeof v == "string" && v.length === 1;
+ },
+ msg: "The item must have a single character accesskey, or no accesskey."
+ },
+ image: {
+ map: stringOrNull,
+ is: ["string", "undefined", "null"],
+ ok: function (url) {
+ if (!url)
+ return true;
+ return isValidURI(url);
+ },
+ msg: "Image URL validation failed"
+ }
+});
+
+// Additional validation rules for Item
+var itemRules = mix(labelledItemRules, {
+ data: {
+ map: stringOrNull,
+ is: ["string", "undefined", "null"]
+ }
+});
+
+// Additional validation rules for Menu
+var menuRules = mix(labelledItemRules, {
+ items: {
+ is: ["array", "undefined"],
+ ok: function (v) {
+ if (!v)
+ return true;
+ return v.every(function (item) {
+ return item instanceof BaseItem;
+ });
+ },
+ msg: "items must be an array, and each element in the array must be an " +
+ "Item, Menu, or Separator."
+ }
+});
+
+// Returns true if any contexts match. If there are no contexts then a
+// PageContext is tested instead
+function hasMatchingContext(contexts, addonInfo) {
+ for (let context of contexts) {
+ if (!(internal(context).id in addonInfo.contextStates)) {
+ console.error("Missing state for context " + internal(context).id + " this is an error in the SDK modules.");
+ return false;
+ }
+ if (!context.isCurrent(addonInfo.contextStates[internal(context).id]))
+ return false;
+ }
+
+ return true;
+}
+
+// Tests whether an item should be visible or not based on its contexts and
+// content scripts
+function isItemVisible(item, addonInfo, usePageWorker) {
+ if (!item.context.length) {
+ if (!addonInfo.hasWorker)
+ return usePageWorker ? addonInfo.pageContext : true;
+ }
+
+ if (!hasMatchingContext(item.context, addonInfo))
+ return false;
+
+ let context = addonInfo.workerContext;
+ if (typeof(context) === "string" && context != "")
+ item.label = context;
+
+ return !!context;
+}
+
+// Called when an item is clicked to send out click events to the content
+// scripts
+function itemActivated(item, clickedNode) {
+ let items = [internal(item).id];
+ let data = item.data;
+
+ while (item.parentMenu) {
+ item = item.parentMenu;
+ items.push(internal(item).id);
+ }
+
+ let process = processes.getById(lastContextProcessId);
+ if (process)
+ process.port.emit('sdk/contextmenu/activateitems', items, data);
+}
+
+function serializeItem(item) {
+ return {
+ id: internal(item).id,
+ contexts: item.context.map(c => c.serialize()),
+ contentScript: item.contentScript,
+ contentScriptFile: item.contentScriptFile,
+ };
+}
+
+// All things that appear in the context menu extend this
+var BaseItem = Class({
+ initialize: function initialize() {
+ internal(this).id = uuid();
+
+ internal(this).contexts = [];
+ if ("context" in internal(this).options && internal(this).options.context) {
+ let contexts = internal(this).options.context;
+ if (Array.isArray(contexts)) {
+ for (let context of contexts)
+ internal(this).contexts.push(context);
+ }
+ else {
+ internal(this).contexts.push(contexts);
+ }
+ }
+
+ let parentMenu = internal(this).options.parentMenu;
+ if (!parentMenu)
+ parentMenu = contentContextMenu;
+
+ parentMenu.addItem(this);
+
+ Object.defineProperty(this, "contentScript", {
+ enumerable: true,
+ value: internal(this).options.contentScript
+ });
+
+ // Resolve URIs here as tests may have overriden self
+ let files = internal(this).options.contentScriptFile;
+ if (files) {
+ if (!Array.isArray(files))
+ files = [files];
+ files = files.map(self.data.url);
+ }
+ internal(this).options.contentScriptFile = files;
+ Object.defineProperty(this, "contentScriptFile", {
+ enumerable: true,
+ value: internal(this).options.contentScriptFile
+ });
+
+ // Notify all frames of this new item
+ sendItems([serializeItem(this)]);
+ },
+
+ destroy: function destroy() {
+ if (internal(this).destroyed)
+ return;
+
+ // Tell all existing frames that this item has been destroyed
+ processes.port.emit("sdk/contextmenu/destroyitems", [internal(this).id]);
+
+ if (this.parentMenu)
+ this.parentMenu.removeItem(this);
+
+ internal(this).destroyed = true;
+ },
+
+ get context() {
+ let contexts = internal(this).contexts.slice(0);
+ contexts.add = (context) => {
+ internal(this).contexts.push(context);
+ // Notify all frames that this item has changed
+ sendItems([serializeItem(this)]);
+ };
+ contexts.remove = (context) => {
+ internal(this).contexts = internal(this).contexts.filter(c => {
+ return c != context;
+ });
+ // Notify all frames that this item has changed
+ sendItems([serializeItem(this)]);
+ };
+ return contexts;
+ },
+
+ set context(val) {
+ internal(this).contexts = val.slice(0);
+ // Notify all frames that this item has changed
+ sendItems([serializeItem(this)]);
+ },
+
+ get parentMenu() {
+ return internal(this).parentMenu;
+ },
+});
+
+function workerMessageReceived(process, id, args) {
+ if (internal(this).id != id)
+ return;
+
+ emit(this, ...JSON.parse(args));
+}
+
+// All things that have a label on the context menu extend this
+var LabelledItem = Class({
+ extends: BaseItem,
+ implements: [ EventTarget ],
+
+ initialize: function initialize(options) {
+ BaseItem.prototype.initialize.call(this);
+ EventTarget.prototype.initialize.call(this, options);
+
+ internal(this).messageListener = workerMessageReceived.bind(this);
+ processes.port.on('sdk/worker/event', internal(this).messageListener);
+ },
+
+ destroy: function destroy() {
+ if (internal(this).destroyed)
+ return;
+
+ processes.port.off('sdk/worker/event', internal(this).messageListener);
+
+ BaseItem.prototype.destroy.call(this);
+ },
+
+ get label() {
+ return internal(this).options.label;
+ },
+
+ set label(val) {
+ internal(this).options.label = val;
+
+ MenuManager.updateItem(this);
+ },
+
+ get accesskey() {
+ return internal(this).options.accesskey;
+ },
+
+ set accesskey(val) {
+ internal(this).options.accesskey = val;
+
+ MenuManager.updateItem(this);
+ },
+
+ get image() {
+ return internal(this).options.image;
+ },
+
+ set image(val) {
+ internal(this).options.image = val;
+
+ MenuManager.updateItem(this);
+ },
+
+ get data() {
+ return internal(this).options.data;
+ },
+
+ set data(val) {
+ internal(this).options.data = val;
+ }
+});
+
+var Item = Class({
+ extends: LabelledItem,
+
+ initialize: function initialize(options) {
+ internal(this).options = validateOptions(options, itemRules);
+
+ LabelledItem.prototype.initialize.call(this, options);
+ },
+
+ toString: function toString() {
+ return "[object Item \"" + this.label + "\"]";
+ },
+
+ get data() {
+ return internal(this).options.data;
+ },
+
+ set data(val) {
+ internal(this).options.data = val;
+
+ MenuManager.updateItem(this);
+ },
+});
+exports.Item = Item;
+
+var ItemContainer = Class({
+ initialize: function initialize() {
+ internal(this).children = [];
+ },
+
+ destroy: function destroy() {
+ // Destroys the entire hierarchy
+ for (let item of internal(this).children)
+ item.destroy();
+ },
+
+ addItem: function addItem(item) {
+ let oldParent = item.parentMenu;
+
+ // Don't just call removeItem here as that would remove the corresponding
+ // UI element which is more costly than just moving it to the right place
+ if (oldParent)
+ internal(oldParent).children = removeItemFromArray(internal(oldParent).children, item);
+
+ let after = null;
+ let children = internal(this).children;
+ if (children.length > 0)
+ after = children[children.length - 1];
+
+ children.push(item);
+ internal(item).parentMenu = this;
+
+ // If there was an old parent then we just have to move the item, otherwise
+ // it needs to be created
+ if (oldParent)
+ MenuManager.moveItem(item, after);
+ else
+ MenuManager.createItem(item, after);
+ },
+
+ removeItem: function removeItem(item) {
+ // If the item isn't a child of this menu then ignore this call
+ if (item.parentMenu !== this)
+ return;
+
+ MenuManager.removeItem(item);
+
+ internal(this).children = removeItemFromArray(internal(this).children, item);
+ internal(item).parentMenu = null;
+ },
+
+ get items() {
+ return internal(this).children.slice(0);
+ },
+
+ set items(val) {
+ // Validate the arguments before making any changes
+ if (!Array.isArray(val))
+ throw new Error(menuOptionRules.items.msg);
+
+ for (let item of val) {
+ if (!(item instanceof BaseItem))
+ throw new Error(menuOptionRules.items.msg);
+ }
+
+ // Remove the old items and add the new ones
+ for (let item of internal(this).children)
+ this.removeItem(item);
+
+ for (let item of val)
+ this.addItem(item);
+ },
+});
+
+var Menu = Class({
+ extends: LabelledItem,
+ implements: [ItemContainer],
+
+ initialize: function initialize(options) {
+ internal(this).options = validateOptions(options, menuRules);
+
+ LabelledItem.prototype.initialize.call(this, options);
+ ItemContainer.prototype.initialize.call(this);
+
+ if (internal(this).options.items) {
+ for (let item of internal(this).options.items)
+ this.addItem(item);
+ }
+ },
+
+ destroy: function destroy() {
+ ItemContainer.prototype.destroy.call(this);
+ LabelledItem.prototype.destroy.call(this);
+ },
+
+ toString: function toString() {
+ return "[object Menu \"" + this.label + "\"]";
+ },
+});
+exports.Menu = Menu;
+
+var Separator = Class({
+ extends: BaseItem,
+
+ initialize: function initialize(options) {
+ internal(this).options = validateOptions(options, baseItemRules);
+
+ BaseItem.prototype.initialize.call(this);
+ },
+
+ toString: function toString() {
+ return "[object Separator]";
+ }
+});
+exports.Separator = Separator;
+
+// Holds items for the content area context menu
+var contentContextMenu = ItemContainer();
+exports.contentContextMenu = contentContextMenu;
+
+function getContainerItems(container) {
+ let items = [];
+ for (let item of internal(container).children) {
+ items.push(serializeItem(item));
+ if (item instanceof Menu)
+ items = items.concat(getContainerItems(item));
+ }
+ return items;
+}
+
+// Notify all frames of these new or changed items
+function sendItems(items) {
+ processes.port.emit("sdk/contextmenu/createitems", items);
+}
+
+// Called when a new process is created and needs to get the current list of items
+function remoteItemRequest(process) {
+ let items = getContainerItems(contentContextMenu);
+ if (items.length == 0)
+ return;
+
+ process.port.emit("sdk/contextmenu/createitems", items);
+}
+processes.forEvery(remoteItemRequest);
+
+when(function() {
+ contentContextMenu.destroy();
+});
+
+// App specific UI code lives here, it should handle populating the context
+// menu and passing clicks etc. through to the items.
+
+function countVisibleItems(nodes) {
+ return Array.reduce(nodes, function(sum, node) {
+ return node.hidden ? sum : sum + 1;
+ }, 0);
+}
+
+var MenuWrapper = Class({
+ initialize: function initialize(winWrapper, items, contextMenu) {
+ this.winWrapper = winWrapper;
+ this.window = winWrapper.window;
+ this.items = items;
+ this.contextMenu = contextMenu;
+ this.populated = false;
+ this.menuMap = new Map();
+
+ // updateItemVisibilities will run first, updateOverflowState will run after
+ // all other instances of this module have run updateItemVisibilities
+ this._updateItemVisibilities = this.updateItemVisibilities.bind(this);
+ this.contextMenu.addEventListener("popupshowing", this._updateItemVisibilities, true);
+ this._updateOverflowState = this.updateOverflowState.bind(this);
+ this.contextMenu.addEventListener("popupshowing", this._updateOverflowState, false);
+ },
+
+ destroy: function destroy() {
+ this.contextMenu.removeEventListener("popupshowing", this._updateOverflowState, false);
+ this.contextMenu.removeEventListener("popupshowing", this._updateItemVisibilities, true);
+
+ if (!this.populated)
+ return;
+
+ // If we're getting unloaded at runtime then we must remove all the
+ // generated XUL nodes
+ let oldParent = null;
+ for (let item of internal(this.items).children) {
+ let xulNode = this.getXULNodeForItem(item);
+ oldParent = xulNode.parentNode;
+ oldParent.removeChild(xulNode);
+ }
+
+ if (oldParent)
+ this.onXULRemoved(oldParent);
+ },
+
+ get separator() {
+ return this.contextMenu.querySelector("." + SEPARATOR_CLASS);
+ },
+
+ get overflowMenu() {
+ return this.contextMenu.querySelector("." + OVERFLOW_MENU_CLASS);
+ },
+
+ get overflowPopup() {
+ return this.contextMenu.querySelector("." + OVERFLOW_POPUP_CLASS);
+ },
+
+ get topLevelItems() {
+ return this.contextMenu.querySelectorAll("." + TOPLEVEL_ITEM_CLASS);
+ },
+
+ get overflowItems() {
+ return this.contextMenu.querySelectorAll("." + OVERFLOW_ITEM_CLASS);
+ },
+
+ getXULNodeForItem: function getXULNodeForItem(item) {
+ return this.menuMap.get(item);
+ },
+
+ // Recurses through the item hierarchy creating XUL nodes for everything
+ populate: function populate(menu) {
+ for (let i = 0; i < internal(menu).children.length; i++) {
+ let item = internal(menu).children[i];
+ let after = i === 0 ? null : internal(menu).children[i - 1];
+ this.createItem(item, after);
+
+ if (item instanceof Menu)
+ this.populate(item);
+ }
+ },
+
+ // Recurses through the menu setting the visibility of items. Returns true
+ // if any of the items in this menu were visible
+ setVisibility: function setVisibility(menu, addonInfo, usePageWorker) {
+ let anyVisible = false;
+
+ for (let item of internal(menu).children) {
+ let visible = isItemVisible(item, addonInfo[internal(item).id], usePageWorker);
+
+ // Recurse through Menus, if none of the sub-items were visible then the
+ // menu is hidden too.
+ if (visible && (item instanceof Menu))
+ visible = this.setVisibility(item, addonInfo, false);
+
+ let xulNode = this.getXULNodeForItem(item);
+ xulNode.hidden = !visible;
+
+ anyVisible = anyVisible || visible;
+ }
+
+ return anyVisible;
+ },
+
+ // Works out where to insert a XUL node for an item in a browser window
+ insertIntoXUL: function insertIntoXUL(item, node, after) {
+ let menupopup = null;
+ let before = null;
+
+ let menu = item.parentMenu;
+ if (menu === this.items) {
+ // Insert into the overflow popup if it exists, otherwise the normal
+ // context menu
+ menupopup = this.overflowPopup;
+ if (!menupopup)
+ menupopup = this.contextMenu;
+ }
+ else {
+ let xulNode = this.getXULNodeForItem(menu);
+ menupopup = xulNode.firstChild;
+ }
+
+ if (after) {
+ let afterNode = this.getXULNodeForItem(after);
+ before = afterNode.nextSibling;
+ }
+ else if (menupopup === this.contextMenu) {
+ let topLevel = this.topLevelItems;
+ if (topLevel.length > 0)
+ before = topLevel[topLevel.length - 1].nextSibling;
+ else
+ before = this.separator.nextSibling;
+ }
+
+ menupopup.insertBefore(node, before);
+ },
+
+ // Sets the right class for XUL nodes
+ updateXULClass: function updateXULClass(xulNode) {
+ if (xulNode.parentNode == this.contextMenu)
+ xulNode.classList.add(TOPLEVEL_ITEM_CLASS);
+ else
+ xulNode.classList.remove(TOPLEVEL_ITEM_CLASS);
+
+ if (xulNode.parentNode == this.overflowPopup)
+ xulNode.classList.add(OVERFLOW_ITEM_CLASS);
+ else
+ xulNode.classList.remove(OVERFLOW_ITEM_CLASS);
+ },
+
+ // Creates a XUL node for an item
+ createItem: function createItem(item, after) {
+ if (!this.populated)
+ return;
+
+ // Create the separator if it doesn't already exist
+ if (!this.separator) {
+ let separator = this.window.document.createElement("menuseparator");
+ separator.setAttribute("class", SEPARATOR_CLASS);
+
+ // Insert before the separator created by the old context-menu if it
+ // exists to avoid bug 832401
+ let oldSeparator = this.window.document.getElementById("jetpack-context-menu-separator");
+ if (oldSeparator && oldSeparator.parentNode != this.contextMenu)
+ oldSeparator = null;
+ this.contextMenu.insertBefore(separator, oldSeparator);
+ }
+
+ let type = "menuitem";
+ if (item instanceof Menu)
+ type = "menu";
+ else if (item instanceof Separator)
+ type = "menuseparator";
+
+ let xulNode = this.window.document.createElement(type);
+ xulNode.setAttribute("class", ITEM_CLASS);
+ if (item instanceof LabelledItem) {
+ xulNode.setAttribute("label", item.label);
+ if (item.accesskey)
+ xulNode.setAttribute("accesskey", item.accesskey);
+ if (item.image) {
+ xulNode.setAttribute("image", item.image);
+ if (item instanceof Menu)
+ xulNode.classList.add("menu-iconic");
+ else
+ xulNode.classList.add("menuitem-iconic");
+ }
+ if (item.data)
+ xulNode.setAttribute("value", item.data);
+
+ let self = this;
+ xulNode.addEventListener("command", function(event) {
+ // Only care about clicks directly on this item
+ if (event.target !== xulNode)
+ return;
+
+ itemActivated(item, xulNode);
+ }, false);
+ }
+
+ this.insertIntoXUL(item, xulNode, after);
+ this.updateXULClass(xulNode);
+ xulNode.data = item.data;
+
+ if (item instanceof Menu) {
+ let menupopup = this.window.document.createElement("menupopup");
+ xulNode.appendChild(menupopup);
+ }
+
+ this.menuMap.set(item, xulNode);
+ },
+
+ // Updates the XUL node for an item in this window
+ updateItem: function updateItem(item) {
+ if (!this.populated)
+ return;
+
+ let xulNode = this.getXULNodeForItem(item);
+
+ // TODO figure out why this requires setAttribute
+ xulNode.setAttribute("label", item.label);
+ xulNode.setAttribute("accesskey", item.accesskey || "");
+
+ if (item.image) {
+ xulNode.setAttribute("image", item.image);
+ if (item instanceof Menu)
+ xulNode.classList.add("menu-iconic");
+ else
+ xulNode.classList.add("menuitem-iconic");
+ }
+ else {
+ xulNode.removeAttribute("image");
+ xulNode.classList.remove("menu-iconic");
+ xulNode.classList.remove("menuitem-iconic");
+ }
+
+ if (item.data)
+ xulNode.setAttribute("value", item.data);
+ else
+ xulNode.removeAttribute("value");
+ },
+
+ // Moves the XUL node for an item in this window to its new place in the
+ // hierarchy
+ moveItem: function moveItem(item, after) {
+ if (!this.populated)
+ return;
+
+ let xulNode = this.getXULNodeForItem(item);
+ let oldParent = xulNode.parentNode;
+
+ this.insertIntoXUL(item, xulNode, after);
+ this.updateXULClass(xulNode);
+ this.onXULRemoved(oldParent);
+ },
+
+ // Removes the XUL nodes for an item in every window we've ever populated.
+ removeItem: function removeItem(item) {
+ if (!this.populated)
+ return;
+
+ let xulItem = this.getXULNodeForItem(item);
+
+ let oldParent = xulItem.parentNode;
+
+ oldParent.removeChild(xulItem);
+ this.menuMap.delete(item);
+
+ this.onXULRemoved(oldParent);
+ },
+
+ // Called when any XUL nodes have been removed from a menupopup. This handles
+ // making sure the separator and overflow are correct
+ onXULRemoved: function onXULRemoved(parent) {
+ if (parent == this.contextMenu) {
+ let toplevel = this.topLevelItems;
+
+ // If there are no more items then remove the separator
+ if (toplevel.length == 0) {
+ let separator = this.separator;
+ if (separator)
+ separator.parentNode.removeChild(separator);
+ }
+ }
+ else if (parent == this.overflowPopup) {
+ // If there are no more items then remove the overflow menu and separator
+ if (parent.childNodes.length == 0) {
+ let separator = this.separator;
+ separator.parentNode.removeChild(separator);
+ this.contextMenu.removeChild(parent.parentNode);
+ }
+ }
+ },
+
+ // Recurses through all the items owned by this module and sets their hidden
+ // state
+ updateItemVisibilities: function updateItemVisibilities(event) {
+ try {
+ if (event.type != "popupshowing")
+ return;
+ if (event.target != this.contextMenu)
+ return;
+
+ if (internal(this.items).children.length == 0)
+ return;
+
+ if (!this.populated) {
+ this.populated = true;
+ this.populate(this.items);
+ }
+
+ let mainWindow = event.target.ownerDocument.defaultView;
+ this.contextMenuContentData = mainWindow.gContextMenuContentData
+ if (!(self.id in this.contextMenuContentData.addonInfo)) {
+ console.warn("No context menu state data was provided.");
+ return;
+ }
+ let addonInfo = this.contextMenuContentData.addonInfo[self.id];
+ lastContextProcessId = addonInfo.processID;
+ this.setVisibility(this.items, addonInfo.items, true);
+ }
+ catch (e) {
+ console.exception(e);
+ }
+ },
+
+ // Counts the number of visible items across all modules and makes sure they
+ // are in the right place between the top level context menu and the overflow
+ // menu
+ updateOverflowState: function updateOverflowState(event) {
+ try {
+ if (event.type != "popupshowing")
+ return;
+ if (event.target != this.contextMenu)
+ return;
+
+ // The main items will be in either the top level context menu or the
+ // overflow menu at this point. Count the visible ones and if they are in
+ // the wrong place move them
+ let toplevel = this.topLevelItems;
+ let overflow = this.overflowItems;
+ let visibleCount = countVisibleItems(toplevel) +
+ countVisibleItems(overflow);
+
+ if (visibleCount == 0) {
+ let separator = this.separator;
+ if (separator)
+ separator.hidden = true;
+ let overflowMenu = this.overflowMenu;
+ if (overflowMenu)
+ overflowMenu.hidden = true;
+ }
+ else if (visibleCount > MenuManager.overflowThreshold) {
+ this.separator.hidden = false;
+ let overflowPopup = this.overflowPopup;
+ if (overflowPopup)
+ overflowPopup.parentNode.hidden = false;
+
+ if (toplevel.length > 0) {
+ // The overflow menu shouldn't exist here but let's play it safe
+ if (!overflowPopup) {
+ let overflowMenu = this.window.document.createElement("menu");
+ overflowMenu.setAttribute("class", OVERFLOW_MENU_CLASS);
+ overflowMenu.setAttribute("label", OVERFLOW_MENU_LABEL);
+ overflowMenu.setAttribute("accesskey", OVERFLOW_MENU_ACCESSKEY);
+ this.contextMenu.insertBefore(overflowMenu, this.separator.nextSibling);
+
+ overflowPopup = this.window.document.createElement("menupopup");
+ overflowPopup.setAttribute("class", OVERFLOW_POPUP_CLASS);
+ overflowMenu.appendChild(overflowPopup);
+ }
+
+ for (let xulNode of toplevel) {
+ overflowPopup.appendChild(xulNode);
+ this.updateXULClass(xulNode);
+ }
+ }
+ }
+ else {
+ this.separator.hidden = false;
+
+ if (overflow.length > 0) {
+ // Move all the overflow nodes out of the overflow menu and position
+ // them immediately before it
+ for (let xulNode of overflow) {
+ this.contextMenu.insertBefore(xulNode, xulNode.parentNode.parentNode);
+ this.updateXULClass(xulNode);
+ }
+ this.contextMenu.removeChild(this.overflowMenu);
+ }
+ }
+ }
+ catch (e) {
+ console.exception(e);
+ }
+ }
+});
+
+// This wraps every window that we've seen
+var WindowWrapper = Class({
+ initialize: function initialize(window) {
+ this.window = window;
+ this.menus = [
+ new MenuWrapper(this, contentContextMenu, window.document.getElementById("contentAreaContextMenu")),
+ ];
+ },
+
+ destroy: function destroy() {
+ for (let menuWrapper of this.menus)
+ menuWrapper.destroy();
+ },
+
+ getMenuWrapperForItem: function getMenuWrapperForItem(item) {
+ let root = item.parentMenu;
+ while (root.parentMenu)
+ root = root.parentMenu;
+
+ for (let wrapper of this.menus) {
+ if (wrapper.items === root)
+ return wrapper;
+ }
+
+ return null;
+ }
+});
+
+var MenuManager = {
+ windowMap: new Map(),
+
+ get overflowThreshold() {
+ let prefs = require("./preferences/service");
+ return prefs.get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT);
+ },
+
+ // When a new window is added start watching it for context menu shows
+ onTrack: function onTrack(window) {
+ if (!isBrowser(window))
+ return;
+
+ // Generally shouldn't happen, but just in case
+ if (this.windowMap.has(window)) {
+ console.warn("Already seen this window");
+ return;
+ }
+
+ let winWrapper = WindowWrapper(window);
+ this.windowMap.set(window, winWrapper);
+ },
+
+ onUntrack: function onUntrack(window) {
+ if (!isBrowser(window))
+ return;
+
+ let winWrapper = this.windowMap.get(window);
+ // This shouldn't happen but protect against it anyway
+ if (!winWrapper)
+ return;
+ winWrapper.destroy();
+
+ this.windowMap.delete(window);
+ },
+
+ // Creates a XUL node for an item in every window we've already populated
+ createItem: function createItem(item, after) {
+ for (let [window, winWrapper] of this.windowMap) {
+ let menuWrapper = winWrapper.getMenuWrapperForItem(item);
+ if (menuWrapper)
+ menuWrapper.createItem(item, after);
+ }
+ },
+
+ // Updates the XUL node for an item in every window we've already populated
+ updateItem: function updateItem(item) {
+ for (let [window, winWrapper] of this.windowMap) {
+ let menuWrapper = winWrapper.getMenuWrapperForItem(item);
+ if (menuWrapper)
+ menuWrapper.updateItem(item);
+ }
+ },
+
+ // Moves the XUL node for an item in every window we've ever populated to its
+ // new place in the hierarchy
+ moveItem: function moveItem(item, after) {
+ for (let [window, winWrapper] of this.windowMap) {
+ let menuWrapper = winWrapper.getMenuWrapperForItem(item);
+ if (menuWrapper)
+ menuWrapper.moveItem(item, after);
+ }
+ },
+
+ // Removes the XUL nodes for an item in every window we've ever populated.
+ removeItem: function removeItem(item) {
+ for (let [window, winWrapper] of this.windowMap) {
+ let menuWrapper = winWrapper.getMenuWrapperForItem(item);
+ if (menuWrapper)
+ menuWrapper.removeItem(item);
+ }
+ }
+};
+
+WindowTracker(MenuManager);
diff --git a/addon-sdk/source/lib/sdk/context-menu/context.js b/addon-sdk/source/lib/sdk/context-menu/context.js
new file mode 100644
index 000000000..fc5aea500
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/context-menu/context.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/context-menu/core.js b/addon-sdk/source/lib/sdk/context-menu/core.js
new file mode 100644
index 000000000..c64cddfe8
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/context-menu/core.js
@@ -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);
diff --git a/addon-sdk/source/lib/sdk/context-menu/readers.js b/addon-sdk/source/lib/sdk/context-menu/readers.js
new file mode 100644
index 000000000..5078f8f29
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/context-menu/readers.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/context-menu@2.js b/addon-sdk/source/lib/sdk/context-menu@2.js
new file mode 100644
index 000000000..45ad804e9
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/context-menu@2.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+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);
diff --git a/addon-sdk/source/lib/sdk/core/disposable.js b/addon-sdk/source/lib/sdk/core/disposable.js
new file mode 100644
index 000000000..19f7eaa9f
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/core/disposable.js
@@ -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 => {});
diff --git a/addon-sdk/source/lib/sdk/core/heritage.js b/addon-sdk/source/lib/sdk/core/heritage.js
new file mode 100644
index 000000000..fc87ba1f5
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/core/heritage.js
@@ -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);
diff --git a/addon-sdk/source/lib/sdk/core/namespace.js b/addon-sdk/source/lib/sdk/core/namespace.js
new file mode 100644
index 000000000..3ceb73b72
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/core/namespace.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/core/observer.js b/addon-sdk/source/lib/sdk/core/observer.js
new file mode 100644
index 000000000..7e11bf8f9
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/core/observer.js
@@ -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);
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/core/promise.js b/addon-sdk/source/lib/sdk/core/promise.js
new file mode 100644
index 000000000..f4bd7b0f5
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/core/promise.js
@@ -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
+ });
+}
+
diff --git a/addon-sdk/source/lib/sdk/core/reference.js b/addon-sdk/source/lib/sdk/core/reference.js
new file mode 100644
index 000000000..04549cd0f
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/core/reference.js
@@ -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);
diff --git a/addon-sdk/source/lib/sdk/deprecated/api-utils.js b/addon-sdk/source/lib/sdk/deprecated/api-utils.js
new file mode 100644
index 000000000..856fc50cb
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/deprecated/api-utils.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/deprecated/events/assembler.js b/addon-sdk/source/lib/sdk/deprecated/events/assembler.js
new file mode 100644
index 000000000..bb297c24f
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/deprecated/events/assembler.js
@@ -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);
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/deprecated/sync-worker.js b/addon-sdk/source/lib/sdk/deprecated/sync-worker.js
new file mode 100644
index 000000000..71cadac36
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/deprecated/sync-worker.js
@@ -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));
+}
diff --git a/addon-sdk/source/lib/sdk/deprecated/unit-test-finder.js b/addon-sdk/source/lib/sdk/deprecated/unit-test-finder.js
new file mode 100644
index 000000000..e38629f45
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/deprecated/unit-test-finder.js
@@ -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())
+ };
+ });
+ }
+};
diff --git a/addon-sdk/source/lib/sdk/deprecated/unit-test.js b/addon-sdk/source/lib/sdk/deprecated/unit-test.js
new file mode 100644
index 000000000..32bba8f6b
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/deprecated/unit-test.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/deprecated/window-utils.js b/addon-sdk/source/lib/sdk/deprecated/window-utils.js
new file mode 100644
index 000000000..93c0ab7b8
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/deprecated/window-utils.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/dom/events-shimmed.js b/addon-sdk/source/lib/sdk/dom/events-shimmed.js
new file mode 100644
index 000000000..7a1727681
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/dom/events-shimmed.js
@@ -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);
diff --git a/addon-sdk/source/lib/sdk/dom/events.js b/addon-sdk/source/lib/sdk/dom/events.js
new file mode 100644
index 000000000..502d2350f
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/dom/events.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/dom/events/keys.js b/addon-sdk/source/lib/sdk/dom/events/keys.js
new file mode 100644
index 000000000..e6f1483a2
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/dom/events/keys.js
@@ -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));
+};
+
diff --git a/addon-sdk/source/lib/sdk/event/chrome.js b/addon-sdk/source/lib/sdk/event/chrome.js
new file mode 100644
index 000000000..9044fef99
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/event/chrome.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/event/core.js b/addon-sdk/source/lib/sdk/event/core.js
new file mode 100644
index 000000000..c16dd2df5
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/event/core.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/event/dom.js b/addon-sdk/source/lib/sdk/event/dom.js
new file mode 100644
index 000000000..da99dec7a
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/event/dom.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/event/target.js b/addon-sdk/source/lib/sdk/event/target.js
new file mode 100644
index 000000000..3a1f5e5f0
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/event/target.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/event/utils.js b/addon-sdk/source/lib/sdk/event/utils.js
new file mode 100644
index 000000000..f193b6785
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/event/utils.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/frame/hidden-frame.js b/addon-sdk/source/lib/sdk/frame/hidden-frame.js
new file mode 100644
index 000000000..97e0b7974
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/frame/hidden-frame.js
@@ -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));
diff --git a/addon-sdk/source/lib/sdk/frame/utils.js b/addon-sdk/source/lib/sdk/frame/utils.js
new file mode 100644
index 000000000..d9fccec4d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/frame/utils.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/fs/path.js b/addon-sdk/source/lib/sdk/fs/path.js
new file mode 100644
index 000000000..4474b2b4a
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/fs/path.js
@@ -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;
+ };
+} \ No newline at end of file
diff --git a/addon-sdk/source/lib/sdk/hotkeys.js b/addon-sdk/source/lib/sdk/hotkeys.js
new file mode 100644
index 000000000..00081455e
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/hotkeys.js
@@ -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);
+};
diff --git a/addon-sdk/source/lib/sdk/indexed-db.js b/addon-sdk/source/lib/sdk/indexed-db.js
new file mode 100644
index 000000000..d4d166c02
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/indexed-db.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/input/browser.js b/addon-sdk/source/lib/sdk/input/browser.js
new file mode 100644
index 000000000..daea875bf
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/input/browser.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/input/customizable-ui.js b/addon-sdk/source/lib/sdk/input/customizable-ui.js
new file mode 100644
index 000000000..a41d0971a
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/input/customizable-ui.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/input/frame.js b/addon-sdk/source/lib/sdk/input/frame.js
new file mode 100644
index 000000000..50efaa745
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/input/frame.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/input/system.js b/addon-sdk/source/lib/sdk/input/system.js
new file mode 100644
index 000000000..66bc6daec
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/input/system.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/io/buffer.js b/addon-sdk/source/lib/sdk/io/buffer.js
new file mode 100644
index 000000000..5ea169402
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/io/buffer.js
@@ -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);
+ }
+ });
+});
diff --git a/addon-sdk/source/lib/sdk/io/byte-streams.js b/addon-sdk/source/lib/sdk/io/byte-streams.js
new file mode 100644
index 000000000..6afab4369
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/io/byte-streams.js
@@ -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;
+ }
+};
diff --git a/addon-sdk/source/lib/sdk/io/file.js b/addon-sdk/source/lib/sdk/io/file.js
new file mode 100644
index 000000000..47467df87
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/io/file.js
@@ -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);
+ }
+};
diff --git a/addon-sdk/source/lib/sdk/io/fs.js b/addon-sdk/source/lib/sdk/io/fs.js
new file mode 100644
index 000000000..860a884a5
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/io/fs.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/io/stream.js b/addon-sdk/source/lib/sdk/io/stream.js
new file mode 100644
index 000000000..0698b8e32
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/io/stream.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/io/text-streams.js b/addon-sdk/source/lib/sdk/io/text-streams.js
new file mode 100644
index 000000000..ed4ec4972
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/io/text-streams.js
@@ -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;
+}
diff --git a/addon-sdk/source/lib/sdk/keyboard/hotkeys.js b/addon-sdk/source/lib/sdk/keyboard/hotkeys.js
new file mode 100644
index 000000000..a179502b8
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/keyboard/hotkeys.js
@@ -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();
+ }
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/keyboard/observer.js b/addon-sdk/source/lib/sdk/keyboard/observer.js
new file mode 100644
index 000000000..b8e32b95c
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/keyboard/observer.js
@@ -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();
diff --git a/addon-sdk/source/lib/sdk/keyboard/utils.js b/addon-sdk/source/lib/sdk/keyboard/utils.js
new file mode 100644
index 000000000..1b7df4ce3
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/keyboard/utils.js
@@ -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);
+};
diff --git a/addon-sdk/source/lib/sdk/l10n.js b/addon-sdk/source/lib/sdk/l10n.js
new file mode 100644
index 000000000..db5a9d7b6
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n.js
@@ -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;
+}
diff --git a/addon-sdk/source/lib/sdk/l10n/core.js b/addon-sdk/source/lib/sdk/l10n/core.js
new file mode 100644
index 000000000..2f8f84c04
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n/core.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/l10n/html.js b/addon-sdk/source/lib/sdk/l10n/html.js
new file mode 100644
index 000000000..fa2cf9cf0
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n/html.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+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");
+});
diff --git a/addon-sdk/source/lib/sdk/l10n/json/core.js b/addon-sdk/source/lib/sdk/l10n/json/core.js
new file mode 100644
index 000000000..af52f956f
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n/json/core.js
@@ -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";
+}
diff --git a/addon-sdk/source/lib/sdk/l10n/loader.js b/addon-sdk/source/lib/sdk/l10n/loader.js
new file mode 100644
index 000000000..60e219e44
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n/loader.js
@@ -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
+ };
+ });
+ });
+}
diff --git a/addon-sdk/source/lib/sdk/l10n/locale.js b/addon-sdk/source/lib/sdk/l10n/locale.js
new file mode 100644
index 000000000..950b33b20
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n/locale.js
@@ -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;
+}
diff --git a/addon-sdk/source/lib/sdk/l10n/plural-rules.js b/addon-sdk/source/lib/sdk/l10n/plural-rules.js
new file mode 100644
index 000000000..a3ef48a5e
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n/plural-rules.js
@@ -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];
+}
+
diff --git a/addon-sdk/source/lib/sdk/l10n/prefs.js b/addon-sdk/source/lib/sdk/l10n/prefs.js
new file mode 100644
index 000000000..8ee26fc5b
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n/prefs.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/l10n/properties/core.js b/addon-sdk/source/lib/sdk/l10n/properties/core.js
new file mode 100644
index 000000000..7a9081d0b
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n/properties/core.js
@@ -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));
diff --git a/addon-sdk/source/lib/sdk/lang/functional.js b/addon-sdk/source/lib/sdk/lang/functional.js
new file mode 100644
index 000000000..66e30edfa
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/lang/functional.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/lang/functional/concurrent.js b/addon-sdk/source/lib/sdk/lang/functional/concurrent.js
new file mode 100644
index 000000000..85e8cff46
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/lang/functional/concurrent.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/lang/functional/core.js b/addon-sdk/source/lib/sdk/lang/functional/core.js
new file mode 100644
index 000000000..0d9143364
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/lang/functional/core.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/lang/functional/helpers.js b/addon-sdk/source/lib/sdk/lang/functional/helpers.js
new file mode 100644
index 000000000..60f4e3300
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/lang/functional/helpers.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/lang/type.js b/addon-sdk/source/lib/sdk/lang/type.js
new file mode 100644
index 000000000..b50e6be4c
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/lang/type.js
@@ -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);
+};
diff --git a/addon-sdk/source/lib/sdk/lang/weak-set.js b/addon-sdk/source/lib/sdk/lang/weak-set.js
new file mode 100644
index 000000000..8972602a5
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/lang/weak-set.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/loader/cuddlefish.js b/addon-sdk/source/lib/sdk/loader/cuddlefish.js
new file mode 100644
index 000000000..6ba19157b
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/loader/cuddlefish.js
@@ -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
+});
diff --git a/addon-sdk/source/lib/sdk/loader/sandbox.js b/addon-sdk/source/lib/sdk/loader/sandbox.js
new file mode 100644
index 000000000..791dbc086
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/loader/sandbox.js
@@ -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
diff --git a/addon-sdk/source/lib/sdk/messaging.js b/addon-sdk/source/lib/sdk/messaging.js
new file mode 100644
index 000000000..07580eb33
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/messaging.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/model/core.js b/addon-sdk/source/lib/sdk/model/core.js
new file mode 100644
index 000000000..315f8b1cd
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/model/core.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+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;
diff --git a/addon-sdk/source/lib/sdk/net/url.js b/addon-sdk/source/lib/sdk/net/url.js
new file mode 100644
index 000000000..5502171ee
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/net/url.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/net/xhr.js b/addon-sdk/source/lib/sdk/net/xhr.js
new file mode 100644
index 000000000..415b9cbf4
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/net/xhr.js
@@ -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.
diff --git a/addon-sdk/source/lib/sdk/notifications.js b/addon-sdk/source/lib/sdk/notifications.js
new file mode 100644
index 000000000..752e08fb1
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/notifications.js
@@ -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"]
+ }
+ });
+}
diff --git a/addon-sdk/source/lib/sdk/output/system.js b/addon-sdk/source/lib/sdk/output/system.js
new file mode 100644
index 000000000..4fb16dcd5
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/output/system.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/page-mod.js b/addon-sdk/source/lib/sdk/page-mod.js
new file mode 100644
index 000000000..538be2732
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/page-mod.js
@@ -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);
+});
diff --git a/addon-sdk/source/lib/sdk/page-mod/match-pattern.js b/addon-sdk/source/lib/sdk/page-mod/match-pattern.js
new file mode 100644
index 000000000..afbbd401e
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/page-mod/match-pattern.js
@@ -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");
diff --git a/addon-sdk/source/lib/sdk/page-worker.js b/addon-sdk/source/lib/sdk/page-worker.js
new file mode 100644
index 000000000..837cf774b
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/page-worker.js
@@ -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);
+});
diff --git a/addon-sdk/source/lib/sdk/panel.js b/addon-sdk/source/lib/sdk/panel.js
new file mode 100644
index 000000000..4b625799d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/panel.js
@@ -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);
+});
diff --git a/addon-sdk/source/lib/sdk/panel/events.js b/addon-sdk/source/lib/sdk/panel/events.js
new file mode 100644
index 000000000..f3040a11d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/panel/events.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/panel/utils.js b/addon-sdk/source/lib/sdk/panel/utils.js
new file mode 100644
index 000000000..c85b274bc
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/panel/utils.js
@@ -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;
diff --git a/addon-sdk/source/lib/sdk/passwords.js b/addon-sdk/source/lib/sdk/passwords.js
new file mode 100644
index 000000000..70f0aa4da
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/passwords.js
@@ -0,0 +1,61 @@
+/* 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 { search, remove, store } = require("./passwords/utils");
+const { defer, delay } = require("./lang/functional");
+
+/**
+ * Utility function that returns `onComplete` and `onError` callbacks form the
+ * given `options` objects. Also properties are removed from the passed
+ * `options` objects.
+ * @param {Object} options
+ * Object that is passed to the exported functions of this module.
+ * @returns {Function[]}
+ * Array with two elements `onComplete` and `onError` functions.
+ */
+function getCallbacks(options) {
+ let value = [
+ 'onComplete' in options ? options.onComplete : null,
+ 'onError' in options ? defer(options.onError) : console.exception
+ ];
+
+ delete options.onComplete;
+ delete options.onError;
+
+ return value;
+};
+
+/**
+ * Creates a wrapper function that tries to call `onComplete` with a return
+ * value of the wrapped function or falls back to `onError` if wrapped function
+ * throws an exception.
+ */
+function createWrapperMethod(wrapped) {
+ return function (options) {
+ let [ onComplete, onError ] = getCallbacks(options);
+ try {
+ let value = wrapped(options);
+ if (onComplete) {
+ delay(function() {
+ try {
+ onComplete(value);
+ } catch (exception) {
+ onError(exception);
+ }
+ });
+ }
+ } catch (exception) {
+ onError(exception);
+ }
+ };
+}
+
+exports.search = createWrapperMethod(search);
+exports.store = createWrapperMethod(store);
+exports.remove = createWrapperMethod(remove);
diff --git a/addon-sdk/source/lib/sdk/passwords/utils.js b/addon-sdk/source/lib/sdk/passwords/utils.js
new file mode 100644
index 000000000..334efa490
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/passwords/utils.js
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { Cc, Ci, CC } = require("chrome");
+const { uri: ADDON_URI } = require("../self");
+const loginManager = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+const { URL: parseURL } = require("../url");
+const LoginInfo = CC("@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo", "init");
+
+function filterMatchingLogins(loginInfo) {
+ return Object.keys(this).every(key => loginInfo[key] === this[key], this);
+}
+
+/**
+ * Removes `user`, `password` and `path` fields from the given `url` if it's
+ * 'http', 'https' or 'ftp'. All other URLs are returned unchanged.
+ * @example
+ * http://user:pass@www.site.com/foo/?bar=baz#bang -> http://www.site.com
+ */
+function normalizeURL(url) {
+ let { scheme, host, port } = parseURL(url);
+ // We normalize URL only if it's `http`, `https` or `ftp`. All other types of
+ // URLs (`resource`, `chrome`, etc..) should not be normalized as they are
+ // used with add-on associated credentials path.
+ return scheme === "http" || scheme === "https" || scheme === "ftp" ?
+ scheme + "://" + (host || "") + (port ? ":" + port : "") :
+ url
+}
+
+function Login(options) {
+ let login = Object.create(Login.prototype);
+ Object.keys(options || {}).forEach(function(key) {
+ if (key === 'url')
+ login.hostname = normalizeURL(options.url);
+ else if (key === 'formSubmitURL')
+ login.formSubmitURL = options.formSubmitURL ?
+ normalizeURL(options.formSubmitURL) : null;
+ else if (key === 'realm')
+ login.httpRealm = options.realm;
+ else
+ login[key] = options[key];
+ });
+
+ return login;
+}
+Login.prototype.toJSON = function toJSON() {
+ return {
+ url: this.hostname || ADDON_URI,
+ realm: this.httpRealm || null,
+ formSubmitURL: this.formSubmitURL || null,
+ username: this.username || null,
+ password: this.password || null,
+ usernameField: this.usernameField || '',
+ passwordField: this.passwordField || '',
+ }
+};
+Login.prototype.toLoginInfo = function toLoginInfo() {
+ let { url, realm, formSubmitURL, username, password, usernameField,
+ passwordField } = this.toJSON();
+
+ return new LoginInfo(url, formSubmitURL, realm, username, password,
+ usernameField, passwordField);
+};
+
+function loginToJSON(value) {
+ return Login(value).toJSON();
+}
+
+/**
+ * Returns array of `nsILoginInfo` objects that are stored in the login manager
+ * and have all the properties with matching values as a given `options` object.
+ * @param {Object} options
+ * @returns {nsILoginInfo[]}
+ */
+exports.search = function search(options) {
+ return loginManager.getAllLogins()
+ .filter(filterMatchingLogins, Login(options))
+ .map(loginToJSON);
+};
+
+/**
+ * Stores login info created from the given `options` to the applications
+ * built-in login management system.
+ * @param {Object} options.
+ */
+exports.store = function store(options) {
+ loginManager.addLogin(Login(options).toLoginInfo());
+};
+
+/**
+ * Removes login info from the applications built-in login management system.
+ * _Please note: When removing a login info the specified properties must
+ * exactly match to the one that is already stored or exception will be thrown._
+ * @param {Object} options.
+ */
+exports.remove = function remove(options) {
+ loginManager.removeLogin(Login(options).toLoginInfo());
+};
diff --git a/addon-sdk/source/lib/sdk/places/bookmarks.js b/addon-sdk/source/lib/sdk/places/bookmarks.js
new file mode 100644
index 000000000..c4f9528f1
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/bookmarks.js
@@ -0,0 +1,395 @@
+/* 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",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+/*
+ * Requiring hosts so they can subscribe to client messages
+ */
+require('./host/host-bookmarks');
+require('./host/host-tags');
+require('./host/host-query');
+
+const { Cc, Ci } = require('chrome');
+const { Class } = require('../core/heritage');
+const { send } = require('../addon/events');
+const { defer, reject, all, resolve, promised } = require('../core/promise');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const { identity, defer:async } = require('../lang/functional');
+const { extend, merge } = require('../util/object');
+const { fromIterator } = require('../util/array');
+const {
+ constructTree, fetchItem, createQuery,
+ isRootGroup, createQueryOptions
+} = require('./utils');
+const {
+ bookmarkContract, groupContract, separatorContract
+} = require('./contract');
+const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+/*
+ * Mapping of uncreated bookmarks with their created
+ * counterparts
+ */
+const itemMap = new WeakMap();
+
+/*
+ * Constant used by nsIHistoryQuery; 1 is a bookmark query
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+ */
+const BOOKMARK_QUERY = 1;
+
+/*
+ * Bookmark Item classes
+ */
+
+const Bookmark = Class({
+ extends: [
+ bookmarkContract.properties(identity)
+ ],
+ initialize: function initialize (options) {
+ merge(this, bookmarkContract(extend(defaults, options)));
+ },
+ type: 'bookmark',
+ toString: () => '[object Bookmark]'
+});
+exports.Bookmark = Bookmark;
+
+const Group = Class({
+ extends: [
+ groupContract.properties(identity)
+ ],
+ initialize: function initialize (options) {
+ // Don't validate if root group
+ if (isRootGroup(options))
+ merge(this, options);
+ else
+ merge(this, groupContract(extend(defaults, options)));
+ },
+ type: 'group',
+ toString: () => '[object Group]'
+});
+exports.Group = Group;
+
+const Separator = Class({
+ extends: [
+ separatorContract.properties(identity)
+ ],
+ initialize: function initialize (options) {
+ merge(this, separatorContract(extend(defaults, options)));
+ },
+ type: 'separator',
+ toString: () => '[object Separator]'
+});
+exports.Separator = Separator;
+
+/*
+ * Functions
+ */
+
+function save (items, options) {
+ items = [].concat(items);
+ options = options || {};
+ let emitter = EventTarget();
+ let results = [];
+ let errors = [];
+ let root = constructTree(items);
+ let cache = new Map();
+
+ let isExplicitSave = item => !!~items.indexOf(item);
+ // `walk` returns an aggregate promise indicating the completion
+ // of the `commitItem` on each node, not whether or not that
+ // commit was successful
+
+ // Force this to be async, as if a ducktype fails validation,
+ // the promise implementation will fire an error event, which will
+ // not trigger the handler as it's not yet bound
+ //
+ // Can remove after `Promise.jsm` is implemented in Bug 881047,
+ // which will guarantee next tick execution
+ async(() => root.walk(preCommitItem).then(commitComplete))();
+
+ function preCommitItem ({value:item}) {
+ // Do nothing if tree root, default group (unsavable),
+ // or if it's a dependency and not explicitly saved (in the list
+ // of items to be saved), and not needed to be saved
+ if (item === null || // node is the tree root
+ isRootGroup(item) ||
+ (getId(item) && !isExplicitSave(item)))
+ return;
+
+ return promised(validate)(item)
+ .then(() => commitItem(item, options))
+ .then(data => construct(data, cache))
+ .then(savedItem => {
+ // If item was just created, make a map between
+ // the creation object and created object,
+ // so we can reference the item that doesn't have an id
+ if (!getId(item))
+ saveId(item, savedItem.id);
+
+ // Emit both the processed item, and original item
+ // so a mapping can be understood in handler
+ emit(emitter, 'data', savedItem, item);
+
+ // Push to results iff item was explicitly saved
+ if (isExplicitSave(item))
+ results[items.indexOf(item)] = savedItem;
+ }, reason => {
+ // Force reason to be a string for consistency
+ reason = reason + '';
+ // Emit both the reason, and original item
+ // so a mapping can be understood in handler
+ emit(emitter, 'error', reason + '', item);
+ // Store unsaved item in results list
+ results[items.indexOf(item)] = item;
+ errors.push(reason);
+ });
+ }
+
+ // Called when traversal of the node tree is completed and all
+ // items have been committed
+ function commitComplete () {
+ emit(emitter, 'end', results);
+ }
+
+ return emitter;
+}
+exports.save = save;
+
+function search (queries, options) {
+ queries = [].concat(queries);
+ let emitter = EventTarget();
+ let cache = new Map();
+ let queryObjs = queries.map(createQuery.bind(null, BOOKMARK_QUERY));
+ let optionsObj = createQueryOptions(BOOKMARK_QUERY, options);
+
+ // Can remove after `Promise.jsm` is implemented in Bug 881047,
+ // which will guarantee next tick execution
+ async(() => {
+ send('sdk-places-query', { queries: queryObjs, options: optionsObj })
+ .then(handleQueryResponse);
+ })();
+
+ function handleQueryResponse (data) {
+ let deferreds = data.map(item => {
+ return construct(item, cache).then(bookmark => {
+ emit(emitter, 'data', bookmark);
+ return bookmark;
+ }, reason => {
+ emit(emitter, 'error', reason);
+ errors.push(reason);
+ });
+ });
+
+ all(deferreds).then(data => {
+ emit(emitter, 'end', data);
+ }, () => emit(emitter, 'end', []));
+ }
+
+ return emitter;
+}
+exports.search = search;
+
+function remove (items) {
+ return [].concat(items).map(item => {
+ item.remove = true;
+ return item;
+ });
+}
+
+exports.remove = remove;
+
+/*
+ * Internal Utilities
+ */
+
+function commitItem (item, options) {
+ // Get the item's ID, or getId it's saved version if it exists
+ let id = getId(item);
+ let data = normalize(item);
+ let promise;
+
+ data.id = id;
+
+ if (!id) {
+ promise = send('sdk-places-bookmarks-create', data);
+ } else if (item.remove) {
+ promise = send('sdk-places-bookmarks-remove', { id: id });
+ } else {
+ promise = send('sdk-places-bookmarks-last-updated', {
+ id: id
+ }).then(function (updated) {
+ // If attempting to save an item that is not the
+ // latest snapshot of a bookmark item, execute
+ // the resolution function
+ if (updated !== item.updated && options.resolve)
+ return fetchItem(id)
+ .then(options.resolve.bind(null, data));
+ else
+ return data;
+ }).then(send.bind(null, 'sdk-places-bookmarks-save'));
+ }
+
+ return promise;
+}
+
+/*
+ * Turns a bookmark item into a plain object,
+ * converts `tags` from Set to Array, group instance to an id
+ */
+function normalize (item) {
+ let data = merge({}, item);
+ // Circumvent prototype property of `type`
+ delete data.type;
+ data.type = item.type;
+ data.tags = [];
+ if (item.tags) {
+ data.tags = fromIterator(item.tags);
+ }
+ data.group = getId(data.group) || exports.UNSORTED.id;
+
+ return data;
+}
+
+/*
+ * Takes a data object and constructs a BookmarkItem instance
+ * of it, recursively generating parent instances as well.
+ *
+ * Pass in a `cache` Map to reuse instances of
+ * bookmark items to reduce overhead;
+ * The cache object is a map of id to a deferred with a
+ * promise that resolves to the bookmark item.
+ */
+function construct (object, cache, forced) {
+ let item = instantiate(object);
+ let deferred = defer();
+
+ // Item could not be instantiated
+ if (!item)
+ return resolve(null);
+
+ // Return promise for item if found in the cache,
+ // and not `forced`. `forced` indicates that this is the construct
+ // call that should not read from cache, but should actually perform
+ // the construction, as it was set before several async calls
+ if (cache.has(item.id) && !forced)
+ return cache.get(item.id).promise;
+ else if (cache.has(item.id))
+ deferred = cache.get(item.id);
+ else
+ cache.set(item.id, deferred);
+
+ // When parent group is found in cache, use
+ // the same deferred value
+ if (item.group && cache.has(item.group)) {
+ cache.get(item.group).promise.then(group => {
+ item.group = group;
+ deferred.resolve(item);
+ });
+
+ // If not in the cache, and a root group, return
+ // the premade instance
+ } else if (rootGroups.get(item.group)) {
+ item.group = rootGroups.get(item.group);
+ deferred.resolve(item);
+
+ // If not in the cache or a root group, fetch the parent
+ } else {
+ cache.set(item.group, defer());
+ fetchItem(item.group).then(group => {
+ return construct(group, cache, true);
+ }).then(group => {
+ item.group = group;
+ deferred.resolve(item);
+ }, deferred.reject);
+ }
+
+ return deferred.promise;
+}
+
+function instantiate (object) {
+ if (object.type === 'bookmark')
+ return Bookmark(object);
+ if (object.type === 'group')
+ return Group(object);
+ if (object.type === 'separator')
+ return Separator(object);
+ return null;
+}
+
+/**
+ * Validates a bookmark item; will throw an error if ininvalid,
+ * to be used with `promised`. As bookmark items check on their class,
+ * this only checks ducktypes
+ */
+function validate (object) {
+ if (!isDuckType(object)) return true;
+ let contract = object.type === 'bookmark' ? bookmarkContract :
+ object.type === 'group' ? groupContract :
+ object.type === 'separator' ? separatorContract :
+ null;
+ if (!contract) {
+ throw Error('No type specified');
+ }
+
+ // If object has a property set, and undefined,
+ // manually override with default as it'll fail otherwise
+ let withDefaults = Object.keys(defaults).reduce((obj, prop) => {
+ if (obj[prop] == null) obj[prop] = defaults[prop];
+ return obj;
+ }, extend(object));
+
+ contract(withDefaults);
+}
+
+function isDuckType (item) {
+ return !(item instanceof Bookmark) &&
+ !(item instanceof Group) &&
+ !(item instanceof Separator);
+}
+
+function saveId (unsaved, id) {
+ itemMap.set(unsaved, id);
+}
+
+// Fetches an item's ID from itself, or from the mapped items
+function getId (item) {
+ return typeof item === 'number' ? item :
+ item ? item.id || itemMap.get(item) :
+ null;
+}
+
+/*
+ * Set up the default, root groups
+ */
+
+var defaultGroupMap = {
+ MENU: bmsrv.bookmarksMenuFolder,
+ TOOLBAR: bmsrv.toolbarFolder,
+ UNSORTED: bmsrv.unfiledBookmarksFolder
+};
+
+var rootGroups = new Map();
+
+for (let i in defaultGroupMap) {
+ let group = Object.freeze(Group({ title: i, id: defaultGroupMap[i] }));
+ rootGroups.set(defaultGroupMap[i], group);
+ exports[i] = group;
+}
+
+var defaults = {
+ group: exports.UNSORTED,
+ index: -1
+};
diff --git a/addon-sdk/source/lib/sdk/places/contract.js b/addon-sdk/source/lib/sdk/places/contract.js
new file mode 100644
index 000000000..a3541c34d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/contract.js
@@ -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";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { Cc, Ci } = require('chrome');
+const { isValidURI, URL } = require('../url');
+const { contract } = require('../util/contract');
+const { extend } = require('../util/object');
+
+// map of property validations
+const validItem = {
+ id: {
+ is: ['number', 'undefined', 'null'],
+ },
+ group: {
+ is: ['object', 'number', 'undefined', 'null'],
+ ok: function (value) {
+ return value &&
+ (value.toString && value.toString() === '[object Group]') ||
+ typeof value === 'number' ||
+ value.type === 'group';
+ },
+ msg: 'The `group` property must be a valid Group object'
+ },
+ index: {
+ is: ['undefined', 'null', 'number'],
+ map: value => value == null ? -1 : value,
+ msg: 'The `index` property must be a number.'
+ },
+ updated: {
+ is: ['number', 'undefined']
+ }
+};
+
+const validTitle = {
+ title: {
+ is: ['string'],
+ msg: 'The `title` property must be defined.'
+ }
+};
+
+const validURL = {
+ url: {
+ is: ['string'],
+ ok: isValidURI,
+ msg: 'The `url` property must be a valid URL.'
+ }
+};
+
+const validTags = {
+ tags: {
+ is: ['object'],
+ ok: tags => tags instanceof Set,
+ map: function (tags) {
+ if (Array.isArray(tags))
+ return new Set(tags);
+ if (tags == null)
+ return new Set();
+ return tags;
+ },
+ msg: 'The `tags` property must be a Set, or an array'
+ }
+};
+
+exports.bookmarkContract = contract(
+ extend(validItem, validTitle, validURL, validTags));
+exports.separatorContract = contract(validItem);
+exports.groupContract = contract(extend(validItem, validTitle));
diff --git a/addon-sdk/source/lib/sdk/places/events.js b/addon-sdk/source/lib/sdk/places/events.js
new file mode 100644
index 000000000..a3f95ee03
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/events.js
@@ -0,0 +1,128 @@
+/* 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',
+ 'engines': {
+ 'Firefox': '*',
+ "SeaMonkey": '*'
+ }
+};
+
+const { Cc, Ci } = require('chrome');
+const { Unknown } = require('../platform/xpcom');
+const { Class } = require('../core/heritage');
+const { merge } = require('../util/object');
+const bookmarkService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
+ .getService(Ci.nsINavBookmarksService);
+const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
+ .getService(Ci.nsINavHistoryService);
+const { mapBookmarkItemType } = require('./utils');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const { when } = require('../system/unload');
+
+const emitter = EventTarget();
+
+var HISTORY_ARGS = {
+ onBeginUpdateBatch: [],
+ onEndUpdateBatch: [],
+ onClearHistory: [],
+ onDeleteURI: ['url'],
+ onDeleteVisits: ['url', 'visitTime'],
+ onPageChanged: ['url', 'property', 'value'],
+ onTitleChanged: ['url', 'title'],
+ onVisit: [
+ 'url', 'visitId', 'time', 'sessionId', 'referringId', 'transitionType'
+ ]
+};
+
+var HISTORY_EVENTS = {
+ onBeginUpdateBatch: 'history-start-batch',
+ onEndUpdateBatch: 'history-end-batch',
+ onClearHistory: 'history-start-clear',
+ onDeleteURI: 'history-delete-url',
+ onDeleteVisits: 'history-delete-visits',
+ onPageChanged: 'history-page-changed',
+ onTitleChanged: 'history-title-changed',
+ onVisit: 'history-visit'
+};
+
+var BOOKMARK_ARGS = {
+ onItemAdded: [
+ 'id', 'parentId', 'index', 'type', 'url', 'title', 'dateAdded'
+ ],
+ onItemChanged: [
+ 'id', 'property', null, 'value', 'lastModified', 'type', 'parentId'
+ ],
+ onItemMoved: [
+ 'id', 'previousParentId', 'previousIndex', 'currentParentId',
+ 'currentIndex', 'type'
+ ],
+ onItemRemoved: ['id', 'parentId', 'index', 'type', 'url'],
+ onItemVisited: ['id', 'visitId', 'time', 'transitionType', 'url', 'parentId']
+};
+
+var BOOKMARK_EVENTS = {
+ onItemAdded: 'bookmark-item-added',
+ onItemChanged: 'bookmark-item-changed',
+ onItemMoved: 'bookmark-item-moved',
+ onItemRemoved: 'bookmark-item-removed',
+ onItemVisited: 'bookmark-item-visited',
+};
+
+function createHandler (type, propNames) {
+ propNames = propNames || [];
+ return function (...args) {
+ let data = propNames.reduce((acc, prop, i) => {
+ if (prop)
+ acc[prop] = formatValue(prop, args[i]);
+ return acc;
+ }, {});
+
+ emit(emitter, 'data', {
+ type: type,
+ data: data
+ });
+ };
+}
+
+/*
+ * Creates an observer, creating handlers based off of
+ * the `events` names, and ordering arguments from `propNames` hash
+ */
+function createObserverInstance (events, propNames) {
+ let definition = Object.keys(events).reduce((prototype, eventName) => {
+ prototype[eventName] = createHandler(events[eventName], propNames[eventName]);
+ return prototype;
+ }, {});
+
+ return Class(merge(definition, { extends: Unknown }))();
+}
+
+/*
+ * Formats `data` based off of the value of `type`
+ */
+function formatValue (type, data) {
+ if (type === 'type')
+ return mapBookmarkItemType(data);
+ if (type === 'url' && data)
+ return data.spec;
+ return data;
+}
+
+var historyObserver = createObserverInstance(HISTORY_EVENTS, HISTORY_ARGS);
+historyService.addObserver(historyObserver, false);
+
+var bookmarkObserver = createObserverInstance(BOOKMARK_EVENTS, BOOKMARK_ARGS);
+bookmarkService.addObserver(bookmarkObserver, false);
+
+when(() => {
+ historyService.removeObserver(historyObserver);
+ bookmarkService.removeObserver(bookmarkObserver);
+});
+
+exports.events = emitter;
diff --git a/addon-sdk/source/lib/sdk/places/favicon.js b/addon-sdk/source/lib/sdk/places/favicon.js
new file mode 100644
index 000000000..05b057db1
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/favicon.js
@@ -0,0 +1,49 @@
+/* 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",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Cc, Ci, Cu } = require("chrome");
+const { defer, reject } = require("../core/promise");
+const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"].
+ getService(Ci.nsIFaviconService);
+const AsyncFavicons = FaviconService.QueryInterface(Ci.mozIAsyncFavicons);
+const { isValidURI } = require("../url");
+const { newURI, getURL } = require("../url/utils");
+
+/**
+ * Takes an object of several possible types and
+ * returns a promise that resolves to the page's favicon URI.
+ * @param {String|Tab} object
+ * @param {Function} (callback)
+ * @returns {Promise}
+ */
+
+function getFavicon (object, callback) {
+ let url = getURL(object);
+ let deferred = defer();
+
+ if (url && isValidURI(url)) {
+ AsyncFavicons.getFaviconURLForPage(newURI(url), function (aURI) {
+ if (aURI && aURI.spec)
+ deferred.resolve(aURI.spec.toString());
+ else
+ deferred.reject(null);
+ });
+ } else {
+ deferred.reject(null);
+ }
+
+ if (callback) deferred.promise.then(callback, callback);
+ return deferred.promise;
+}
+exports.getFavicon = getFavicon;
diff --git a/addon-sdk/source/lib/sdk/places/history.js b/addon-sdk/source/lib/sdk/places/history.js
new file mode 100644
index 000000000..b243b024c
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/history.js
@@ -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",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+/*
+ * Requiring hosts so they can subscribe to client messages
+ */
+require('./host/host-bookmarks');
+require('./host/host-tags');
+require('./host/host-query');
+
+const { Cc, Ci } = require('chrome');
+const { Class } = require('../core/heritage');
+const { events, send } = require('../addon/events');
+const { defer, reject, all } = require('../core/promise');
+const { uuid } = require('../util/uuid');
+const { flatten } = require('../util/array');
+const { has, extend, merge, pick } = require('../util/object');
+const { emit } = require('../event/core');
+const { defer: async } = require('../lang/functional');
+const { EventTarget } = require('../event/target');
+const {
+ urlQueryParser, createQuery, createQueryOptions
+} = require('./utils');
+
+/*
+ * Constant used by nsIHistoryQuery; 0 is a history query
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+ */
+const HISTORY_QUERY = 0;
+
+var search = function query (queries, options) {
+ queries = [].concat(queries);
+ let emitter = EventTarget();
+ let queryObjs = queries.map(createQuery.bind(null, HISTORY_QUERY));
+ let optionsObj = createQueryOptions(HISTORY_QUERY, options);
+
+ // Can remove after `Promise.jsm` is implemented in Bug 881047,
+ // which will guarantee next tick execution
+ async(() => {
+ send('sdk-places-query', {
+ query: queryObjs,
+ options: optionsObj
+ }).then(results => {
+ results.map(item => emit(emitter, 'data', item));
+ emit(emitter, 'end', results);
+ }, reason => {
+ emit(emitter, 'error', reason);
+ emit(emitter, 'end', []);
+ });
+ })();
+
+ return emitter;
+};
+exports.search = search;
diff --git a/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js b/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js
new file mode 100644
index 000000000..3245c4070
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js
@@ -0,0 +1,238 @@
+/* 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",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Cc, Ci } = require('chrome');
+const browserHistory = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsIBrowserHistory);
+const asyncHistory = Cc["@mozilla.org/browser/history;1"].
+ getService(Ci.mozIAsyncHistory);
+const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+const taggingService = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+const ios = Cc['@mozilla.org/network/io-service;1'].
+ getService(Ci.nsIIOService);
+const { query } = require('./host-query');
+const {
+ defer, all, resolve, promised, reject
+} = require('../../core/promise');
+const { request, response } = require('../../addon/host');
+const { send } = require('../../addon/events');
+const { on, emit } = require('../../event/core');
+const { filter } = require('../../event/utils');
+const { URL, isValidURI } = require('../../url');
+const { newURI } = require('../../url/utils');
+
+const DEFAULT_INDEX = bmsrv.DEFAULT_INDEX;
+const UNSORTED_ID = bmsrv.unfiledBookmarksFolder;
+const ROOT_FOLDERS = [
+ bmsrv.unfiledBookmarksFolder, bmsrv.toolbarFolder,
+ bmsrv.tagsFolder, bmsrv.bookmarksMenuFolder
+];
+
+const EVENT_MAP = {
+ 'sdk-places-bookmarks-create': createBookmarkItem,
+ 'sdk-places-bookmarks-save': saveBookmarkItem,
+ 'sdk-places-bookmarks-last-updated': getBookmarkLastUpdated,
+ 'sdk-places-bookmarks-get': getBookmarkItem,
+ 'sdk-places-bookmarks-remove': removeBookmarkItem,
+ 'sdk-places-bookmarks-get-all': getAllBookmarks,
+ 'sdk-places-bookmarks-get-children': getChildren
+};
+
+function typeMap (type) {
+ if (typeof type === 'number') {
+ if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark';
+ if (bmsrv.TYPE_FOLDER === type) return 'group';
+ if (bmsrv.TYPE_SEPARATOR === type) return 'separator';
+ } else {
+ if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK;
+ if ('group' === type) return bmsrv.TYPE_FOLDER;
+ if ('separator' === type) return bmsrv.TYPE_SEPARATOR;
+ }
+}
+
+function getBookmarkLastUpdated ({id}) {
+ return resolve(bmsrv.getItemLastModified(id));
+}
+exports.getBookmarkLastUpdated;
+
+function createBookmarkItem (data) {
+ let error;
+
+ if (data.group == null) data.group = UNSORTED_ID;
+ if (data.index == null) data.index = DEFAULT_INDEX;
+
+ if (data.type === 'group')
+ data.id = bmsrv.createFolder(
+ data.group, data.title, data.index
+ );
+ else if (data.type === 'separator')
+ data.id = bmsrv.insertSeparator(
+ data.group, data.index
+ );
+ else
+ data.id = bmsrv.insertBookmark(
+ data.group, newURI(data.url), data.index, data.title
+ );
+
+ // In the event where default or no index is provided (-1),
+ // query the actual index for the response
+ if (data.index === -1)
+ data.index = bmsrv.getItemIndex(data.id);
+
+ try {
+ data.updated = bmsrv.getItemLastModified(data.id);
+ }
+ catch (e) {
+ console.exception(e);
+ }
+
+ return tag(data, true).then(() => data);
+}
+exports.createBookmarkItem = createBookmarkItem;
+
+function saveBookmarkItem (data) {
+ let id = data.id;
+ if (!id)
+ reject('Item is missing id');
+
+ let group = bmsrv.getFolderIdForItem(id);
+ let index = bmsrv.getItemIndex(id);
+ let type = bmsrv.getItemType(id);
+ let title = typeMap(type) !== 'separator' ?
+ bmsrv.getItemTitle(id) :
+ undefined;
+ let url = typeMap(type) === 'bookmark' ?
+ bmsrv.getBookmarkURI(id).spec :
+ undefined;
+
+ if (url != data.url)
+ bmsrv.changeBookmarkURI(id, newURI(data.url));
+ else if (typeMap(type) === 'bookmark')
+ data.url = url;
+
+ if (title != data.title)
+ bmsrv.setItemTitle(id, data.title);
+ else if (typeMap(type) !== 'separator')
+ data.title = title;
+
+ if (data.group && data.group !== group)
+ bmsrv.moveItem(id, data.group, data.index || -1);
+ else if (data.index != null && data.index !== index) {
+ // We use moveItem here instead of setItemIndex
+ // so we don't have to manage the indicies of the siblings
+ bmsrv.moveItem(id, group, data.index);
+ } else if (data.index == null)
+ data.index = index;
+
+ data.updated = bmsrv.getItemLastModified(data.id);
+
+ return tag(data).then(() => data);
+}
+exports.saveBookmarkItem = saveBookmarkItem;
+
+function removeBookmarkItem (data) {
+ let id = data.id;
+
+ if (!id)
+ reject('Item is missing id');
+
+ bmsrv.removeItem(id);
+ return resolve(null);
+}
+exports.removeBookmarkItem = removeBookmarkItem;
+
+function getBookmarkItem (data) {
+ let id = data.id;
+
+ if (!id)
+ reject('Item is missing id');
+
+ let type = bmsrv.getItemType(id);
+
+ data.type = typeMap(type);
+
+ if (type === bmsrv.TYPE_BOOKMARK || type === bmsrv.TYPE_FOLDER)
+ data.title = bmsrv.getItemTitle(id);
+
+ if (type === bmsrv.TYPE_BOOKMARK) {
+ data.url = bmsrv.getBookmarkURI(id).spec;
+ // Should be moved into host-tags as a method
+ data.tags = taggingService.getTagsForURI(newURI(data.url), {});
+ }
+
+ data.group = bmsrv.getFolderIdForItem(id);
+ data.index = bmsrv.getItemIndex(id);
+ data.updated = bmsrv.getItemLastModified(data.id);
+
+ return resolve(data);
+}
+exports.getBookmarkItem = getBookmarkItem;
+
+function getAllBookmarks () {
+ return query({}, { queryType: 1 }).then(bookmarks =>
+ all(bookmarks.map(getBookmarkItem)));
+}
+exports.getAllBookmarks = getAllBookmarks;
+
+function getChildren ({ id }) {
+ if (typeMap(bmsrv.getItemType(id)) !== 'group') return [];
+ let ids = [];
+ for (let i = 0; ids[ids.length - 1] !== -1; i++)
+ ids.push(bmsrv.getIdForItemAt(id, i));
+ ids.pop();
+ return all(ids.map(id => getBookmarkItem({ id: id })));
+}
+exports.getChildren = getChildren;
+
+/*
+ * Hook into host
+ */
+
+var reqStream = filter(request, (data) => /sdk-places-bookmarks/.test(data.event));
+on(reqStream, 'data', ({ event, id, data }) => {
+ if (!EVENT_MAP[event]) return;
+
+ let resData = { id: id, event: event };
+
+ promised(EVENT_MAP[event])(data).
+ then(res => resData.data = res, e => resData.error = e).
+ then(() => emit(response, 'data', resData));
+});
+
+function tag (data, isNew) {
+ // If a new item, we can skip checking what other tags
+ // are on the item
+ if (data.type !== 'bookmark') {
+ return resolve();
+ }
+ else if (!isNew) {
+ return send('sdk-places-tags-get-tags-by-url', { url: data.url })
+ .then(tags => {
+ return send('sdk-places-tags-untag', {
+ tags: tags.filter(tag => !~data.tags.indexOf(tag)),
+ url: data.url
+ });
+ }).then(() => send('sdk-places-tags-tag', {
+ url: data.url, tags: data.tags
+ }));
+ }
+ else if (data.tags && data.tags.length) {
+ return send('sdk-places-tags-tag', { url: data.url, tags: data.tags });
+ }
+ else
+ return resolve();
+}
+
diff --git a/addon-sdk/source/lib/sdk/places/host/host-query.js b/addon-sdk/source/lib/sdk/places/host/host-query.js
new file mode 100644
index 000000000..f2dbd6550
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/host/host-query.js
@@ -0,0 +1,179 @@
+/* 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",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Cc, Ci } = require('chrome');
+const { all } = require('../../core/promise');
+const { safeMerge, omit } = require('../../util/object');
+const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
+ .getService(Ci.nsINavHistoryService);
+const bookmarksService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
+ .getService(Ci.nsINavBookmarksService);
+const { request, response } = require('../../addon/host');
+const { newURI } = require('../../url/utils');
+const { send } = require('../../addon/events');
+const { on, emit } = require('../../event/core');
+const { filter } = require('../../event/utils');
+
+const ROOT_FOLDERS = [
+ bookmarksService.unfiledBookmarksFolder, bookmarksService.toolbarFolder,
+ bookmarksService.bookmarksMenuFolder
+];
+
+const EVENT_MAP = {
+ 'sdk-places-query': queryReceiver
+};
+
+// Properties that need to be manually
+// copied into a nsINavHistoryQuery object
+const MANUAL_QUERY_PROPERTIES = [
+ 'uri', 'folder', 'tags', 'url', 'folder'
+];
+
+const PLACES_PROPERTIES = [
+ 'uri', 'title', 'accessCount', 'time'
+];
+
+function execute (queries, options) {
+ return new Promise(resolve => {
+ let root = historyService
+ .executeQueries(queries, queries.length, options).root;
+ // Let's extract an eventual uri wildcard, if both domain and uri are set.
+ // See utils.js::urlQueryParser() for more details.
+ // In case of multiple queries, we only retain the first found wildcard.
+ let uriWildcard = queries.reduce((prev, query) => {
+ if (query.uri && query.domain) {
+ if (!prev)
+ prev = query.uri.spec;
+ query.uri = null;
+ }
+ return prev;
+ }, "");
+ resolve(collect([], root, uriWildcard));
+ });
+}
+
+function collect (acc, node, uriWildcard) {
+ node.containerOpen = true;
+ for (let i = 0; i < node.childCount; i++) {
+ let child = node.getChild(i);
+
+ if (!uriWildcard || child.uri.startsWith(uriWildcard)) {
+ acc.push(child);
+ }
+ if (child.type === child.RESULT_TYPE_FOLDER) {
+ let container = child.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ collect(acc, container, uriWildcard);
+ }
+ }
+ node.containerOpen = false;
+ return acc;
+}
+
+function query (queries, options) {
+ return new Promise((resolve, reject) => {
+ queries = queries || [];
+ options = options || {};
+ let optionsObj, queryObjs;
+
+ optionsObj = historyService.getNewQueryOptions();
+ queryObjs = [].concat(queries).map(createQuery);
+ if (!queryObjs.length) {
+ queryObjs = [historyService.getNewQuery()];
+ }
+ safeMerge(optionsObj, options);
+
+ /*
+ * Currently `places:` queries are not supported
+ */
+ optionsObj.excludeQueries = true;
+
+ execute(queryObjs, optionsObj).then((results) => {
+ if (optionsObj.queryType === 0) {
+ return results.map(normalize);
+ }
+ else if (optionsObj.queryType === 1) {
+ // Formats query results into more standard
+ // data structures for returning
+ return all(results.map(({itemId}) =>
+ send('sdk-places-bookmarks-get', { id: itemId })));
+ }
+ }).then(resolve, reject);
+ });
+}
+exports.query = query;
+
+function createQuery (query) {
+ query = query || {};
+ let queryObj = historyService.getNewQuery();
+
+ safeMerge(queryObj, omit(query, MANUAL_QUERY_PROPERTIES));
+
+ if (query.tags && Array.isArray(query.tags))
+ queryObj.tags = query.tags;
+ if (query.uri || query.url)
+ queryObj.uri = newURI(query.uri || query.url);
+ if (query.folder)
+ queryObj.setFolders([query.folder], 1);
+ return queryObj;
+}
+
+function queryReceiver (message) {
+ let queries = message.data.queries || message.data.query;
+ let options = message.data.options;
+ let resData = {
+ id: message.id,
+ event: message.event
+ };
+
+ query(queries, options).then(results => {
+ resData.data = results;
+ respond(resData);
+ }, reason => {
+ resData.error = reason;
+ respond(resData);
+ });
+}
+
+/*
+ * Converts a nsINavHistoryResultNode into a plain object
+ *
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
+ */
+function normalize (historyObj) {
+ return PLACES_PROPERTIES.reduce((obj, prop) => {
+ if (prop === 'uri')
+ obj.url = historyObj.uri;
+ else if (prop === 'time') {
+ // Cast from microseconds to milliseconds
+ obj.time = Math.floor(historyObj.time / 1000)
+ }
+ else if (prop === 'accessCount')
+ obj.visitCount = historyObj[prop];
+ else
+ obj[prop] = historyObj[prop];
+ return obj;
+ }, {});
+}
+
+/*
+ * Hook into host
+ */
+
+var reqStream = filter(request, data => /sdk-places-query/.test(data.event));
+on(reqStream, 'data', function (e) {
+ if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e);
+});
+
+function respond (data) {
+ emit(response, 'data', data);
+}
diff --git a/addon-sdk/source/lib/sdk/places/host/host-tags.js b/addon-sdk/source/lib/sdk/places/host/host-tags.js
new file mode 100644
index 000000000..929a5d5af
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/host/host-tags.js
@@ -0,0 +1,92 @@
+/* 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",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Cc, Ci } = require('chrome');
+const taggingService = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+const ios = Cc['@mozilla.org/network/io-service;1'].
+ getService(Ci.nsIIOService);
+const { URL } = require('../../url');
+const { newURI } = require('../../url/utils');
+const { request, response } = require('../../addon/host');
+const { on, emit } = require('../../event/core');
+const { filter } = require('../../event/utils');
+
+const EVENT_MAP = {
+ 'sdk-places-tags-tag': tag,
+ 'sdk-places-tags-untag': untag,
+ 'sdk-places-tags-get-tags-by-url': getTagsByURL,
+ 'sdk-places-tags-get-urls-by-tag': getURLsByTag
+};
+
+function tag (message) {
+ let data = message.data;
+ let resData = {
+ id: message.id,
+ event: message.event
+ };
+
+ resData.data = taggingService.tagURI(newURI(data.url), data.tags);
+ respond(resData);
+}
+
+function untag (message) {
+ let data = message.data;
+ let resData = {
+ id: message.id,
+ event: message.event
+ };
+
+ resData.data = taggingService.untagURI(newURI(data.url), data.tags);
+ respond(resData);
+}
+
+function getURLsByTag (message) {
+ let data = message.data;
+ let resData = {
+ id: message.id,
+ event: message.event
+ };
+
+ resData.data = taggingService
+ .getURIsForTag(data.tag).map(uri => uri.spec);
+ respond(resData);
+}
+
+function getTagsByURL (message) {
+ let data = message.data;
+ let resData = {
+ id: message.id,
+ event: message.event
+ };
+
+ resData.data = taggingService.getTagsForURI(newURI(data.url), {});
+ respond(resData);
+}
+
+/*
+ * Hook into host
+ */
+
+var reqStream = filter(request, function (data) {
+ return /sdk-places-tags/.test(data.event);
+});
+
+on(reqStream, 'data', function (e) {
+ if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e);
+});
+
+function respond (data) {
+ emit(response, 'data', data);
+}
diff --git a/addon-sdk/source/lib/sdk/places/utils.js b/addon-sdk/source/lib/sdk/places/utils.js
new file mode 100644
index 000000000..44366d2aa
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/places/utils.js
@@ -0,0 +1,268 @@
+/* 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",
+ "engines": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Cc, Ci, Cu } = require('chrome');
+const { Class } = require('../core/heritage');
+const { method } = require('../lang/functional');
+const { defer, promised, all } = require('../core/promise');
+const { send } = require('../addon/events');
+const { EventTarget } = require('../event/target');
+const { merge } = require('../util/object');
+const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+Cu.importGlobalProperties(["URL"]);
+
+/*
+ * TreeNodes are used to construct dependency trees
+ * for BookmarkItems
+ */
+var TreeNode = Class({
+ initialize: function (value) {
+ this.value = value;
+ this.children = [];
+ },
+ add: function (values) {
+ [].concat(values).forEach(value => {
+ this.children.push(value instanceof TreeNode ? value : TreeNode(value));
+ });
+ },
+ get length () {
+ let count = 0;
+ this.walk(() => count++);
+ // Do not count the current node
+ return --count;
+ },
+ get: method(get),
+ walk: method(walk),
+ toString: () => '[object TreeNode]'
+});
+exports.TreeNode = TreeNode;
+
+/*
+ * Descends down from `node` applying `fn` to each in order.
+ * `fn` can return values or promises -- if promise returned,
+ * children are not processed until resolved. `fn` is passed
+ * one argument, the current node, `curr`.
+ */
+function walk (curr, fn) {
+ return promised(fn)(curr).then(val => {
+ return all(curr.children.map(child => walk(child, fn)));
+ });
+}
+
+/*
+ * Descends from the TreeNode `node`, returning
+ * the node with value `value` if found or `null`
+ * otherwise
+ */
+function get (node, value) {
+ if (node.value === value) return node;
+ for (let child of node.children) {
+ let found = get(child, value);
+ if (found) return found;
+ }
+ return null;
+}
+
+/*
+ * Constructs a tree of bookmark nodes
+ * returning the root (value: null);
+ */
+
+function constructTree (items) {
+ let root = TreeNode(null);
+ items.forEach(treeify.bind(null, root));
+
+ function treeify (root, item) {
+ // If node already exists, skip
+ let node = root.get(item);
+ if (node) return node;
+ node = TreeNode(item);
+
+ let parentNode = item.group ? treeify(root, item.group) : root;
+ parentNode.add(node);
+
+ return node;
+ }
+
+ return root;
+}
+exports.constructTree = constructTree;
+
+/*
+ * Shortcut for converting an id, or an object with an id, into
+ * an object with corresponding bookmark data
+ */
+function fetchItem (item) {
+ return send('sdk-places-bookmarks-get', { id: item.id || item });
+}
+exports.fetchItem = fetchItem;
+
+/*
+ * Takes an ID or an object with ID and checks it against
+ * the root bookmark folders
+ */
+function isRootGroup (id) {
+ id = id && id.id;
+ return ~[bmsrv.bookmarksMenuFolder, bmsrv.toolbarFolder,
+ bmsrv.unfiledBookmarksFolder
+ ].indexOf(id);
+}
+exports.isRootGroup = isRootGroup;
+
+/*
+ * Merges appropriate options into query based off of url
+ * 4 scenarios:
+ *
+ * 'moz.com' // domain: moz.com, domainIsHost: true
+ * --> 'http://moz.com', 'http://moz.com/thunderbird'
+ * '*.moz.com' // domain: moz.com, domainIsHost: false
+ * --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test'
+ * 'http://moz.com' // uri: http://moz.com/
+ * --> 'http://moz.com/'
+ * 'http://moz.com/*' // uri: http://moz.com/, domain: moz.com, domainIsHost: true
+ * --> 'http://moz.com/', 'http://moz.com/thunderbird'
+ */
+
+function urlQueryParser (query, url) {
+ if (!url) return;
+ if (/^https?:\/\//.test(url)) {
+ query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/';
+ if (/\*$/.test(url)) {
+ // Wildcard searches on URIs are not supported, so try to extract a
+ // domain and filter the data later.
+ url = url.replace(/\*$/, '');
+ try {
+ query.domain = new URL(url).hostname;
+ query.domainIsHost = true;
+ // Unfortunately here we cannot use an expando to store the wildcard,
+ // cause the query is a wrapped native XPCOM object, so we reuse uri.
+ // We clearly don't want to query for both uri and domain, thus we'll
+ // have to handle this in host-query.js::execute()
+ query.uri = url;
+ } catch (ex) {
+ // Cannot extract an host cause it's not a valid uri, the query will
+ // just return nothing.
+ }
+ }
+ } else {
+ if (/^\*/.test(url)) {
+ query.domain = url.replace(/^\*\./, '');
+ query.domainIsHost = false;
+ } else {
+ query.domain = url;
+ query.domainIsHost = true;
+ }
+ }
+}
+exports.urlQueryParser = urlQueryParser;
+
+/*
+ * Takes an EventEmitter and returns a promise that
+ * aggregates results and handles a bulk resolve and reject
+ */
+
+function promisedEmitter (emitter) {
+ let { promise, resolve, reject } = defer();
+ let errors = [];
+ emitter.on('error', error => errors.push(error));
+ emitter.on('end', (items) => {
+ if (errors.length) reject(errors[0]);
+ else resolve(items);
+ });
+ return promise;
+}
+exports.promisedEmitter = promisedEmitter;
+
+
+// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+function createQuery (type, query) {
+ query = query || {};
+ let qObj = {
+ searchTerms: query.query
+ };
+
+ urlQueryParser(qObj, query.url);
+
+ // 0 === history
+ if (type === 0) {
+ // PRTime used by query is in microseconds, not milliseconds
+ qObj.beginTime = (query.from || 0) * 1000;
+ qObj.endTime = (query.to || new Date()) * 1000;
+
+ // Set reference time to Epoch
+ qObj.beginTimeReference = 0;
+ qObj.endTimeReference = 0;
+ }
+ // 1 === bookmarks
+ else if (type === 1) {
+ qObj.tags = query.tags;
+ qObj.folder = query.group && query.group.id;
+ }
+ // 2 === unified (not implemented on platform)
+ else if (type === 2) {
+
+ }
+
+ return qObj;
+}
+exports.createQuery = createQuery;
+
+// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
+
+const SORT_MAP = {
+ title: 1,
+ date: 3, // sort by visit date
+ url: 5,
+ visitCount: 7,
+ // keywords currently unsupported
+ // keyword: 9,
+ dateAdded: 11, // bookmarks only
+ lastModified: 13 // bookmarks only
+};
+
+function createQueryOptions (type, options) {
+ options = options || {};
+ let oObj = {};
+ oObj.sortingMode = SORT_MAP[options.sort] || 0;
+ if (options.descending && options.sort)
+ oObj.sortingMode++;
+
+ // Resolve to default sort if ineligible based on query type
+ if (type === 0 && // history
+ (options.sort === 'dateAdded' || options.sort === 'lastModified'))
+ oObj.sortingMode = 0;
+
+ oObj.maxResults = typeof options.count === 'number' ? options.count : 0;
+
+ oObj.queryType = type;
+
+ return oObj;
+}
+exports.createQueryOptions = createQueryOptions;
+
+
+function mapBookmarkItemType (type) {
+ if (typeof type === 'number') {
+ if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark';
+ if (bmsrv.TYPE_FOLDER === type) return 'group';
+ if (bmsrv.TYPE_SEPARATOR === type) return 'separator';
+ } else {
+ if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK;
+ if ('group' === type) return bmsrv.TYPE_FOLDER;
+ if ('separator' === type) return bmsrv.TYPE_SEPARATOR;
+ }
+}
+exports.mapBookmarkItemType = mapBookmarkItemType;
diff --git a/addon-sdk/source/lib/sdk/platform/xpcom.js b/addon-sdk/source/lib/sdk/platform/xpcom.js
new file mode 100644
index 000000000..383baf67a
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/platform/xpcom.js
@@ -0,0 +1,241 @@
+/* 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, Cm, components: { classesByID } } = require('chrome');
+const { registerFactory, unregisterFactory, isCIDRegistered } =
+ Cm.QueryInterface(Ci.nsIComponentRegistrar);
+
+const { merge } = require('../util/object');
+const { Class, extend, mix } = require('../core/heritage');
+const { uuid } = require('../util/uuid');
+
+// This is a base prototype, that provides bare bones of XPCOM. JS based
+// components can be easily implement by extending it.
+const Unknown = new function() {
+ function hasInterface(component, iid) {
+ return component && component.interfaces &&
+ ( component.interfaces.some(id => iid.equals(Ci[id])) ||
+ component.implements.some($ => hasInterface($, iid)) ||
+ hasInterface(Object.getPrototypeOf(component), iid));
+ }
+
+ return Class({
+ /**
+ * The `QueryInterface` method provides runtime type discovery used by XPCOM.
+ * This method return queried instance of `this` if given `iid` is listed in
+ * the `interfaces` property or in equivalent properties of objects in it's
+ * prototype chain. In addition it will look up in the prototypes under
+ * `implements` array property, this ways compositions made via `Class`
+ * utility will carry interfaces implemented by composition components.
+ */
+ QueryInterface: function QueryInterface(iid) {
+ // For some reason there are cases when `iid` is `null`. In such cases we
+ // just return `this`. Otherwise we verify that component implements given
+ // `iid` interface. This will be no longer necessary once Bug 748003 is
+ // fixed.
+ if (iid && !hasInterface(this, iid))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+
+ return this;
+ },
+ /**
+ * Array of `XPCOM` interfaces (as strings) implemented by this component.
+ * All components implement `nsISupports` by default which is default value
+ * here. Provide array of interfaces implemented by an object when
+ * extending, to append them to this list (Please note that there is no
+ * need to repeat interfaces implemented by super as they will be added
+ * automatically).
+ */
+ interfaces: Object.freeze([ 'nsISupports' ])
+ });
+}
+exports.Unknown = Unknown;
+
+// Base exemplar for creating instances implementing `nsIFactory` interface,
+// that maybe registered into runtime via `register` function. Instances of
+// this factory create instances of enclosed component on `createInstance`.
+const Factory = Class({
+ extends: Unknown,
+ interfaces: [ 'nsIFactory' ],
+ /**
+ * All the descendants will get auto generated `id` (also known as `classID`
+ * in XPCOM world) unless one is manually provided.
+ */
+ get id() { throw Error('Factory must implement `id` property') },
+ /**
+ * XPCOM `contractID` may optionally be provided to associate this factory
+ * with it. `contract` is a unique string that has a following format:
+ * '@vendor.com/unique/id;1'.
+ */
+ contract: null,
+ /**
+ * Class description that is being registered. This value is intended as a
+ * human-readable description for the given class and does not needs to be
+ * globally unique.
+ */
+ description: 'Jetpack generated factory',
+ /**
+ * This method is required by `nsIFactory` interfaces, but as in most
+ * implementations it does nothing interesting.
+ */
+ lockFactory: function lockFactory(lock) {
+ return undefined;
+ },
+ /**
+ * If property is `true` XPCOM service / factory will be registered
+ * automatically on creation.
+ */
+ register: true,
+ /**
+ * If property is `true` XPCOM factory will be unregistered prior to add-on
+ * unload.
+ */
+ unregister: true,
+ /**
+ * Method is called on `Service.new(options)` passing given `options` to
+ * it. Options is expected to have `component` property holding XPCOM
+ * component implementation typically decedent of `Unknown` or any custom
+ * implementation with a `new` method and optional `register`, `unregister`
+ * flags. Unless `register` is `false` Service / Factory will be
+ * automatically registered. Unless `unregister` is `false` component will
+ * be automatically unregistered on add-on unload.
+ */
+ initialize: function initialize(options) {
+ merge(this, {
+ id: 'id' in options ? options.id : uuid(),
+ register: 'register' in options ? options.register : this.register,
+ unregister: 'unregister' in options ? options.unregister : this.unregister,
+ contract: 'contract' in options ? options.contract : null,
+ Component: options.Component
+ });
+
+ // If service / factory has auto registration enabled then register.
+ if (this.register)
+ register(this);
+ },
+ /**
+ * Creates an instance of the class associated with this factory.
+ */
+ createInstance: function createInstance(outer, iid) {
+ try {
+ if (outer)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return this.create().QueryInterface(iid);
+ }
+ catch (error) {
+ throw error instanceof Ci.nsIException ? error : Cr.NS_ERROR_FAILURE;
+ }
+ },
+ create: function create() {
+ return this.Component();
+ }
+});
+exports.Factory = Factory;
+
+// Exemplar for creating services that implement `nsIFactory` interface, that
+// can be registered into runtime via call to `register`. This services return
+// enclosed `component` on `getService`.
+const Service = Class({
+ extends: Factory,
+ initialize: function initialize(options) {
+ this.component = options.Component();
+ Factory.prototype.initialize.call(this, options);
+ },
+ description: 'Jetpack generated service',
+ /**
+ * Creates an instance of the class associated with this factory.
+ */
+ create: function create() {
+ return this.component;
+ }
+});
+exports.Service = Service;
+
+function isRegistered({ id }) {
+ return isCIDRegistered(id);
+}
+exports.isRegistered = isRegistered;
+
+/**
+ * Registers given `component` object to be used to instantiate a particular
+ * class identified by `component.id`, and creates an association of class
+ * name and `component.contract` with the class.
+ */
+function register(factory) {
+ if (!(factory instanceof Factory)) {
+ throw new Error("xpcom.register() expect a Factory instance.\n" +
+ "Please refactor your code to new xpcom module if you" +
+ " are repacking an addon from SDK <= 1.5:\n" +
+ "https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/platform_xpcom");
+ }
+
+ registerFactory(factory.id, factory.description, factory.contract, factory);
+
+ if (factory.unregister)
+ require('../system/unload').when(unregister.bind(null, factory));
+}
+exports.register = register;
+
+/**
+ * Unregister a factory associated with a particular class identified by
+ * `factory.classID`.
+ */
+function unregister(factory) {
+ if (isRegistered(factory))
+ unregisterFactory(factory.id, factory);
+}
+exports.unregister = unregister;
+
+function autoRegister(path) {
+ // TODO: This assumes that the url points to a directory
+ // that contains subdirectories corresponding to OS/ABI and then
+ // further subdirectories corresponding to Gecko platform version.
+ // we should probably either behave intelligently here or allow
+ // the caller to pass-in more options if e.g. there aren't
+ // Gecko-specific binaries for a component (which will be the case
+ // if only frozen interfaces are used).
+
+ var runtime = require("../system/runtime");
+ var osDirName = runtime.OS + "_" + runtime.XPCOMABI;
+ var platformVersion = require("../system/xul-app").platformVersion.substring(0, 5);
+
+ var file = Cc['@mozilla.org/file/local;1']
+ .createInstance(Ci.nsILocalFile);
+ file.initWithPath(path);
+ file.append(osDirName);
+ file.append(platformVersion);
+
+ if (!(file.exists() && file.isDirectory()))
+ throw new Error("component not available for OS/ABI " +
+ osDirName + " and platform " + platformVersion);
+
+ Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ Cm.autoRegister(file);
+}
+exports.autoRegister = autoRegister;
+
+/**
+ * Returns registered factory that has a given `id` or `null` if not found.
+ */
+function factoryByID(id) {
+ return classesByID[id] || null;
+}
+exports.factoryByID = factoryByID;
+
+/**
+ * Returns factory registered with a given `contract` or `null` if not found.
+ * In contrast to `Cc[contract]` that does ignores new factory registration
+ * with a given `contract` this will return a factory currently associated
+ * with a `contract`.
+ */
+function factoryByContract(contract) {
+ return factoryByID(Cm.contractIDToCID(contract));
+}
+exports.factoryByContract = factoryByContract;
diff --git a/addon-sdk/source/lib/sdk/preferences/event-target.js b/addon-sdk/source/lib/sdk/preferences/event-target.js
new file mode 100644
index 000000000..b64ba303c
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/preferences/event-target.js
@@ -0,0 +1,61 @@
+/* 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 { Class } = require('../core/heritage');
+const { EventTarget } = require('../event/target');
+const { Branch } = require('./service');
+const { emit, off } = require('../event/core');
+const { when: unload } = require('../system/unload');
+
+const prefTargetNS = require('../core/namespace').ns();
+
+const PrefsTarget = Class({
+ extends: EventTarget,
+ initialize: function(options) {
+ options = options || {};
+ EventTarget.prototype.initialize.call(this, options);
+
+ let branchName = options.branchName || '';
+ let branch = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService).
+ getBranch(branchName).
+ QueryInterface(Ci.nsIPrefBranch2);
+ prefTargetNS(this).branch = branch;
+
+ // provides easy access to preference values
+ this.prefs = Branch(branchName);
+
+ // start listening to preference changes
+ let observer = prefTargetNS(this).observer = onChange.bind(this);
+ branch.addObserver('', observer, false);
+
+ // Make sure to destroy this on unload
+ unload(destroy.bind(this));
+ }
+});
+exports.PrefsTarget = PrefsTarget;
+
+/* HELPERS */
+
+function onChange(subject, topic, name) {
+ if (topic === 'nsPref:changed') {
+ emit(this, name, name);
+ emit(this, '', name);
+ }
+}
+
+function destroy() {
+ off(this);
+
+ // stop listening to preference changes
+ let branch = prefTargetNS(this).branch;
+ branch.removeObserver('', prefTargetNS(this).observer, false);
+ prefTargetNS(this).observer = null;
+}
diff --git a/addon-sdk/source/lib/sdk/preferences/native-options.js b/addon-sdk/source/lib/sdk/preferences/native-options.js
new file mode 100644
index 000000000..840997df9
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/preferences/native-options.js
@@ -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 { Cc, Ci, Cu } = require('chrome');
+const { on } = require('../system/events');
+const { id, preferencesBranch } = require('../self');
+const { localizeInlineOptions } = require('../l10n/prefs');
+const { Services } = require("resource://gre/modules/Services.jsm");
+const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm");
+const { defer } = require("sdk/core/promise");
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";;
+const DEFAULT_OPTIONS_URL = 'data:text/xml,<placeholder/>';
+
+const VALID_PREF_TYPES = ['bool', 'boolint', 'integer', 'string', 'color',
+ 'file', 'directory', 'control', 'menulist', 'radio'];
+
+const isFennec = require("sdk/system/xul-app").is("Fennec");
+
+function enable({ preferences, id }) {
+ let enabled = defer();
+
+ validate(preferences);
+
+ setDefaults(preferences, preferencesBranch);
+
+ // allow the use of custom options.xul
+ AddonManager.getAddonByID(id, (addon) => {
+ on('addon-options-displayed', onAddonOptionsDisplayed, true);
+ enabled.resolve({ id: id });
+ });
+
+ function onAddonOptionsDisplayed({ subject: doc, data }) {
+ if (data === id) {
+ let parent;
+
+ if (isFennec) {
+ parent = doc.querySelector('.options-box');
+
+ // NOTE: This disable the CSS rule that makes the options invisible
+ let item = doc.querySelector('#addons-details .addon-item');
+ item.removeAttribute("optionsURL");
+ } else {
+ parent = doc.getElementById('detail-downloads').parentNode;
+ }
+
+ if (parent) {
+ injectOptions({
+ preferences: preferences,
+ preferencesBranch: preferencesBranch,
+ document: doc,
+ parent: parent,
+ id: id
+ });
+ localizeInlineOptions(doc);
+ } else {
+ throw Error("Preferences parent node not found in Addon Details. The configured custom preferences will not be visible.");
+ }
+ }
+ }
+
+ return enabled.promise;
+}
+exports.enable = enable;
+
+// centralized sanity checks
+function validate(preferences) {
+ for (let { name, title, type, label, options } of preferences) {
+ // make sure the title is set and non-empty
+ if (!title)
+ throw Error("The '" + name + "' pref requires a title");
+
+ // make sure that pref type is a valid inline option type
+ if (!~VALID_PREF_TYPES.indexOf(type))
+ throw Error("The '" + name + "' pref must be of valid type");
+
+ // if it's a control, make sure it has a label
+ if (type === 'control' && !label)
+ throw Error("The '" + name + "' control requires a label");
+
+ // if it's a menulist or radio, make sure it has options
+ if (type === 'menulist' || type === 'radio') {
+ if (!options)
+ throw Error("The '" + name + "' pref requires options");
+
+ // make sure each option has a value and a label
+ for (let item of options) {
+ if (!('value' in item) || !('label' in item))
+ throw Error("Each option requires both a value and a label");
+ }
+ }
+
+ // TODO: check that pref type matches default value type
+ }
+}
+exports.validate = validate;
+
+// initializes default preferences, emulates defaults/prefs.js
+function setDefaults(preferences, preferencesBranch) {
+ const branch = Cc['@mozilla.org/preferences-service;1'].
+ getService(Ci.nsIPrefService).
+ getDefaultBranch('extensions.' + preferencesBranch + '.');
+ for (let { name, value } of preferences) {
+ switch (typeof value) {
+ case 'boolean':
+ branch.setBoolPref(name, value);
+ break;
+ case 'number':
+ // must be integer, ignore otherwise
+ if (value % 1 === 0) {
+ branch.setIntPref(name, value);
+ }
+ break;
+ case 'string':
+ let str = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ str.data = value;
+ branch.setComplexValue(name, Ci.nsISupportsString, str);
+ break;
+ }
+ }
+}
+exports.setDefaults = setDefaults;
+
+// dynamically injects inline options into about:addons page at runtime
+// NOTE: on Firefox Desktop the about:addons page is a xul page document,
+// on Firefox for Android the about:addons page is an xhtml page, to support both
+// the XUL xml namespace have to be enforced.
+function injectOptions({ preferences, preferencesBranch, document, parent, id }) {
+ preferences.forEach(({name, type, hidden, title, description, label, options, on, off}) => {
+ if (hidden) {
+ return;
+ }
+
+ let setting = document.createElementNS(XUL_NS, 'setting');
+ setting.setAttribute('pref-name', name);
+ setting.setAttribute('data-jetpack-id', id);
+ setting.setAttribute('pref', 'extensions.' + preferencesBranch + '.' + name);
+ setting.setAttribute('type', type);
+ setting.setAttribute('title', title);
+ if (description)
+ setting.setAttribute('desc', description);
+
+ if (type === 'file' || type === 'directory') {
+ setting.setAttribute('fullpath', 'true');
+ }
+ else if (type === 'control') {
+ let button = document.createElementNS(XUL_NS, 'button');
+ button.setAttribute('pref-name', name);
+ button.setAttribute('data-jetpack-id', id);
+ button.setAttribute('label', label);
+ button.addEventListener('command', function() {
+ Services.obs.notifyObservers(null, `${id}-cmdPressed`, name);
+ }, true);
+ setting.appendChild(button);
+ }
+ else if (type === 'boolint') {
+ setting.setAttribute('on', on);
+ setting.setAttribute('off', off);
+ }
+ else if (type === 'menulist') {
+ let menulist = document.createElementNS(XUL_NS, 'menulist');
+ let menupopup = document.createElementNS(XUL_NS, 'menupopup');
+ for (let { value, label } of options) {
+ let menuitem = document.createElementNS(XUL_NS, 'menuitem');
+ menuitem.setAttribute('value', value);
+ menuitem.setAttribute('label', label);
+ menupopup.appendChild(menuitem);
+ }
+ menulist.appendChild(menupopup);
+ setting.appendChild(menulist);
+ }
+ else if (type === 'radio') {
+ let radiogroup = document.createElementNS(XUL_NS, 'radiogroup');
+ for (let { value, label } of options) {
+ let radio = document.createElementNS(XUL_NS, 'radio');
+ radio.setAttribute('value', value);
+ radio.setAttribute('label', label);
+ radiogroup.appendChild(radio);
+ }
+ setting.appendChild(radiogroup);
+ }
+
+ parent.appendChild(setting);
+ });
+}
+exports.injectOptions = injectOptions;
diff --git a/addon-sdk/source/lib/sdk/preferences/service.js b/addon-sdk/source/lib/sdk/preferences/service.js
new file mode 100644
index 000000000..231cd8e14
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/preferences/service.js
@@ -0,0 +1,137 @@
+/* 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"
+};
+
+// The minimum and maximum integers that can be set as preferences.
+// The range of valid values is narrower than the range of valid JS values
+// because the native preferences code treats integers as NSPR PRInt32s,
+// which are 32-bit signed integers on all platforms.
+const MAX_INT = 0x7FFFFFFF;
+const MIN_INT = -0x80000000;
+
+const {Cc,Ci,Cr} = require("chrome");
+
+const prefService = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService);
+const prefSvc = prefService.getBranch(null);
+const defaultBranch = prefService.getDefaultBranch(null);
+
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+const prefs = new Preferences({});
+
+const branchKeys = branchName =>
+ keys(branchName).map($ => $.replace(branchName, ""));
+
+const Branch = function(branchName) {
+ return new Proxy(Branch.prototype, {
+ getOwnPropertyDescriptor(target, name, receiver) {
+ return {
+ configurable: true,
+ enumerable: true,
+ writable: false,
+ value: this.get(target, name, receiver)
+ };
+ },
+ ownKeys(target) {
+ return branchKeys(branchName);
+ },
+ get(target, name, receiver) {
+ return get(`${branchName}${name}`);
+ },
+ set(target, name, value, receiver) {
+ set(`${branchName}${name}`, value);
+ return true;
+ },
+ has(target, name) {
+ return this.hasOwn(target, name);
+ },
+ hasOwn(target, name) {
+ return has(`${branchName}${name}`);
+ },
+ deleteProperty(target, name) {
+ reset(`${branchName}${name}`);
+ return true;
+ }
+ });
+}
+
+
+function get(name, defaultValue) {
+ return prefs.get(name, defaultValue);
+}
+exports.get = get;
+
+
+function set(name, value) {
+ var prefType;
+ if (typeof value != "undefined" && value != null)
+ prefType = value.constructor.name;
+
+ switch (prefType) {
+ case "Number":
+ if (value % 1 != 0)
+ throw new Error("cannot store non-integer number: " + value);
+ }
+
+ prefs.set(name, value);
+}
+exports.set = set;
+
+const has = prefs.has.bind(prefs)
+exports.has = has;
+
+function keys(root) {
+ return prefSvc.getChildList(root);
+}
+exports.keys = keys;
+
+const isSet = prefs.isSet.bind(prefs);
+exports.isSet = isSet;
+
+function reset(name) {
+ try {
+ prefSvc.clearUserPref(name);
+ }
+ catch (e) {
+ // The pref service throws NS_ERROR_UNEXPECTED when the caller tries
+ // to reset a pref that doesn't exist or is already set to its default
+ // value. This interface fails silently in those cases, so callers
+ // can unconditionally reset a pref without having to check if it needs
+ // resetting first or trap exceptions after the fact. It passes through
+ // other exceptions, however, so callers know about them, since we don't
+ // know what other exceptions might be thrown and what they might mean.
+ if (e.result != Cr.NS_ERROR_UNEXPECTED) {
+ throw e;
+ }
+ }
+}
+exports.reset = reset;
+
+function getLocalized(name, defaultValue) {
+ let value = null;
+ try {
+ value = prefSvc.getComplexValue(name, Ci.nsIPrefLocalizedString).data;
+ }
+ finally {
+ return value || defaultValue;
+ }
+}
+exports.getLocalized = getLocalized;
+
+function setLocalized(name, value) {
+ // We can't use `prefs.set` here as we have to use `getDefaultBranch`
+ // (instead of `getBranch`) in order to have `mIsDefault` set to true, here:
+ // http://mxr.mozilla.org/mozilla-central/source/modules/libpref/src/nsPrefBranch.cpp#233
+ // Otherwise, we do not enter into this expected condition:
+ // http://mxr.mozilla.org/mozilla-central/source/modules/libpref/src/nsPrefBranch.cpp#244
+ defaultBranch.setCharPref(name, value);
+}
+exports.setLocalized = setLocalized;
+
+exports.Branch = Branch;
+
diff --git a/addon-sdk/source/lib/sdk/preferences/utils.js b/addon-sdk/source/lib/sdk/preferences/utils.js
new file mode 100644
index 000000000..1d5769c37
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/preferences/utils.js
@@ -0,0 +1,42 @@
+/* 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 { openTab, getBrowserForTab, getTabId } = require("sdk/tabs/utils");
+const { on, off } = require("sdk/system/events");
+const { getMostRecentBrowserWindow } = require('../window/utils');
+
+// Opens about:addons in a new tab, then displays the inline
+// preferences of the provided add-on
+const open = ({ id }) => new Promise((resolve, reject) => {
+ // opening the about:addons page in a new tab
+ let tab = openTab(getMostRecentBrowserWindow(), "about:addons");
+ let browser = getBrowserForTab(tab);
+
+ // waiting for the about:addons page to load
+ browser.addEventListener("load", function onPageLoad() {
+ browser.removeEventListener("load", onPageLoad, true);
+ let window = browser.contentWindow;
+
+ // wait for the add-on's "addon-options-displayed"
+ on("addon-options-displayed", function onPrefDisplayed({ subject: doc, data }) {
+ if (data === id) {
+ off("addon-options-displayed", onPrefDisplayed);
+ resolve({
+ id: id,
+ tabId: getTabId(tab),
+ "document": doc
+ });
+ }
+ }, true);
+
+ // display the add-on inline preferences page
+ window.gViewController.commands.cmd_showItemDetails.doCommand({ id: id }, true);
+ }, true);
+});
+exports.open = open;
diff --git a/addon-sdk/source/lib/sdk/private-browsing.js b/addon-sdk/source/lib/sdk/private-browsing.js
new file mode 100644
index 000000000..29ca16185
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/private-browsing.js
@@ -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": "stable"
+};
+
+const { isPrivate } = require('./private-browsing/utils');
+
+exports.isPrivate = isPrivate;
diff --git a/addon-sdk/source/lib/sdk/private-browsing/utils.js b/addon-sdk/source/lib/sdk/private-browsing/utils.js
new file mode 100644
index 000000000..8b012f0ce
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/private-browsing/utils.js
@@ -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';
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { Cc, Ci, Cu } = require('chrome');
+const { is } = require('../system/xul-app');
+const { isWindowPrivate } = require('../window/utils');
+const { isPrivateBrowsingSupported } = require('../self');
+const { dispatcher } = require("../util/dispatcher");
+
+var PrivateBrowsingUtils;
+
+// Private browsing is only supported in Fx
+try {
+ PrivateBrowsingUtils = Cu.import('resource://gre/modules/PrivateBrowsingUtils.jsm', {}).PrivateBrowsingUtils;
+}
+catch (e) {}
+
+exports.isGlobalPBSupported = false;
+
+// checks that per-window private browsing is implemented
+var isWindowPBSupported = exports.isWindowPBSupported =
+ !!PrivateBrowsingUtils && is('Firefox');
+
+// checks that per-tab private browsing is implemented
+var isTabPBSupported = exports.isTabPBSupported =
+ !!PrivateBrowsingUtils && is('Fennec');
+
+function isPermanentPrivateBrowsing() {
+ return !!(PrivateBrowsingUtils && PrivateBrowsingUtils.permanentPrivateBrowsing);
+}
+exports.isPermanentPrivateBrowsing = isPermanentPrivateBrowsing;
+
+function ignoreWindow(window) {
+ return !isPrivateBrowsingSupported && isWindowPrivate(window);
+}
+exports.ignoreWindow = ignoreWindow;
+
+var getMode = function getMode(chromeWin) {
+ return (chromeWin !== undefined && isWindowPrivate(chromeWin));
+};
+exports.getMode = getMode;
+
+const isPrivate = dispatcher("isPrivate");
+isPrivate.when(isPermanentPrivateBrowsing, _ => true);
+isPrivate.when(x => x instanceof Ci.nsIDOMWindow, isWindowPrivate);
+isPrivate.when(x => Ci.nsIPrivateBrowsingChannel && x instanceof Ci.nsIPrivateBrowsingChannel, x => x.isChannelPrivate);
+isPrivate.define(() => false);
+exports.isPrivate = isPrivate;
diff --git a/addon-sdk/source/lib/sdk/querystring.js b/addon-sdk/source/lib/sdk/querystring.js
new file mode 100644
index 000000000..9982a00ab
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/querystring.js
@@ -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/. */
+
+'use strict';
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+var unescape = decodeURIComponent;
+exports.unescape = unescape;
+
+// encodes a string safely for application/x-www-form-urlencoded
+// adheres to RFC 3986
+// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURIComponent
+function escape(query) {
+ return encodeURIComponent(query).
+ replace(/%20/g, '+').
+ replace(/!/g, '%21').
+ replace(/'/g, '%27').
+ replace(/\(/g, '%28').
+ replace(/\)/g, '%29').
+ replace(/\*/g, '%2A');
+}
+exports.escape = escape;
+
+// Converts an object of unordered key-vals to a string that can be passed
+// as part of a request
+function stringify(options, separator, assigner) {
+ separator = separator || '&';
+ assigner = assigner || '=';
+ // Explicitly return null if we have null, and empty string, or empty object.
+ if (!options)
+ return '';
+
+ // If content is already a string, just return it as is.
+ if (typeof(options) == 'string')
+ return options;
+
+ // At this point we have a k:v object. Iterate over it and encode each value.
+ // Arrays and nested objects will get encoded as needed. For example...
+ //
+ // { foo: [1, 2, { omg: 'bbq', 'all your base!': 'are belong to us' }], bar: 'baz' }
+ //
+ // will be encoded as
+ //
+ // foo[0]=1&foo[1]=2&foo[2][omg]=bbq&foo[2][all+your+base!]=are+belong+to+us&bar=baz
+ //
+ // Keys (including '[' and ']') and values will be encoded with
+ // `escape` before returning.
+ //
+ // Execution was inspired by jQuery, but some details have changed and numeric
+ // array keys are included (whereas they are not in jQuery).
+
+ let encodedContent = [];
+ function add(key, val) {
+ encodedContent.push(escape(key) + assigner + escape(val));
+ }
+
+ function make(key, value) {
+ if (value && typeof(value) === 'object')
+ Object.keys(value).forEach(function(name) {
+ make(key + '[' + name + ']', value[name]);
+ });
+ else
+ add(key, value);
+ }
+
+ Object.keys(options).forEach(function(name) { make(name, options[name]); });
+ return encodedContent.join(separator);
+
+ //XXXzpao In theory, we can just use a FormData object on 1.9.3, but I had
+ // trouble getting that working. It would also be nice to stay
+ // backwards-compat as long as possible. Keeping this in for now...
+ // let formData = Cc['@mozilla.org/files/formdata;1'].
+ // createInstance(Ci.nsIDOMFormData);
+ // for ([k, v] in Iterator(content)) {
+ // formData.append(k, v);
+ // }
+ // return formData;
+}
+exports.stringify = stringify;
+
+// Exporting aliases that nodejs implements just for the sake of
+// interoperability.
+exports.encode = stringify;
+exports.serialize = stringify;
+
+// Note: That `stringify` and `parse` aren't bijective as we use `stringify`
+// as it was implement in request module, but implement `parse` to match nodejs
+// behavior.
+// TODO: Make `stringify` implement API as in nodejs and figure out backwards
+// compatibility.
+function parse(query, separator, assigner) {
+ separator = separator || '&';
+ assigner = assigner || '=';
+ let result = {};
+
+ if (typeof query !== 'string' || query.length === 0)
+ return result;
+
+ query.split(separator).forEach(function(chunk) {
+ let pair = chunk.split(assigner);
+ let key = unescape(pair[0]);
+ let value = unescape(pair.slice(1).join(assigner));
+
+ if (!(key in result))
+ result[key] = value;
+ else if (Array.isArray(result[key]))
+ result[key].push(value);
+ else
+ result[key] = [result[key], value];
+ });
+
+ return result;
+};
+exports.parse = parse;
+// Exporting aliases that nodejs implements just for the sake of
+// interoperability.
+exports.decode = parse;
diff --git a/addon-sdk/source/lib/sdk/remote/child.js b/addon-sdk/source/lib/sdk/remote/child.js
new file mode 100644
index 000000000..4ccfa661a
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/remote/child.js
@@ -0,0 +1,284 @@
+/* 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 { isChildLoader } = require('./core');
+if (!isChildLoader)
+ throw new Error("Cannot load sdk/remote/child in a main process loader.");
+
+const { Ci, Cc, Cu } = require('chrome');
+const runtime = require('../system/runtime');
+const { Class } = require('../core/heritage');
+const { Namespace } = require('../core/namespace');
+const { omit } = require('../util/object');
+const { when } = require('../system/unload');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const { Disposable } = require('../core/disposable');
+const { EventParent } = require('./utils');
+const { addListItem, removeListItem } = require('../util/list');
+
+const loaderID = require('@loader/options').loaderID;
+
+const MAIN_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+const mm = Cc['@mozilla.org/childprocessmessagemanager;1'].
+ getService(Ci.nsISyncMessageSender);
+
+const ns = Namespace();
+
+const process = {
+ port: new EventTarget(),
+ get id() {
+ return runtime.processID;
+ },
+ get isRemote() {
+ return runtime.processType != MAIN_PROCESS;
+ }
+};
+exports.process = process;
+
+function definePort(obj, name) {
+ obj.port.emit = (event, ...args) => {
+ let manager = ns(obj).messageManager;
+ if (!manager)
+ return;
+
+ manager.sendAsyncMessage(name, { loaderID, event, args });
+ };
+}
+
+function messageReceived({ data, objects }) {
+ // Ignore messages from other loaders
+ if (data.loaderID != loaderID)
+ return;
+
+ let keys = Object.keys(objects);
+ if (keys.length) {
+ // If any objects are CPOWs then ignore this message. We don't want child
+ // processes interracting with CPOWs
+ if (!keys.every(name => !Cu.isCrossProcessWrapper(objects[name])))
+ return;
+
+ data.args.push(objects);
+ }
+
+ emit(this.port, data.event, this, ...data.args);
+}
+
+ns(process).messageManager = mm;
+definePort(process, 'sdk/remote/process/message');
+let processMessageReceived = messageReceived.bind(process);
+mm.addMessageListener('sdk/remote/process/message', processMessageReceived);
+
+when(() => {
+ mm.removeMessageListener('sdk/remote/process/message', processMessageReceived);
+ frames = null;
+});
+
+process.port.on('sdk/remote/require', (process, uri) => {
+ require(uri);
+});
+
+function listenerEquals(a, b) {
+ for (let prop of ["type", "callback", "isCapturing"]) {
+ if (a[prop] != b[prop])
+ return false;
+ }
+ return true;
+}
+
+function listenerFor(type, callback, isCapturing = false) {
+ return {
+ type,
+ callback,
+ isCapturing,
+ registeredCallback: undefined,
+ get args() {
+ return [
+ this.type,
+ this.registeredCallback ? this.registeredCallback : this.callback,
+ this.isCapturing
+ ];
+ }
+ };
+}
+
+function removeListenerFromArray(array, listener) {
+ let index = array.findIndex(l => listenerEquals(l, listener));
+ if (index < 0)
+ return;
+ array.splice(index, 1);
+}
+
+function getListenerFromArray(array, listener) {
+ return array.find(l => listenerEquals(l, listener));
+}
+
+function arrayContainsListener(array, listener) {
+ return !!getListenerFromArray(array, listener);
+}
+
+function makeFrameEventListener(frame, callback) {
+ return callback.bind(frame);
+}
+
+var FRAME_ID = 0;
+var tabMap = new Map();
+
+const Frame = Class({
+ implements: [ Disposable ],
+ extends: EventTarget,
+ setup: function(contentFrame) {
+ // This ID should be unique for this loader across all processes
+ let priv = ns(this);
+
+ priv.id = runtime.processID + ":" + FRAME_ID++;
+
+ priv.contentFrame = contentFrame;
+ priv.messageManager = contentFrame;
+ priv.domListeners = [];
+
+ tabMap.set(contentFrame.docShell, this);
+
+ priv.messageReceived = messageReceived.bind(this);
+ priv.messageManager.addMessageListener('sdk/remote/frame/message', priv.messageReceived);
+
+ this.port = new EventTarget();
+ definePort(this, 'sdk/remote/frame/message');
+
+ priv.messageManager.sendAsyncMessage('sdk/remote/frame/attach', {
+ loaderID,
+ frameID: priv.id,
+ processID: runtime.processID
+ });
+
+ frames.attachItem(this);
+ },
+
+ dispose: function() {
+ let priv = ns(this);
+
+ emit(this, 'detach', this);
+
+ for (let listener of priv.domListeners)
+ priv.contentFrame.removeEventListener(...listener.args);
+
+ priv.messageManager.removeMessageListener('sdk/remote/frame/message', priv.messageReceived);
+ tabMap.delete(priv.contentFrame.docShell);
+ priv.contentFrame = null;
+ },
+
+ get content() {
+ return ns(this).contentFrame.content;
+ },
+
+ get isTab() {
+ let docShell = ns(this).contentFrame.docShell;
+ if (process.isRemote) {
+ // We don't want to roundtrip to the main process to get this property.
+ // This hack relies on the host app having defined webBrowserChrome only
+ // in frames that are part of the tabs. Since only Firefox has remote
+ // processes right now and does this this works.
+ let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsITabChild);
+ return !!tabchild.webBrowserChrome;
+ }
+ else {
+ // This is running in the main process so we can break out to the browser
+ // And check we can find a tab for the browser element directly.
+ let browser = docShell.chromeEventHandler;
+ let tab = require('../tabs/utils').getTabForBrowser(browser);
+ return !!tab;
+ }
+ },
+
+ addEventListener: function(...args) {
+ let priv = ns(this);
+
+ let listener = listenerFor(...args);
+ if (arrayContainsListener(priv.domListeners, listener))
+ return;
+
+ listener.registeredCallback = makeFrameEventListener(this, listener.callback);
+
+ priv.domListeners.push(listener);
+ priv.contentFrame.addEventListener(...listener.args);
+ },
+
+ removeEventListener: function(...args) {
+ let priv = ns(this);
+
+ let listener = getListenerFromArray(priv.domListeners, listenerFor(...args));
+ if (!listener)
+ return;
+
+ removeListenerFromArray(priv.domListeners, listener);
+ priv.contentFrame.removeEventListener(...listener.args);
+ }
+});
+
+const FrameList = Class({
+ implements: [ EventParent, Disposable ],
+ extends: EventTarget,
+ setup: function() {
+ EventParent.prototype.initialize.call(this);
+
+ this.port = new EventTarget();
+ ns(this).domListeners = [];
+
+ this.on('attach', frame => {
+ for (let listener of ns(this).domListeners)
+ frame.addEventListener(...listener.args);
+ });
+ },
+
+ dispose: function() {
+ // The only case where we get destroyed is when the loader is unloaded in
+ // which case each frame will clean up its own event listeners.
+ ns(this).domListeners = null;
+ },
+
+ getFrameForWindow: function(window) {
+ let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+
+ return tabMap.get(docShell) || null;
+ },
+
+ addEventListener: function(...args) {
+ let listener = listenerFor(...args);
+ if (arrayContainsListener(ns(this).domListeners, listener))
+ return;
+
+ ns(this).domListeners.push(listener);
+ for (let frame of this)
+ frame.addEventListener(...listener.args);
+ },
+
+ removeEventListener: function(...args) {
+ let listener = listenerFor(...args);
+ if (!arrayContainsListener(ns(this).domListeners, listener))
+ return;
+
+ removeListenerFromArray(ns(this).domListeners, listener);
+ for (let frame of this)
+ frame.removeEventListener(...listener.args);
+ }
+});
+var frames = exports.frames = new FrameList();
+
+function registerContentFrame(contentFrame) {
+ let frame = new Frame(contentFrame);
+}
+exports.registerContentFrame = registerContentFrame;
+
+function unregisterContentFrame(contentFrame) {
+ let frame = tabMap.get(contentFrame.docShell);
+ if (!frame)
+ return;
+
+ frame.destroy();
+}
+exports.unregisterContentFrame = unregisterContentFrame;
diff --git a/addon-sdk/source/lib/sdk/remote/core.js b/addon-sdk/source/lib/sdk/remote/core.js
new file mode 100644
index 000000000..78bb673fd
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/remote/core.js
@@ -0,0 +1,8 @@
+/* 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 options = require("@loader/options");
+
+exports.isChildLoader = options.childLoader;
diff --git a/addon-sdk/source/lib/sdk/remote/parent.js b/addon-sdk/source/lib/sdk/remote/parent.js
new file mode 100644
index 000000000..f110fe3f6
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/remote/parent.js
@@ -0,0 +1,338 @@
+/* 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 { isChildLoader } = require('./core');
+if (isChildLoader)
+ throw new Error("Cannot load sdk/remote/parent in a child loader.");
+
+const { Cu, Ci, Cc } = require('chrome');
+const runtime = require('../system/runtime');
+
+const MAIN_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+if (runtime.processType != MAIN_PROCESS) {
+ throw new Error('Cannot use sdk/remote/parent in a child process.');
+}
+
+const { Class } = require('../core/heritage');
+const { Namespace } = require('../core/namespace');
+const { Disposable } = require('../core/disposable');
+const { omit } = require('../util/object');
+const { when } = require('../system/unload');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const system = require('../system/events');
+const { EventParent } = require('./utils');
+const options = require('@loader/options');
+const loaderModule = require('toolkit/loader');
+const { getTabForBrowser } = require('../tabs/utils');
+
+const appInfo = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULRuntime);
+
+exports.useRemoteProcesses = appInfo.browserTabsRemoteAutostart;
+
+// Chose the right function for resolving relative a module id
+var moduleResolve;
+if (options.isNative) {
+ moduleResolve = (id, requirer) => loaderModule.nodeResolve(id, requirer, { rootURI: options.rootURI });
+}
+else {
+ moduleResolve = loaderModule.resolve;
+}
+// Build the sorted path mapping structure that resolveURI requires
+var pathMapping = Object.keys(options.paths)
+ .sort((a, b) => b.length - a.length)
+ .map(p => [p, options.paths[p]]);
+
+// Load the scripts in the child processes
+var { getNewLoaderID } = require('../../framescript/FrameScriptManager.jsm');
+var PATH = options.paths[''];
+
+const childOptions = omit(options, ['modules', 'globals', 'resolve', 'load']);
+childOptions.modules = {};
+// @l10n/data is just JSON data and can be safely sent across to the child loader
+try {
+ childOptions.modules["@l10n/data"] = require("@l10n/data");
+}
+catch (e) {
+ // There may be no l10n data
+}
+const loaderID = getNewLoaderID();
+childOptions.loaderID = loaderID;
+childOptions.childLoader = true;
+
+const ppmm = Cc['@mozilla.org/parentprocessmessagemanager;1'].
+ getService(Ci.nsIMessageBroadcaster);
+const gmm = Cc['@mozilla.org/globalmessagemanager;1'].
+ getService(Ci.nsIMessageBroadcaster);
+
+const ns = Namespace();
+
+var processMap = new Map();
+
+function definePort(obj, name) {
+ obj.port.emitCPOW = (event, args, cpows = {}) => {
+ let manager = ns(obj).messageManager;
+ if (!manager)
+ return;
+
+ let method = manager instanceof Ci.nsIMessageBroadcaster ?
+ "broadcastAsyncMessage" : "sendAsyncMessage";
+
+ manager[method](name, { loaderID, event, args }, cpows);
+ };
+
+ obj.port.emit = (event, ...args) => obj.port.emitCPOW(event, args);
+}
+
+function messageReceived({ target, data }) {
+ // Ignore messages from other loaders
+ if (data.loaderID != loaderID)
+ return;
+
+ emit(this.port, data.event, this, ...data.args);
+}
+
+// Process represents a gecko process that can load webpages. Each process
+// contains a number of Frames. This class is used to send and receive messages
+// from a single process.
+const Process = Class({
+ implements: [ Disposable ],
+ extends: EventTarget,
+ setup: function(id, messageManager, isRemote) {
+ ns(this).id = id;
+ ns(this).isRemote = isRemote;
+ ns(this).messageManager = messageManager;
+ ns(this).messageReceived = messageReceived.bind(this);
+ this.destroy = this.destroy.bind(this);
+ ns(this).messageManager.addMessageListener('sdk/remote/process/message', ns(this).messageReceived);
+ ns(this).messageManager.addMessageListener('child-process-shutdown', this.destroy);
+
+ this.port = new EventTarget();
+ definePort(this, 'sdk/remote/process/message');
+
+ // Load any remote modules
+ for (let module of remoteModules.values())
+ this.port.emit('sdk/remote/require', module);
+
+ processMap.set(ns(this).id, this);
+ processes.attachItem(this);
+ },
+
+ dispose: function() {
+ emit(this, 'detach', this);
+ processMap.delete(ns(this).id);
+ ns(this).messageManager.removeMessageListener('sdk/remote/process/message', ns(this).messageReceived);
+ ns(this).messageManager.removeMessageListener('child-process-shutdown', this.destroy);
+ ns(this).messageManager = null;
+ },
+
+ // Returns true if this process is a child process
+ get isRemote() {
+ return ns(this).isRemote;
+ }
+});
+
+// Processes gives an API for enumerating an sending and receiving messages from
+// all processes as well as detecting when a new process starts.
+const Processes = Class({
+ implements: [ EventParent ],
+ extends: EventTarget,
+ initialize: function() {
+ EventParent.prototype.initialize.call(this);
+ ns(this).messageManager = ppmm;
+
+ this.port = new EventTarget();
+ definePort(this, 'sdk/remote/process/message');
+ },
+
+ getById: function(id) {
+ return processMap.get(id);
+ }
+});
+var processes = exports.processes = new Processes();
+
+var frameMap = new Map();
+
+function setFrameProcess(frame, process) {
+ ns(frame).process = process;
+ frames.attachItem(frame);
+}
+
+// Frames display webpages in a process. In the main process every Frame is
+// linked with a <browser> or <iframe> element.
+const Frame = Class({
+ implements: [ Disposable ],
+ extends: EventTarget,
+ setup: function(id, node) {
+ ns(this).id = id;
+ ns(this).node = node;
+
+ let frameLoader = node.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ ns(this).messageManager = frameLoader.messageManager;
+
+ ns(this).messageReceived = messageReceived.bind(this);
+ ns(this).messageManager.addMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
+
+ this.port = new EventTarget();
+ definePort(this, 'sdk/remote/frame/message');
+
+ frameMap.set(ns(this).messageManager, this);
+ },
+
+ dispose: function() {
+ emit(this, 'detach', this);
+ ns(this).messageManager.removeMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
+
+ frameMap.delete(ns(this).messageManager);
+ ns(this).messageManager = null;
+ },
+
+ // Returns the browser or iframe element this frame displays in
+ get frameElement() {
+ return ns(this).node;
+ },
+
+ // Returns the process that this frame loads in
+ get process() {
+ return ns(this).process;
+ },
+
+ // Returns true if this frame is a tab in a main browser window
+ get isTab() {
+ let tab = getTabForBrowser(ns(this).node);
+ return !!tab;
+ }
+});
+
+function managerDisconnected({ subject: manager }) {
+ let frame = frameMap.get(manager);
+ if (frame)
+ frame.destroy();
+}
+system.on('message-manager-disconnect', managerDisconnected);
+
+// Provides an API for enumerating and sending and receiving messages from all
+// Frames
+const FrameList = Class({
+ implements: [ EventParent ],
+ extends: EventTarget,
+ initialize: function() {
+ EventParent.prototype.initialize.call(this);
+ ns(this).messageManager = gmm;
+
+ this.port = new EventTarget();
+ definePort(this, 'sdk/remote/frame/message');
+ },
+
+ // Returns the frame for a browser element
+ getFrameForBrowser: function(browser) {
+ for (let frame of this) {
+ if (frame.frameElement == browser)
+ return frame;
+ }
+ return null;
+ },
+});
+var frames = exports.frames = new FrameList();
+
+// Create the module loader in any existing processes
+ppmm.broadcastAsyncMessage('sdk/remote/process/load', {
+ modulePath: PATH,
+ loaderID,
+ options: childOptions,
+ reason: "broadcast"
+});
+
+// A loader has started in a remote process
+function processLoaderStarted({ target, data }) {
+ if (data.loaderID != loaderID)
+ return;
+
+ if (processMap.has(data.processID)) {
+ console.error("Saw the same process load the same loader twice. This is a bug in the SDK.");
+ return;
+ }
+
+ let process = new Process(data.processID, target, data.isRemote);
+
+ if (pendingFrames.has(data.processID)) {
+ for (let frame of pendingFrames.get(data.processID))
+ setFrameProcess(frame, process);
+ pendingFrames.delete(data.processID);
+ }
+}
+
+// A new process has started
+function processStarted({ target, data: { modulePath } }) {
+ if (modulePath != PATH)
+ return;
+
+ // Have it load a loader if it hasn't already
+ target.sendAsyncMessage('sdk/remote/process/load', {
+ modulePath,
+ loaderID,
+ options: childOptions,
+ reason: "response"
+ });
+}
+
+var pendingFrames = new Map();
+
+// A new frame has been created in the remote process
+function frameAttached({ target, data }) {
+ if (data.loaderID != loaderID)
+ return;
+
+ let frame = new Frame(data.frameID, target);
+
+ let process = processMap.get(data.processID);
+ if (process) {
+ setFrameProcess(frame, process);
+ return;
+ }
+
+ // In some cases frame messages can arrive earlier than process messages
+ // causing us to see a new frame appear before its process. In this case
+ // cache the frame data until we see the process. See bug 1131375.
+ if (!pendingFrames.has(data.processID))
+ pendingFrames.set(data.processID, [frame]);
+ else
+ pendingFrames.get(data.processID).push(frame);
+}
+
+// Wait for new processes and frames
+ppmm.addMessageListener('sdk/remote/process/attach', processLoaderStarted);
+ppmm.addMessageListener('sdk/remote/process/start', processStarted);
+gmm.addMessageListener('sdk/remote/frame/attach', frameAttached);
+
+when(reason => {
+ ppmm.removeMessageListener('sdk/remote/process/attach', processLoaderStarted);
+ ppmm.removeMessageListener('sdk/remote/process/start', processStarted);
+ gmm.removeMessageListener('sdk/remote/frame/attach', frameAttached);
+
+ ppmm.broadcastAsyncMessage('sdk/remote/process/unload', { loaderID, reason });
+});
+
+var remoteModules = new Set();
+
+// Ensures a module is loaded in every child process. It is safe to send
+// messages to this module immediately after calling this.
+// Pass a module to resolve the id relatively.
+function remoteRequire(id, module = null) {
+ // Resolve relative to calling module if passed
+ if (module)
+ id = moduleResolve(id, module.id);
+ let uri = loaderModule.resolveURI(id, pathMapping);
+
+ // Don't reload the same module
+ if (remoteModules.has(uri))
+ return;
+
+ remoteModules.add(uri);
+ processes.port.emit('sdk/remote/require', uri);
+}
+exports.remoteRequire = remoteRequire;
diff --git a/addon-sdk/source/lib/sdk/remote/utils.js b/addon-sdk/source/lib/sdk/remote/utils.js
new file mode 100644
index 000000000..5a5e39198
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/remote/utils.js
@@ -0,0 +1,39 @@
+/* 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 { List, addListItem, removeListItem } = require('../util/list');
+const { emit } = require('../event/core');
+const { pipe } = require('../event/utils');
+
+// A helper class that maintains a list of EventTargets. Any events emitted
+// to an EventTarget are also emitted by the EventParent. Likewise for an
+// EventTarget's port property.
+const EventParent = Class({
+ implements: [ List ],
+
+ attachItem: function(item) {
+ addListItem(this, item);
+
+ pipe(item.port, this.port);
+ pipe(item, this);
+
+ item.once('detach', () => {
+ removeListItem(this, item);
+ })
+
+ emit(this, 'attach', item);
+ },
+
+ // Calls listener for every object already in the list and every object
+ // subsequently added to the list.
+ forEvery: function(listener) {
+ for (let item of this)
+ listener(item);
+
+ this.on('attach', listener);
+ }
+});
+exports.EventParent = EventParent;
diff --git a/addon-sdk/source/lib/sdk/request.js b/addon-sdk/source/lib/sdk/request.js
new file mode 100644
index 000000000..96bb1e6d7
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/request.js
@@ -0,0 +1,248 @@
+/* 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 { ns } = require("./core/namespace");
+const { emit } = require("./event/core");
+const { merge } = require("./util/object");
+const { stringify } = require("./querystring");
+const { EventTarget } = require("./event/target");
+const { Class } = require("./core/heritage");
+const { XMLHttpRequest, forceAllowThirdPartyCookie } = require("./net/xhr");
+const apiUtils = require("./deprecated/api-utils");
+const { isValidURI } = require("./url.js");
+
+const response = ns();
+const request = ns();
+
+// Instead of creating a new validator for each request, just make one and
+// reuse it.
+const { validateOptions, validateSingleOption } = new OptionsValidator({
+ url: {
+ // Also converts a URL instance to string, bug 857902
+ map: url => url.toString(),
+ ok: isValidURI
+ },
+ headers: {
+ map: v => v || {},
+ is: ["object"],
+ },
+ content: {
+ map: v => v || null,
+ is: ["string", "object", "null"],
+ },
+ contentType: {
+ map: v => v || "application/x-www-form-urlencoded",
+ is: ["string"],
+ },
+ overrideMimeType: {
+ map: v => v || null,
+ is: ["string", "null"],
+ },
+ anonymous: {
+ map: v => v || false,
+ is: ["boolean", "null"],
+ }
+});
+
+const REUSE_ERROR = "This request object has been used already. You must " +
+ "create a new one to make a new request."
+
+// Utility function to prep the request since it's the same between
+// request types
+function runRequest(mode, target) {
+ let source = request(target)
+ let { xhr, url, content, contentType, headers, overrideMimeType, anonymous } = source;
+
+ let isGetOrHead = (mode == "GET" || mode == "HEAD");
+
+ // If this request has already been used, then we can't reuse it.
+ // Throw an error.
+ if (xhr)
+ throw new Error(REUSE_ERROR);
+
+ xhr = source.xhr = new XMLHttpRequest({
+ mozAnon: anonymous
+ });
+
+ // Build the data to be set. For GET or HEAD requests, we want to append that
+ // to the URL before opening the request.
+ let data = stringify(content);
+ // If the URL already has ? in it, then we want to just use &
+ if (isGetOrHead && data)
+ url = url + (/\?/.test(url) ? "&" : "?") + data;
+
+ // open the request
+ xhr.open(mode, url);
+
+
+ forceAllowThirdPartyCookie(xhr);
+
+ // request header must be set after open, but before send
+ xhr.setRequestHeader("Content-Type", contentType);
+
+ // set other headers
+ Object.keys(headers).forEach(function(name) {
+ xhr.setRequestHeader(name, headers[name]);
+ });
+
+ // set overrideMimeType
+ if (overrideMimeType)
+ xhr.overrideMimeType(overrideMimeType);
+
+ // handle the readystate, create the response, and call the callback
+ xhr.onreadystatechange = function onreadystatechange() {
+ if (xhr.readyState === 4) {
+ let response = Response(xhr);
+ source.response = response;
+ emit(target, 'complete', response);
+ }
+ };
+
+ // actually send the request.
+ // We don't want to send data on GET or HEAD requests.
+ xhr.send(!isGetOrHead ? data : null);
+}
+
+const Request = Class({
+ extends: EventTarget,
+ initialize: function initialize(options) {
+ // `EventTarget.initialize` will set event listeners that are named
+ // like `onEvent` in this case `onComplete` listener will be set to
+ // `complete` event.
+ EventTarget.prototype.initialize.call(this, options);
+
+ // Copy normalized options.
+ merge(request(this), validateOptions(options));
+ },
+ get url() { return request(this).url; },
+ set url(value) { request(this).url = validateSingleOption('url', value); },
+ get headers() { return request(this).headers; },
+ set headers(value) {
+ return request(this).headers = validateSingleOption('headers', value);
+ },
+ get content() { return request(this).content; },
+ set content(value) {
+ request(this).content = validateSingleOption('content', value);
+ },
+ get contentType() { return request(this).contentType; },
+ set contentType(value) {
+ request(this).contentType = validateSingleOption('contentType', value);
+ },
+ get anonymous() { return request(this).anonymous; },
+ get response() { return request(this).response; },
+ delete: function() {
+ runRequest('DELETE', this);
+ return this;
+ },
+ get: function() {
+ runRequest('GET', this);
+ return this;
+ },
+ post: function() {
+ runRequest('POST', this);
+ return this;
+ },
+ put: function() {
+ runRequest('PUT', this);
+ return this;
+ },
+ head: function() {
+ runRequest('HEAD', this);
+ return this;
+ }
+});
+exports.Request = Request;
+
+const Response = Class({
+ initialize: function initialize(request) {
+ response(this).request = request;
+ },
+ // more about responseURL: https://bugzilla.mozilla.org/show_bug.cgi?id=998076
+ get url() {
+ return response(this).request.responseURL;
+ },
+ get text() {
+ return response(this).request.responseText;
+ },
+ get xml() {
+ throw new Error("Sorry, the 'xml' property is no longer available. " +
+ "see bug 611042 for more information.");
+ },
+ get status() {
+ return response(this).request.status;
+ },
+ get statusText() {
+ return response(this).request.statusText;
+ },
+ get json() {
+ try {
+ return JSON.parse(this.text);
+ } catch(error) {
+ return null;
+ }
+ },
+ get headers() {
+ let headers = {}, lastKey;
+ // Since getAllResponseHeaders() will return null if there are no headers,
+ // defend against it by defaulting to ""
+ let rawHeaders = response(this).request.getAllResponseHeaders() || "";
+ rawHeaders.split("\n").forEach(function (h) {
+ // According to the HTTP spec, the header string is terminated by an empty
+ // line, so we can just skip it.
+ if (!h.length) {
+ return;
+ }
+
+ let index = h.indexOf(":");
+ // The spec allows for leading spaces, so instead of assuming a single
+ // leading space, just trim the values.
+ let key = h.substring(0, index).trim(),
+ val = h.substring(index + 1).trim();
+
+ // For empty keys, that means that the header value spanned multiple lines.
+ // In that case we should append the value to the value of lastKey with a
+ // new line. We'll assume lastKey will be set because there should never
+ // be an empty key on the first pass.
+ if (key) {
+ headers[key] = val;
+ lastKey = key;
+ }
+ else {
+ headers[lastKey] += "\n" + val;
+ }
+ });
+ return headers;
+ },
+ get anonymous() {
+ return response(this).request.mozAnon;
+ }
+});
+
+// apiUtils.validateOptions doesn't give the ability to easily validate single
+// options, so this is a wrapper that provides that ability.
+function OptionsValidator(rules) {
+ return {
+ validateOptions: function (options) {
+ return apiUtils.validateOptions(options, rules);
+ },
+ validateSingleOption: function (field, value) {
+ // We need to create a single rule object from our listed rules. To avoid
+ // JavaScript String warnings, check for the field & default to an empty object.
+ let singleRule = {};
+ if (field in rules) {
+ singleRule[field] = rules[field];
+ }
+ let singleOption = {};
+ singleOption[field] = value;
+ // This should throw if it's invalid, which will bubble up & out.
+ return apiUtils.validateOptions(singleOption, singleRule)[field];
+ }
+ };
+}
diff --git a/addon-sdk/source/lib/sdk/selection.js b/addon-sdk/source/lib/sdk/selection.js
new file mode 100644
index 000000000..8682e8c6d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/selection.js
@@ -0,0 +1,470 @@
+/* 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": {
+ "Firefox": "*",
+ "SeaMonkey": "*"
+ }
+};
+
+const { Ci, Cc } = require("chrome"),
+ { setTimeout } = require("./timers"),
+ { emit, off } = require("./event/core"),
+ { Class, obscure } = require("./core/heritage"),
+ { EventTarget } = require("./event/target"),
+ { ns } = require("./core/namespace"),
+ { when: unload } = require("./system/unload"),
+ { ignoreWindow } = require('./private-browsing/utils'),
+ { getTabs, getTabForContentWindow,
+ getAllTabContentWindows } = require('./tabs/utils'),
+ winUtils = require("./window/utils"),
+ events = require("./system/events");
+
+// The selection types
+const HTML = 0x01,
+ TEXT = 0x02,
+ DOM = 0x03; // internal use only
+
+// A more developer-friendly message than the caught exception when is not
+// possible change a selection.
+const ERR_CANNOT_CHANGE_SELECTION =
+ "It isn't possible to change the selection, as there isn't currently a selection";
+
+const selections = ns();
+
+const Selection = Class({
+ /**
+ * Creates an object from which a selection can be set, get, etc. Each
+ * object has an associated with a range number. Range numbers are the
+ * 0-indexed counter of selection ranges as explained at
+ * https://developer.mozilla.org/en/DOM/Selection.
+ *
+ * @param rangeNumber
+ * The zero-based range index into the selection
+ */
+ initialize: function initialize(rangeNumber) {
+ // In order to hide the private `rangeNumber` argument from API consumers
+ // while still enabling Selection getters/setters to access it, we define
+ // it as non enumerable, non configurable property. While consumers still
+ // may discover it they won't be able to do any harm which is good enough
+ // in this case.
+ Object.defineProperties(this, {
+ rangeNumber: {
+ enumerable: false,
+ configurable: false,
+ value: rangeNumber
+ }
+ });
+ },
+ get text() { return getSelection(TEXT, this.rangeNumber); },
+ set text(value) { setSelection(TEXT, value, this.rangeNumber); },
+ get html() { return getSelection(HTML, this.rangeNumber); },
+ set html(value) { setSelection(HTML, value, this.rangeNumber); },
+ get isContiguous() {
+
+ // If there are multiple non empty ranges, the selection is definitely
+ // discontiguous. It returns `false` also if there are no valid selection.
+ let count = 0;
+ for (let sel in selectionIterator)
+ if (++count > 1)
+ break;
+
+ return count === 1;
+ }
+});
+
+const selectionListener = {
+ notifySelectionChanged: function (document, selection, reason) {
+ if (!["SELECTALL", "KEYPRESS", "MOUSEUP"].some(type => reason &
+ Ci.nsISelectionListener[type + "_REASON"]) || selection.toString() == "")
+ return;
+
+ this.onSelect();
+ },
+
+ onSelect: function() {
+ emit(module.exports, "select");
+ }
+}
+
+/**
+ * Defines iterators so that discontiguous selections can be iterated.
+ * Empty selections are skipped - see `safeGetRange` for further details.
+ *
+ * If discontiguous selections are in a text field, only the first one
+ * is returned because the text field selection APIs doesn't support
+ * multiple selections.
+ */
+function* forOfIterator() {
+ let selection = getSelection(DOM);
+ let count = 0;
+
+ if (selection)
+ count = selection.rangeCount || (getElementWithSelection() ? 1 : 0);
+
+ for (let i = 0; i < count; i++) {
+ let sel = Selection(i);
+
+ if (sel.text)
+ yield Selection(i);
+ }
+}
+
+const selectionIteratorOptions = {
+ __iterator__: function() {
+ for (let item of this)
+ yield item;
+ }
+}
+selectionIteratorOptions[Symbol.iterator] = forOfIterator;
+const selectionIterator = obscure(selectionIteratorOptions);
+
+/**
+ * Returns the most recent focused window.
+ * if private browsing window is most recent and not supported,
+ * then ignore it and return `null`, because the focused window
+ * can't be targeted.
+ */
+function getFocusedWindow() {
+ let window = winUtils.getFocusedWindow();
+
+ return ignoreWindow(window) ? null : window;
+}
+
+/**
+ * Returns the focused element in the most recent focused window
+ * if private browsing window is most recent and not supported,
+ * then ignore it and return `null`, because the focused element
+ * can't be targeted.
+ */
+function getFocusedElement() {
+ let element = winUtils.getFocusedElement();
+
+ if (!element || ignoreWindow(element.ownerDocument.defaultView))
+ return null;
+
+ return element;
+}
+
+/**
+ * Returns the current selection from most recent content window. Depending on
+ * the specified |type|, the value returned can be a string of text, stringified
+ * HTML, or a DOM selection object as described at
+ * https://developer.mozilla.org/en/DOM/Selection.
+ *
+ * @param type
+ * Specifies the return type of the selection. Valid values are the one
+ * of the constants HTML, TEXT, or DOM.
+ *
+ * @param rangeNumber
+ * Specifies the zero-based range index of the returned selection.
+ */
+function getSelection(type, rangeNumber) {
+ let window, selection;
+ try {
+ window = getFocusedWindow();
+ selection = window.getSelection();
+ }
+ catch (e) {
+ return null;
+ }
+
+ // Get the selected content as the specified type
+ if (type == DOM) {
+ return selection;
+ }
+ else if (type == TEXT) {
+ let range = safeGetRange(selection, rangeNumber);
+
+ if (range)
+ return range.toString();
+
+ let node = getElementWithSelection();
+
+ if (!node)
+ return null;
+
+ return node.value.substring(node.selectionStart, node.selectionEnd);
+ }
+ else if (type == HTML) {
+ let range = safeGetRange(selection, rangeNumber);
+ // Another way, but this includes the xmlns attribute for all elements in
+ // Gecko 1.9.2+ :
+ // return Cc["@mozilla.org/xmlextras/xmlserializer;1"].
+ // createInstance(Ci.nsIDOMSerializer).serializeToSTring(range.
+ // cloneContents());
+ if (!range)
+ return null;
+
+ let node = window.document.createElement("span");
+ node.appendChild(range.cloneContents());
+ return node.innerHTML;
+ }
+
+ throw new Error("Type " + type + " is unrecognized.");
+}
+
+/**
+ * Sets the current selection of the most recent content document by changing
+ * the existing selected text/HTML range to the specified value.
+ *
+ * @param val
+ * The value for the new selection
+ *
+ * @param rangeNumber
+ * The zero-based range index of the selection to be set
+ *
+ */
+function setSelection(type, val, rangeNumber) {
+ // Make sure we have a window context & that there is a current selection.
+ // Selection cannot be set unless there is an existing selection.
+ let window, selection;
+
+ try {
+ window = getFocusedWindow();
+ selection = window.getSelection();
+ }
+ catch (e) {
+ throw new Error(ERR_CANNOT_CHANGE_SELECTION);
+ }
+
+ let range = safeGetRange(selection, rangeNumber);
+
+ if (range) {
+ let fragment;
+
+ if (type === HTML)
+ fragment = range.createContextualFragment(val);
+ else {
+ fragment = range.createContextualFragment("");
+ fragment.textContent = val;
+ }
+
+ range.deleteContents();
+ range.insertNode(fragment);
+ }
+ else {
+ let node = getElementWithSelection();
+
+ if (!node)
+ throw new Error(ERR_CANNOT_CHANGE_SELECTION);
+
+ let { value, selectionStart, selectionEnd } = node;
+
+ let newSelectionEnd = selectionStart + val.length;
+
+ node.value = value.substring(0, selectionStart) +
+ val +
+ value.substring(selectionEnd, value.length);
+
+ node.setSelectionRange(selectionStart, newSelectionEnd);
+ }
+}
+
+/**
+ * Returns the specified range in a selection without throwing an exception.
+ *
+ * @param selection
+ * A selection object as described at
+ * https://developer.mozilla.org/en/DOM/Selection
+ *
+ * @param [rangeNumber]
+ * Specifies the zero-based range index of the returned selection.
+ * If it's not provided the function will return the first non empty
+ * range, if any.
+ */
+function safeGetRange(selection, rangeNumber) {
+ try {
+ let { rangeCount } = selection;
+ let range = null;
+
+ if (typeof rangeNumber === "undefined")
+ rangeNumber = 0;
+ else
+ rangeCount = rangeNumber + 1;
+
+ for (; rangeNumber < rangeCount; rangeNumber++ ) {
+ range = selection.getRangeAt(rangeNumber);
+
+ if (range && range.toString())
+ break;
+
+ range = null;
+ }
+
+ return range;
+ }
+ catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Returns a reference of the DOM's active element for the window given, if it
+ * supports the text field selection API and has a text selected.
+ *
+ * Note:
+ * we need this method because window.getSelection doesn't return a selection
+ * for text selected in a form field (see bug 85686)
+ */
+function getElementWithSelection() {
+ let element = getFocusedElement();
+
+ 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) {
+ return null;
+ }
+
+}
+
+/**
+ * Adds the Selection Listener to the content's window given
+ */
+function addSelectionListener(window) {
+ let selection = window.getSelection();
+
+ // Don't add the selection's listener more than once to the same window,
+ // if the selection object is the same
+ if ("selection" in selections(window) && selections(window).selection === selection)
+ return;
+
+ // We ensure that the current selection is an instance of
+ // `nsISelectionPrivate` before working on it, in case is `null`.
+ //
+ // If it's `null` it's likely too early to add the listener, and we demand
+ // that operation to `document-shown` - it can easily happens for frames
+ if (selection instanceof Ci.nsISelectionPrivate)
+ selection.addSelectionListener(selectionListener);
+
+ // nsISelectionListener implementation seems not fire a notification if
+ // a selection is in a text field, therefore we need to add a listener to
+ // window.onselect, that is fired only for text fields.
+ // For consistency, we add it only when the nsISelectionListener is added.
+ //
+ // https://developer.mozilla.org/en/DOM/window.onselect
+ window.addEventListener("select", selectionListener.onSelect, true);
+
+ selections(window).selection = selection;
+};
+
+/**
+ * Removes the Selection Listener to the content's window given
+ */
+function removeSelectionListener(window) {
+ // Don't remove the selection's listener to a window that wasn't handled.
+ if (!("selection" in selections(window)))
+ return;
+
+ let selection = window.getSelection();
+ let isSameSelection = selection === selections(window).selection;
+
+ // Before remove the listener, we ensure that the current selection is an
+ // instance of `nsISelectionPrivate` (it could be `null`), and that is still
+ // the selection we managed for this window (it could be detached).
+ if (selection instanceof Ci.nsISelectionPrivate && isSameSelection)
+ selection.removeSelectionListener(selectionListener);
+
+ window.removeEventListener("select", selectionListener.onSelect, true);
+
+ delete selections(window).selection;
+};
+
+function onContent(event) {
+ let window = event.subject.defaultView;
+
+ // We are not interested in documents without valid defaultView (e.g. XML)
+ // that aren't in a tab (e.g. Panel); or in private windows
+ if (window && getTabForContentWindow(window) && !ignoreWindow(window)) {
+ addSelectionListener(window);
+ }
+}
+
+// Adds Selection listener to new documents
+// Note that strong reference is needed for documents that are loading slowly or
+// where the server didn't close the connection (e.g. "comet").
+events.on("document-element-inserted", onContent, true);
+
+// Adds Selection listeners to existing documents
+getAllTabContentWindows().forEach(addSelectionListener);
+
+// When a document is not visible anymore the selection object is detached, and
+// a new selection object is created when it becomes visible again.
+// That makes the previous selection's listeners added previously totally
+// useless – the listeners are not notified anymore.
+// To fix that we're listening for `document-shown` event in order to add
+// the listeners to the new selection object created.
+//
+// See bug 665386 for further details.
+
+function onShown(event) {
+ let window = event.subject.defaultView;
+
+ // We are not interested in documents without valid defaultView.
+ // For example XML documents don't have windows and we don't yet support them.
+ if (!window)
+ return;
+
+ // We want to handle only the windows where we added selection's listeners
+ if ("selection" in selections(window)) {
+ let currentSelection = window.getSelection();
+ let { selection } = selections(window);
+
+ // If the current selection for the window given is different from the one
+ // stored in the namespace, we need to add the listeners again, and replace
+ // the previous selection in our list with the new one.
+ //
+ // Notice that we don't have to remove the listeners from the old selection,
+ // because is detached. An attempt to remove the listener, will raise an
+ // error (see http://mxr.mozilla.org/mozilla-central/source/layout/generic/nsSelection.cpp#5343 )
+ //
+ // We ensure that the current selection is an instance of
+ // `nsISelectionPrivate` before working on it, in case is `null`.
+ if (currentSelection instanceof Ci.nsISelectionPrivate &&
+ currentSelection !== selection) {
+
+ window.addEventListener("select", selectionListener.onSelect, true);
+ currentSelection.addSelectionListener(selectionListener);
+ selections(window).selection = currentSelection;
+ }
+ }
+}
+
+events.on("document-shown", onShown, true);
+
+// Removes Selection listeners when the add-on is unloaded
+unload(function(){
+ getAllTabContentWindows().forEach(removeSelectionListener);
+
+ events.off("document-element-inserted", onContent);
+ events.off("document-shown", onShown);
+
+ off(exports);
+});
+
+const selection = Class({
+ extends: EventTarget,
+ implements: [ Selection, selectionIterator ]
+})();
+
+module.exports = selection;
diff --git a/addon-sdk/source/lib/sdk/self.js b/addon-sdk/source/lib/sdk/self.js
new file mode 100644
index 000000000..c2114a926
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/self.js
@@ -0,0 +1,61 @@
+/* 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 } = require('chrome');
+const options = require('@loader/options');
+
+const { get } = require("./preferences/service");
+const { readURISync } = require('./net/url');
+
+const id = options.id;
+
+const readPref = key => get("extensions." + id + ".sdk." + key);
+
+const name = readPref("name") || options.name;
+const version = readPref("version") || options.version;
+const loadReason = readPref("load.reason") || options.loadReason;
+const rootURI = readPref("rootURI") || options.rootURI || "";
+const baseURI = readPref("baseURI") || options.prefixURI + name + "/"
+const addonDataURI = baseURI + "data/";
+const metadata = options.metadata || {};
+const permissions = metadata.permissions || {};
+const isPacked = rootURI && rootURI.indexOf("jar:") === 0;
+
+const isPrivateBrowsingSupported = 'private-browsing' in permissions &&
+ permissions['private-browsing'] === true;
+
+const uri = (path="") =>
+ path.includes(":") ? path : addonDataURI + path.replace(/^\.\//, "");
+
+var preferencesBranch = ("preferences-branch" in metadata)
+ ? metadata["preferences-branch"]
+ : options.preferencesBranch
+
+if (/[^\w{@}.-]/.test(preferencesBranch)) {
+ preferencesBranch = id;
+ console.warn("Ignoring preferences-branch (not a valid branch name)");
+}
+
+// Some XPCOM APIs require valid URIs as an argument for certain operations
+// (see `nsILoginManager` for example). This property represents add-on
+// associated unique URI string that can be used for that.
+exports.uri = 'addon:' + id;
+exports.id = id;
+exports.preferencesBranch = preferencesBranch || id;
+exports.name = name;
+exports.loadReason = loadReason;
+exports.version = version;
+exports.packed = isPacked;
+exports.data = Object.freeze({
+ url: uri,
+ load: function read(path) {
+ return readURISync(uri(path));
+ }
+});
+exports.isPrivateBrowsingSupported = isPrivateBrowsingSupported;
diff --git a/addon-sdk/source/lib/sdk/simple-prefs.js b/addon-sdk/source/lib/sdk/simple-prefs.js
new file mode 100644
index 000000000..3472f4418
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/simple-prefs.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { emit, off } = require("./event/core");
+const { PrefsTarget } = require("./preferences/event-target");
+const { preferencesBranch, id } = require("./self");
+const { on } = require("./system/events");
+
+const ADDON_BRANCH = "extensions." + preferencesBranch + ".";
+const BUTTON_PRESSED = id + "-cmdPressed";
+
+const target = PrefsTarget({ branchName: ADDON_BRANCH });
+
+// Listen to clicks on buttons
+function buttonClick({ data }) {
+ emit(target, data);
+}
+on(BUTTON_PRESSED, buttonClick);
+
+module.exports = target;
diff --git a/addon-sdk/source/lib/sdk/simple-storage.js b/addon-sdk/source/lib/sdk/simple-storage.js
new file mode 100644
index 000000000..bcf9b1cb9
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/simple-storage.js
@@ -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": "stable"
+};
+
+const { Cc, Ci } = require("chrome");
+const file = require("./io/file");
+const prefs = require("./preferences/service");
+const jpSelf = require("./self");
+const timer = require("./timers");
+const unload = require("./system/unload");
+const { emit, on, off } = require("./event/core");
+
+const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod";
+const WRITE_PERIOD_DEFAULT = 300000; // 5 minutes
+
+const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota";
+const QUOTA_DEFAULT = 5242880; // 5 MiB
+
+const JETPACK_DIR_BASENAME = "jetpack";
+
+Object.defineProperties(exports, {
+ storage: {
+ enumerable: true,
+ get: function() { return manager.root; },
+ set: function(value) { manager.root = value; }
+ },
+ quotaUsage: {
+ get: function() { return manager.quotaUsage; }
+ }
+});
+
+// A generic JSON store backed by a file on disk. This should be isolated
+// enough to move to its own module if need be...
+function JsonStore(options) {
+ this.filename = options.filename;
+ this.quota = options.quota;
+ this.writePeriod = options.writePeriod;
+ this.onOverQuota = options.onOverQuota;
+ this.onWrite = options.onWrite;
+
+ unload.ensure(this);
+
+ this.writeTimer = timer.setInterval(this.write.bind(this),
+ this.writePeriod);
+}
+
+JsonStore.prototype = {
+ // The store's root.
+ get root() {
+ return this.isRootInited ? this._root : {};
+ },
+
+ // Performs some type checking.
+ set root(val) {
+ let types = ["array", "boolean", "null", "number", "object", "string"];
+ if (types.indexOf(typeof(val)) < 0) {
+ throw new Error("storage must be one of the following types: " +
+ types.join(", "));
+ }
+ this._root = val;
+ return val;
+ },
+
+ // True if the root has ever been set (either via the root setter or by the
+ // backing file's having been read).
+ get isRootInited() {
+ return this._root !== undefined;
+ },
+
+ // Percentage of quota used, as a number [0, Inf). > 1 implies over quota.
+ // Undefined if there is no quota.
+ get quotaUsage() {
+ return this.quota > 0 ?
+ JSON.stringify(this.root).length / this.quota :
+ undefined;
+ },
+
+ // Removes the backing file and all empty subdirectories.
+ purge: function JsonStore_purge() {
+ try {
+ // This'll throw if the file doesn't exist.
+ file.remove(this.filename);
+ let parentPath = this.filename;
+ do {
+ parentPath = file.dirname(parentPath);
+ // This'll throw if the dir isn't empty.
+ file.rmdir(parentPath);
+ } while (file.basename(parentPath) !== JETPACK_DIR_BASENAME);
+ }
+ catch (err) {}
+ },
+
+ // Initializes the root by reading the backing file.
+ read: function JsonStore_read() {
+ try {
+ let str = file.read(this.filename);
+
+ // Ideally we'd log the parse error with console.error(), but logged
+ // errors cause tests to fail. Supporting "known" errors in the test
+ // harness appears to be non-trivial. Maybe later.
+ this.root = JSON.parse(str);
+ }
+ catch (err) {
+ this.root = {};
+ }
+ },
+
+ // If the store is under quota, writes the root to the backing file.
+ // Otherwise quota observers are notified and nothing is written.
+ write: function JsonStore_write() {
+ if (this.quotaUsage > 1)
+ this.onOverQuota(this);
+ else
+ this._write();
+ },
+
+ // Cleans up on unload. If unloading because of uninstall, the store is
+ // purged; otherwise it's written.
+ unload: function JsonStore_unload(reason) {
+ timer.clearInterval(this.writeTimer);
+ this.writeTimer = null;
+
+ if (reason === "uninstall")
+ this.purge();
+ else
+ this._write();
+ },
+
+ // True if the root is an empty object.
+ get _isEmpty() {
+ if (this.root && typeof(this.root) === "object") {
+ let empty = true;
+ for (let key in this.root) {
+ empty = false;
+ break;
+ }
+ return empty;
+ }
+ return false;
+ },
+
+ // Writes the root to the backing file, notifying write observers when
+ // complete. If the store is over quota or if it's empty and the store has
+ // never been written, nothing is written and write observers aren't notified.
+ _write: function JsonStore__write() {
+ // Don't write if the root is uninitialized or if the store is empty and the
+ // backing file doesn't yet exist.
+ if (!this.isRootInited || (this._isEmpty && !file.exists(this.filename)))
+ return;
+
+ // If the store is over quota, don't write. The current under-quota state
+ // should persist.
+ if (this.quotaUsage > 1)
+ return;
+
+ // Finally, write.
+ let stream = file.open(this.filename, "w");
+ try {
+ stream.writeAsync(JSON.stringify(this.root), function writeAsync(err) {
+ if (err)
+ console.error("Error writing simple storage file: " + this.filename);
+ else if (this.onWrite)
+ this.onWrite(this);
+ }.bind(this));
+ }
+ catch (err) {
+ // writeAsync closes the stream after it's done, so only close on error.
+ stream.close();
+ }
+ }
+};
+
+
+// This manages a JsonStore singleton and tailors its use to simple storage.
+// The root of the JsonStore is lazy-loaded: The backing file is only read the
+// first time the root's gotten.
+var manager = ({
+ jsonStore: null,
+
+ // The filename of the store, based on the profile dir and extension ID.
+ get filename() {
+ let storeFile = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties).
+ get("ProfD", Ci.nsIFile);
+ storeFile.append(JETPACK_DIR_BASENAME);
+ storeFile.append(jpSelf.id);
+ storeFile.append("simple-storage");
+ file.mkpath(storeFile.path);
+ storeFile.append("store.json");
+ return storeFile.path;
+ },
+
+ get quotaUsage() {
+ return this.jsonStore.quotaUsage;
+ },
+
+ get root() {
+ if (!this.jsonStore.isRootInited)
+ this.jsonStore.read();
+ return this.jsonStore.root;
+ },
+
+ set root(val) {
+ return this.jsonStore.root = val;
+ },
+
+ unload: function manager_unload() {
+ off(this);
+ },
+
+ new: function manager_constructor() {
+ let manager = Object.create(this);
+ unload.ensure(manager);
+
+ manager.jsonStore = new JsonStore({
+ filename: manager.filename,
+ writePeriod: prefs.get(WRITE_PERIOD_PREF, WRITE_PERIOD_DEFAULT),
+ quota: prefs.get(QUOTA_PREF, QUOTA_DEFAULT),
+ onOverQuota: emit.bind(null, exports, "OverQuota")
+ });
+
+ return manager;
+ }
+}).new();
+
+exports.on = on.bind(null, exports);
+exports.removeListener = function(type, listener) {
+ off(exports, type, listener);
+};
diff --git a/addon-sdk/source/lib/sdk/stylesheet/style.js b/addon-sdk/source/lib/sdk/stylesheet/style.js
new file mode 100644
index 000000000..7ec0787e1
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/stylesheet/style.js
@@ -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";
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Cc, Ci } = require("chrome");
+const { Class } = require("../core/heritage");
+const { URL, isLocalURL } = require('../url');
+const events = require("../system/events");
+const { loadSheet, removeSheet, isTypeValid } = require("./utils");
+const { isString } = require("../lang/type");
+const { attachTo, detachFrom } = require("../content/mod");
+const { data } = require('../self');
+
+const { freeze, create } = Object;
+
+function Style({ source, uri, type }) {
+ source = source == null ? null : freeze([].concat(source));
+ uri = uri == null ? null : freeze([].concat(uri));
+ type = type == null ? "author" : type;
+
+ if (source && !source.every(isString))
+ throw new Error('Style.source must be a string or an array of strings.');
+
+ if (uri && !uri.every(isLocalURL))
+ throw new Error('Style.uri must be a local URL or an array of local URLs');
+
+ if (type && !isTypeValid(type))
+ throw new Error('Style.type must be "agent", "user" or "author"');
+
+ return freeze(create(Style.prototype, {
+ "source": { value: source, enumerable: true },
+ "uri": { value: uri, enumerable: true },
+ "type": { value: type, enumerable: true }
+ }));
+};
+
+exports.Style = Style;
+
+attachTo.define(Style, function (style, window) {
+ if (style.uri) {
+ for (let uri of style.uri)
+ loadSheet(window, data.url(uri), style.type);
+ }
+
+ if (style.source) {
+ let uri = "data:text/css;charset=utf-8,";
+
+ uri += encodeURIComponent(style.source.join(""));
+
+ loadSheet(window, uri, style.type);
+ }
+});
+
+detachFrom.define(Style, function (style, window) {
+ if (style.uri)
+ for (let uri of style.uri)
+ removeSheet(window, data.url(uri));
+
+ if (style.source) {
+ let uri = "data:text/css;charset=utf-8,";
+
+ uri += encodeURIComponent(style.source.join(""));
+
+ removeSheet(window, uri, style.type);
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/stylesheet/utils.js b/addon-sdk/source/lib/sdk/stylesheet/utils.js
new file mode 100644
index 000000000..844996bf3
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/stylesheet/utils.js
@@ -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/. */
+"use strict";
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Ci } = require("chrome");
+
+const SHEET_TYPE = {
+ "agent": "AGENT_SHEET",
+ "user": "USER_SHEET",
+ "author": "AUTHOR_SHEET"
+};
+
+function getDOMWindowUtils(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+};
+
+/**
+ * Synchronously loads a style sheet from `uri` and adds it to the list of
+ * additional style sheets of the document.
+ * The sheets added takes effect immediately, and only on the document of the
+ * `window` given.
+ */
+function loadSheet(window, url, type) {
+ if (!(type && type in SHEET_TYPE))
+ type = "author";
+
+ type = SHEET_TYPE[type];
+
+ if (url instanceof Ci.nsIURI)
+ url = url.spec;
+
+ let winUtils = getDOMWindowUtils(window);
+ try {
+ winUtils.loadSheetUsingURIString(url, winUtils[type]);
+ }
+ catch (e) {};
+};
+exports.loadSheet = loadSheet;
+
+/**
+ * Remove the document style sheet at `sheetURI` from the list of additional
+ * style sheets of the document. The removal takes effect immediately.
+ */
+function removeSheet(window, url, type) {
+ if (!(type && type in SHEET_TYPE))
+ type = "author";
+
+ type = SHEET_TYPE[type];
+
+ if (url instanceof Ci.nsIURI)
+ url = url.spec;
+
+ let winUtils = getDOMWindowUtils(window);
+
+ try {
+ winUtils.removeSheetUsingURIString(url, winUtils[type]);
+ }
+ catch (e) {};
+};
+exports.removeSheet = removeSheet;
+
+/**
+ * Returns `true` if the `type` given is valid, otherwise `false`.
+ * The values currently accepted are: "agent", "user" and "author".
+ */
+function isTypeValid(type) {
+ return type in SHEET_TYPE;
+}
+exports.isTypeValid = isTypeValid;
diff --git a/addon-sdk/source/lib/sdk/system.js b/addon-sdk/source/lib/sdk/system.js
new file mode 100644
index 000000000..1acfe8c8c
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system.js
@@ -0,0 +1,172 @@
+/* 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, CC } = require('chrome');
+const options = require('@loader/options');
+const runtime = require("./system/runtime");
+const { when: unload } = require("./system/unload");
+
+const appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
+ getService(Ci.nsIAppStartup);
+const appInfo = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULAppInfo);
+const directoryService = Cc['@mozilla.org/file/directory_service;1'].
+ getService(Ci.nsIProperties);
+
+const PR_WRONLY = parseInt("0x02");
+const PR_CREATE_FILE = parseInt("0x08");
+const PR_APPEND = parseInt("0x10");
+const PR_TRUNCATE = parseInt("0x20");
+
+function openFile(path, mode) {
+ let file = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ file.initWithPath(path);
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ stream.init(file, mode, -1, 0);
+ return stream
+}
+
+const { eAttemptQuit: E_ATTEMPT, eForceQuit: E_FORCE } = appStartup;
+
+/**
+ * Parsed JSON object that was passed via `cfx --static-args "{ foo: 'bar' }"`
+ */
+exports.staticArgs = options.staticArgs;
+
+/**
+ * Environment variables. Environment variables are non-enumerable properties
+ * of this object (key is name and value is value).
+ */
+exports.env = require('./system/environment').env;
+
+/**
+ * Ends the process with the specified `code`. If omitted, exit uses the
+ * 'success' code 0. To exit with failure use `1`.
+ * TODO: Improve platform to actually quit with an exit code.
+ */
+var forcedExit = false;
+exports.exit = function exit(code) {
+ if (forcedExit) {
+ // a forced exit was already tried
+ // NOTE: exit(0) is called twice sometimes (ex when using cfx testaddons)
+ return;
+ }
+
+ let resultsFile = 'resultFile' in options && options.resultFile;
+ function unloader() {
+ if (!options.resultFile) {
+ return;
+ }
+
+ // This is used by 'cfx' to find out exit code.
+ let mode = PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE;
+ let stream = openFile(options.resultFile, mode);
+ let status = code ? 'FAIL' : 'OK';
+ stream.write(status, status.length);
+ stream.flush();
+ stream.close();
+ return;
+ }
+
+ if (code == 0) {
+ forcedExit = true;
+ }
+
+ // Bug 856999: Prevent automatic kill of Firefox when running tests
+ if (options.noQuit) {
+ return unload(unloader);
+ }
+
+ unloader();
+ appStartup.quit(code ? E_ATTEMPT : E_FORCE);
+};
+
+// Adapter for nodejs's stdout & stderr:
+// http://nodejs.org/api/process.html#process_process_stdout
+var stdout = Object.freeze({ write: dump, end: dump });
+exports.stdout = stdout;
+exports.stderr = stdout;
+
+/**
+ * Returns a path of the system's or application's special directory / file
+ * associated with a given `id`. For list of possible `id`s please see:
+ * https://developer.mozilla.org/en-US/docs/Code_snippets/File_I_O#Getting_files_in_special_directories
+ * http://dxr.mozilla.org/mozilla-central/source/xpcom/io/nsAppDirectoryServiceDefs.h
+ * @example
+ *
+ * // get firefox profile path
+ * let profilePath = require('system').pathFor('ProfD');
+ * // get OS temp files directory (/tmp)
+ * let temps = require('system').pathFor('TmpD');
+ * // get OS desktop path for an active user (~/Desktop on linux
+ * // or C:\Documents and Settings\username\Desktop on windows).
+ * let desktopPath = require('system').pathFor('Desk');
+ */
+exports.pathFor = function pathFor(id) {
+ return directoryService.get(id, Ci.nsIFile).path;
+};
+
+/**
+ * What platform you're running on (all lower case string).
+ * For possible values see:
+ * https://developer.mozilla.org/en/OS_TARGET
+ */
+exports.platform = runtime.OS.toLowerCase();
+
+const [, architecture, compiler] = runtime.XPCOMABI ?
+ runtime.XPCOMABI.match(/^([^-]*)-(.*)$/) :
+ [, null, null];
+
+/**
+ * What processor architecture you're running on:
+ * `'arm', 'ia32', or 'x64'`.
+ */
+exports.architecture = architecture;
+
+/**
+ * What compiler used for build:
+ * `'msvc', 'n32', 'gcc2', 'gcc3', 'sunc', 'ibmc'...`
+ */
+exports.compiler = compiler;
+
+/**
+ * The application's build ID/date, for example "2004051604".
+ */
+exports.build = appInfo.appBuildID;
+
+/**
+ * The XUL application's UUID.
+ * This has traditionally been in the form
+ * `{AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}` but for some applications it may
+ * be: "appname@vendor.tld".
+ */
+exports.id = appInfo.ID;
+
+/**
+ * The name of the application.
+ */
+exports.name = appInfo.name;
+
+/**
+ * The XUL application's version, for example "0.8.0+" or "3.7a1pre".
+ */
+exports.version = appInfo.version;
+
+/**
+ * XULRunner version.
+ */
+exports.platformVersion = appInfo.platformVersion;
+
+
+/**
+ * The name of the application vendor, for example "Mozilla".
+ */
+exports.vendor = appInfo.vendor;
diff --git a/addon-sdk/source/lib/sdk/system/child_process.js b/addon-sdk/source/lib/sdk/system/child_process.js
new file mode 100644
index 000000000..8ea1f4f80
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/child_process.js
@@ -0,0 +1,332 @@
+/* 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 { Ci } = require('chrome');
+var subprocess = require('./child_process/subprocess');
+var { EventTarget } = require('../event/target');
+var { Stream } = require('../io/stream');
+var { on, emit, off } = require('../event/core');
+var { Class } = require('../core/heritage');
+var { platform } = require('../system');
+var { isFunction, isArray } = require('../lang/type');
+var { delay } = require('../lang/functional');
+var { merge } = require('../util/object');
+var { setTimeout, clearTimeout } = require('../timers');
+var isWindows = platform.indexOf('win') === 0;
+
+var processes = new WeakMap();
+
+
+/**
+ * The `Child` class wraps a subprocess command, exposes
+ * the stdio streams, and methods to manipulate the subprocess
+ */
+var Child = Class({
+ implements: [EventTarget],
+ initialize: function initialize (options) {
+ let child = this;
+ let proc;
+
+ this.killed = false;
+ this.exitCode = undefined;
+ this.signalCode = undefined;
+
+ this.stdin = Stream();
+ this.stdout = Stream();
+ this.stderr = Stream();
+
+ try {
+ proc = subprocess.call({
+ command: options.file,
+ arguments: options.cmdArgs,
+ environment: serializeEnv(options.env),
+ workdir: options.cwd,
+ charset: options.encoding,
+ stdout: data => emit(child.stdout, 'data', data),
+ stderr: data => emit(child.stderr, 'data', data),
+ stdin: stream => {
+ child.stdin.on('data', pumpStdin);
+ child.stdin.on('end', function closeStdin () {
+ child.stdin.off('data', pumpStdin);
+ child.stdin.off('end', closeStdin);
+ stream.close();
+ });
+ function pumpStdin (data) {
+ stream.write(data);
+ }
+ },
+ done: function (result, error) {
+ if (error)
+ return handleError(error);
+
+ // Only emit if child is not killed; otherwise,
+ // the `kill` method will handle this
+ if (!child.killed) {
+ child.exitCode = result.exitCode;
+ child.signalCode = null;
+
+ // If process exits with < 0, there was an error
+ if (child.exitCode < 0) {
+ handleError(new Error('Process exited with exit code ' + child.exitCode));
+ }
+ else {
+ // Also do 'exit' event as there's not much of
+ // a difference in our implementation as we're not using
+ // node streams
+ emit(child, 'exit', child.exitCode, child.signalCode);
+ }
+
+ // Emit 'close' event with exit code and signal,
+ // which is `null`, as it was not a killed process
+ emit(child, 'close', child.exitCode, child.signalCode);
+ }
+ }
+ });
+ processes.set(child, proc);
+ } catch (e) {
+ // Delay the error handling so an error handler can be set
+ // during the same tick that the Child was created
+ delay(() => handleError(e));
+ }
+
+ // `handleError` is called when process could not even
+ // be spawned
+ function handleError (e) {
+ // If error is an nsIObject, make a fresh error object
+ // so we're not exposing nsIObjects, and we can modify it
+ // with additional process information, like node
+ let error = e;
+ if (e instanceof Ci.nsISupports) {
+ error = new Error(e.message, e.filename, e.lineNumber);
+ }
+ emit(child, 'error', error);
+ child.exitCode = -1;
+ child.signalCode = null;
+ emit(child, 'close', child.exitCode, child.signalCode);
+ }
+ },
+ kill: function kill (signal) {
+ let proc = processes.get(this);
+ proc.kill(signal);
+ this.killed = true;
+ this.exitCode = null;
+ this.signalCode = signal;
+ emit(this, 'exit', this.exitCode, this.signalCode);
+ emit(this, 'close', this.exitCode, this.signalCode);
+ },
+ get pid() { return processes.get(this, {}).pid || -1; }
+});
+
+function spawn (file, ...args) {
+ let cmdArgs = [];
+ // Default options
+ let options = {
+ cwd: null,
+ env: null,
+ encoding: 'UTF-8'
+ };
+
+ if (args[1]) {
+ merge(options, args[1]);
+ cmdArgs = args[0];
+ }
+ else {
+ if (isArray(args[0]))
+ cmdArgs = args[0];
+ else
+ merge(options, args[0]);
+ }
+
+ if ('gid' in options)
+ console.warn('`gid` option is not yet supported for `child_process`');
+ if ('uid' in options)
+ console.warn('`uid` option is not yet supported for `child_process`');
+ if ('detached' in options)
+ console.warn('`detached` option is not yet supported for `child_process`');
+
+ options.file = file;
+ options.cmdArgs = cmdArgs;
+
+ return Child(options);
+}
+
+exports.spawn = spawn;
+
+/**
+ * exec(command, options, callback)
+ */
+function exec (cmd, ...args) {
+ let file, cmdArgs, callback, options = {};
+
+ if (isFunction(args[0]))
+ callback = args[0];
+ else {
+ merge(options, args[0]);
+ callback = args[1];
+ }
+
+ if (isWindows) {
+ file = 'C:\\Windows\\System32\\cmd.exe';
+ cmdArgs = ['/S/C', cmd || ''];
+ }
+ else {
+ file = '/bin/sh';
+ cmdArgs = ['-c', cmd];
+ }
+
+ // Undocumented option from node being able to specify shell
+ if (options && options.shell)
+ file = options.shell;
+
+ return execFile(file, cmdArgs, options, callback);
+}
+exports.exec = exec;
+/**
+ * execFile (file, args, options, callback)
+ */
+function execFile (file, ...args) {
+ let cmdArgs = [], callback;
+ // Default options
+ let options = {
+ cwd: null,
+ env: null,
+ encoding: 'utf8',
+ timeout: 0,
+ maxBuffer: 204800, //200 KB (200*1024 bytes)
+ killSignal: 'SIGTERM'
+ };
+
+ if (isFunction(args[args.length - 1]))
+ callback = args[args.length - 1];
+
+ if (isArray(args[0])) {
+ cmdArgs = args[0];
+ merge(options, args[1]);
+ } else if (!isFunction(args[0]))
+ merge(options, args[0]);
+
+ let child = spawn(file, cmdArgs, options);
+ let exited = false;
+ let stdout = '';
+ let stderr = '';
+ let error = null;
+ let timeoutId = null;
+
+ child.stdout.setEncoding(options.encoding);
+ child.stderr.setEncoding(options.encoding);
+
+ on(child.stdout, 'data', pumpStdout);
+ on(child.stderr, 'data', pumpStderr);
+ on(child, 'close', exitHandler);
+ on(child, 'error', errorHandler);
+
+ if (options.timeout > 0) {
+ setTimeout(() => {
+ kill();
+ timeoutId = null;
+ }, options.timeout);
+ }
+
+ function exitHandler (code, signal) {
+
+ // Return if exitHandler called previously, occurs
+ // when multiple maxBuffer errors thrown and attempt to kill multiple
+ // times
+ if (exited) return;
+ exited = true;
+
+ if (!isFunction(callback)) return;
+
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ timeoutId = null;
+ }
+
+ if (!error && (code !== 0 || signal !== null))
+ error = createProcessError(new Error('Command failed: ' + stderr), {
+ code: code,
+ signal: signal,
+ killed: !!child.killed
+ });
+
+ callback(error, stdout, stderr);
+
+ off(child.stdout, 'data', pumpStdout);
+ off(child.stderr, 'data', pumpStderr);
+ off(child, 'close', exitHandler);
+ off(child, 'error', errorHandler);
+ }
+
+ function errorHandler (e) {
+ error = e;
+ exitHandler();
+ }
+
+ function kill () {
+ try {
+ child.kill(options.killSignal);
+ } catch (e) {
+ // In the scenario where the kill signal happens when
+ // the process is already closing, just abort the kill fail
+ if (/library is not open/.test(e))
+ return;
+ error = e;
+ exitHandler(-1, options.killSignal);
+ }
+ }
+
+ function pumpStdout (data) {
+ stdout += data;
+ if (stdout.length > options.maxBuffer) {
+ error = new Error('stdout maxBuffer exceeded');
+ kill();
+ }
+ }
+
+ function pumpStderr (data) {
+ stderr += data;
+ if (stderr.length > options.maxBuffer) {
+ error = new Error('stderr maxBuffer exceeded');
+ kill();
+ }
+ }
+
+ return child;
+}
+exports.execFile = execFile;
+
+exports.fork = function fork () {
+ throw new Error("child_process#fork is not currently supported");
+};
+
+function serializeEnv (obj) {
+ return Object.keys(obj || {}).map(prop => prop + '=' + obj[prop]);
+}
+
+function createProcessError (err, options = {}) {
+ // If code and signal look OK, this was probably a failure
+ // attempting to spawn the process (like ENOENT in node) -- use
+ // the code from the error message
+ if (!options.code && !options.signal) {
+ let match = err.message.match(/(NS_ERROR_\w*)/);
+ if (match && match.length > 1)
+ err.code = match[1];
+ else {
+ // If no good error message found, use the passed in exit code;
+ // this occurs when killing a process that's already closing,
+ // where we want both a valid exit code (0) and the error
+ err.code = options.code != null ? options.code : null;
+ }
+ }
+ else
+ err.code = options.code != null ? options.code : null;
+ err.signal = options.signal || null;
+ err.killed = options.killed || false;
+ return err;
+}
diff --git a/addon-sdk/source/lib/sdk/system/child_process/subprocess.js b/addon-sdk/source/lib/sdk/system/child_process/subprocess.js
new file mode 100644
index 000000000..e3454e95b
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/child_process/subprocess.js
@@ -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';
+
+const { Ci, Cu } = require("chrome");
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Subprocess.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+const Runtime = require("sdk/system/runtime");
+const Environment = require("sdk/system/environment").env;
+const DEFAULT_ENVIRONMENT = [];
+if (Runtime.OS == "Linux" && "DISPLAY" in Environment) {
+ DEFAULT_ENVIRONMENT.push("DISPLAY=" + Environment.DISPLAY);
+}
+
+function awaitPromise(promise) {
+ let value;
+ let resolved = null;
+ promise.then(val => {
+ resolved = true;
+ value = val;
+ }, val => {
+ resolved = false;
+ value = val;
+ });
+
+ while (resolved === null)
+ Services.tm.mainThread.processNextEvent(true);
+
+ if (resolved === true)
+ return value;
+ throw value;
+}
+
+let readAllData = Task.async(function* (pipe, read, callback) {
+ let string;
+ while (string = yield read(pipe))
+ callback(string);
+});
+
+let write = (pipe, data) => {
+ let buffer = new Uint8Array(Array.from(data, c => c.charCodeAt(0)));
+ return pipe.write(data);
+};
+
+var subprocess = {
+ call: function(options) {
+ var result;
+
+ let procPromise = Task.spawn(function*() {
+ let opts = {};
+
+ if (options.mergeStderr) {
+ opts.stderr = "stdout"
+ } else if (options.stderr) {
+ opts.stderr = "pipe";
+ }
+
+ if (options.command instanceof Ci.nsIFile) {
+ opts.command = options.command.path;
+ } else {
+ opts.command = yield Subprocess.pathSearch(options.command);
+ }
+
+ if (options.workdir) {
+ opts.workdir = options.workdir;
+ }
+
+ opts.arguments = options.arguments || [];
+
+
+ // Set up environment
+
+ let envVars = options.environment || DEFAULT_ENVIRONMENT;
+ if (envVars.length) {
+ let environment = {};
+ for (let val of envVars) {
+ let idx = val.indexOf("=");
+ if (idx >= 0)
+ environment[val.slice(0, idx)] = val.slice(idx + 1);
+ }
+
+ opts.environment = environment;
+ }
+
+
+ let proc = yield Subprocess.call(opts);
+
+ Object.defineProperty(result, "pid", {
+ value: proc.pid,
+ enumerable: true,
+ configurable: true,
+ });
+
+
+ let promises = [];
+
+ // Set up IO handlers.
+
+ let read = pipe => pipe.readString();
+ if (options.charset === null) {
+ read = pipe => {
+ return pipe.read().then(buffer => {
+ return String.fromCharCode(...buffer);
+ });
+ };
+ }
+
+ if (options.stdout)
+ promises.push(readAllData(proc.stdout, read, options.stdout));
+
+ if (options.stderr && proc.stderr)
+ promises.push(readAllData(proc.stderr, read, options.stderr));
+
+ // Process stdin
+
+ if (typeof options.stdin === "string") {
+ write(proc.stdin, options.stdin);
+ proc.stdin.close();
+ }
+
+
+ // Handle process completion
+
+ if (options.done)
+ Promise.all(promises)
+ .then(() => proc.wait())
+ .then(options.done);
+
+ return proc;
+ });
+
+ procPromise.catch(e => {
+ if (options.done)
+ options.done({exitCode: -1}, e);
+ else
+ Cu.reportError(e instanceof Error ? e : e.message || e);
+ });
+
+ if (typeof options.stdin === "function") {
+ // Unfortunately, some callers (child_process.js) depend on this
+ // being called synchronously.
+ options.stdin({
+ write(val) {
+ procPromise.then(proc => {
+ write(proc.stdin, val);
+ });
+ },
+
+ close() {
+ procPromise.then(proc => {
+ proc.stdin.close();
+ });
+ },
+ });
+ }
+
+ result = {
+ get pid() {
+ return awaitPromise(procPromise.then(proc => {
+ return proc.pid;
+ }));
+ },
+
+ wait() {
+ return awaitPromise(procPromise.then(proc => {
+ return proc.wait().then(({exitCode}) => exitCode);
+ }));
+ },
+
+ kill(hard = false) {
+ procPromise.then(proc => {
+ proc.kill(hard ? 0 : undefined);
+ });
+ },
+ };
+
+ return result;
+ },
+};
+
+module.exports = subprocess;
diff --git a/addon-sdk/source/lib/sdk/system/environment.js b/addon-sdk/source/lib/sdk/system/environment.js
new file mode 100644
index 000000000..13621a696
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/environment.js
@@ -0,0 +1,33 @@
+/* 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 } = require('chrome');
+const { get, set, exists } = Cc['@mozilla.org/process/environment;1'].
+ getService(Ci.nsIEnvironment);
+
+exports.env = new Proxy({}, {
+ deleteProperty(target, property) {
+ set(property, null);
+ return true;
+ },
+
+ get(target, property, receiver) {
+ return get(property) || undefined;
+ },
+
+ has(target, property) {
+ return exists(property);
+ },
+
+ set(target, property, value, receiver) {
+ set(property, value);
+ return true;
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/system/events-shimmed.js b/addon-sdk/source/lib/sdk/system/events-shimmed.js
new file mode 100644
index 000000000..14496f1f0
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/events-shimmed.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+module.metadata = {
+ 'stability': 'unstable'
+};
+
+const events = require('./events.js');
+
+exports.emit = (type, event) => events.emit(type, event, true);
+exports.on = (type, listener, strong) => events.on(type, listener, strong, true);
+exports.once = (type, listener) => events.once(type, listener, true);
+exports.off = (type, listener) => events.off(type, listener, true);
diff --git a/addon-sdk/source/lib/sdk/system/events.js b/addon-sdk/source/lib/sdk/system/events.js
new file mode 100644
index 000000000..0cf525aa1
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/events.js
@@ -0,0 +1,181 @@
+/* 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 { Unknown } = require('../platform/xpcom');
+const { Class } = require('../core/heritage');
+const { ns } = require('../core/namespace');
+const observerService =
+ Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
+const { addObserver, removeObserver, notifyObservers } = observerService;
+const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
+const addObserverNoShim = ShimWaiver.getProperty(observerService, "addObserver");
+const removeObserverNoShim = ShimWaiver.getProperty(observerService, "removeObserver");
+const notifyObserversNoShim = ShimWaiver.getProperty(observerService, "notifyObservers");
+const unloadSubject = require('@loader/unload');
+
+const Subject = Class({
+ extends: Unknown,
+ initialize: function initialize(object) {
+ // Double-wrap the object and set a property identifying the
+ // wrappedJSObject as one of our wrappers to distinguish between
+ // subjects that are one of our wrappers (which we should unwrap
+ // when notifying our observers) and those that are real JS XPCOM
+ // components (which we should pass through unaltered).
+ this.wrappedJSObject = {
+ observersModuleSubjectWrapper: true,
+ object: object
+ };
+ },
+ getScriptableHelper: function() {},
+ getInterfaces: function() {}
+});
+
+function emit(type, event, shimmed = false) {
+ // From bug 910599
+ // We must test to see if 'subject' or 'data' is a defined property
+ // of the event object, but also allow primitives to be passed in,
+ // which the `in` operator breaks, yet `null` is an object, hence
+ // the long conditional
+ let subject = event && typeof event === 'object' && 'subject' in event ?
+ Subject(event.subject) :
+ null;
+ let data = event && typeof event === 'object' ?
+ // An object either returns its `data` property or null
+ ('data' in event ? event.data : null) :
+ // All other types return themselves (and cast to strings/null
+ // via observer service)
+ event;
+ if (shimmed) {
+ notifyObservers(subject, type, data);
+ } else {
+ notifyObserversNoShim(subject, type, data);
+ }
+}
+exports.emit = emit;
+
+const Observer = Class({
+ extends: Unknown,
+ initialize: function initialize(listener) {
+ this.listener = listener;
+ },
+ interfaces: [ 'nsIObserver', 'nsISupportsWeakReference' ],
+ observe: function(subject, topic, data) {
+ // Extract the wrapped object for subjects that are one of our
+ // wrappers around a JS object. This way we support both wrapped
+ // subjects created using this module and those that are real
+ // XPCOM components.
+ if (subject && typeof(subject) == 'object' &&
+ ('wrappedJSObject' in subject) &&
+ ('observersModuleSubjectWrapper' in subject.wrappedJSObject))
+ subject = subject.wrappedJSObject.object;
+
+ try {
+ this.listener({
+ type: topic,
+ subject: subject,
+ data: data
+ });
+ }
+ catch (error) {
+ console.exception(error);
+ }
+ }
+});
+
+const subscribers = ns();
+
+function on(type, listener, strong, shimmed = false) {
+ // Unless last optional argument is `true` we use a weak reference to a
+ // listener.
+ let weak = !strong;
+ // Take list of observers associated with given `listener` function.
+ let observers = subscribers(listener);
+ // If `observer` for the given `type` is not registered yet, then
+ // associate an `observer` and register it.
+ if (!(type in observers)) {
+ let observer = Observer(listener);
+ observers[type] = observer;
+ if (shimmed) {
+ addObserver(observer, type, weak);
+ } else {
+ addObserverNoShim(observer, type, weak);
+ }
+ // WeakRef gymnastics to remove all alive observers on unload
+ let ref = Cu.getWeakReference(observer);
+ weakRefs.set(observer, ref);
+ stillAlive.set(ref, type);
+ wasShimmed.set(ref, shimmed);
+ }
+}
+exports.on = on;
+
+function once(type, listener, shimmed = false) {
+ // Note: this code assumes order in which listeners are called, which is fine
+ // as long as dispatch happens in same order as listener registration which
+ // is the case now. That being said we should be aware that this may break
+ // in a future if order will change.
+ on(type, listener, shimmed);
+ on(type, function cleanup() {
+ off(type, listener, shimmed);
+ off(type, cleanup, shimmed);
+ }, true, shimmed);
+}
+exports.once = once;
+
+function off(type, listener, shimmed = false) {
+ // Take list of observers as with the given `listener`.
+ let observers = subscribers(listener);
+ // If `observer` for the given `type` is registered, then
+ // remove it & unregister.
+ if (type in observers) {
+ let observer = observers[type];
+ delete observers[type];
+ if (shimmed) {
+ removeObserver(observer, type);
+ } else {
+ removeObserverNoShim(observer, type);
+ }
+ stillAlive.delete(weakRefs.get(observer));
+ wasShimmed.delete(weakRefs.get(observer));
+ }
+}
+exports.off = off;
+
+// must use WeakMap to keep reference to all the WeakRefs (!), see bug 986115
+var weakRefs = new WeakMap();
+
+// and we're out of beta, we're releasing on time!
+var stillAlive = new Map();
+
+var wasShimmed = new Map();
+
+on('sdk:loader:destroy', function onunload({ subject, data: reason }) {
+ // using logic from ./unload, to avoid a circular module reference
+ if (subject.wrappedJSObject === unloadSubject) {
+ off('sdk:loader:destroy', onunload, false);
+
+ // don't bother
+ if (reason === 'shutdown')
+ return;
+
+ stillAlive.forEach( (type, ref) => {
+ let observer = ref.get();
+ if (observer) {
+ if (wasShimmed.get(ref)) {
+ removeObserver(observer, type);
+ } else {
+ removeObserverNoShim(observer, type);
+ }
+ }
+ })
+ }
+ // a strong reference
+}, true, false);
diff --git a/addon-sdk/source/lib/sdk/system/globals.js b/addon-sdk/source/lib/sdk/system/globals.js
new file mode 100644
index 000000000..a1a6cf9a2
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/globals.js
@@ -0,0 +1,46 @@
+/* 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 { Cc, Ci, CC } = require('chrome');
+var { PlainTextConsole } = require('../console/plain-text');
+var { stdout } = require('../system');
+var ScriptError = CC('@mozilla.org/scripterror;1', 'nsIScriptError');
+var consoleService = Cc['@mozilla.org/consoleservice;1'].getService(Ci.nsIConsoleService);
+
+// On windows dump does not writes into stdout so cfx can't read thous dumps.
+// To workaround this issue we write to a special file from which cfx will
+// read and print to the console.
+// For more details see: bug-673383
+exports.dump = stdout.write;
+
+exports.console = new PlainTextConsole();
+
+// Provide CommonJS `define` to allow authoring modules in a format that can be
+// loaded both into jetpack and into browser via AMD loaders.
+Object.defineProperty(exports, 'define', {
+ // `define` is provided as a lazy getter that binds below defined `define`
+ // function to the module scope, so that require, exports and module
+ // variables remain accessible.
+ configurable: true,
+ get: function() {
+ let sandbox = this;
+ return function define(factory) {
+ factory = Array.slice(arguments).pop();
+ factory.call(sandbox, sandbox.require, sandbox.exports, sandbox.module);
+ }
+ },
+ set: function(value) {
+ Object.defineProperty(this, 'define', {
+ configurable: true,
+ enumerable: true,
+ value,
+ });
+ },
+});
diff --git a/addon-sdk/source/lib/sdk/system/process.js b/addon-sdk/source/lib/sdk/system/process.js
new file mode 100644
index 000000000..f44a36658
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/process.js
@@ -0,0 +1,62 @@
+/* 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 {
+ exit, version, stdout, stderr, platform, architecture
+} = require("../system");
+
+/**
+ * Supported
+ */
+
+exports.stdout = stdout;
+exports.stderr = stderr;
+exports.version = version;
+exports.versions = {};
+exports.config = {};
+exports.arch = architecture;
+exports.platform = platform;
+exports.exit = exit;
+
+/**
+ * Partial support
+ */
+
+// An alias to `setTimeout(fn, 0)`, which isn't the same as node's `nextTick`,
+// but atleast ensures it'll occur asynchronously
+exports.nextTick = (callback) => setTimeout(callback, 0);
+
+/**
+ * Unsupported
+ */
+
+exports.maxTickDepth = 1000;
+exports.pid = 0;
+exports.title = "";
+exports.stdin = {};
+exports.argv = [];
+exports.execPath = "";
+exports.execArgv = [];
+exports.abort = function () {};
+exports.chdir = function () {};
+exports.cwd = function () {};
+exports.env = {};
+exports.getgid = function () {};
+exports.setgid = function () {};
+exports.getuid = function () {};
+exports.setuid = function () {};
+exports.getgroups = function () {};
+exports.setgroups = function () {};
+exports.initgroups = function () {};
+exports.kill = function () {};
+exports.memoryUsage = function () {};
+exports.umask = function () {};
+exports.uptime = function () {};
+exports.hrtime = function () {};
diff --git a/addon-sdk/source/lib/sdk/system/runtime.js b/addon-sdk/source/lib/sdk/system/runtime.js
new file mode 100644
index 000000000..9a70f142d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/runtime.js
@@ -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";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { Cc, Ci } = require("chrome");
+const runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
+
+exports.inSafeMode = runtime.inSafeMode;
+exports.OS = runtime.OS;
+exports.processType = runtime.processType;
+exports.widgetToolkit = runtime.widgetToolkit;
+exports.processID = runtime.processID;
+
+// Attempt to access `XPCOMABI` may throw exception, in which case exported
+// `XPCOMABI` will be set to `null`.
+// https://mxr.mozilla.org/mozilla-central/source/toolkit/xre/nsAppRunner.cpp#732
+try {
+ exports.XPCOMABI = runtime.XPCOMABI;
+}
+catch (error) {
+ exports.XPCOMABI = null;
+}
diff --git a/addon-sdk/source/lib/sdk/system/unload.js b/addon-sdk/source/lib/sdk/system/unload.js
new file mode 100644
index 000000000..98ab5f8f3
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/unload.js
@@ -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/. */
+
+// Parts of this module were taken from narwhal:
+//
+// http://narwhaljs.org
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Cu } = require('chrome');
+const { on, off } = require('./events');
+const unloadSubject = require('@loader/unload');
+
+const observers = [];
+const unloaders = [];
+
+function WeakObserver(inner) {
+ this._inner = Cu.getWeakReference(inner);
+}
+
+Object.defineProperty(WeakObserver.prototype, 'value', {
+ get: function() { this._inner.get() }
+});
+
+var when = exports.when = function when(observer, opts) {
+ opts = opts || {};
+ for (var i = 0; i < observers.length; ++i) {
+ if (observers[i] === observer || observers[i].value === observer) {
+ return;
+ }
+ }
+ if (opts.weak) {
+ observers.unshift(new WeakObserver(observer));
+ } else {
+ observers.unshift(observer);
+ }
+};
+
+var ensure = exports.ensure = function ensure(obj, destructorName) {
+ if (!destructorName)
+ destructorName = "unload";
+ if (!(destructorName in obj))
+ throw new Error("object has no '" + destructorName + "' property");
+
+ let called = false;
+ let originalDestructor = obj[destructorName];
+
+ function unloadWrapper(reason) {
+ if (!called) {
+ called = true;
+ let index = unloaders.indexOf(unloadWrapper);
+ if (index == -1)
+ throw new Error("internal error: unloader not found");
+ unloaders.splice(index, 1);
+ originalDestructor.call(obj, reason);
+ originalDestructor = null;
+ destructorName = null;
+ obj = null;
+ }
+ };
+
+ // TODO: Find out why the order is inverted here. It seems that
+ // it may be causing issues!
+ unloaders.push(unloadWrapper);
+
+ obj[destructorName] = unloadWrapper;
+};
+
+function unload(reason) {
+ observers.forEach(function(observer) {
+ try {
+ if (observer instanceof WeakObserver) {
+ observer = observer.value;
+ }
+ if (typeof observer === 'function') {
+ observer(reason);
+ }
+ }
+ catch (error) {
+ console.exception(error);
+ }
+ });
+}
+
+when(function(reason) {
+ unloaders.slice().forEach(function(unloadWrapper) {
+ unloadWrapper(reason);
+ });
+});
+
+on('sdk:loader:destroy', function onunload({ subject, data: reason }) {
+ // If this loader is unload then `subject.wrappedJSObject` will be
+ // `destructor`.
+ if (subject.wrappedJSObject === unloadSubject) {
+ off('sdk:loader:destroy', onunload);
+ unload(reason);
+ }
+// Note that we use strong reference to listener here to make sure it's not
+// GC-ed, which may happen otherwise since nothing keeps reference to `onunolad`
+// function.
+}, true);
diff --git a/addon-sdk/source/lib/sdk/system/xul-app.js b/addon-sdk/source/lib/sdk/system/xul-app.js
new file mode 100644
index 000000000..612386f77
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/xul-app.js
@@ -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 { XulApp } = require("./xul-app.jsm");
+
+Object.keys(XulApp).forEach(k => exports[k] = XulApp[k]);
diff --git a/addon-sdk/source/lib/sdk/system/xul-app.jsm b/addon-sdk/source/lib/sdk/system/xul-app.jsm
new file mode 100644
index 000000000..90681bb1b
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/xul-app.jsm
@@ -0,0 +1,242 @@
+/* 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.EXPORTED_SYMBOLS = [ "XulApp" ];
+
+var { classes: Cc, interfaces: Ci } = Components;
+
+var exports = {};
+this.XulApp = exports;
+
+var appInfo;
+
+// NOTE: below is required to avoid failing xpcshell tests,
+// which do not implement nsIXULAppInfo
+// See Bug 1114752 https://bugzilla.mozilla.org/show_bug.cgi?id=1114752
+try {
+ appInfo = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULAppInfo);
+}
+catch (e) {
+ // xpcshell test case
+ appInfo = {};
+}
+var vc = Cc["@mozilla.org/xpcom/version-comparator;1"]
+ .getService(Ci.nsIVersionComparator);
+
+var ID = exports.ID = appInfo.ID;
+var name = exports.name = appInfo.name;
+var version = exports.version = appInfo.version;
+var platformVersion = exports.platformVersion = appInfo.platformVersion;
+
+// The following mapping of application names to GUIDs was taken from:
+//
+// https://addons.mozilla.org/en-US/firefox/pages/appversions
+//
+// Using the GUID instead of the app's name is preferable because sometimes
+// re-branded versions of a product have different names: for instance,
+// Firefox, Minefield, Iceweasel, and Shiretoko all have the same
+// GUID.
+
+var ids = exports.ids = {
+ Firefox: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+ Mozilla: "{86c18b42-e466-45a9-ae7a-9b95ba6f5640}",
+ SeaMonkey: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}",
+ Fennec: "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ Thunderbird: "{3550f703-e582-4d05-9a08-453d09bdfdc6}",
+ Instantbird: "{33cb9019-c295-46dd-be21-8c4936574bee}"
+};
+
+function is(name) {
+ if (!(name in ids))
+ throw new Error("Unkown Mozilla Application: " + name);
+ return ID == ids[name];
+};
+exports.is = is;
+
+function isOneOf(names) {
+ for (var i = 0; i < names.length; i++)
+ if (is(names[i]))
+ return true;
+ return false;
+};
+exports.isOneOf = isOneOf;
+
+/**
+ * Use this to check whether the given version (e.g. xulApp.platformVersion)
+ * is in the given range. Versions must be in version comparator-compatible
+ * format. See MDC for details:
+ * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIVersionComparator
+ */
+var versionInRange = exports.versionInRange =
+function versionInRange(version, lowInclusive, highExclusive) {
+ return (vc.compare(version, lowInclusive) >= 0) &&
+ (vc.compare(version, highExclusive) < 0);
+}
+
+const reVersionRange = /^((?:<|>)?=?)?\s*((?:\d+[\S]*)|\*)(?:\s+((?:<|>)=?)?(\d+[\S]+))?$/;
+const reOnlyInifinity = /^[<>]?=?\s*[*x]$/;
+const reSubInfinity = /\.[*x]/g;
+const reHyphenRange = /^(\d+.*?)\s*-\s*(\d+.*?)$/;
+const reRangeSeparator = /\s*\|\|\s*/;
+
+const compares = {
+ "=": function (c) { return c === 0 },
+ ">=": function (c) { return c >= 0 },
+ "<=": function (c) { return c <= 0},
+ "<": function (c) { return c < 0 },
+ ">": function (c) { return c > 0 }
+}
+
+function normalizeRange(range) {
+ return range
+ .replace(reOnlyInifinity, "")
+ .replace(reSubInfinity, ".*")
+ .replace(reHyphenRange, ">=$1 <=$2")
+}
+
+/**
+ * Compare the versions given, using the comparison operator provided.
+ * Internal use only.
+ *
+ * @example
+ * compareVersion("1.2", "<=", "1.*") // true
+ *
+ * @param {String} version
+ * A version to compare
+ *
+ * @param {String} comparison
+ * The comparison operator
+ *
+ * @param {String} compareVersion
+ * A version to compare
+ */
+function compareVersion(version, comparison, compareVersion) {
+ let hasWildcard = compareVersion.indexOf("*") !== -1;
+
+ comparison = comparison || "=";
+
+ if (hasWildcard) {
+ switch (comparison) {
+ case "=":
+ let zeroVersion = compareVersion.replace(reSubInfinity, ".0");
+ return versionInRange(version, zeroVersion, compareVersion);
+ case ">=":
+ compareVersion = compareVersion.replace(reSubInfinity, ".0");
+ break;
+ }
+ }
+
+ let compare = compares[comparison];
+
+ return typeof compare === "function" && compare(vc.compare(version, compareVersion));
+}
+
+/**
+ * Returns `true` if `version` satisfies the `versionRange` given.
+ * If only an argument is passed, is used as `versionRange` and compared against
+ * `xulApp.platformVersion`.
+ *
+ * `versionRange` is either a string which has one or more space-separated
+ * descriptors, or a range like "fromVersion - toVersion".
+ * Version range descriptors may be any of the following styles:
+ *
+ * - "version" Must match `version` exactly
+ * - "=version" Same as just `version`
+ * - ">version" Must be greater than `version`
+ * - ">=version" Must be greater or equal than `version`
+ * - "<version" Must be less than `version`
+ * - "<=version" Must be less or equal than `version`
+ * - "1.2.x" or "1.2.*" See 'X version ranges' below
+ * - "*" or "" (just an empty string) Matches any version
+ * - "version1 - version2" Same as ">=version1 <=version2"
+ * - "range1 || range2" Passes if either `range1` or `range2` are satisfied
+ *
+ * For example, these are all valid:
+ * - "1.0.0 - 2.9999.9999"
+ * - ">=1.0.2 <2.1.2"
+ * - ">1.0.2 <=2.3.4"
+ * - "2.0.1"
+ * - "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0"
+ * - "2.x" (equivalent to "2.*")
+ * - "1.2.x" (equivalent to "1.2.*" and ">=1.2.0 <1.3.0")
+ */
+function satisfiesVersion(version, versionRange) {
+ if (arguments.length === 1) {
+ versionRange = version;
+ version = appInfo.version;
+ }
+
+ let ranges = versionRange.trim().split(reRangeSeparator);
+
+ return ranges.some(function(range) {
+ range = normalizeRange(range);
+
+ // No versions' range specified means that any version satisfies the
+ // requirements.
+ if (range === "")
+ return true;
+
+ let matches = range.match(reVersionRange);
+
+ if (!matches)
+ return false;
+
+ let [, lowMod, lowVer, highMod, highVer] = matches;
+
+ return compareVersion(version, lowMod, lowVer) && (highVer !== undefined
+ ? compareVersion(version, highMod, highVer)
+ : true);
+ });
+}
+exports.satisfiesVersion = satisfiesVersion;
+
+/**
+ * Ensure the current application satisfied the requirements specified in the
+ * module given. If not, an exception related to the incompatibility is
+ * returned; `null` otherwise.
+ *
+ * @param {Object} module
+ * The module to check
+ * @returns {Error}
+ */
+function incompatibility(module) {
+ let { metadata, id } = module;
+
+ // if metadata or engines are not specified we assume compatibility is not
+ // an issue.
+ if (!metadata || !("engines" in metadata))
+ return null;
+
+ let { engines } = metadata;
+
+ if (engines === null || typeof(engines) !== "object")
+ return new Error("Malformed engines' property in metadata");
+
+ let applications = Object.keys(engines);
+
+ let versionRange;
+ applications.forEach(function(name) {
+ if (is(name)) {
+ versionRange = engines[name];
+ // Continue iteration. We want to ensure the module doesn't
+ // contain a typo in the applications' name or some unknown
+ // application - `is` function throws an exception in that case.
+ }
+ });
+
+ if (typeof(versionRange) === "string") {
+ if (satisfiesVersion(versionRange))
+ return null;
+
+ return new Error("Unsupported Application version: The module " + id +
+ " currently supports only version " + versionRange + " of " +
+ name + ".");
+ }
+
+ return new Error("Unsupported Application: The module " + id +
+ " currently supports only " + applications.join(", ") + ".")
+}
+exports.incompatibility = incompatibility;
diff --git a/addon-sdk/source/lib/sdk/tab/events.js b/addon-sdk/source/lib/sdk/tab/events.js
new file mode 100644
index 000000000..e431cc9d2
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tab/events.js
@@ -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";
+
+// This module provides temporary shim until Bug 843901 is shipped.
+// It basically registers tab event listeners on all windows that get
+// opened and forwards them through observer notifications.
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Ci } = require("chrome");
+const { windows, isInteractive } = require("../window/utils");
+const { events } = require("../browser/events");
+const { open } = require("../event/dom");
+const { filter, map, merge, expand } = require("../event/utils");
+const isFennec = require("sdk/system/xul-app").is("Fennec");
+
+// Module provides event stream (in nodejs style) that emits data events
+// for all the tab events that happen in running firefox. At the moment
+// it does it by registering listeners on all browser windows and then
+// forwarding events when they occur to a stream. This will become obsolete
+// once Bug 843901 is fixed, and we'll just leverage observer notifications.
+
+// Set of tab events that this module going to aggregate and expose.
+const TYPES = ["TabOpen","TabClose","TabSelect","TabMove","TabPinned",
+ "TabUnpinned"];
+
+// Utility function that given a browser `window` returns stream of above
+// defined tab events for all tabs on the given window.
+function tabEventsFor(window) {
+ // Map supported event types to a streams of those events on the given
+ // `window` and than merge these streams into single form stream off
+ // all events.
+ let channels = TYPES.map(type => open(window, type));
+ return merge(channels);
+}
+
+// Create our event channels. We do this in a separate function to
+// minimize the chance of leaking intermediate objects on the global.
+function makeEvents() {
+ // Filter DOMContentLoaded events from all the browser events.
+ var readyEvents = filter(events, e => e.type === "DOMContentLoaded");
+ // Map DOMContentLoaded events to it's target browser windows.
+ var futureWindows = map(readyEvents, e => e.target);
+ // Expand all browsers that will become interactive to supported tab events
+ // on these windows. Result will be a tab events from all tabs of all windows
+ // that will become interactive.
+ var eventsFromFuture = expand(futureWindows, tabEventsFor);
+
+ // Above covers only windows that will become interactive in a future, but some
+ // windows may already be interactive so we pick those and expand to supported
+ // tab events for them too.
+ var interactiveWindows = windows("navigator:browser", { includePrivate: true }).
+ filter(isInteractive);
+ var eventsFromInteractive = merge(interactiveWindows.map(tabEventsFor));
+
+
+ // Finally merge stream of tab events from future windows and current windows
+ // to cover all tab events on all windows that will open.
+ return merge([eventsFromInteractive, eventsFromFuture]);
+}
+
+// Map events to Fennec format if necessary
+exports.events = map(makeEvents(), function (event) {
+ return !isFennec ? event : {
+ type: event.type,
+ target: event.target.ownerDocument.defaultView.BrowserApp
+ .getTabForBrowser(event.target)
+ };
+});
diff --git a/addon-sdk/source/lib/sdk/tabs.js b/addon-sdk/source/lib/sdk/tabs.js
new file mode 100644
index 000000000..f61cad478
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs.js
@@ -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": "unstable"
+};
+
+if (require("./system/xul-app").is("Fennec")) {
+ module.exports = require("./windows/tabs-fennec").tabs;
+}
+else {
+ module.exports = require("./tabs/tabs-firefox");
+}
+
+const tabs = module.exports;
diff --git a/addon-sdk/source/lib/sdk/tabs/common.js b/addon-sdk/source/lib/sdk/tabs/common.js
new file mode 100644
index 000000000..9ee512a7b
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/common.js
@@ -0,0 +1,34 @@
+/* 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 { validateOptions } = require("../deprecated/api-utils");
+const { data } = require("../self");
+
+function Options(options) {
+ if ('string' === typeof options)
+ options = { url: options };
+
+ return validateOptions(options, {
+ url: {
+ is: ["string"],
+ map: (v) => v ? data.url(v) : v
+ },
+ inBackground: {
+ map: Boolean,
+ is: ["undefined", "boolean"]
+ },
+ isPinned: { is: ["undefined", "boolean"] },
+ isPrivate: { is: ["undefined", "boolean"] },
+ inNewWindow: { is: ["undefined", "boolean"] },
+ onOpen: { is: ["undefined", "function"] },
+ onClose: { is: ["undefined", "function"] },
+ onReady: { is: ["undefined", "function"] },
+ onLoad: { is: ["undefined", "function"] },
+ onPageShow: { is: ["undefined", "function"] },
+ onActivate: { is: ["undefined", "function"] },
+ onDeactivate: { is: ["undefined", "function"] }
+ });
+}
+exports.Options = Options;
diff --git a/addon-sdk/source/lib/sdk/tabs/events.js b/addon-sdk/source/lib/sdk/tabs/events.js
new file mode 100644
index 000000000..65650f9dc
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/events.js
@@ -0,0 +1,39 @@
+/* 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 ON_PREFIX = "on";
+const TAB_PREFIX = "Tab";
+
+const EVENTS = {
+ ready: "DOMContentLoaded",
+ load: "load", // Used for non-HTML content
+ pageshow: "pageshow", // Used for cached content
+ open: "TabOpen",
+ close: "TabClose",
+ activate: "TabSelect",
+ deactivate: null,
+ pinned: "TabPinned",
+ unpinned: "TabUnpinned"
+}
+exports.EVENTS = EVENTS;
+
+Object.keys(EVENTS).forEach(function(name) {
+ EVENTS[name] = {
+ name: name,
+ listener: createListenerName(name),
+ dom: EVENTS[name]
+ }
+});
+
+function createListenerName (name) {
+ if (name === 'pageshow')
+ return 'onPageShow';
+ else
+ return ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1);
+}
diff --git a/addon-sdk/source/lib/sdk/tabs/helpers.js b/addon-sdk/source/lib/sdk/tabs/helpers.js
new file mode 100644
index 000000000..b2c8aa013
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/helpers.js
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ 'stability': 'unstable'
+};
+
+
+// NOTE: This file should only export Tab instances
+
+
+const { getTabForBrowser: getRawTabForBrowser } = require('./utils');
+const { modelFor } = require('../model/core');
+
+exports.getTabForRawTab = modelFor;
+
+function getTabForBrowser(browser) {
+ return modelFor(getRawTabForBrowser(browser)) || null;
+}
+exports.getTabForBrowser = getTabForBrowser;
diff --git a/addon-sdk/source/lib/sdk/tabs/namespace.js b/addon-sdk/source/lib/sdk/tabs/namespace.js
new file mode 100644
index 000000000..3553b1a99
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/namespace.js
@@ -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 { ns } = require('../core/namespace');
+
+exports.tabsNS = ns();
+exports.tabNS = ns();
+exports.rawTabNS = ns();
diff --git a/addon-sdk/source/lib/sdk/tabs/observer.js b/addon-sdk/source/lib/sdk/tabs/observer.js
new file mode 100644
index 000000000..4e935cd62
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/observer.js
@@ -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';
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { EventTarget } = require("../event/target");
+const { emit } = require("../event/core");
+const { DOMEventAssembler } = require("../deprecated/events/assembler");
+const { Class } = require("../core/heritage");
+const { getActiveTab, getTabs } = require("./utils");
+const { browserWindowIterator } = require("../deprecated/window-utils");
+const { isBrowser, windows, getMostRecentBrowserWindow } = require("../window/utils");
+const { observer: windowObserver } = require("../windows/observer");
+const { when } = require("../system/unload");
+
+const EVENTS = {
+ "TabOpen": "open",
+ "TabClose": "close",
+ "TabSelect": "select",
+ "TabMove": "move",
+ "TabPinned": "pinned",
+ "TabUnpinned": "unpinned"
+};
+
+const selectedTab = Symbol("observer/state/selectedTab");
+
+// Event emitter objects used to register listeners and emit events on them
+// when they occur.
+const Observer = Class({
+ implements: [EventTarget, DOMEventAssembler],
+ initialize() {
+ this[selectedTab] = null;
+ // Currently Gecko does not dispatch any event on the previously selected
+ // tab before / after "TabSelect" is dispatched. In order to work around this
+ // limitation we keep track of selected tab and emit "deactivate" event with
+ // that before emitting "activate" on selected tab.
+ this.on("select", tab => {
+ const selected = this[selectedTab];
+ if (selected !== tab) {
+ if (selected) {
+ emit(this, 'deactivate', selected);
+ }
+
+ if (tab) {
+ this[selectedTab] = tab;
+ emit(this, 'activate', this[selectedTab]);
+ }
+ }
+ });
+
+
+ // We also observe opening / closing windows in order to add / remove it's
+ // containers to the observed list.
+ windowObserver.on("open", chromeWindow => {
+ if (isBrowser(chromeWindow)) {
+ this.observe(chromeWindow);
+ }
+ });
+
+ windowObserver.on("close", chromeWindow => {
+ if (isBrowser(chromeWindow)) {
+ // Bug 751546: Emit `deactivate` event on window close immediatly
+ // Otherwise we are going to face "dead object" exception on `select` event
+ if (getActiveTab(chromeWindow) === this[selectedTab]) {
+ emit(this, "deactivate", this[selectedTab]);
+ this[selectedTab] = null;
+ }
+ this.ignore(chromeWindow);
+ }
+ });
+
+
+ // Currently gecko does not dispatches "TabSelect" events when different
+ // window gets activated. To work around this limitation we emulate "select"
+ // event for this case.
+ windowObserver.on("activate", chromeWindow => {
+ if (isBrowser(chromeWindow)) {
+ emit(this, "select", getActiveTab(chromeWindow));
+ }
+ });
+
+ // We should synchronize state, since probably we already have at least one
+ // window open.
+ for (let chromeWindow of browserWindowIterator()) {
+ this.observe(chromeWindow);
+ }
+
+ when(_ => {
+ // Don't dispatch a deactivate event during unload.
+ this[selectedTab] = null;
+ });
+ },
+ /**
+ * Events that are supported and emitted by the module.
+ */
+ supportedEventsTypes: Object.keys(EVENTS),
+ /**
+ * 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: function handleEvent(event) {
+ emit(this, EVENTS[event.type], event.target, event);
+ }
+});
+
+exports.observer = new Observer();
diff --git a/addon-sdk/source/lib/sdk/tabs/tab-fennec.js b/addon-sdk/source/lib/sdk/tabs/tab-fennec.js
new file mode 100644
index 000000000..3927337f6
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/tab-fennec.js
@@ -0,0 +1,249 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const { Cc, Ci } = require('chrome');
+const { Class } = require('../core/heritage');
+const { tabNS, rawTabNS } = require('./namespace');
+const { EventTarget } = require('../event/target');
+const { activateTab, getTabTitle, setTabTitle, closeTab, getTabURL,
+ getTabContentWindow, getTabForBrowser, setTabURL, getOwnerWindow,
+ getTabContentDocument, getTabContentType, getTabId, isTab } = require('./utils');
+const { emit } = require('../event/core');
+const { isPrivate } = require('../private-browsing/utils');
+const { isWindowPrivate } = require('../window/utils');
+const { when: unload } = require('../system/unload');
+const { BLANK } = require('../content/thumbnail');
+const { viewFor } = require('../view/core');
+const { EVENTS } = require('./events');
+const { modelFor } = require('../model/core');
+
+const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec';
+
+const Tab = Class({
+ extends: EventTarget,
+ initialize: function initialize(options) {
+ options = options.tab ? options : { tab: options };
+ let tab = options.tab;
+
+ EventTarget.prototype.initialize.call(this, options);
+ let tabInternals = tabNS(this);
+ rawTabNS(tab).tab = this;
+
+ let window = tabInternals.window = options.window || getOwnerWindow(tab);
+ tabInternals.tab = tab;
+
+ // TabReady
+ let onReady = tabInternals.onReady = onTabReady.bind(this);
+ tab.browser.addEventListener(EVENTS.ready.dom, onReady, false);
+
+ // TabPageShow
+ let onPageShow = tabInternals.onPageShow = onTabPageShow.bind(this);
+ tab.browser.addEventListener(EVENTS.pageshow.dom, onPageShow, false);
+
+ // TabLoad
+ let onLoad = tabInternals.onLoad = onTabLoad.bind(this);
+ tab.browser.addEventListener(EVENTS.load.dom, onLoad, true);
+
+ // TabClose
+ let onClose = tabInternals.onClose = onTabClose.bind(this);
+ window.BrowserApp.deck.addEventListener(EVENTS.close.dom, onClose, false);
+
+ unload(cleanupTab.bind(null, this));
+ },
+
+ /**
+ * The title of the page currently loaded in the tab.
+ * Changing this property changes an actual title.
+ * @type {String}
+ */
+ get title() {
+ return getTabTitle(tabNS(this).tab);
+ },
+ set title(title) {
+ setTabTitle(tabNS(this).tab, title);
+ },
+
+ /**
+ * Location of the page currently loaded in this tab.
+ * Changing this property will loads page under under the specified location.
+ * @type {String}
+ */
+ get url() {
+ return tabNS(this).closed ? undefined : getTabURL(tabNS(this).tab);
+ },
+ set url(url) {
+ setTabURL(tabNS(this).tab, url);
+ },
+
+ getThumbnail: function() {
+ // TODO: implement!
+ console.error(ERR_FENNEC_MSG);
+
+ // return 80x45 blank default
+ return BLANK;
+ },
+
+ /**
+ * tab's document readyState, or 'uninitialized' if it doesn't even exist yet.
+ */
+ get readyState() {
+ let doc = getTabContentDocument(tabNS(this).tab);
+ return doc && doc.readyState || 'uninitialized';
+ },
+
+ get id() {
+ return getTabId(tabNS(this).tab);
+ },
+
+ /**
+ * The index of the tab relative to other tabs in the application window.
+ * Changing this property will change order of the actual position of the tab.
+ * @type {Number}
+ */
+ get index() {
+ if (tabNS(this).closed) return undefined;
+
+ let tabs = tabNS(this).window.BrowserApp.tabs;
+ let tab = tabNS(this).tab;
+ for (var i = tabs.length; i >= 0; i--) {
+ if (tabs[i] === tab)
+ return i;
+ }
+ return null;
+ },
+ set index(value) {
+ console.error(ERR_FENNEC_MSG); // TODO
+ },
+
+ /**
+ * Whether or not tab is pinned (Is an app-tab).
+ * @type {Boolean}
+ */
+ get isPinned() {
+ console.error(ERR_FENNEC_MSG); // TODO
+ return false; // TODO
+ },
+ pin: function pin() {
+ console.error(ERR_FENNEC_MSG); // TODO
+ },
+ unpin: function unpin() {
+ console.error(ERR_FENNEC_MSG); // TODO
+ },
+
+ /**
+ * Returns the MIME type that the document loaded in the tab is being
+ * rendered as.
+ * @type {String}
+ */
+ get contentType() {
+ return getTabContentType(tabNS(this).tab);
+ },
+
+ /**
+ * Create a worker for this tab, first argument is options given to Worker.
+ * @type {Worker}
+ */
+ attach: function attach(options) {
+ // BUG 792946 https://bugzilla.mozilla.org/show_bug.cgi?id=792946
+ // TODO: fix this circular dependency
+ let { Worker } = require('./worker');
+ return Worker(options, getTabContentWindow(tabNS(this).tab));
+ },
+
+ /**
+ * Make this tab active.
+ */
+ activate: function activate() {
+ activateTab(tabNS(this).tab, tabNS(this).window);
+ },
+
+ /**
+ * Close the tab
+ */
+ close: function close(callback) {
+ let tab = this;
+ this.once(EVENTS.close.name, function () {
+ tabNS(tab).closed = true;
+ if (callback) callback();
+ });
+
+ closeTab(tabNS(this).tab);
+ },
+
+ /**
+ * Reload the tab
+ */
+ reload: function reload() {
+ tabNS(this).tab.browser.reload();
+ }
+});
+exports.Tab = Tab;
+
+// Implement `viewFor` polymorphic function for the Tab
+// instances.
+viewFor.define(Tab, x => tabNS(x).tab);
+
+function cleanupTab(tab) {
+ let tabInternals = tabNS(tab);
+ if (!tabInternals.tab)
+ return;
+
+ if (tabInternals.tab.browser) {
+ tabInternals.tab.browser.removeEventListener(EVENTS.ready.dom, tabInternals.onReady, false);
+ tabInternals.tab.browser.removeEventListener(EVENTS.pageshow.dom, tabInternals.onPageShow, false);
+ tabInternals.tab.browser.removeEventListener(EVENTS.load.dom, tabInternals.onLoad, true);
+ }
+ tabInternals.onReady = null;
+ tabInternals.onPageShow = null;
+ tabInternals.onLoad = null;
+ tabInternals.window.BrowserApp.deck.removeEventListener(EVENTS.close.dom, tabInternals.onClose, false);
+ tabInternals.onClose = null;
+ rawTabNS(tabInternals.tab).tab = null;
+ tabInternals.tab = null;
+ tabInternals.window = null;
+}
+
+function onTabReady(event) {
+ let win = event.target.defaultView;
+
+ // ignore frames
+ if (win === win.top) {
+ emit(this, 'ready', this);
+ }
+}
+
+function onTabLoad (event) {
+ let win = event.target.defaultView;
+
+ // ignore frames
+ if (win === win.top) {
+ emit(this, 'load', this);
+ }
+}
+
+function onTabPageShow(event) {
+ let win = event.target.defaultView;
+ if (win === win.top)
+ emit(this, 'pageshow', this, event.persisted);
+}
+
+// TabClose
+function onTabClose(event) {
+ let rawTab = getTabForBrowser(event.target);
+ if (tabNS(this).tab !== rawTab)
+ return;
+
+ emit(this, EVENTS.close.name, this);
+ cleanupTab(this);
+};
+
+isPrivate.implement(Tab, tab => {
+ return isWindowPrivate(getTabContentWindow(tabNS(tab).tab));
+});
+
+// Implement `modelFor` function for the Tab instances.
+modelFor.when(isTab, rawTab => {
+ return rawTabNS(rawTab).tab;
+});
diff --git a/addon-sdk/source/lib/sdk/tabs/tab-firefox.js b/addon-sdk/source/lib/sdk/tabs/tab-firefox.js
new file mode 100644
index 000000000..f1da92379
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/tab-firefox.js
@@ -0,0 +1,353 @@
+/* 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 { observer } = require('./observer');
+const { observer: windowObserver } = require('../windows/observer');
+const { addListItem, removeListItem } = require('../util/list');
+const { viewFor } = require('../view/core');
+const { modelFor } = require('../model/core');
+const { emit, setListeners } = require('../event/core');
+const { EventTarget } = require('../event/target');
+const { getBrowserForTab, setTabURL, getTabId, getTabURL, getTabForBrowser,
+ getTabs, getTabTitle, setTabTitle, getIndex, closeTab, reload, move,
+ activateTab, pin, unpin, isTab } = require('./utils');
+const { isBrowser, getInnerId, isWindowPrivate } = require('../window/utils');
+const { getThumbnailURIForWindow, BLANK } = require("../content/thumbnail");
+const { when } = require('../system/unload');
+const { ignoreWindow, isPrivate } = require('../private-browsing/utils')
+const { defer } = require('../lang/functional');
+const { getURL } = require('../url/utils');
+const { frames, remoteRequire } = require('../remote/parent');
+remoteRequire('sdk/content/tab-events');
+
+const modelsFor = new WeakMap();
+const viewsFor = new WeakMap();
+const destroyed = new WeakMap();
+
+const tabEvents = {};
+exports.tabEvents = tabEvents;
+
+function browser(tab) {
+ return getBrowserForTab(viewsFor.get(tab));
+}
+
+function isDestroyed(tab) {
+ return destroyed.has(tab);
+}
+
+function isClosed(tab) {
+ if (!viewsFor.has(tab))
+ return true;
+ return viewsFor.get(tab).closing;
+}
+
+// private tab attribute where the remote cached value is stored
+const remoteReadyStateCached = Symbol("remoteReadyStateCached");
+
+const Tab = Class({
+ implements: [EventTarget],
+ initialize: function(tabElement, options = null) {
+ modelsFor.set(tabElement, this);
+ viewsFor.set(this, tabElement);
+
+ if (options) {
+ EventTarget.prototype.initialize.call(this, options);
+
+ if (options.isPinned)
+ this.pin();
+
+ // Note that activate is defered and so will run after any open event
+ // is sent out
+ if (!options.inBackground)
+ this.activate();
+ }
+
+ getURL.implement(this, tab => tab.url);
+ isPrivate.implement(this, tab => {
+ return isWindowPrivate(viewsFor.get(tab).ownerDocument.defaultView);
+ });
+ },
+
+ get id() {
+ return isDestroyed(this) ? undefined : getTabId(viewsFor.get(this));
+ },
+
+ get title() {
+ return isDestroyed(this) ? undefined : getTabTitle(viewsFor.get(this));
+ },
+
+ set title(val) {
+ if (isDestroyed(this))
+ return;
+
+ setTabTitle(viewsFor.get(this), val);
+ },
+
+ get url() {
+ return isDestroyed(this) ? undefined : getTabURL(viewsFor.get(this));
+ },
+
+ set url(val) {
+ if (isDestroyed(this))
+ return;
+
+ setTabURL(viewsFor.get(this), val);
+ },
+
+ get contentType() {
+ return isDestroyed(this) ? undefined : browser(this).documentContentType;
+ },
+
+ get index() {
+ return isDestroyed(this) ? undefined : getIndex(viewsFor.get(this));
+ },
+
+ set index(val) {
+ if (isDestroyed(this))
+ return;
+
+ move(viewsFor.get(this), val);
+ },
+
+ get isPinned() {
+ return isDestroyed(this) ? undefined : viewsFor.get(this).pinned;
+ },
+
+ get window() {
+ if (isClosed(this))
+ return undefined;
+
+ // TODO: Remove the dependency on the windows module, see bug 792670
+ require('../windows');
+ let tabElement = viewsFor.get(this);
+ let domWindow = tabElement.ownerDocument.defaultView;
+ return modelFor(domWindow);
+ },
+
+ get readyState() {
+ return isDestroyed(this) ? undefined : this[remoteReadyStateCached] || "uninitialized";
+ },
+
+ pin: function() {
+ if (isDestroyed(this))
+ return;
+
+ pin(viewsFor.get(this));
+ },
+
+ unpin: function() {
+ if (isDestroyed(this))
+ return;
+
+ unpin(viewsFor.get(this));
+ },
+
+ close: function(callback) {
+ let tabElement = viewsFor.get(this);
+
+ if (isDestroyed(this) || !tabElement || !tabElement.parentNode) {
+ if (callback)
+ callback();
+ return;
+ }
+
+ this.once('close', () => {
+ this.destroy();
+ if (callback)
+ callback();
+ });
+
+ closeTab(tabElement);
+ },
+
+ reload: function() {
+ if (isDestroyed(this))
+ return;
+
+ reload(viewsFor.get(this));
+ },
+
+ activate: defer(function() {
+ if (isDestroyed(this))
+ return;
+
+ activateTab(viewsFor.get(this));
+ }),
+
+ getThumbnail: function() {
+ if (isDestroyed(this))
+ return BLANK;
+
+ // TODO: This is unimplemented in e10s: bug 1148601
+ if (browser(this).isRemoteBrowser) {
+ console.error('This method is not supported with E10S');
+ return BLANK;
+ }
+ return getThumbnailURIForWindow(browser(this).contentWindow);
+ },
+
+ attach: function(options) {
+ if (isDestroyed(this))
+ return;
+
+ let { Worker } = require('../content/worker');
+ let { connect, makeChildOptions } = require('../content/utils');
+
+ let worker = Worker(options);
+ worker.once("detach", () => {
+ worker.destroy();
+ });
+
+ let attach = frame => {
+ let childOptions = makeChildOptions(options);
+ frame.port.emit("sdk/tab/attach", childOptions);
+ connect(worker, frame, { id: childOptions.id, url: this.url });
+ };
+
+ // Do this synchronously if possible
+ let frame = frames.getFrameForBrowser(browser(this));
+ if (frame) {
+ attach(frame);
+ }
+ else {
+ let listener = (frame) => {
+ if (frame.frameElement != browser(this))
+ return;
+
+ frames.off("attach", listener);
+ attach(frame);
+ };
+ frames.on("attach", listener);
+ }
+
+ return worker;
+ },
+
+ destroy: function() {
+ if (isDestroyed(this))
+ return;
+
+ destroyed.set(this, true);
+ }
+});
+exports.Tab = Tab;
+
+viewFor.define(Tab, tab => viewsFor.get(tab));
+
+// Returns the high-level window for this DOM window if the windows module has
+// ever been loaded otherwise returns null
+function maybeWindowFor(domWindow) {
+ try {
+ return modelFor(domWindow);
+ }
+ catch (e) {
+ return null;
+ }
+}
+
+function tabEmit(tab, event, ...args) {
+ // Don't emit events for destroyed tabs
+ if (isDestroyed(tab))
+ return;
+
+ // If the windows module was never loaded this will return null. We don't need
+ // to emit to the window.tabs object in this case as nothing can be listening.
+ let tabElement = viewsFor.get(tab);
+ let window = maybeWindowFor(tabElement.ownerDocument.defaultView);
+ if (window)
+ emit(window.tabs, event, tab, ...args);
+
+ emit(tabEvents, event, tab, ...args);
+ emit(tab, event, tab, ...args);
+}
+
+function windowClosed(domWindow) {
+ if (!isBrowser(domWindow))
+ return;
+
+ for (let tabElement of getTabs(domWindow)) {
+ tabEventListener("close", tabElement);
+ }
+}
+windowObserver.on('close', windowClosed);
+
+// Don't want to send close events after unloaded
+when(_ => {
+ windowObserver.off('close', windowClosed);
+});
+
+// Listen for tabbrowser events
+function tabEventListener(event, tabElement, ...args) {
+ let domWindow = tabElement.ownerDocument.defaultView;
+
+ if (ignoreWindow(domWindow))
+ return;
+
+ // Don't send events for tabs that are already closing
+ if (event != "close" && (tabElement.closing || !tabElement.parentNode))
+ return;
+
+ let tab = modelsFor.get(tabElement);
+ if (!tab)
+ tab = new Tab(tabElement);
+
+ let window = maybeWindowFor(domWindow);
+
+ if (event == "open") {
+ // Note, add to the window tabs first because if this is the first access to
+ // window.tabs it will be prefilling itself with everything from tabs
+ if (window)
+ addListItem(window.tabs, tab);
+ // The tabs module will take care of adding to its internal list
+ }
+ else if (event == "close") {
+ if (window)
+ removeListItem(window.tabs, tab);
+ // The tabs module will take care of removing from its internal list
+ }
+ else if (event == "init" || event == "create" || event == "ready" || event == "load") {
+ // Ignore load events from before browser windows have fully loaded, these
+ // are for about:blank in the initial tab
+ if (isBrowser(domWindow) && !domWindow.gBrowserInit.delayedStartupFinished)
+ return;
+
+ // update the cached remote readyState value
+ let { readyState } = args[0] || {};
+ tab[remoteReadyStateCached] = readyState;
+ }
+
+ if (event == "init") {
+ // Do not emit events for the detected existent tabs, we only need to cache
+ // their current document.readyState value.
+ return;
+ }
+
+ tabEmit(tab, event, ...args);
+
+ // The tab object shouldn't be reachable after closed
+ if (event == "close") {
+ viewsFor.delete(tab);
+ modelsFor.delete(tabElement);
+ }
+}
+observer.on('*', tabEventListener);
+
+// Listen for tab events from content
+frames.port.on('sdk/tab/event', (frame, event, ...args) => {
+ if (!frame.isTab)
+ return;
+
+ let tabElement = getTabForBrowser(frame.frameElement);
+ if (!tabElement)
+ return;
+
+ tabEventListener(event, tabElement, ...args);
+});
+
+// Implement `modelFor` function for the Tab instances..
+modelFor.when(isTab, view => {
+ return modelsFor.get(view);
+});
diff --git a/addon-sdk/source/lib/sdk/tabs/tab.js b/addon-sdk/source/lib/sdk/tabs/tab.js
new file mode 100644
index 000000000..fa2272494
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/tab.js
@@ -0,0 +1,24 @@
+/* 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 { getTargetWindow } = require("../content/mod");
+const { getTabContentWindow, isTab } = require("./utils");
+const { viewFor } = require("../view/core");
+
+if (require('../system/xul-app').name == 'Fennec') {
+ module.exports = require('./tab-fennec');
+}
+else {
+ module.exports = require('./tab-firefox');
+}
+
+getTargetWindow.when(isTab, tab => getTabContentWindow(tab));
+
+getTargetWindow.when(x => x instanceof module.exports.Tab,
+ tab => getTabContentWindow(viewFor(tab)));
diff --git a/addon-sdk/source/lib/sdk/tabs/tabs-firefox.js b/addon-sdk/source/lib/sdk/tabs/tabs-firefox.js
new file mode 100644
index 000000000..1eefecb4c
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/tabs-firefox.js
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Class } = require('../core/heritage');
+const { Tab, tabEvents } = require('./tab');
+const { EventTarget } = require('../event/target');
+const { emit, setListeners } = require('../event/core');
+const { pipe } = require('../event/utils');
+const { observer: windowObserver } = require('../windows/observer');
+const { List, addListItem, removeListItem } = require('../util/list');
+const { modelFor } = require('../model/core');
+const { viewFor } = require('../view/core');
+const { getTabs, getSelectedTab } = require('./utils');
+const { getMostRecentBrowserWindow, isBrowser } = require('../window/utils');
+const { Options } = require('./common');
+const { isPrivate } = require('../private-browsing');
+const { ignoreWindow, isWindowPBSupported } = require('../private-browsing/utils')
+const { isPrivateBrowsingSupported } = require('sdk/self');
+
+const supportPrivateTabs = isPrivateBrowsingSupported && isWindowPBSupported;
+
+const Tabs = Class({
+ implements: [EventTarget],
+ extends: List,
+ initialize: function() {
+ List.prototype.initialize.call(this);
+
+ // We must do the list manipulation here where the object is extensible
+ this.on("open", tab => {
+ addListItem(this, tab);
+ });
+
+ this.on("close", tab => {
+ removeListItem(this, tab);
+ });
+ },
+
+ get activeTab() {
+ let activeDomWin = getMostRecentBrowserWindow();
+ if (!activeDomWin)
+ return null;
+ return modelFor(getSelectedTab(activeDomWin));
+ },
+
+ open: function(options) {
+ options = Options(options);
+
+ // TODO: Remove the dependency on the windows module: bug 792670
+ let windows = require('../windows').browserWindows;
+ let activeWindow = windows.activeWindow;
+
+ let privateState = supportPrivateTabs && options.isPrivate;
+ // When no isPrivate option was passed use the private state of the active
+ // window
+ if (activeWindow && privateState === undefined)
+ privateState = isPrivate(activeWindow);
+
+ function getWindow(privateState) {
+ for (let window of windows) {
+ if (privateState === isPrivate(window)) {
+ return window;
+ }
+ }
+ return null;
+ }
+
+ function openNewWindowWithTab() {
+ windows.open({
+ url: options.url,
+ isPrivate: privateState,
+ onOpen: function(newWindow) {
+ let tab = newWindow.tabs[0];
+ setListeners(tab, options);
+
+ if (options.isPinned)
+ tab.pin();
+
+ // We don't emit the open event for the first tab in a new window so
+ // do it now the listeners are attached
+ emit(tab, "open", tab);
+ }
+ });
+ }
+
+ if (options.inNewWindow)
+ return openNewWindowWithTab();
+
+ // if the active window is in the state that we need then use it
+ if (activeWindow && (privateState === isPrivate(activeWindow)))
+ return activeWindow.tabs.open(options);
+
+ // find a window in the state that we need
+ let window = getWindow(privateState);
+ if (window)
+ return window.tabs.open(options);
+
+ return openNewWindowWithTab();
+ }
+});
+
+const allTabs = new Tabs();
+// Export a new object with allTabs as the prototype, otherwise allTabs becomes
+// frozen and addListItem and removeListItem don't work correctly.
+module.exports = Object.create(allTabs);
+pipe(tabEvents, module.exports);
+
+function addWindowTab(window, tabElement) {
+ let tab = new Tab(tabElement);
+ if (window)
+ addListItem(window.tabs, tab);
+ addListItem(allTabs, tab);
+ emit(allTabs, "open", tab);
+}
+
+// Find tabs in already open windows
+for (let tabElement of getTabs())
+ addWindowTab(null, tabElement);
+
+// Detect tabs in new windows
+windowObserver.on('open', domWindow => {
+ if (!isBrowser(domWindow) || ignoreWindow(domWindow))
+ return;
+
+ let window = null;
+ try {
+ modelFor(domWindow);
+ }
+ catch (e) { }
+
+ for (let tabElement of getTabs(domWindow)) {
+ addWindowTab(window, tabElement);
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/tabs/utils.js b/addon-sdk/source/lib/sdk/tabs/utils.js
new file mode 100644
index 000000000..eae3d41fe
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/utils.js
@@ -0,0 +1,370 @@
+/* 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'
+};
+
+
+// NOTE: This file should only deal with xul/native tabs
+
+
+const { Ci, Cu } = require('chrome');
+const { defer } = require("../lang/functional");
+const { windows, isBrowser } = require('../window/utils');
+const { isPrivateBrowsingSupported } = require('../self');
+const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
+
+// Bug 834961: ignore private windows when they are not supported
+function getWindows() {
+ return windows(null, { includePrivate: isPrivateBrowsingSupported });
+}
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+// Define predicate functions that can be used to detech weather
+// we deal with fennec tabs or firefox tabs.
+
+// Predicate to detect whether tab is XUL "Tab" node.
+const isXULTab = tab =>
+ tab instanceof Ci.nsIDOMNode &&
+ tab.nodeName === "tab" &&
+ tab.namespaceURI === XUL_NS;
+exports.isXULTab = isXULTab;
+
+// Predicate to detecet whether given tab is a fettec tab.
+// Unfortunately we have to guess via duck typinng of:
+// http://mxr.mozilla.org/mozilla-central/source/mobile/android/chrome/content/browser.js#2583
+const isFennecTab = tab =>
+ tab &&
+ tab.QueryInterface &&
+ Ci.nsIBrowserTab &&
+ tab.QueryInterface(Ci.nsIBrowserTab) === tab;
+exports.isFennecTab = isFennecTab;
+
+const isTab = x => isXULTab(x) || isFennecTab(x);
+exports.isTab = isTab;
+
+function activateTab(tab, window) {
+ let gBrowser = getTabBrowserForTab(tab);
+
+ // normal case
+ if (gBrowser) {
+ gBrowser.selectedTab = tab;
+ }
+ // fennec ?
+ else if (window && window.BrowserApp) {
+ window.BrowserApp.selectTab(tab);
+ }
+ return null;
+}
+exports.activateTab = activateTab;
+
+function getTabBrowser(window) {
+ // bug 1009938 - may be null in SeaMonkey
+ return window.gBrowser || window.getBrowser();
+}
+exports.getTabBrowser = getTabBrowser;
+
+function getTabContainer(window) {
+ return getTabBrowser(window).tabContainer;
+}
+exports.getTabContainer = getTabContainer;
+
+/**
+ * Returns the tabs for the `window` if given, or the tabs
+ * across all the browser's windows otherwise.
+ *
+ * @param {nsIWindow} [window]
+ * A reference to a window
+ *
+ * @returns {Array} an array of Tab objects
+ */
+function getTabs(window) {
+ if (arguments.length === 0) {
+ return getWindows().
+ filter(isBrowser).
+ reduce((tabs, window) => tabs.concat(getTabs(window)), []);
+ }
+
+ // fennec
+ if (window.BrowserApp)
+ return window.BrowserApp.tabs;
+
+ // firefox - default
+ return Array.filter(getTabContainer(window).children, t => !t.closing);
+}
+exports.getTabs = getTabs;
+
+function getActiveTab(window) {
+ return getSelectedTab(window);
+}
+exports.getActiveTab = getActiveTab;
+
+function getOwnerWindow(tab) {
+ // normal case
+ if (tab.ownerDocument)
+ return tab.ownerDocument.defaultView;
+
+ // try fennec case
+ return getWindowHoldingTab(tab);
+}
+exports.getOwnerWindow = getOwnerWindow;
+
+// fennec
+function getWindowHoldingTab(rawTab) {
+ for (let window of getWindows()) {
+ // this function may be called when not using fennec,
+ // but BrowserApp is only defined on Fennec
+ if (!window.BrowserApp)
+ continue;
+
+ for (let tab of window.BrowserApp.tabs) {
+ if (tab === rawTab)
+ return window;
+ }
+ }
+
+ return null;
+}
+
+function openTab(window, url, options) {
+ options = options || {};
+
+ // fennec?
+ if (window.BrowserApp) {
+ return window.BrowserApp.addTab(url, {
+ selected: options.inBackground ? false : true,
+ pinned: options.isPinned || false,
+ isPrivate: options.isPrivate || false,
+ parentId: window.BrowserApp.selectedTab.id
+ });
+ }
+
+ // firefox
+ let newTab = window.gBrowser.addTab(url);
+ if (!options.inBackground) {
+ activateTab(newTab);
+ }
+ return newTab;
+};
+exports.openTab = openTab;
+
+function isTabOpen(tab) {
+ // try normal case then fennec case
+ return !!((tab.linkedBrowser) || getWindowHoldingTab(tab));
+}
+exports.isTabOpen = isTabOpen;
+
+function closeTab(tab) {
+ let gBrowser = getTabBrowserForTab(tab);
+ // normal case?
+ if (gBrowser) {
+ // Bug 699450: the tab may already have been detached
+ if (!tab.parentNode)
+ return;
+ return gBrowser.removeTab(tab);
+ }
+
+ let window = getWindowHoldingTab(tab);
+ // fennec?
+ if (window && window.BrowserApp) {
+ // Bug 699450: the tab may already have been detached
+ if (!tab.browser)
+ return;
+ return window.BrowserApp.closeTab(tab);
+ }
+ return null;
+}
+exports.closeTab = closeTab;
+
+function getURI(tab) {
+ if (tab.browser) // fennec
+ return tab.browser.currentURI.spec;
+ return tab.linkedBrowser.currentURI.spec;
+}
+exports.getURI = getURI;
+
+function getTabBrowserForTab(tab) {
+ let outerWin = getOwnerWindow(tab);
+ if (outerWin)
+ return getOwnerWindow(tab).gBrowser;
+ return null;
+}
+exports.getTabBrowserForTab = getTabBrowserForTab;
+
+function getBrowserForTab(tab) {
+ if (tab.browser) // fennec
+ return tab.browser;
+
+ return tab.linkedBrowser;
+}
+exports.getBrowserForTab = getBrowserForTab;
+
+function getTabId(tab) {
+ if (tab.browser) // fennec
+ return tab.id
+
+ return String.split(tab.linkedPanel, 'panel').pop();
+}
+exports.getTabId = getTabId;
+
+function getTabForId(id) {
+ return getTabs().find(tab => getTabId(tab) === id) || null;
+}
+exports.getTabForId = getTabForId;
+
+function getTabTitle(tab) {
+ return getBrowserForTab(tab).contentTitle || tab.label || "";
+}
+exports.getTabTitle = getTabTitle;
+
+function setTabTitle(tab, title) {
+ title = String(title);
+ if (tab.browser) {
+ // Fennec
+ tab.browser.contentDocument.title = title;
+ }
+ else {
+ let browser = getBrowserForTab(tab);
+ // Note that we aren't actually setting the document title in e10s, just
+ // the title the browser thinks the content has
+ if (browser.isRemoteBrowser)
+ browser._contentTitle = title;
+ else
+ browser.contentDocument.title = title;
+ }
+ tab.label = String(title);
+}
+exports.setTabTitle = setTabTitle;
+
+function getTabContentDocument(tab) {
+ return getBrowserForTab(tab).contentDocument;
+}
+exports.getTabContentDocument = getTabContentDocument;
+
+function getTabContentWindow(tab) {
+ return getBrowserForTab(tab).contentWindow;
+}
+exports.getTabContentWindow = getTabContentWindow;
+
+/**
+ * Returns all tabs' content windows across all the browsers' windows
+ */
+function getAllTabContentWindows() {
+ return getTabs().map(getTabContentWindow);
+}
+exports.getAllTabContentWindows = getAllTabContentWindows;
+
+// gets the tab containing the provided window
+function getTabForContentWindow(window) {
+ return getTabs().find(tab => getTabContentWindow(tab) === window.top) || null;
+}
+exports.getTabForContentWindow = getTabForContentWindow;
+
+// only sdk/selection.js is relying on shims
+function getTabForContentWindowNoShim(window) {
+ function getTabContentWindowNoShim(tab) {
+ let browser = getBrowserForTab(tab);
+ return ShimWaiver.getProperty(browser, "contentWindow");
+ }
+ return getTabs().find(tab => getTabContentWindowNoShim(tab) === window.top) || null;
+}
+exports.getTabForContentWindowNoShim = getTabForContentWindowNoShim;
+
+function getTabURL(tab) {
+ return String(getBrowserForTab(tab).currentURI.spec);
+}
+exports.getTabURL = getTabURL;
+
+function setTabURL(tab, url) {
+ let browser = getBrowserForTab(tab);
+ browser.loadURI(String(url));
+}
+// "TabOpen" event is fired when it's still "about:blank" is loaded in the
+// changing `location` property of the `contentDocument` has no effect since
+// seems to be either ignored or overridden by internal listener, there for
+// location change is enqueued for the next turn of event loop.
+exports.setTabURL = defer(setTabURL);
+
+function getTabContentType(tab) {
+ return getBrowserForTab(tab).contentDocument.contentType;
+}
+exports.getTabContentType = getTabContentType;
+
+function getSelectedTab(window) {
+ if (window.BrowserApp) // fennec?
+ return window.BrowserApp.selectedTab;
+ if (window.gBrowser)
+ return window.gBrowser.selectedTab;
+ return null;
+}
+exports.getSelectedTab = getSelectedTab;
+
+
+function getTabForBrowser(browser) {
+ for (let window of getWindows()) {
+ // this function may be called when not using fennec
+ if (!window.BrowserApp)
+ continue;
+
+ for (let tab of window.BrowserApp.tabs) {
+ if (tab.browser === browser)
+ return tab;
+ }
+ }
+
+ let tabbrowser = browser.getTabBrowser && browser.getTabBrowser()
+ return !!tabbrowser && tabbrowser.getTabForBrowser(browser);
+}
+exports.getTabForBrowser = getTabForBrowser;
+
+function pin(tab) {
+ let gBrowser = getTabBrowserForTab(tab);
+ // TODO: Implement Fennec support
+ if (gBrowser) gBrowser.pinTab(tab);
+}
+exports.pin = pin;
+
+function unpin(tab) {
+ let gBrowser = getTabBrowserForTab(tab);
+ // TODO: Implement Fennec support
+ if (gBrowser) gBrowser.unpinTab(tab);
+}
+exports.unpin = unpin;
+
+function isPinned(tab) {
+ return !!tab.pinned;
+}
+exports.isPinned = isPinned;
+
+function reload(tab) {
+ getBrowserForTab(tab).reload();
+}
+exports.reload = reload
+
+function getIndex(tab) {
+ let gBrowser = getTabBrowserForTab(tab);
+ // Firefox
+ if (gBrowser) {
+ return tab._tPos;
+ }
+ // Fennec
+ else {
+ let window = getWindowHoldingTab(tab)
+ let tabs = window.BrowserApp.tabs;
+ for (let i = tabs.length; i >= 0; i--)
+ if (tabs[i] === tab) return i;
+ }
+}
+exports.getIndex = getIndex;
+
+function move(tab, index) {
+ let gBrowser = getTabBrowserForTab(tab);
+ // Firefox
+ if (gBrowser) gBrowser.moveTabTo(tab, index);
+ // TODO: Implement fennec support
+}
+exports.move = move;
diff --git a/addon-sdk/source/lib/sdk/tabs/worker.js b/addon-sdk/source/lib/sdk/tabs/worker.js
new file mode 100644
index 000000000..d2ba33696
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tabs/worker.js
@@ -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';
+
+const ContentWorker = require('../content/worker').Worker;
+
+function Worker(options, window) {
+ options.window = window;
+
+ let worker = ContentWorker(options);
+ worker.once("detach", function detach() {
+ worker.destroy();
+ });
+ return worker;
+}
+exports.Worker = Worker; \ No newline at end of file
diff --git a/addon-sdk/source/lib/sdk/test.js b/addon-sdk/source/lib/sdk/test.js
new file mode 100644
index 000000000..e7e3df840
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test.js
@@ -0,0 +1,114 @@
+/* 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 { Task } = require("resource://gre/modules/Task.jsm", {});
+const { defer } = require("sdk/core/promise");
+const BaseAssert = require("sdk/test/assert").Assert;
+const { isFunction, isObject, isGenerator } = require("sdk/lang/type");
+const { extend } = require("sdk/util/object");
+
+exports.Assert = BaseAssert;
+
+/**
+ * Function takes test `suite` object in CommonJS format and defines all of the
+ * tests from that suite and nested suites in a jetpack format on a given
+ * `target` object. Optionally third argument `prefix` can be passed to prefix
+ * all the test names.
+ */
+function defineTestSuite(target, suite, prefix) {
+ prefix = prefix || "";
+ // If suite defines `Assert` that's what `assert` object have to be created
+ // from and passed to a test function (This allows custom assertion functions)
+ // See for details: http://wiki.commonjs.org/wiki/Unit_Testing/1.1
+ let Assert = suite.Assert || BaseAssert;
+ // Going through each item in the test suite and wrapping it into a
+ // Jetpack test format.
+ Object.keys(suite).forEach(function(key) {
+ // If name starts with test then it's a test function or suite.
+ if (key.indexOf("test") === 0) {
+ let test = suite[key];
+
+ // For each test function so we create a wrapper test function in a
+ // jetpack format and copy that to a `target` exports.
+ if (isFunction(test)) {
+
+ // Since names of the test may match across suites we use full object
+ // path as a name to avoid overriding same function.
+ target[prefix + key] = function(options) {
+
+ // Creating `assert` functions for this test.
+ let assert = Assert(options);
+ assert.end = () => options.done();
+
+ // If test function is a generator use a task JS to allow yield-ing
+ // style test runs.
+ if (isGenerator(test)) {
+ options.waitUntilDone();
+ Task.spawn(test.bind(null, assert)).
+ catch(assert.fail).
+ then(assert.end);
+ }
+
+ // If CommonJS test function expects more than one argument
+ // it means that test is async and second argument is a callback
+ // to notify that test is finished.
+ else if (1 < test.length) {
+ // Letting test runner know that test is executed async and
+ // creating a callback function that CommonJS tests will call
+ // once it's done.
+ options.waitUntilDone();
+ test(assert, function() {
+ options.done();
+ });
+ }
+
+ // Otherwise CommonJS test is synchronous so we call it only with
+ // one argument.
+ else {
+ test(assert);
+ }
+ }
+ }
+
+ // If it's an object then it's a test suite containing test function
+ // and / or nested test suites. In that case we just extend prefix used
+ // and call this function to copy and wrap tests from nested suite.
+ else if (isObject(test)) {
+ // We need to clone `tests` instead of modifying it, since it's very
+ // likely that it is frozen (usually test suites imported modules).
+ test = extend(Object.prototype, test, {
+ Assert: test.Assert || Assert
+ });
+ defineTestSuite(target, test, prefix + key + ".");
+ }
+ }
+ });
+}
+
+/**
+ * This function is a CommonJS test runner function, but since Jetpack test
+ * runner and test format is different from CommonJS this function shims given
+ * `exports` with all its tests into a Jetpack test format so that the built-in
+ * test runner will be able to run CommonJS test without manual changes.
+ */
+exports.run = function run(exports) {
+ // We can't leave old properties on exports since those are test in a CommonJS
+ // format that why we move everything to a new `suite` object.
+ let suite = {};
+ Object.keys(exports).forEach(function(key) {
+ suite[key] = exports[key];
+ delete exports[key];
+ });
+
+ // Now we wrap all the CommonJS tests to a Jetpack format and define
+ // those to a given `exports` object since that where jetpack test runner
+ // will look for them.
+ defineTestSuite(exports, suite);
+};
diff --git a/addon-sdk/source/lib/sdk/test/assert.js b/addon-sdk/source/lib/sdk/test/assert.js
new file mode 100644
index 000000000..8478c8414
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/assert.js
@@ -0,0 +1,366 @@
+/* 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 { isFunction, isNull, isObject, isString,
+ isRegExp, isArray, isDate, isPrimitive,
+ isUndefined, instanceOf, source } = require("../lang/type");
+
+/**
+ * The `AssertionError` is defined in assert.
+ * @extends Error
+ * @example
+ * new assert.AssertionError({
+ * message: message,
+ * actual: actual,
+ * expected: expected
+ * })
+ */
+function AssertionError(options) {
+ let assertionError = Object.create(AssertionError.prototype);
+
+ if (isString(options))
+ options = { message: options };
+ if ("actual" in options)
+ assertionError.actual = options.actual;
+ if ("expected" in options)
+ assertionError.expected = options.expected;
+ if ("operator" in options)
+ assertionError.operator = options.operator;
+
+ assertionError.message = options.message;
+ assertionError.stack = new Error().stack;
+ return assertionError;
+}
+AssertionError.prototype = Object.create(Error.prototype, {
+ constructor: { value: AssertionError },
+ name: { value: "AssertionError", enumerable: true },
+ toString: { value: function toString() {
+ let value;
+ if (this.message) {
+ value = this.name + " : " + this.message;
+ }
+ else {
+ value = [
+ this.name + " : ",
+ source(this.expected),
+ this.operator,
+ source(this.actual)
+ ].join(" ");
+ }
+ return value;
+ }}
+});
+exports.AssertionError = AssertionError;
+
+function Assert(logger) {
+ let assert = Object.create(Assert.prototype, { _log: { value: logger }});
+
+ assert.fail = assert.fail.bind(assert);
+ assert.pass = assert.pass.bind(assert);
+
+ return assert;
+}
+
+Assert.prototype = {
+ fail: function fail(e) {
+ if (!e || typeof(e) !== 'object') {
+ this._log.fail(e);
+ return;
+ }
+ let message = e.message;
+ try {
+ if ('operator' in e) {
+ message += [
+ " -",
+ source(e.actual),
+ e.operator,
+ source(e.expected)
+ ].join(" ");
+ }
+ }
+ catch(e) {}
+ this._log.fail(message);
+ },
+ pass: function pass(message) {
+ this._log.pass(message);
+ return true;
+ },
+ error: function error(e) {
+ this._log.exception(e);
+ },
+ ok: function ok(value, message) {
+ if (!!!value) {
+ this.fail({
+ actual: value,
+ expected: true,
+ message: message,
+ operator: "=="
+ });
+ return false;
+ }
+
+ this.pass(message);
+ return true;
+ },
+
+ /**
+ * The equality assertion tests shallow, coercive equality with `==`.
+ * @example
+ * assert.equal(1, 1, "one is one");
+ */
+ equal: function equal(actual, expected, message) {
+ if (actual == expected) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "=="
+ });
+ return false;
+ },
+
+ /**
+ * The non-equality assertion tests for whether two objects are not equal
+ * with `!=`.
+ * @example
+ * assert.notEqual(1, 2, "one is not two");
+ */
+ notEqual: function notEqual(actual, expected, message) {
+ if (actual != expected) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "!=",
+ });
+ return false;
+ },
+
+ /**
+ * The equivalence assertion tests a deep (with `===`) equality relation.
+ * @example
+ * assert.deepEqual({ a: "foo" }, { a: "foo" }, "equivalent objects")
+ */
+ deepEqual: function deepEqual(actual, expected, message) {
+ if (isDeepEqual(actual, expected)) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "deepEqual"
+ });
+ return false;
+ },
+
+ /**
+ * The non-equivalence assertion tests for any deep (with `===`) inequality.
+ * @example
+ * assert.notDeepEqual({ a: "foo" }, Object.create({ a: "foo" }),
+ * "object's inherit from different prototypes");
+ */
+ notDeepEqual: function notDeepEqual(actual, expected, message) {
+ if (!isDeepEqual(actual, expected)) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "notDeepEqual"
+ });
+ return false;
+ },
+
+ /**
+ * The strict equality assertion tests strict equality, as determined by
+ * `===`.
+ * @example
+ * assert.strictEqual(null, null, "`null` is `null`")
+ */
+ strictEqual: function strictEqual(actual, expected, message) {
+ if (actual === expected) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "==="
+ });
+ return false;
+ },
+
+ /**
+ * The strict non-equality assertion tests for strict inequality, as
+ * determined by `!==`.
+ * @example
+ * assert.notStrictEqual(null, undefined, "`null` is not `undefined`");
+ */
+ notStrictEqual: function notStrictEqual(actual, expected, message) {
+ if (actual !== expected) {
+ this.pass(message);
+ return true;
+ }
+
+ this.fail({
+ actual: actual,
+ expected: expected,
+ message: message,
+ operator: "!=="
+ });
+ return false;
+ },
+
+ /**
+ * The assertion whether or not given `block` throws an exception. If optional
+ * `Error` argument is provided and it's type of function thrown error is
+ * asserted to be an instance of it, if type of `Error` is string then message
+ * of throw exception is asserted to contain it.
+ * @param {Function} block
+ * Function that is expected to throw.
+ * @param {Error|RegExp} [Error]
+ * Error constructor that is expected to be thrown or a string that
+ * must be contained by a message of the thrown exception, or a RegExp
+ * matching a message of the thrown exception.
+ * @param {String} message
+ * Description message
+ *
+ * @examples
+ *
+ * assert.throws(function block() {
+ * doSomething(4)
+ * }, "Object is expected", "Incorrect argument is passed");
+ *
+ * assert.throws(function block() {
+ * Object.create(5)
+ * }, TypeError, "TypeError is thrown");
+ */
+ throws: function throws(block, Error, message) {
+ let threw = false;
+ let exception = null;
+
+ // If third argument is not provided and second argument is a string it
+ // means that optional `Error` argument was not passed, so we shift
+ // arguments.
+ if (isString(Error) && isUndefined(message)) {
+ message = Error;
+ Error = undefined;
+ }
+
+ // Executing given `block`.
+ try {
+ block();
+ }
+ catch (e) {
+ threw = true;
+ exception = e;
+ }
+
+ // If exception was thrown and `Error` argument was not passed assert is
+ // passed.
+ if (threw && (isUndefined(Error) ||
+ // If passed `Error` is RegExp using it's test method to
+ // assert thrown exception message.
+ (isRegExp(Error) && (Error.test(exception.message) || Error.test(exception.toString()))) ||
+ // If passed `Error` is a constructor function testing if
+ // thrown exception is an instance of it.
+ (isFunction(Error) && instanceOf(exception, Error))))
+ {
+ this.pass(message);
+ return true;
+ }
+
+ // Otherwise we report assertion failure.
+ let failure = {
+ message: message,
+ operator: "matches"
+ };
+
+ if (exception) {
+ failure.actual = exception.message || exception.toString();
+ }
+
+ if (Error) {
+ failure.expected = Error.toString();
+ }
+
+ this.fail(failure);
+ return false;
+ }
+};
+exports.Assert = Assert;
+
+function isDeepEqual(actual, expected) {
+ // 7.1. All identical values are equivalent, as determined by ===.
+ if (actual === expected) {
+ return true;
+ }
+
+ // 7.2. If the expected value is a Date object, the actual value is
+ // equivalent if it is also a Date object that refers to the same time.
+ else if (isDate(actual) && isDate(expected)) {
+ return actual.getTime() === expected.getTime();
+ }
+
+ // XXX specification bug: this should be specified
+ else if (isPrimitive(actual) || isPrimitive(expected)) {
+ return expected === actual;
+ }
+
+ // 7.3. Other pairs that do not both pass typeof value == "object",
+ // equivalence is determined by ==.
+ else if (!isObject(actual) && !isObject(expected)) {
+ return actual == expected;
+ }
+
+ // 7.4. For all other Object pairs, including Array objects, equivalence is
+ // determined by having the same number of owned properties (as verified
+ // with Object.prototype.hasOwnProperty.call), the same set of keys
+ // (although not necessarily the same order), equivalent values for every
+ // corresponding key, and an identical "prototype" property. Note: this
+ // accounts for both named and indexed properties on Arrays.
+ else {
+ return actual.prototype === expected.prototype &&
+ isEquivalent(actual, expected);
+ }
+}
+
+function isEquivalent(a, b, stack) {
+ let aKeys = Object.keys(a);
+ let bKeys = Object.keys(b);
+
+ return aKeys.length === bKeys.length &&
+ isArrayEquivalent(aKeys.sort(), bKeys.sort()) &&
+ aKeys.every(function(key) {
+ return isDeepEqual(a[key], b[key], stack)
+ });
+}
+
+function isArrayEquivalent(a, b, stack) {
+ return isArray(a) && isArray(b) &&
+ a.every(function(value, index) {
+ return isDeepEqual(value, b[index]);
+ });
+}
diff --git a/addon-sdk/source/lib/sdk/test/harness.js b/addon-sdk/source/lib/sdk/test/harness.js
new file mode 100644
index 000000000..1b31a1c79
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/harness.js
@@ -0,0 +1,645 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Cc, Ci, Cu } = require("chrome");
+const { Loader } = require('./loader');
+const { serializeStack, parseStack } = require("toolkit/loader");
+const { setTimeout } = require('../timers');
+const { PlainTextConsole } = require("../console/plain-text");
+const { when: unload } = require("../system/unload");
+const { format, fromException } = require("../console/traceback");
+const system = require("../system");
+const { gc: gcPromise } = require('./memory');
+const { defer } = require('../core/promise');
+const { extend } = require('../core/heritage');
+
+// Trick manifest builder to make it think we need these modules ?
+const unit = require("../deprecated/unit-test");
+const test = require("../../test");
+const url = require("../url");
+
+function emptyPromise() {
+ let { promise, resolve } = defer();
+ resolve();
+ return promise;
+}
+
+var cService = Cc['@mozilla.org/consoleservice;1'].getService(Ci.nsIConsoleService);
+
+// The console used to log messages
+var testConsole;
+
+// Cuddlefish loader in which we load and execute tests.
+var loader;
+
+// Function to call when we're done running tests.
+var onDone;
+
+// Function to print text to a console, w/o CR at the end.
+var print;
+
+// How many more times to run all tests.
+var iterationsLeft;
+
+// Whether to report memory profiling information.
+var profileMemory;
+
+// Whether we should stop as soon as a test reports a failure.
+var stopOnError;
+
+// Function to call to retrieve a list of tests to execute
+var findAndRunTests;
+
+// Combined information from all test runs.
+var results;
+
+// A list of the compartments and windows loaded after startup
+var startLeaks;
+
+// JSON serialization of last memory usage stats; we keep it stringified
+// so we don't actually change the memory usage stats (in terms of objects)
+// of the JSRuntime we're profiling.
+var lastMemoryUsage;
+
+function analyzeRawProfilingData(data) {
+ var graph = data.graph;
+ var shapes = {};
+
+ // Convert keys in the graph from strings to ints.
+ // TODO: Can we get rid of this ridiculousness?
+ var newGraph = {};
+ for (id in graph) {
+ newGraph[parseInt(id)] = graph[id];
+ }
+ graph = newGraph;
+
+ var modules = 0;
+ var moduleIds = [];
+ var moduleObjs = {UNKNOWN: 0};
+ for (let name in data.namedObjects) {
+ moduleObjs[name] = 0;
+ moduleIds[data.namedObjects[name]] = name;
+ modules++;
+ }
+
+ var count = 0;
+ for (id in graph) {
+ var parent = graph[id].parent;
+ while (parent) {
+ if (parent in moduleIds) {
+ var name = moduleIds[parent];
+ moduleObjs[name]++;
+ break;
+ }
+ if (!(parent in graph)) {
+ moduleObjs.UNKNOWN++;
+ break;
+ }
+ parent = graph[parent].parent;
+ }
+ count++;
+ }
+
+ print("\nobject count is " + count + " in " + modules + " modules" +
+ " (" + data.totalObjectCount + " across entire JS runtime)\n");
+ if (lastMemoryUsage) {
+ var last = JSON.parse(lastMemoryUsage);
+ var diff = {
+ moduleObjs: dictDiff(last.moduleObjs, moduleObjs),
+ totalObjectClasses: dictDiff(last.totalObjectClasses,
+ data.totalObjectClasses)
+ };
+
+ for (let name in diff.moduleObjs)
+ print(" " + diff.moduleObjs[name] + " in " + name + "\n");
+ for (let name in diff.totalObjectClasses)
+ print(" " + diff.totalObjectClasses[name] + " instances of " +
+ name + "\n");
+ }
+ lastMemoryUsage = JSON.stringify(
+ {moduleObjs: moduleObjs,
+ totalObjectClasses: data.totalObjectClasses}
+ );
+}
+
+function dictDiff(last, curr) {
+ var diff = {};
+
+ for (let name in last) {
+ var result = (curr[name] || 0) - last[name];
+ if (result)
+ diff[name] = (result > 0 ? "+" : "") + result;
+ }
+ for (let name in curr) {
+ var result = curr[name] - (last[name] || 0);
+ if (result)
+ diff[name] = (result > 0 ? "+" : "") + result;
+ }
+ return diff;
+}
+
+function reportMemoryUsage() {
+ if (!profileMemory) {
+ return emptyPromise();
+ }
+
+ return gcPromise().then((() => {
+ var mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
+ .getService(Ci.nsIMemoryReporterManager);
+ let count = 0;
+ function logReporter(process, path, kind, units, amount, description) {
+ print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n");
+ }
+ mgr.getReportsForThisProcess(logReporter, null, /* anonymize = */ false);
+ }));
+}
+
+var gWeakrefInfo;
+
+function checkMemory() {
+ return gcPromise().then(_ => {
+ let leaks = getPotentialLeaks();
+
+ let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) {
+ return !(url in startLeaks.compartments);
+ });
+
+ let windowURLs = Object.keys(leaks.windows).filter(function(url) {
+ return !(url in startLeaks.windows);
+ });
+
+ for (let url of compartmentURLs)
+ console.warn("LEAKED", leaks.compartments[url]);
+
+ for (let url of windowURLs)
+ console.warn("LEAKED", leaks.windows[url]);
+ }).then(showResults);
+}
+
+function showResults() {
+ let { promise, resolve } = defer();
+
+ if (gWeakrefInfo) {
+ gWeakrefInfo.forEach(
+ function(info) {
+ var ref = info.weakref.get();
+ if (ref !== null) {
+ var data = ref.__url__ ? ref.__url__ : ref;
+ var warning = data == "[object Object]"
+ ? "[object " + data.constructor.name + "(" +
+ Object.keys(data).join(", ") + ")]"
+ : data;
+ console.warn("LEAK", warning, info.bin);
+ }
+ }
+ );
+ }
+
+ onDone(results);
+
+ resolve();
+ return promise;
+}
+
+function cleanup() {
+ let coverObject = {};
+ try {
+ loader.unload();
+
+ if (loader.globals.console.errorsLogged && !results.failed) {
+ results.failed++;
+ console.error("warnings and/or errors were logged.");
+ }
+
+ if (consoleListener.errorsLogged && !results.failed) {
+ console.warn(consoleListener.errorsLogged + " " +
+ "warnings or errors were logged to the " +
+ "platform's nsIConsoleService, which could " +
+ "be of no consequence; however, they could also " +
+ "be indicative of aberrant behavior.");
+ }
+
+ // read the code coverage object, if it exists, from CoverJS-moz
+ if (typeof loader.globals.global == "object") {
+ coverObject = loader.globals.global['__$coverObject'] || {};
+ }
+
+ consoleListener.errorsLogged = 0;
+ loader = null;
+
+ consoleListener.unregister();
+
+ Cu.forceGC();
+ }
+ catch (e) {
+ results.failed++;
+ console.error("unload.send() threw an exception.");
+ console.exception(e);
+ };
+
+ setTimeout(require("./options").checkMemory ? checkMemory : showResults, 1);
+
+ // dump the coverobject
+ if (Object.keys(coverObject).length){
+ const self = require('sdk/self');
+ const {pathFor} = require("sdk/system");
+ let file = require('sdk/io/file');
+ const {env} = require('sdk/system/environment');
+ console.log("CWD:", env.PWD);
+ let out = file.join(env.PWD,'coverstats-'+self.id+'.json');
+ console.log('coverstats:', out);
+ let outfh = file.open(out,'w');
+ outfh.write(JSON.stringify(coverObject,null,2));
+ outfh.flush();
+ outfh.close();
+ }
+}
+
+function getPotentialLeaks() {
+ Cu.forceGC();
+
+ // Things we can assume are part of the platform and so aren't leaks
+ let GOOD_BASE_URLS = [
+ "chrome://",
+ "resource:///",
+ "resource://app/",
+ "resource://gre/",
+ "resource://gre-resources/",
+ "resource://pdf.js/",
+ "resource://pdf.js.components/",
+ "resource://services-common/",
+ "resource://services-crypto/",
+ "resource://services-sync/"
+ ];
+
+ let ioService = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ let uri = ioService.newURI("chrome://global/content/", "UTF-8", null);
+ let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIChromeRegistry);
+ uri = chromeReg.convertChromeURL(uri);
+ let spec = uri.spec;
+ let pos = spec.indexOf("!/");
+ GOOD_BASE_URLS.push(spec.substring(0, pos + 2));
+
+ let zoneRegExp = new RegExp("^explicit/js-non-window/zones/zone[^/]+/compartment\\((.+)\\)");
+ let compartmentRegexp = new RegExp("^explicit/js-non-window/compartments/non-window-global/compartment\\((.+)\\)/");
+ let compartmentDetails = new RegExp("^([^,]+)(?:, (.+?))?(?: \\(from: (.*)\\))?$");
+ let windowRegexp = new RegExp("^explicit/window-objects/top\\((.*)\\)/active");
+ let windowDetails = new RegExp("^(.*), id=.*$");
+
+ function isPossibleLeak(item) {
+ if (!item.location)
+ return false;
+
+ for (let url of GOOD_BASE_URLS) {
+ if (item.location.substring(0, url.length) == url) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ let compartments = {};
+ let windows = {};
+ function logReporter(process, path, kind, units, amount, description) {
+ let matches;
+
+ if ((matches = compartmentRegexp.exec(path)) || (matches = zoneRegExp.exec(path))) {
+ if (matches[1] in compartments)
+ return;
+
+ let details = compartmentDetails.exec(matches[1]);
+ if (!details) {
+ console.error("Unable to parse compartment detail " + matches[1]);
+ return;
+ }
+
+ let item = {
+ path: matches[1],
+ principal: details[1],
+ location: details[2] ? details[2].replace(/\\/g, "/") : undefined,
+ source: details[3] ? details[3].split(" -> ").reverse() : undefined,
+ toString: function() {
+ return this.location;
+ }
+ };
+
+ if (!isPossibleLeak(item))
+ return;
+
+ compartments[matches[1]] = item;
+ return;
+ }
+
+ if ((matches = windowRegexp.exec(path))) {
+ if (matches[1] in windows)
+ return;
+
+ let details = windowDetails.exec(matches[1]);
+ if (!details) {
+ console.error("Unable to parse window detail " + matches[1]);
+ return;
+ }
+
+ let item = {
+ path: matches[1],
+ location: details[1].replace(/\\/g, "/"),
+ source: [details[1].replace(/\\/g, "/")],
+ toString: function() {
+ return this.location;
+ }
+ };
+
+ if (!isPossibleLeak(item))
+ return;
+
+ windows[matches[1]] = item;
+ }
+ }
+
+ Cc["@mozilla.org/memory-reporter-manager;1"]
+ .getService(Ci.nsIMemoryReporterManager)
+ .getReportsForThisProcess(logReporter, null, /* anonymize = */ false);
+
+ return { compartments: compartments, windows: windows };
+}
+
+function nextIteration(tests) {
+ if (tests) {
+ results.passed += tests.passed;
+ results.failed += tests.failed;
+
+ reportMemoryUsage().then(_ => {
+ let testRun = [];
+ for (let test of tests.testRunSummary) {
+ let testCopy = {};
+ for (let info in test) {
+ testCopy[info] = test[info];
+ }
+ testRun.push(testCopy);
+ }
+
+ results.testRuns.push(testRun);
+ iterationsLeft--;
+
+ checkForEnd();
+ })
+ }
+ else {
+ checkForEnd();
+ }
+}
+
+function checkForEnd() {
+ if (iterationsLeft && (!stopOnError || results.failed == 0)) {
+ // Pass the loader which has a hooked console that doesn't dispatch
+ // errors to the JS console and avoid firing false alarm in our
+ // console listener
+ findAndRunTests(loader, nextIteration);
+ }
+ else {
+ setTimeout(cleanup, 0);
+ }
+}
+
+var POINTLESS_ERRORS = [
+ 'Invalid chrome URI:',
+ 'OpenGL LayerManager Initialized Succesfully.',
+ '[JavaScript Error: "TelemetryStopwatch:',
+ 'reference to undefined property',
+ '[JavaScript Error: "The character encoding of the HTML document was ' +
+ 'not declared.',
+ '[Javascript Warning: "Error: Failed to preserve wrapper of wrapped ' +
+ 'native weak map key',
+ '[JavaScript Warning: "Duplicate resource declaration for',
+ 'file: "chrome://browser/content/',
+ 'file: "chrome://global/content/',
+ '[JavaScript Warning: "The character encoding of a framed document was ' +
+ 'not declared.',
+ 'file: "chrome://browser/skin/'
+];
+
+// These are messages that will cause a test to fail if logged through the
+// console service
+var IMPORTANT_ERRORS = [
+ 'Sending message that cannot be cloned. Are you trying to send an XPCOM object?',
+];
+
+var consoleListener = {
+ registered: false,
+
+ register: function() {
+ if (this.registered)
+ return;
+ cService.registerListener(this);
+ this.registered = true;
+ },
+
+ unregister: function() {
+ if (!this.registered)
+ return;
+ cService.unregisterListener(this);
+ this.registered = false;
+ },
+
+ errorsLogged: 0,
+
+ observe: function(object) {
+ if (!(object instanceof Ci.nsIScriptError))
+ return;
+ this.errorsLogged++;
+ var message = object.QueryInterface(Ci.nsIConsoleMessage).message;
+ if (IMPORTANT_ERRORS.find(msg => message.indexOf(msg) >= 0)) {
+ testConsole.error(message);
+ return;
+ }
+ var pointless = POINTLESS_ERRORS.filter(err => message.indexOf(err) >= 0);
+ if (pointless.length == 0 && message)
+ testConsole.log(message);
+ }
+};
+
+function TestRunnerConsole(base, options) {
+ let proto = extend(base, {
+ errorsLogged: 0,
+ warn: function warn() {
+ this.errorsLogged++;
+ base.warn.apply(base, arguments);
+ },
+ error: function error() {
+ this.errorsLogged++;
+ base.error.apply(base, arguments);
+ },
+ info: function info(first) {
+ if (options.verbose)
+ base.info.apply(base, arguments);
+ else
+ if (first == "pass:")
+ print(".");
+ },
+ });
+ return Object.create(proto);
+}
+
+function stringify(arg) {
+ try {
+ return String(arg);
+ }
+ catch(ex) {
+ return "<toString() error>";
+ }
+}
+
+function stringifyArgs(args) {
+ return Array.map(args, stringify).join(" ");
+}
+
+function TestRunnerTinderboxConsole(base, options) {
+ this.base = base;
+ this.print = options.print;
+ this.verbose = options.verbose;
+ this.errorsLogged = 0;
+
+ // Binding all the public methods to an instance so that they can be used
+ // as callback / listener functions straightaway.
+ this.log = this.log.bind(this);
+ this.info = this.info.bind(this);
+ this.warn = this.warn.bind(this);
+ this.error = this.error.bind(this);
+ this.debug = this.debug.bind(this);
+ this.exception = this.exception.bind(this);
+ this.trace = this.trace.bind(this);
+};
+
+TestRunnerTinderboxConsole.prototype = {
+ testMessage: function testMessage(pass, expected, test, message) {
+ let type = "TEST-";
+ if (expected) {
+ if (pass)
+ type += "PASS";
+ else
+ type += "KNOWN-FAIL";
+ }
+ else {
+ this.errorsLogged++;
+ if (pass)
+ type += "UNEXPECTED-PASS";
+ else
+ type += "UNEXPECTED-FAIL";
+ }
+
+ this.print(type + " | " + test + " | " + message + "\n");
+ if (!expected)
+ this.trace();
+ },
+
+ log: function log() {
+ this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n");
+ },
+
+ info: function info(first) {
+ this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n");
+ },
+
+ warn: function warn() {
+ this.errorsLogged++;
+ this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n");
+ },
+
+ error: function error() {
+ this.errorsLogged++;
+ this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n");
+ this.base.error.apply(this.base, arguments);
+ },
+
+ debug: function debug() {
+ this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n");
+ },
+
+ exception: function exception(e) {
+ this.print("An exception occurred.\n" +
+ require("../console/traceback").format(e) + "\n" + e + "\n");
+ },
+
+ trace: function trace() {
+ var traceback = require("../console/traceback");
+ var stack = traceback.get();
+ stack.splice(-1, 1);
+ this.print("TEST-INFO | " + stringify(traceback.format(stack)) + "\n");
+ }
+};
+
+var runTests = exports.runTests = function runTests(options) {
+ iterationsLeft = options.iterations;
+ profileMemory = options.profileMemory;
+ stopOnError = options.stopOnError;
+ onDone = options.onDone;
+ print = options.print;
+ findAndRunTests = options.findAndRunTests;
+
+ results = {
+ passed: 0,
+ failed: 0,
+ testRuns: []
+ };
+
+ try {
+ consoleListener.register();
+ print("Running tests on " + system.name + " " + system.version +
+ "/Gecko " + system.platformVersion + " (Build " +
+ system.build + ") (" + system.id + ") under " +
+ system.platform + "/" + system.architecture + ".\n");
+
+ if (options.parseable)
+ testConsole = new TestRunnerTinderboxConsole(new PlainTextConsole(), options);
+ else
+ testConsole = new TestRunnerConsole(new PlainTextConsole(), options);
+
+ loader = Loader(module, {
+ console: testConsole,
+ global: {} // useful for storing things like coverage testing.
+ });
+
+ // Load these before getting initial leak stats as they will still be in
+ // memory when we check later
+ require("../deprecated/unit-test");
+ require("../deprecated/unit-test-finder");
+ if (profileMemory)
+ startLeaks = getPotentialLeaks();
+
+ nextIteration();
+ } catch (e) {
+ let frames = fromException(e).reverse().reduce(function(frames, frame) {
+ if (frame.fileName.split("/").pop() === "unit-test-finder.js")
+ frames.done = true
+ if (!frames.done) frames.push(frame)
+
+ return frames
+ }, [])
+
+ let prototype = typeof(e) === "object" ? e.constructor.prototype :
+ Error.prototype;
+ let stack = serializeStack(frames.reverse());
+
+ let error = Object.create(prototype, {
+ message: { value: e.message, writable: true, configurable: true },
+ fileName: { value: e.fileName, writable: true, configurable: true },
+ lineNumber: { value: e.lineNumber, writable: true, configurable: true },
+ stack: { value: stack, writable: true, configurable: true },
+ toString: { value: () => String(e), writable: true, configurable: true },
+ });
+
+ print("Error: " + error + " \n " + format(error));
+ onDone({passed: 0, failed: 1});
+ }
+};
+
+unload(_ => consoleListener.unregister());
diff --git a/addon-sdk/source/lib/sdk/test/httpd.js b/addon-sdk/source/lib/sdk/test/httpd.js
new file mode 100644
index 000000000..218493924
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/httpd.js
@@ -0,0 +1,6 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+throw new Error(`This file was removed. A copy can be obtained from:
+ https://github.com/mozilla/addon-sdk/blob/master/test/lib/httpd.js`);
diff --git a/addon-sdk/source/lib/sdk/test/loader.js b/addon-sdk/source/lib/sdk/test/loader.js
new file mode 100644
index 000000000..33ba2ca5a
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/loader.js
@@ -0,0 +1,123 @@
+/* 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 { resolveURI, Require,
+ unload, override, descriptor } = require('../../toolkit/loader');
+const { ensure } = require('../system/unload');
+const addonWindow = require('../addon/window');
+const { PlainTextConsole } = require('sdk/console/plain-text');
+
+var defaultGlobals = override(require('../system/globals'), {
+ console: console
+});
+
+function CustomLoader(module, globals, packaging, overrides={}) {
+ let options = packaging || require("@loader/options");
+ options = override(options, {
+ id: overrides.id || options.id,
+ globals: override(defaultGlobals, globals || {}),
+ modules: override(override(options.modules || {}, overrides.modules || {}), {
+ 'sdk/addon/window': addonWindow
+ })
+ });
+
+ let loaderModule = options.isNative ? '../../toolkit/loader' : '../loader/cuddlefish';
+ let { Loader } = require(loaderModule);
+ let loader = Loader(options);
+ let wrapper = Object.create(loader, descriptor({
+ require: Require(loader, module),
+ sandbox: function(id) {
+ let requirement = loader.resolve(id, module.id);
+ if (!requirement)
+ requirement = id;
+ let uri = resolveURI(requirement, loader.mapping);
+ return loader.sandboxes[uri];
+ },
+ unload: function(reason) {
+ unload(loader, reason);
+ }
+ }));
+ ensure(wrapper);
+ return wrapper;
+};
+exports.Loader = CustomLoader;
+
+function HookedPlainTextConsole(hook, print, innerID) {
+ this.log = hook.bind(null, "log", innerID);
+ this.info = hook.bind(null, "info", innerID);
+ this.warn = hook.bind(null, "warn", innerID);
+ this.error = hook.bind(null, "error", innerID);
+ this.debug = hook.bind(null, "debug", innerID);
+ this.exception = hook.bind(null, "exception", innerID);
+ this.time = hook.bind(null, "time", innerID);
+ this.timeEnd = hook.bind(null, "timeEnd", innerID);
+
+ this.__exposedProps__ = {
+ log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
+ exception: "rw", time: "rw", timeEnd: "rw"
+ };
+}
+
+// Creates a custom loader instance whose console module is hooked in order
+// to avoid printing messages to the console, and instead, expose them in the
+// returned `messages` array attribute
+exports.LoaderWithHookedConsole = function (module, callback) {
+ let messages = [];
+ function hook(type, innerID, msg) {
+ messages.push({ type: type, msg: msg, innerID: innerID });
+ if (callback)
+ callback(type, msg, innerID);
+ }
+
+ return {
+ loader: CustomLoader(module, {
+ console: new HookedPlainTextConsole(hook, null, null)
+ }, null, {
+ modules: {
+ 'sdk/console/plain-text': {
+ PlainTextConsole: HookedPlainTextConsole.bind(null, hook)
+ }
+ }
+ }),
+ messages: messages
+ };
+}
+
+// Same than LoaderWithHookedConsole with lower level, instead we get what is
+// actually printed to the command line console
+exports.LoaderWithHookedConsole2 = function (module, callback) {
+ let messages = [];
+ return {
+ loader: CustomLoader(module, {
+ console: new PlainTextConsole(function (msg) {
+ messages.push(msg);
+ if (callback)
+ callback(msg);
+ })
+ }),
+ messages: messages
+ };
+}
+
+// Creates a custom loader with a filtered console. The callback is passed every
+// console message type and message and if it returns false the message will
+// not be logged normally
+exports.LoaderWithFilteredConsole = function (module, callback) {
+ function hook(type, innerID, msg) {
+ if (callback && callback(type, msg, innerID) == false)
+ return;
+ console[type](msg);
+ }
+
+ return CustomLoader(module, {
+ console: new HookedPlainTextConsole(hook, null, null)
+ }, null, {
+ modules: {
+ 'sdk/console/plain-text': {
+ PlainTextConsole: HookedPlainTextConsole.bind(null, hook)
+ }
+ }
+ });
+}
diff --git a/addon-sdk/source/lib/sdk/test/memory.js b/addon-sdk/source/lib/sdk/test/memory.js
new file mode 100644
index 000000000..bd1198bfe
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/memory.js
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const { Cu } = require("chrome");
+
+function gc() {
+ return new Promise(resolve => Cu.schedulePreciseGC(resolve));
+}
+exports.gc = gc;
diff --git a/addon-sdk/source/lib/sdk/test/options.js b/addon-sdk/source/lib/sdk/test/options.js
new file mode 100644
index 000000000..9bc611ca5
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/options.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const options = require("@test/options");
+const { id } = require("../self");
+const { get } = require("../preferences/service");
+
+const readPref = (key) => get("extensions." + id + ".sdk." + key);
+
+exports.iterations = readPref("test.iterations") || options.iterations;
+exports.filter = readPref("test.filter") || options.filter;
+exports.profileMemory = readPref("profile.memory") || options.profileMemory;
+exports.stopOnError = readPref("test.stop") || options.stopOnError;
+exports.keepOpen = readPref("test.keepOpen") || false;
+exports.verbose = (readPref("output.logLevel") == "verbose") || options.verbose;
+exports.parseable = (readPref("output.format") == "tbpl") || options.parseable;
+exports.checkMemory = readPref("profile.leaks") || options.check_memory;
diff --git a/addon-sdk/source/lib/sdk/test/runner.js b/addon-sdk/source/lib/sdk/test/runner.js
new file mode 100644
index 000000000..ea37ac84f
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/runner.js
@@ -0,0 +1,131 @@
+/* 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 { exit, stdout } = require("../system");
+var cfxArgs = require("../test/options");
+var events = require("../system/events");
+const { resolve } = require("../core/promise");
+
+function runTests(findAndRunTests) {
+ var harness = require("./harness");
+
+ function onDone(tests) {
+ stdout.write("\n");
+ var total = tests.passed + tests.failed;
+ stdout.write(tests.passed + " of " + total + " tests passed.\n");
+
+ events.emit("sdk:test:results", { data: JSON.stringify(tests) });
+
+ if (tests.failed == 0) {
+ if (tests.passed === 0)
+ stdout.write("No tests were run\n");
+ if (!cfxArgs.keepOpen)
+ exit(0);
+ } else {
+ if (cfxArgs.verbose || cfxArgs.parseable)
+ printFailedTests(tests, stdout.write);
+ if (!cfxArgs.keepOpen)
+ exit(1);
+ }
+ };
+
+ // We may have to run test on next cycle, otherwise XPCOM components
+ // are not correctly updated.
+ // For ex: nsIFocusManager.getFocusedElementForWindow may throw
+ // NS_ERROR_ILLEGAL_VALUE exception.
+ require("../timers").setTimeout(_ => harness.runTests({
+ findAndRunTests: findAndRunTests,
+ iterations: cfxArgs.iterations || 1,
+ filter: cfxArgs.filter,
+ profileMemory: cfxArgs.profileMemory,
+ stopOnError: cfxArgs.stopOnError,
+ verbose: cfxArgs.verbose,
+ parseable: cfxArgs.parseable,
+ print: stdout.write,
+ onDone: onDone
+ }));
+}
+
+function printFailedTests(tests, print) {
+ let iterationNumber = 0;
+ let singleIteration = (tests.testRuns || []).length == 1;
+ let padding = singleIteration ? "" : " ";
+
+ print("\nThe following tests failed:\n");
+
+ for (let testRun of tests.testRuns) {
+ iterationNumber++;
+
+ if (!singleIteration)
+ print(" Iteration " + iterationNumber + ":\n");
+
+ for (let test of testRun) {
+ if (test.failed > 0) {
+ print(padding + " " + test.name + ": " + test.errors +"\n");
+ }
+ }
+ print("\n");
+ }
+}
+
+function main() {
+ var testsStarted = false;
+
+ if (!testsStarted) {
+ testsStarted = true;
+ runTests(function findAndRunTests(loader, nextIteration) {
+ loader.require("../deprecated/unit-test").findAndRunTests({
+ testOutOfProcess: false,
+ testInProcess: true,
+ stopOnError: cfxArgs.stopOnError,
+ filter: cfxArgs.filter,
+ onDone: nextIteration
+ });
+ });
+ }
+};
+
+if (require.main === module)
+ main();
+
+exports.runTestsFromModule = function runTestsFromModule(module) {
+ let id = module.id;
+ // Make a copy of exports as it may already be frozen by module loader
+ let exports = {};
+ Object.keys(module.exports).forEach(key => {
+ exports[key] = module.exports[key];
+ });
+
+ runTests(function findAndRunTests(loader, nextIteration) {
+ // Consider that all these tests are CommonJS ones
+ loader.require('../../test').run(exports);
+
+ // Reproduce what is done in sdk/deprecated/unit-test-finder.findTests()
+ let tests = [];
+ for (let name of Object.keys(exports).sort()) {
+ tests.push({
+ setup: exports.setup,
+ teardown: exports.teardown,
+ testFunction: exports[name],
+ name: id + "." + name
+ });
+ }
+
+ // Reproduce what is done by unit-test.findAndRunTests()
+ var { TestRunner } = loader.require("../deprecated/unit-test");
+ var runner = new TestRunner();
+ runner.startMany({
+ tests: {
+ getNext: () => resolve(tests.shift())
+ },
+ stopOnError: cfxArgs.stopOnError,
+ onDone: nextIteration
+ });
+ });
+}
diff --git a/addon-sdk/source/lib/sdk/test/utils.js b/addon-sdk/source/lib/sdk/test/utils.js
new file mode 100644
index 000000000..b01df67d4
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/utils.js
@@ -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': 'unstable'
+};
+
+const { defer } = require('../core/promise');
+const { setInterval, clearInterval } = require('../timers');
+const { getTabs, closeTab } = require("../tabs/utils");
+const { windows: getWindows } = require("../window/utils");
+const { close: closeWindow } = require("../window/helpers");
+const { isGenerator } = require("../lang/type");
+const { env } = require("../system/environment");
+const { Task } = require("resource://gre/modules/Task.jsm");
+
+const getTestNames = (exports) =>
+ Object.keys(exports).filter(name => /^test/.test(name));
+
+const isTestAsync = ({length}) => length > 1;
+const isHelperAsync = ({length}) => length > 2;
+
+/*
+ * Takes an `exports` object of a test file and a function `beforeFn`
+ * to be run before each test. `beforeFn` is called with a `name` string
+ * as the first argument of the test name, and may specify a second
+ * argument function `done` to indicate that this function should
+ * resolve asynchronously
+ */
+function before (exports, beforeFn) {
+ getTestNames(exports).map(name => {
+ let testFn = exports[name];
+
+ // GENERATOR TESTS
+ if (isGenerator(testFn) && isGenerator(beforeFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(beforeFn.bind(null, name, assert));
+ yield Task.spawn(testFn.bind(null, assert));
+ }
+ }
+ else if (isGenerator(testFn) && !isHelperAsync(beforeFn)) {
+ exports[name] = function*(assert) {
+ beforeFn(name, assert);
+ yield Task.spawn(testFn.bind(null, assert));
+ }
+ }
+ else if (isGenerator(testFn) && isHelperAsync(beforeFn)) {
+ exports[name] = function*(assert) {
+ yield new Promise(resolve => beforeFn(name, assert, resolve));
+ yield Task.spawn(testFn.bind(null, assert));
+ }
+ }
+ // SYNC TESTS
+ else if (!isTestAsync(testFn) && isGenerator(beforeFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(beforeFn.bind(null, name, assert));
+ testFn(assert);
+ };
+ }
+ else if (!isTestAsync(testFn) && !isHelperAsync(beforeFn)) {
+ exports[name] = function (assert) {
+ beforeFn(name, assert);
+ testFn(assert);
+ };
+ }
+ else if (!isTestAsync(testFn) && isHelperAsync(beforeFn)) {
+ exports[name] = function (assert, done) {
+ beforeFn(name, assert, () => {
+ testFn(assert);
+ done();
+ });
+ };
+ }
+ // ASYNC TESTS
+ else if (isTestAsync(testFn) && isGenerator(beforeFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(beforeFn.bind(null, name, assert));
+ yield new Promise(resolve => testFn(assert, resolve));
+ };
+ }
+ else if (isTestAsync(testFn) && !isHelperAsync(beforeFn)) {
+ exports[name] = function (assert, done) {
+ beforeFn(name, assert);
+ testFn(assert, done);
+ };
+ }
+ else if (isTestAsync(testFn) && isHelperAsync(beforeFn)) {
+ exports[name] = function (assert, done) {
+ beforeFn(name, assert, () => {
+ testFn(assert, done);
+ });
+ };
+ }
+ });
+}
+exports.before = before;
+
+/*
+ * Takes an `exports` object of a test file and a function `afterFn`
+ * to be run after each test. `afterFn` is called with a `name` string
+ * as the first argument of the test name, and may specify a second
+ * argument function `done` to indicate that this function should
+ * resolve asynchronously
+ */
+function after (exports, afterFn) {
+ getTestNames(exports).map(name => {
+ let testFn = exports[name];
+
+ // GENERATOR TESTS
+ if (isGenerator(testFn) && isGenerator(afterFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(testFn.bind(null, assert));
+ yield Task.spawn(afterFn.bind(null, name, assert));
+ }
+ }
+ else if (isGenerator(testFn) && !isHelperAsync(afterFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(testFn.bind(null, assert));
+ afterFn(name, assert);
+ }
+ }
+ else if (isGenerator(testFn) && isHelperAsync(afterFn)) {
+ exports[name] = function*(assert) {
+ yield Task.spawn(testFn.bind(null, assert));
+ yield new Promise(resolve => afterFn(name, assert, resolve));
+ }
+ }
+ // SYNC TESTS
+ else if (!isTestAsync(testFn) && isGenerator(afterFn)) {
+ exports[name] = function*(assert) {
+ testFn(assert);
+ yield Task.spawn(afterFn.bind(null, name, assert));
+ };
+ }
+ else if (!isTestAsync(testFn) && !isHelperAsync(afterFn)) {
+ exports[name] = function (assert) {
+ testFn(assert);
+ afterFn(name, assert);
+ };
+ }
+ else if (!isTestAsync(testFn) && isHelperAsync(afterFn)) {
+ exports[name] = function (assert, done) {
+ testFn(assert);
+ afterFn(name, assert, done);
+ };
+ }
+ // ASYNC TESTS
+ else if (isTestAsync(testFn) && isGenerator(afterFn)) {
+ exports[name] = function*(assert) {
+ yield new Promise(resolve => testFn(assert, resolve));
+ yield Task.spawn(afterFn.bind(null, name, assert));
+ };
+ }
+ else if (isTestAsync(testFn) && !isHelperAsync(afterFn)) {
+ exports[name] = function*(assert) {
+ yield new Promise(resolve => testFn(assert, resolve));
+ afterFn(name, assert);
+ };
+ }
+ else if (isTestAsync(testFn) && isHelperAsync(afterFn)) {
+ exports[name] = function*(assert) {
+ yield new Promise(resolve => testFn(assert, resolve));
+ yield new Promise(resolve => afterFn(name, assert, resolve));
+ };
+ }
+ });
+}
+exports.after = after;
+
+function waitUntil (predicate, delay) {
+ let { promise, resolve } = defer();
+ let interval = setInterval(() => {
+ if (!predicate()) return;
+ clearInterval(interval);
+ resolve();
+ }, delay || 10);
+ return promise;
+}
+exports.waitUntil = waitUntil;
+
+var cleanUI = function cleanUI() {
+ let { promise, resolve } = defer();
+
+ let windows = getWindows(null, { includePrivate: true });
+ if (windows.length > 1) {
+ return closeWindow(windows[1]).then(cleanUI);
+ }
+
+ getTabs(windows[0]).slice(1).forEach(closeTab);
+
+ resolve();
+
+ return promise;
+}
+exports.cleanUI = cleanUI;
+
+exports.isTravisCI = ("TRAVIS" in env && "CI" in env);
diff --git a/addon-sdk/source/lib/sdk/timers.js b/addon-sdk/source/lib/sdk/timers.js
new file mode 100644
index 000000000..e97db01f2
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/timers.js
@@ -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": "stable"
+};
+
+const { CC, Cc, Ci } = require("chrome");
+const { when: unload } = require("./system/unload");
+
+const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
+const Timer = CC("@mozilla.org/timer;1", "nsITimer");
+const timers = Object.create(null);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].
+ getService(Ci.nsIThreadManager);
+const prefBranch = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService).
+ QueryInterface(Ci.nsIPrefBranch);
+
+var MIN_DELAY = 4;
+// Try to get min timeout delay used by browser.
+try { MIN_DELAY = prefBranch.getIntPref("dom.min_timeout_value"); } finally {}
+
+
+// Last timer id.
+var lastID = 0;
+
+// Sets typer either by timeout or by interval
+// depending on a given type.
+function setTimer(type, callback, delay, ...args) {
+ let id = ++ lastID;
+ let timer = timers[id] = Timer();
+ timer.initWithCallback({
+ notify: function notify() {
+ try {
+ if (type === TYPE_ONE_SHOT)
+ delete timers[id];
+ callback.apply(null, args);
+ }
+ catch(error) {
+ console.exception(error);
+ }
+ }
+ }, Math.max(delay || MIN_DELAY), type);
+ return id;
+}
+
+function unsetTimer(id) {
+ let timer = timers[id];
+ delete timers[id];
+ if (timer) timer.cancel();
+}
+
+var immediates = new Map();
+
+var dispatcher = _ => {
+ // Allow scheduling of a new dispatch loop.
+ dispatcher.scheduled = false;
+ // Take a snapshot of timer `id`'s that have being present before
+ // starting a dispatch loop, in order to ignore timers registered
+ // in side effect to dispatch while also skipping immediates that
+ // were removed in side effect.
+ let ids = [...immediates.keys()];
+ for (let id of ids) {
+ let immediate = immediates.get(id);
+ if (immediate) {
+ immediates.delete(id);
+ try { immediate(); }
+ catch (error) { console.exception(error); }
+ }
+ }
+}
+
+function setImmediate(callback, ...params) {
+ let id = ++ lastID;
+ // register new immediate timer with curried params.
+ immediates.set(id, _ => callback.apply(callback, params));
+ // if dispatch loop is not scheduled schedule one. Own scheduler
+ if (!dispatcher.scheduled) {
+ dispatcher.scheduled = true;
+ threadManager.currentThread.dispatch(dispatcher,
+ Ci.nsIThread.DISPATCH_NORMAL);
+ }
+ return id;
+}
+
+function clearImmediate(id) {
+ immediates.delete(id);
+}
+
+// Bind timers so that toString-ing them looks same as on native timers.
+exports.setImmediate = setImmediate.bind(null);
+exports.clearImmediate = clearImmediate.bind(null);
+exports.setTimeout = setTimer.bind(null, TYPE_ONE_SHOT);
+exports.setInterval = setTimer.bind(null, TYPE_REPEATING_SLACK);
+exports.clearTimeout = unsetTimer.bind(null);
+exports.clearInterval = unsetTimer.bind(null);
+
+// all timers are cleared out on unload.
+unload(function() {
+ immediates.clear();
+ Object.keys(timers).forEach(unsetTimer)
+});
diff --git a/addon-sdk/source/lib/sdk/ui.js b/addon-sdk/source/lib/sdk/ui.js
new file mode 100644
index 000000000..7f9110b26
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ 'stability': 'experimental',
+ 'engines': {
+ 'Firefox': '> 28'
+ }
+};
+
+exports.ActionButton = require('./ui/button/action').ActionButton;
+exports.ToggleButton = require('./ui/button/toggle').ToggleButton;
+exports.Sidebar = require('./ui/sidebar').Sidebar;
+exports.Frame = require('./ui/frame').Frame;
+exports.Toolbar = require('./ui/toolbar').Toolbar;
diff --git a/addon-sdk/source/lib/sdk/ui/button/action.js b/addon-sdk/source/lib/sdk/ui/button/action.js
new file mode 100644
index 000000000..dfb092d0c
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/button/action.js
@@ -0,0 +1,114 @@
+/* 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',
+ 'engines': {
+ 'Firefox': '> 28'
+ }
+};
+
+const { Class } = require('../../core/heritage');
+const { merge } = require('../../util/object');
+const { Disposable } = require('../../core/disposable');
+const { on, off, emit, setListeners } = require('../../event/core');
+const { EventTarget } = require('../../event/target');
+const { getNodeView } = require('../../view/core');
+
+const view = require('./view');
+const { buttonContract, stateContract } = require('./contract');
+const { properties, render, state, register, unregister,
+ getDerivedStateFor } = require('../state');
+const { events: stateEvents } = require('../state/events');
+const { events: viewEvents } = require('./view/events');
+const events = require('../../event/utils');
+
+const { getActiveTab } = require('../../tabs/utils');
+
+const { id: addonID } = require('../../self');
+const { identify } = require('../id');
+
+const buttons = new Map();
+
+const toWidgetId = id =>
+ ('action-button--' + addonID.toLowerCase()+ '-' + id).
+ replace(/[^a-z0-9_-]/g, '');
+
+const ActionButton = Class({
+ extends: EventTarget,
+ implements: [
+ properties(stateContract),
+ state(stateContract),
+ Disposable
+ ],
+ setup: function setup(options) {
+ let state = merge({
+ disabled: false
+ }, buttonContract(options));
+
+ let id = toWidgetId(options.id);
+
+ register(this, state);
+
+ // Setup listeners.
+ setListeners(this, options);
+
+ buttons.set(id, this);
+
+ view.create(merge({}, state, { id: id }));
+ },
+
+ dispose: function dispose() {
+ let id = toWidgetId(this.id);
+ buttons.delete(id);
+
+ off(this);
+
+ view.dispose(id);
+
+ unregister(this);
+ },
+
+ get id() {
+ return this.state().id;
+ },
+
+ click: function click() { view.click(toWidgetId(this.id)) }
+});
+exports.ActionButton = ActionButton;
+
+identify.define(ActionButton, ({id}) => toWidgetId(id));
+
+getNodeView.define(ActionButton, button =>
+ view.nodeFor(toWidgetId(button.id))
+);
+
+var actionButtonStateEvents = events.filter(stateEvents,
+ e => e.target instanceof ActionButton);
+
+var actionButtonViewEvents = events.filter(viewEvents,
+ e => buttons.has(e.target));
+
+var clickEvents = events.filter(actionButtonViewEvents, e => e.type === 'click');
+var updateEvents = events.filter(actionButtonViewEvents, e => e.type === 'update');
+
+on(clickEvents, 'data', ({target: id, window}) => {
+ let button = buttons.get(id);
+ let state = getDerivedStateFor(button, getActiveTab(window));
+
+ emit(button, 'click', state);
+});
+
+on(updateEvents, 'data', ({target: id, window}) => {
+ render(buttons.get(id), window);
+});
+
+on(actionButtonStateEvents, 'data', ({target, window, state}) => {
+ let id = toWidgetId(target.id);
+ view.setIcon(id, window, state.icon);
+ view.setLabel(id, window, state.label);
+ view.setDisabled(id, window, state.disabled);
+ view.setBadge(id, window, state.badge, state.badgeColor);
+});
diff --git a/addon-sdk/source/lib/sdk/ui/button/contract.js b/addon-sdk/source/lib/sdk/ui/button/contract.js
new file mode 100644
index 000000000..ce6e33d95
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/button/contract.js
@@ -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 { contract } = require('../../util/contract');
+const { isLocalURL } = require('../../url');
+const { isNil, isObject, isString } = require('../../lang/type');
+const { required, either, string, boolean, object, number } = require('../../deprecated/api-utils');
+const { merge } = require('../../util/object');
+const { freeze } = Object;
+
+const isIconSet = (icons) =>
+ Object.keys(icons).
+ every(size => String(size >>> 0) === size && isLocalURL(icons[size]));
+
+var iconSet = {
+ is: either(object, string),
+ map: v => isObject(v) ? freeze(merge({}, v)) : v,
+ ok: v => (isString(v) && isLocalURL(v)) || (isObject(v) && isIconSet(v)),
+ msg: 'The option "icon" must be a local URL or an object with ' +
+ 'numeric keys / local URL values pair.'
+}
+
+var id = {
+ is: string,
+ ok: v => /^[a-z-_][a-z0-9-_]*$/i.test(v),
+ msg: 'The option "id" must be a valid alphanumeric id (hyphens and ' +
+ 'underscores are allowed).'
+};
+
+var label = {
+ is: string,
+ ok: v => isNil(v) || v.trim().length > 0,
+ msg: 'The option "label" must be a non empty string'
+}
+
+var badge = {
+ is: either(string, number),
+ msg: 'The option "badge" must be a string or a number'
+}
+
+var badgeColor = {
+ is: string,
+ msg: 'The option "badgeColor" must be a string'
+}
+
+var stateContract = contract({
+ label: label,
+ icon: iconSet,
+ disabled: boolean,
+ badge: badge,
+ badgeColor: badgeColor
+});
+
+exports.stateContract = stateContract;
+
+var buttonContract = contract(merge({}, stateContract.rules, {
+ id: required(id),
+ label: required(label),
+ icon: required(iconSet)
+}));
+
+exports.buttonContract = buttonContract;
+
+exports.toggleStateContract = contract(merge({
+ checked: boolean
+}, stateContract.rules));
+
+exports.toggleButtonContract = contract(merge({
+ checked: boolean
+}, buttonContract.rules));
+
diff --git a/addon-sdk/source/lib/sdk/ui/button/toggle.js b/addon-sdk/source/lib/sdk/ui/button/toggle.js
new file mode 100644
index 000000000..a226b3212
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/button/toggle.js
@@ -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': 'experimental',
+ 'engines': {
+ 'Firefox': '> 28'
+ }
+};
+
+const { Class } = require('../../core/heritage');
+const { merge } = require('../../util/object');
+const { Disposable } = require('../../core/disposable');
+const { on, off, emit, setListeners } = require('../../event/core');
+const { EventTarget } = require('../../event/target');
+const { getNodeView } = require('../../view/core');
+
+const view = require('./view');
+const { toggleButtonContract, toggleStateContract } = require('./contract');
+const { properties, render, state, register, unregister,
+ setStateFor, getStateFor, getDerivedStateFor } = require('../state');
+const { events: stateEvents } = require('../state/events');
+const { events: viewEvents } = require('./view/events');
+const events = require('../../event/utils');
+
+const { getActiveTab } = require('../../tabs/utils');
+
+const { id: addonID } = require('../../self');
+const { identify } = require('../id');
+
+const buttons = new Map();
+
+const toWidgetId = id =>
+ ('toggle-button--' + addonID.toLowerCase()+ '-' + id).
+ replace(/[^a-z0-9_-]/g, '');
+
+const ToggleButton = Class({
+ extends: EventTarget,
+ implements: [
+ properties(toggleStateContract),
+ state(toggleStateContract),
+ Disposable
+ ],
+ setup: function setup(options) {
+ let state = merge({
+ disabled: false,
+ checked: false
+ }, toggleButtonContract(options));
+
+ let id = toWidgetId(options.id);
+
+ register(this, state);
+
+ // Setup listeners.
+ setListeners(this, options);
+
+ buttons.set(id, this);
+
+ view.create(merge({ type: 'checkbox' }, state, { id: id }));
+ },
+
+ dispose: function dispose() {
+ let id = toWidgetId(this.id);
+ buttons.delete(id);
+
+ off(this);
+
+ view.dispose(id);
+
+ unregister(this);
+ },
+
+ get id() {
+ return this.state().id;
+ },
+
+ click: function click() {
+ return view.click(toWidgetId(this.id));
+ }
+});
+exports.ToggleButton = ToggleButton;
+
+identify.define(ToggleButton, ({id}) => toWidgetId(id));
+
+getNodeView.define(ToggleButton, button =>
+ view.nodeFor(toWidgetId(button.id))
+);
+
+var toggleButtonStateEvents = events.filter(stateEvents,
+ e => e.target instanceof ToggleButton);
+
+var toggleButtonViewEvents = events.filter(viewEvents,
+ e => buttons.has(e.target));
+
+var clickEvents = events.filter(toggleButtonViewEvents, e => e.type === 'click');
+var updateEvents = events.filter(toggleButtonViewEvents, e => e.type === 'update');
+
+on(toggleButtonStateEvents, 'data', ({target, window, state}) => {
+ let id = toWidgetId(target.id);
+
+ view.setIcon(id, window, state.icon);
+ view.setLabel(id, window, state.label);
+ view.setDisabled(id, window, state.disabled);
+ view.setChecked(id, window, state.checked);
+ view.setBadge(id, window, state.badge, state.badgeColor);
+});
+
+on(clickEvents, 'data', ({target: id, window, checked }) => {
+ let button = buttons.get(id);
+ let windowState = getStateFor(button, window);
+
+ let newWindowState = merge({}, windowState, { checked: checked });
+
+ setStateFor(button, window, newWindowState);
+
+ let state = getDerivedStateFor(button, getActiveTab(window));
+
+ emit(button, 'click', state);
+
+ emit(button, 'change', state);
+});
+
+on(updateEvents, 'data', ({target: id, window}) => {
+ render(buttons.get(id), window);
+});
diff --git a/addon-sdk/source/lib/sdk/ui/button/view.js b/addon-sdk/source/lib/sdk/ui/button/view.js
new file mode 100644
index 000000000..63b7aea31
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/button/view.js
@@ -0,0 +1,243 @@
+/* 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',
+ 'engines': {
+ 'Firefox': '> 28'
+ }
+};
+
+const { Cu } = require('chrome');
+const { on, off, emit } = require('../../event/core');
+
+const { data } = require('sdk/self');
+
+const { isObject, isNil } = require('../../lang/type');
+
+const { getMostRecentBrowserWindow } = require('../../window/utils');
+const { ignoreWindow } = require('../../private-browsing/utils');
+const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+const { AREA_PANEL, AREA_NAVBAR } = CustomizableUI;
+
+const { events: viewEvents } = require('./view/events');
+
+const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+
+const views = new Map();
+const customizedWindows = new WeakMap();
+
+const buttonListener = {
+ onCustomizeStart: window => {
+ for (let [id, view] of views) {
+ setIcon(id, window, view.icon);
+ setLabel(id, window, view.label);
+ }
+
+ customizedWindows.set(window, true);
+ },
+ onCustomizeEnd: window => {
+ customizedWindows.delete(window);
+
+ for (let [id, ] of views) {
+ let placement = CustomizableUI.getPlacementOfWidget(id);
+
+ if (placement)
+ emit(viewEvents, 'data', { type: 'update', target: id, window: window });
+ }
+ },
+ onWidgetAfterDOMChange: (node, nextNode, container) => {
+ let { id } = node;
+ let view = views.get(id);
+ let window = node.ownerDocument.defaultView;
+
+ if (view) {
+ emit(viewEvents, 'data', { type: 'update', target: id, window: window });
+ }
+ }
+};
+
+CustomizableUI.addListener(buttonListener);
+
+require('../../system/unload').when( _ =>
+ CustomizableUI.removeListener(buttonListener)
+);
+
+function getNode(id, window) {
+ return !views.has(id) || ignoreWindow(window)
+ ? null
+ : CustomizableUI.getWidget(id).forWindow(window).node
+};
+
+function isInToolbar(id) {
+ let placement = CustomizableUI.getPlacementOfWidget(id);
+
+ return placement && CustomizableUI.getAreaType(placement.area) === 'toolbar';
+}
+
+
+function getImage(icon, isInToolbar, pixelRatio) {
+ let targetSize = (isInToolbar ? 18 : 32) * pixelRatio;
+ let bestSize = 0;
+ let image = icon;
+
+ if (isObject(icon)) {
+ for (let size of Object.keys(icon)) {
+ size = +size;
+ let offset = targetSize - size;
+
+ if (offset === 0) {
+ bestSize = size;
+ break;
+ }
+
+ let delta = Math.abs(offset) - Math.abs(targetSize - bestSize);
+
+ if (delta < 0)
+ bestSize = size;
+ }
+
+ image = icon[bestSize];
+ }
+
+ if (image.indexOf('./') === 0)
+ return data.url(image.substr(2));
+
+ return image;
+}
+
+function nodeFor(id, window=getMostRecentBrowserWindow()) {
+ return customizedWindows.has(window) ? null : getNode(id, window);
+};
+exports.nodeFor = nodeFor;
+
+function create(options) {
+ let { id, label, icon, type, badge } = options;
+
+ if (views.has(id))
+ throw new Error('The ID "' + id + '" seems already used.');
+
+ CustomizableUI.createWidget({
+ id: id,
+ type: 'custom',
+ removable: true,
+ defaultArea: AREA_NAVBAR,
+ allowedAreas: [ AREA_PANEL, AREA_NAVBAR ],
+
+ onBuild: function(document) {
+ let window = document.defaultView;
+
+ let node = document.createElementNS(XUL_NS, 'toolbarbutton');
+
+ let image = getImage(icon, true, window.devicePixelRatio);
+
+ if (ignoreWindow(window))
+ node.style.display = 'none';
+
+ node.setAttribute('id', this.id);
+ node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional badged-button');
+ node.setAttribute('type', type);
+ node.setAttribute('label', label);
+ node.setAttribute('tooltiptext', label);
+ node.setAttribute('image', image);
+ node.setAttribute('constrain-size', 'true');
+
+ views.set(id, {
+ area: this.currentArea,
+ icon: icon,
+ label: label
+ });
+
+ node.addEventListener('command', function(event) {
+ if (views.has(id)) {
+ emit(viewEvents, 'data', {
+ type: 'click',
+ target: id,
+ window: event.view,
+ checked: node.checked
+ });
+ }
+ });
+
+ return node;
+ }
+ });
+};
+exports.create = create;
+
+function dispose(id) {
+ if (!views.has(id)) return;
+
+ views.delete(id);
+ CustomizableUI.destroyWidget(id);
+}
+exports.dispose = dispose;
+
+function setIcon(id, window, icon) {
+ let node = getNode(id, window);
+
+ if (node) {
+ icon = customizedWindows.has(window) ? views.get(id).icon : icon;
+ let image = getImage(icon, isInToolbar(id), window.devicePixelRatio);
+
+ node.setAttribute('image', image);
+ }
+}
+exports.setIcon = setIcon;
+
+function setLabel(id, window, label) {
+ let node = nodeFor(id, window);
+
+ if (node) {
+ node.setAttribute('label', label);
+ node.setAttribute('tooltiptext', label);
+ }
+}
+exports.setLabel = setLabel;
+
+function setDisabled(id, window, disabled) {
+ let node = nodeFor(id, window);
+
+ if (node)
+ node.disabled = disabled;
+}
+exports.setDisabled = setDisabled;
+
+function setChecked(id, window, checked) {
+ let node = nodeFor(id, window);
+
+ if (node)
+ node.checked = checked;
+}
+exports.setChecked = setChecked;
+
+function setBadge(id, window, badge, color) {
+ let node = nodeFor(id, window);
+
+ if (node) {
+ // `Array.from` is needed to handle unicode symbol properly:
+ // '𝐀𝐁'.length is 4 where Array.from('𝐀𝐁').length is 2
+ let text = isNil(badge)
+ ? ''
+ : Array.from(String(badge)).slice(0, 4).join('');
+
+ node.setAttribute('badge', text);
+
+ let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
+ 'class', 'toolbarbutton-badge');
+
+ if (badgeNode)
+ badgeNode.style.backgroundColor = isNil(color) ? '' : color;
+ }
+}
+exports.setBadge = setBadge;
+
+function click(id) {
+ let node = nodeFor(id);
+
+ if (node)
+ node.click();
+}
+exports.click = click;
diff --git a/addon-sdk/source/lib/sdk/ui/button/view/events.js b/addon-sdk/source/lib/sdk/ui/button/view/events.js
new file mode 100644
index 000000000..98909656a
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/button/view/events.js
@@ -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',
+ 'engines': {
+ 'Firefox': '*',
+ 'SeaMonkey': '*',
+ 'Thunderbird': '*'
+ }
+};
+
+var channel = {};
+
+exports.events = channel;
diff --git a/addon-sdk/source/lib/sdk/ui/component.js b/addon-sdk/source/lib/sdk/ui/component.js
new file mode 100644
index 000000000..d1f12c95e
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/component.js
@@ -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";
+
+// Internal properties not exposed to the public.
+const cache = Symbol("component/cache");
+const writer = Symbol("component/writer");
+const isFirstWrite = Symbol("component/writer/first-write?");
+const currentState = Symbol("component/state/current");
+const pendingState = Symbol("component/state/pending");
+const isWriting = Symbol("component/writing?");
+
+const isntNull = x => x !== null;
+
+const Component = function(options, children) {
+ this[currentState] = null;
+ this[pendingState] = null;
+ this[writer] = null;
+ this[cache] = null;
+ this[isFirstWrite] = true;
+
+ this[Component.construct](options, children);
+}
+Component.Component = Component;
+// Constructs component.
+Component.construct = Symbol("component/construct");
+// Called with `options` and `children` and must return
+// initial state back.
+Component.initial = Symbol("component/initial");
+
+// Function patches current `state` with a given update.
+Component.patch = Symbol("component/patch");
+// Function that replaces current `state` with a passed state.
+Component.reset = Symbol("component/reset");
+
+// Function that must return render tree from passed state.
+Component.render = Symbol("component/render");
+
+// Path of the component with in the mount point.
+Component.path = Symbol("component/path");
+
+Component.isMounted = component => !!component[writer];
+Component.isWriting = component => !!component[isWriting];
+
+// Internal method that mounts component to a writer.
+// Mounts component to a writer.
+Component.mount = (component, write) => {
+ if (Component.isMounted(component)) {
+ throw Error("Can not mount already mounted component");
+ }
+
+ component[writer] = write;
+ Component.write(component);
+
+ if (component[Component.mounted]) {
+ component[Component.mounted]();
+ }
+}
+
+// Unmounts component from a writer.
+Component.unmount = (component) => {
+ if (Component.isMounted(component)) {
+ component[writer] = null;
+ if (component[Component.unmounted]) {
+ component[Component.unmounted]();
+ }
+ } else {
+ console.warn("Unmounting component that is not mounted is redundant");
+ }
+};
+ // Method invoked once after inital write occurs.
+Component.mounted = Symbol("component/mounted");
+// Internal method that unmounts component from the writer.
+Component.unmounted = Symbol("component/unmounted");
+// Function that must return true if component is changed
+Component.isUpdated = Symbol("component/updated?");
+Component.update = Symbol("component/update");
+Component.updated = Symbol("component/updated");
+
+const writeChild = base => (child, index) => Component.write(child, base, index)
+Component.write = (component, base, index) => {
+ if (component === null) {
+ return component;
+ }
+
+ if (!(component instanceof Component)) {
+ const path = base ? `${base}${component.key || index}/` : `/`;
+ return Object.assign({}, component, {
+ [Component.path]: path,
+ children: component.children && component.children.
+ map(writeChild(path)).
+ filter(isntNull)
+ });
+ }
+
+ component[isWriting] = true;
+
+ try {
+
+ const current = component[currentState];
+ const pending = component[pendingState] || current;
+ const isUpdated = component[Component.isUpdated];
+ const isInitial = component[isFirstWrite];
+
+ if (isUpdated(current, pending) || isInitial) {
+ if (!isInitial && component[Component.update]) {
+ component[Component.update](pending, current)
+ }
+
+ // Note: [Component.update] could have caused more updates so can't use
+ // `pending` as `component[pendingState]` may have changed.
+ component[currentState] = component[pendingState] || current;
+ component[pendingState] = null;
+
+ const tree = component[Component.render](component[currentState]);
+ component[cache] = Component.write(tree, base, index);
+ if (component[writer]) {
+ component[writer].call(null, component[cache]);
+ }
+
+ if (!isInitial && component[Component.updated]) {
+ component[Component.updated](current, pending);
+ }
+ }
+
+ component[isFirstWrite] = false;
+
+ return component[cache];
+ } finally {
+ component[isWriting] = false;
+ }
+};
+
+Component.prototype = Object.freeze({
+ constructor: Component,
+
+ [Component.mounted]: null,
+ [Component.unmounted]: null,
+ [Component.update]: null,
+ [Component.updated]: null,
+
+ get state() {
+ return this[pendingState] || this[currentState];
+ },
+
+
+ [Component.construct](settings, items) {
+ const initial = this[Component.initial];
+ const base = initial(settings, items);
+ const options = Object.assign(Object.create(null), base.options, settings);
+ const children = base.children || items || null;
+ const state = Object.assign(Object.create(null), base, {options, children});
+ this[currentState] = state;
+
+ if (this.setup) {
+ this.setup(state);
+ }
+ },
+ [Component.initial](options, children) {
+ return Object.create(null);
+ },
+ [Component.patch](update) {
+ this[Component.reset](Object.assign({}, this.state, update));
+ },
+ [Component.reset](state) {
+ this[pendingState] = state;
+ if (Component.isMounted(this) && !Component.isWriting(this)) {
+ Component.write(this);
+ }
+ },
+
+ [Component.isUpdated](before, after) {
+ return before != after
+ },
+
+ [Component.render](state) {
+ throw Error("Component must implement [Component.render] member");
+ }
+});
+
+module.exports = Component;
diff --git a/addon-sdk/source/lib/sdk/ui/frame.js b/addon-sdk/source/lib/sdk/ui/frame.js
new file mode 100644
index 000000000..566353cdf
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/frame.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "experimental",
+ "engines": {
+ "Firefox": "> 28"
+ }
+};
+
+require("./frame/view");
+const { Frame } = require("./frame/model");
+
+exports.Frame = Frame;
diff --git a/addon-sdk/source/lib/sdk/ui/frame/model.js b/addon-sdk/source/lib/sdk/ui/frame/model.js
new file mode 100644
index 000000000..627310874
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/frame/model.js
@@ -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";
+
+module.metadata = {
+ "stability": "experimental",
+ "engines": {
+ "Firefox": "> 28"
+ }
+};
+
+const { Class } = require("../../core/heritage");
+const { EventTarget } = require("../../event/target");
+const { emit, off, setListeners } = require("../../event/core");
+const { Reactor, foldp, send, merges } = require("../../event/utils");
+const { Disposable } = require("../../core/disposable");
+const { OutputPort } = require("../../output/system");
+const { InputPort } = require("../../input/system");
+const { identify } = require("../id");
+const { pairs, object, map, each } = require("../../util/sequence");
+const { patch, diff } = require("diffpatcher/index");
+const { isLocalURL } = require("../../url");
+const { compose } = require("../../lang/functional");
+const { contract } = require("../../util/contract");
+const { id: addonID, data: { url: resolve }} = require("../../self");
+const { Frames } = require("../../input/frame");
+
+
+const output = new OutputPort({ id: "frame-change" });
+const mailbox = new OutputPort({ id: "frame-mailbox" });
+const input = Frames;
+
+
+const makeID = url =>
+ ("frame-" + addonID + "-" + url).
+ split("/").join("-").
+ split(".").join("-").
+ replace(/[^A-Za-z0-9_\-]/g, "");
+
+const validate = contract({
+ name: {
+ is: ["string", "undefined"],
+ ok: x => /^[a-z][a-z0-9-_]+$/i.test(x),
+ msg: "The `option.name` must be a valid alphanumeric string (hyphens and " +
+ "underscores are allowed) starting with letter."
+ },
+ url: {
+ map: x => x.toString(),
+ is: ["string"],
+ ok: x => isLocalURL(x),
+ msg: "The `options.url` must be a valid local URI."
+ }
+});
+
+const Source = function({id, ownerID}) {
+ this.id = id;
+ this.ownerID = ownerID;
+};
+Source.postMessage = ({id, ownerID}, data, origin) => {
+ send(mailbox, object([id, {
+ inbox: {
+ target: {id: id, ownerID: ownerID},
+ timeStamp: Date.now(),
+ data: data,
+ origin: origin
+ }
+ }]));
+};
+Source.prototype.postMessage = function(data, origin) {
+ Source.postMessage(this, data, origin);
+};
+
+const Message = function({type, data, source, origin, timeStamp}) {
+ this.type = type;
+ this.data = data;
+ this.origin = origin;
+ this.timeStamp = timeStamp;
+ this.source = new Source(source);
+};
+
+
+const frames = new Map();
+const sources = new Map();
+
+const Frame = Class({
+ extends: EventTarget,
+ implements: [Disposable, Source],
+ initialize: function(params={}) {
+ const options = validate(params);
+ const id = makeID(options.name || options.url);
+
+ if (frames.has(id))
+ throw Error("Frame with this id already exists: " + id);
+
+ const initial = { id: id, url: resolve(options.url) };
+ this.id = id;
+
+ setListeners(this, params);
+
+ frames.set(this.id, this);
+
+ send(output, object([id, initial]));
+ },
+ get url() {
+ const state = reactor.value[this.id];
+ return state && state.url;
+ },
+ destroy: function() {
+ send(output, object([this.id, null]));
+ frames.delete(this.id);
+ off(this);
+ },
+ // `JSON.stringify` serializes objects based of the return
+ // value of this method. For convinienc we provide this method
+ // to serialize actual state data.
+ toJSON: function() {
+ return { id: this.id, url: this.url };
+ }
+});
+identify.define(Frame, frame => frame.id);
+
+exports.Frame = Frame;
+
+const reactor = new Reactor({
+ onStep: (present, past) => {
+ const delta = diff(past, present);
+
+ each(([id, update]) => {
+ const frame = frames.get(id);
+ if (update) {
+ if (!past[id])
+ emit(frame, "register");
+
+ if (update.outbox)
+ emit(frame, "message", new Message(present[id].outbox));
+
+ each(([ownerID, state]) => {
+ const readyState = state ? state.readyState : "detach";
+ const type = readyState === "loading" ? "attach" :
+ readyState === "interactive" ? "ready" :
+ readyState === "complete" ? "load" :
+ readyState;
+
+ // TODO: Cache `Source` instances somewhere to preserve
+ // identity.
+ emit(frame, type, {type: type,
+ source: new Source({id: id, ownerID: ownerID})});
+ }, pairs(update.owners));
+ }
+ }, pairs(delta));
+ }
+});
+reactor.run(input);
diff --git a/addon-sdk/source/lib/sdk/ui/frame/view.html b/addon-sdk/source/lib/sdk/ui/frame/view.html
new file mode 100644
index 000000000..2a405b583
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/frame/view.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ // HACK: This is not an ideal way to deliver chrome messages
+ // to an inner frame content but seems only way that would
+ // make `event.source` this (outer frame) window.
+ window.onmessage = function(event) {
+ var frame = document.querySelector("iframe");
+ var content = frame.contentWindow;
+ // If message is posted from chrome it has no `event.source`.
+ if (event.source === null)
+ content.postMessage(event.data, "*");
+ };
+ </script>
+ </head>
+ <body style="overflow: hidden"></body>
+</html>
diff --git a/addon-sdk/source/lib/sdk/ui/frame/view.js b/addon-sdk/source/lib/sdk/ui/frame/view.js
new file mode 100644
index 000000000..2eb4df2b7
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/frame/view.js
@@ -0,0 +1,150 @@
+/* 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",
+ "engines": {
+ "Firefox": "> 28"
+ }
+};
+
+const { Cu, Ci } = require("chrome");
+const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+const { subscribe, send, Reactor, foldp, lift, merges, keepIf } = require("../../event/utils");
+const { InputPort } = require("../../input/system");
+const { OutputPort } = require("../../output/system");
+const { LastClosed } = require("../../input/browser");
+const { pairs, keys, object, each } = require("../../util/sequence");
+const { curry, compose } = require("../../lang/functional");
+const { getFrameElement, getOuterId,
+ getByOuterId, getOwnerBrowserWindow } = require("../../window/utils");
+const { patch, diff } = require("diffpatcher/index");
+const { encode } = require("../../base64");
+const { Frames } = require("../../input/frame");
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const OUTER_FRAME_URI = module.uri.replace(/\.js$/, ".html");
+
+const mailbox = new OutputPort({ id: "frame-mailbox" });
+
+const frameID = frame => frame.id.replace("outer-", "");
+const windowID = compose(getOuterId, getOwnerBrowserWindow);
+
+const getOuterFrame = (windowID, frameID) =>
+ getByOuterId(windowID).document.getElementById("outer-" + frameID);
+
+const listener = ({target, source, data, origin, timeStamp}) => {
+ // And sent received message to outbox so that frame API model
+ // will deal with it.
+ if (source && source !== target) {
+ const frame = getFrameElement(target);
+ const id = frameID(frame);
+ send(mailbox, object([id, {
+ outbox: {type: "message",
+ source: {id: id, ownerID: windowID(frame)},
+ data: data,
+ origin: origin,
+ timeStamp: timeStamp}}]));
+ }
+};
+
+// Utility function used to create frame with a given `state` and
+// inject it into given `window`.
+const registerFrame = ({id, url}) => {
+ CustomizableUI.createWidget({
+ id: id,
+ type: "custom",
+ removable: true,
+ onBuild: document => {
+ let view = document.createElementNS(XUL_NS, "toolbaritem");
+ view.setAttribute("id", id);
+ view.setAttribute("flex", 2);
+
+ let outerFrame = document.createElementNS(XUL_NS, "iframe");
+ outerFrame.setAttribute("src", OUTER_FRAME_URI);
+ outerFrame.setAttribute("id", "outer-" + id);
+ outerFrame.setAttribute("data-is-sdk-outer-frame", true);
+ outerFrame.setAttribute("type", "content");
+ outerFrame.setAttribute("transparent", true);
+ outerFrame.setAttribute("flex", 2);
+ outerFrame.setAttribute("style", "overflow: hidden;");
+ outerFrame.setAttribute("scrolling", "no");
+ outerFrame.setAttribute("disablehistory", true);
+ outerFrame.setAttribute("seamless", "seamless");
+ outerFrame.addEventListener("load", function onload() {
+ outerFrame.removeEventListener("load", onload, true);
+
+ let doc = outerFrame.contentDocument;
+
+ let innerFrame = doc.createElementNS(HTML_NS, "iframe");
+ innerFrame.setAttribute("id", id);
+ innerFrame.setAttribute("src", url);
+ innerFrame.setAttribute("seamless", "seamless");
+ innerFrame.setAttribute("sandbox", "allow-scripts");
+ innerFrame.setAttribute("scrolling", "no");
+ innerFrame.setAttribute("data-is-sdk-inner-frame", true);
+ innerFrame.setAttribute("style", [ "border:none",
+ "position:absolute", "width:100%", "top: 0",
+ "left: 0", "overflow: hidden"].join(";"));
+
+ doc.body.appendChild(innerFrame);
+ }, true);
+
+ view.appendChild(outerFrame);
+
+ return view;
+ }
+ });
+};
+
+const unregisterFrame = CustomizableUI.destroyWidget;
+
+const deliverMessage = curry((frameID, data, windowID) => {
+ const frame = getOuterFrame(windowID, frameID);
+ const content = frame && frame.contentWindow;
+
+ if (content)
+ content.postMessage(data, content.location.origin);
+});
+
+const updateFrame = (id, {inbox, owners}, present) => {
+ if (inbox) {
+ const { data, target:{ownerID}, source } = present[id].inbox;
+ if (ownerID)
+ deliverMessage(id, data, ownerID);
+ else
+ each(deliverMessage(id, data), keys(present[id].owners));
+ }
+
+ each(setupView(id), pairs(owners));
+};
+
+const setupView = curry((frameID, [windowID, state]) => {
+ if (state && state.readyState === "loading") {
+ const frame = getOuterFrame(windowID, frameID);
+ // Setup a message listener on contentWindow.
+ frame.contentWindow.addEventListener("message", listener);
+ }
+});
+
+
+const reactor = new Reactor({
+ onStep: (present, past) => {
+ const delta = diff(past, present);
+
+ // Apply frame changes
+ each(([id, update]) => {
+ if (update === null)
+ unregisterFrame(id);
+ else if (past[id])
+ updateFrame(id, update, present);
+ else
+ registerFrame(update);
+ }, pairs(delta));
+ },
+ onEnd: state => each(unregisterFrame, keys(state))
+});
+reactor.run(Frames);
diff --git a/addon-sdk/source/lib/sdk/ui/id.js b/addon-sdk/source/lib/sdk/ui/id.js
new file mode 100644
index 000000000..d17eb0a4e
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/id.js
@@ -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';
+
+module.metadata = {
+ 'stability': 'experimental'
+};
+
+const method = require('../../method/core');
+const { uuid } = require('../util/uuid');
+
+// NOTE: use lang/functional memoize when it is updated to use WeakMap
+function memoize(f) {
+ const memo = new WeakMap();
+
+ return function memoizer(o) {
+ let key = o;
+ if (!memo.has(key))
+ memo.set(key, f.apply(this, arguments));
+ return memo.get(key);
+ };
+}
+
+var identify = method('identify');
+identify.define(Object, memoize(function() { return uuid(); }));
+exports.identify = identify;
diff --git a/addon-sdk/source/lib/sdk/ui/sidebar.js b/addon-sdk/source/lib/sdk/ui/sidebar.js
new file mode 100644
index 000000000..59e35ea11
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar.js
@@ -0,0 +1,311 @@
+/* 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',
+ 'engines': {
+ 'Firefox': '*'
+ }
+};
+
+const { Class } = require('../core/heritage');
+const { merge } = require('../util/object');
+const { Disposable } = require('../core/disposable');
+const { off, emit, setListeners } = require('../event/core');
+const { EventTarget } = require('../event/target');
+const { URL } = require('../url');
+const { add, remove, has, clear, iterator } = require('../lang/weak-set');
+const { id: addonID, data } = require('../self');
+const { WindowTracker } = require('../deprecated/window-utils');
+const { isShowing } = require('./sidebar/utils');
+const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = require('../window/utils');
+const { ns } = require('../core/namespace');
+const { remove: removeFromArray } = require('../util/array');
+const { show, hide, toggle } = require('./sidebar/actions');
+const { Worker } = require('../deprecated/sync-worker');
+const { contract: sidebarContract } = require('./sidebar/contract');
+const { create, dispose, updateTitle, updateURL, isSidebarShowing, showSidebar, hideSidebar } = require('./sidebar/view');
+const { defer } = require('../core/promise');
+const { models, views, viewsFor, modelFor } = require('./sidebar/namespace');
+const { isLocalURL } = require('../url');
+const { ensure } = require('../system/unload');
+const { identify } = require('./id');
+const { uuid } = require('../util/uuid');
+const { viewFor } = require('../view/core');
+
+const resolveURL = (url) => url ? data.url(url) : url;
+
+const sidebarNS = ns();
+
+const WEB_PANEL_BROWSER_ID = 'web-panels-browser';
+
+var sidebars = {};
+
+const Sidebar = Class({
+ implements: [ Disposable ],
+ extends: EventTarget,
+ setup: function(options) {
+ // inital validation for the model information
+ let model = sidebarContract(options);
+
+ // save the model information
+ models.set(this, model);
+
+ // generate an id if one was not provided
+ model.id = model.id || addonID + '-' + uuid();
+
+ // further validation for the title and url
+ validateTitleAndURLCombo({}, this.title, this.url);
+
+ const self = this;
+ const internals = sidebarNS(self);
+ const windowNS = internals.windowNS = ns();
+
+ // see bug https://bugzilla.mozilla.org/show_bug.cgi?id=886148
+ ensure(this, 'destroy');
+
+ setListeners(this, options);
+
+ let bars = [];
+ internals.tracker = WindowTracker({
+ onTrack: function(window) {
+ if (!isBrowser(window))
+ return;
+
+ let sidebar = window.document.getElementById('sidebar');
+ let sidebarBox = window.document.getElementById('sidebar-box');
+
+ let bar = create(window, {
+ id: self.id,
+ title: self.title,
+ sidebarurl: self.url
+ });
+ bars.push(bar);
+ windowNS(window).bar = bar;
+
+ bar.addEventListener('command', function() {
+ if (isSidebarShowing(window, self)) {
+ hideSidebar(window, self).catch(() => {});
+ return;
+ }
+
+ showSidebar(window, self);
+ }, false);
+
+ function onSidebarLoad() {
+ // check if the sidebar is ready
+ let isReady = sidebar.docShell && sidebar.contentDocument;
+ if (!isReady)
+ return;
+
+ // check if it is a web panel
+ let panelBrowser = sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
+ if (!panelBrowser) {
+ bar.removeAttribute('checked');
+ return;
+ }
+
+ let sbTitle = window.document.getElementById('sidebar-title');
+ function onWebPanelSidebarCreated() {
+ if (panelBrowser.contentWindow.location != resolveURL(model.url) ||
+ sbTitle.value != model.title) {
+ return;
+ }
+
+ let worker = windowNS(window).worker = Worker({
+ window: panelBrowser.contentWindow,
+ injectInDocument: true
+ });
+
+ function onWebPanelSidebarUnload() {
+ windowNS(window).onWebPanelSidebarUnload = null;
+
+ // uncheck the associated menuitem
+ bar.setAttribute('checked', 'false');
+
+ emit(self, 'hide', {});
+ emit(self, 'detach', worker);
+ windowNS(window).worker = null;
+ }
+ windowNS(window).onWebPanelSidebarUnload = onWebPanelSidebarUnload;
+ panelBrowser.contentWindow.addEventListener('unload', onWebPanelSidebarUnload, true);
+
+ // check the associated menuitem
+ bar.setAttribute('checked', 'true');
+
+ function onWebPanelSidebarReady() {
+ panelBrowser.contentWindow.removeEventListener('DOMContentLoaded', onWebPanelSidebarReady, false);
+ windowNS(window).onWebPanelSidebarReady = null;
+
+ emit(self, 'ready', worker);
+ }
+ windowNS(window).onWebPanelSidebarReady = onWebPanelSidebarReady;
+ panelBrowser.contentWindow.addEventListener('DOMContentLoaded', onWebPanelSidebarReady, false);
+
+ function onWebPanelSidebarLoad() {
+ panelBrowser.contentWindow.removeEventListener('load', onWebPanelSidebarLoad, true);
+ windowNS(window).onWebPanelSidebarLoad = null;
+
+ // TODO: decide if returning worker is acceptable..
+ //emit(self, 'show', { worker: worker });
+ emit(self, 'show', {});
+ }
+ windowNS(window).onWebPanelSidebarLoad = onWebPanelSidebarLoad;
+ panelBrowser.contentWindow.addEventListener('load', onWebPanelSidebarLoad, true);
+
+ emit(self, 'attach', worker);
+ }
+ windowNS(window).onWebPanelSidebarCreated = onWebPanelSidebarCreated;
+ panelBrowser.addEventListener('DOMWindowCreated', onWebPanelSidebarCreated, true);
+ }
+ windowNS(window).onSidebarLoad = onSidebarLoad;
+ sidebar.addEventListener('load', onSidebarLoad, true); // removed properly
+ },
+ onUntrack: function(window) {
+ if (!isBrowser(window))
+ return;
+
+ // hide the sidebar if it is showing
+ hideSidebar(window, self).catch(() => {});
+
+ // kill the menu item
+ let { bar } = windowNS(window);
+ if (bar) {
+ removeFromArray(viewsFor(self), bar);
+ dispose(bar);
+ }
+
+ // kill listeners
+ let sidebar = window.document.getElementById('sidebar');
+
+ if (windowNS(window).onSidebarLoad) {
+ sidebar && sidebar.removeEventListener('load', windowNS(window).onSidebarLoad, true)
+ windowNS(window).onSidebarLoad = null;
+ }
+
+ let panelBrowser = sidebar && sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
+ if (windowNS(window).onWebPanelSidebarCreated) {
+ panelBrowser && panelBrowser.removeEventListener('DOMWindowCreated', windowNS(window).onWebPanelSidebarCreated, true);
+ windowNS(window).onWebPanelSidebarCreated = null;
+ }
+
+ if (windowNS(window).onWebPanelSidebarReady) {
+ panelBrowser && panelBrowser.contentWindow.removeEventListener('DOMContentLoaded', windowNS(window).onWebPanelSidebarReady, false);
+ windowNS(window).onWebPanelSidebarReady = null;
+ }
+
+ if (windowNS(window).onWebPanelSidebarLoad) {
+ panelBrowser && panelBrowser.contentWindow.removeEventListener('load', windowNS(window).onWebPanelSidebarLoad, true);
+ windowNS(window).onWebPanelSidebarLoad = null;
+ }
+
+ if (windowNS(window).onWebPanelSidebarUnload) {
+ panelBrowser && panelBrowser.contentWindow.removeEventListener('unload', windowNS(window).onWebPanelSidebarUnload, true);
+ windowNS(window).onWebPanelSidebarUnload();
+ }
+ }
+ });
+
+ views.set(this, bars);
+
+ add(sidebars, this);
+ },
+ get id() {
+ return (modelFor(this) || {}).id;
+ },
+ get title() {
+ return (modelFor(this) || {}).title;
+ },
+ set title(v) {
+ // destroyed?
+ if (!modelFor(this))
+ return;
+ // validation
+ if (typeof v != 'string')
+ throw Error('title must be a string');
+ validateTitleAndURLCombo(this, v, this.url);
+ // do update
+ updateTitle(this, v);
+ return modelFor(this).title = v;
+ },
+ get url() {
+ return (modelFor(this) || {}).url;
+ },
+ set url(v) {
+ // destroyed?
+ if (!modelFor(this))
+ return;
+
+ // validation
+ if (!isLocalURL(v))
+ throw Error('the url must be a valid local url');
+
+ validateTitleAndURLCombo(this, this.title, v);
+
+ // do update
+ updateURL(this, v);
+ modelFor(this).url = v;
+ },
+ show: function(window) {
+ return showSidebar(viewFor(window), this);
+ },
+ hide: function(window) {
+ return hideSidebar(viewFor(window), this);
+ },
+ dispose: function() {
+ const internals = sidebarNS(this);
+
+ off(this);
+
+ remove(sidebars, this);
+
+ // stop tracking windows
+ if (internals.tracker) {
+ internals.tracker.unload();
+ }
+
+ internals.tracker = null;
+ internals.windowNS = null;
+
+ views.delete(this);
+ models.delete(this);
+ }
+});
+exports.Sidebar = Sidebar;
+
+function validateTitleAndURLCombo(sidebar, title, url) {
+ url = resolveURL(url);
+
+ if (sidebar.title == title && sidebar.url == url) {
+ return false;
+ }
+
+ for (let window of windows(null, { includePrivate: true })) {
+ let sidebar = window.document.querySelector('menuitem[sidebarurl="' + url + '"][label="' + title + '"]');
+ if (sidebar) {
+ throw Error('The provided title and url combination is invalid (already used).');
+ }
+ }
+
+ return false;
+}
+
+isShowing.define(Sidebar, isSidebarShowing.bind(null, null));
+show.define(Sidebar, showSidebar.bind(null, null));
+hide.define(Sidebar, hideSidebar.bind(null, null));
+
+identify.define(Sidebar, function(sidebar) {
+ return sidebar.id;
+});
+
+function toggleSidebar(window, sidebar) {
+ // TODO: make sure this is not private
+ window = window || getMostRecentBrowserWindow();
+ if (isSidebarShowing(window, sidebar)) {
+ return hideSidebar(window, sidebar);
+ }
+ return showSidebar(window, sidebar);
+}
+toggle.define(Sidebar, toggleSidebar.bind(null, null));
diff --git a/addon-sdk/source/lib/sdk/ui/sidebar/actions.js b/addon-sdk/source/lib/sdk/ui/sidebar/actions.js
new file mode 100644
index 000000000..4a52984c9
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar/actions.js
@@ -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';
+
+const method = require('../../../method/core');
+
+exports.show = method('show');
+exports.hide = method('hide');
+exports.toggle = method('toggle');
diff --git a/addon-sdk/source/lib/sdk/ui/sidebar/contract.js b/addon-sdk/source/lib/sdk/ui/sidebar/contract.js
new file mode 100644
index 000000000..b59c37c0b
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar/contract.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const { contract } = require('../../util/contract');
+const { isValidURI, URL, isLocalURL } = require('../../url');
+const { isNil, isObject, isString } = require('../../lang/type');
+
+exports.contract = contract({
+ id: {
+ is: [ 'string', 'undefined' ],
+ ok: v => /^[a-z0-9-_]+$/i.test(v),
+ msg: 'The option "id" must be a valid alphanumeric id (hyphens and ' +
+ 'underscores are allowed).'
+ },
+ title: {
+ is: [ 'string' ],
+ ok: v => v.length
+ },
+ url: {
+ is: [ 'string' ],
+ ok: v => isLocalURL(v),
+ map: v => v.toString(),
+ msg: 'The option "url" must be a valid local URI.'
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/ui/sidebar/namespace.js b/addon-sdk/source/lib/sdk/ui/sidebar/namespace.js
new file mode 100644
index 000000000..d79725d1a
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar/namespace.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const models = exports.models = new WeakMap();
+const views = exports.views = new WeakMap();
+exports.buttons = new WeakMap();
+
+exports.viewsFor = function viewsFor(sidebar) {
+ return views.get(sidebar);
+};
+exports.modelFor = function modelFor(sidebar) {
+ return models.get(sidebar);
+};
diff --git a/addon-sdk/source/lib/sdk/ui/sidebar/utils.js b/addon-sdk/source/lib/sdk/ui/sidebar/utils.js
new file mode 100644
index 000000000..d6145c32e
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar/utils.js
@@ -0,0 +1,8 @@
+/* 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 method = require('../../../method/core');
+
+exports.isShowing = method('isShowing');
diff --git a/addon-sdk/source/lib/sdk/ui/sidebar/view.js b/addon-sdk/source/lib/sdk/ui/sidebar/view.js
new file mode 100644
index 000000000..c91e69d3d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar/view.js
@@ -0,0 +1,214 @@
+/* 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',
+ 'engines': {
+ 'Firefox': '*'
+ }
+};
+
+const { models, buttons, views, viewsFor, modelFor } = require('./namespace');
+const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = require('../../window/utils');
+const { setStateFor } = require('../state');
+const { defer } = require('../../core/promise');
+const { isPrivateBrowsingSupported, data } = require('../../self');
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const WEB_PANEL_BROWSER_ID = 'web-panels-browser';
+
+const resolveURL = (url) => url ? data.url(url) : url;
+
+function create(window, details) {
+ let id = makeID(details.id);
+ let { document } = window;
+
+ if (document.getElementById(id))
+ throw new Error('The ID "' + details.id + '" seems already used.');
+
+ let menuitem = document.createElementNS(XUL_NS, 'menuitem');
+ menuitem.setAttribute('id', id);
+ menuitem.setAttribute('label', details.title);
+ menuitem.setAttribute('sidebarurl', resolveURL(details.sidebarurl));
+ menuitem.setAttribute('checked', 'false');
+ menuitem.setAttribute('type', 'checkbox');
+ menuitem.setAttribute('group', 'sidebar');
+ menuitem.setAttribute('autoCheck', 'false');
+
+ document.getElementById('viewSidebarMenu').appendChild(menuitem);
+
+ return menuitem;
+}
+exports.create = create;
+
+function dispose(menuitem) {
+ menuitem.parentNode.removeChild(menuitem);
+}
+exports.dispose = dispose;
+
+function updateTitle(sidebar, title) {
+ let button = buttons.get(sidebar);
+
+ for (let window of windows(null, { includePrivate: true })) {
+ let { document } = window;
+
+ // update the button
+ if (button) {
+ setStateFor(button, window, { label: title });
+ }
+
+ // update the menuitem
+ let mi = document.getElementById(makeID(sidebar.id));
+ if (mi) {
+ mi.setAttribute('label', title)
+ }
+
+ // update sidebar, if showing
+ if (isSidebarShowing(window, sidebar)) {
+ document.getElementById('sidebar-title').setAttribute('value', title);
+ }
+ }
+}
+exports.updateTitle = updateTitle;
+
+function updateURL(sidebar, url) {
+ let eleID = makeID(sidebar.id);
+
+ url = resolveURL(url);
+
+ for (let window of windows(null, { includePrivate: true })) {
+ // update the menuitem
+ let mi = window.document.getElementById(eleID);
+ if (mi) {
+ mi.setAttribute('sidebarurl', url)
+ }
+
+ // update sidebar, if showing
+ if (isSidebarShowing(window, sidebar)) {
+ showSidebar(window, sidebar, url);
+ }
+ }
+}
+exports.updateURL = updateURL;
+
+function isSidebarShowing(window, sidebar) {
+ let win = window || getMostRecentBrowserWindow();
+
+ // make sure there is a window
+ if (!win) {
+ return false;
+ }
+
+ // make sure there is a sidebar for the window
+ let sb = win.document.getElementById('sidebar');
+ let sidebarTitle = win.document.getElementById('sidebar-title');
+ if (!(sb && sidebarTitle)) {
+ return false;
+ }
+
+ // checks if the sidebar box is hidden
+ let sbb = win.document.getElementById('sidebar-box');
+ if (!sbb || sbb.hidden) {
+ return false;
+ }
+
+ if (sidebarTitle.value == modelFor(sidebar).title) {
+ let url = resolveURL(modelFor(sidebar).url);
+
+ // checks if the sidebar is loading
+ if (win.gWebPanelURI == url) {
+ return true;
+ }
+
+ // checks if the sidebar loaded already
+ let ele = sb.contentDocument && sb.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
+ if (!ele) {
+ return false;
+ }
+
+ if (ele.getAttribute('cachedurl') == url) {
+ return true;
+ }
+
+ if (ele && ele.contentWindow && ele.contentWindow.location == url) {
+ return true;
+ }
+ }
+
+ // default
+ return false;
+}
+exports.isSidebarShowing = isSidebarShowing;
+
+function showSidebar(window, sidebar, newURL) {
+ window = window || getMostRecentBrowserWindow();
+
+ let { promise, resolve, reject } = defer();
+ let model = modelFor(sidebar);
+
+ if (!newURL && isSidebarShowing(window, sidebar)) {
+ resolve({});
+ }
+ else if (!isPrivateBrowsingSupported && isWindowPrivate(window)) {
+ reject(Error('You cannot show a sidebar on private windows'));
+ }
+ else {
+ sidebar.once('show', resolve);
+
+ let menuitem = window.document.getElementById(makeID(model.id));
+ menuitem.setAttribute('checked', true);
+
+ window.openWebPanel(model.title, resolveURL(newURL || model.url));
+ }
+
+ return promise;
+}
+exports.showSidebar = showSidebar;
+
+
+function hideSidebar(window, sidebar) {
+ window = window || getMostRecentBrowserWindow();
+
+ let { promise, resolve, reject } = defer();
+
+ if (!isSidebarShowing(window, sidebar)) {
+ reject(Error('The sidebar is already hidden'));
+ }
+ else {
+ sidebar.once('hide', resolve);
+
+ // Below was taken from http://mxr.mozilla.org/mozilla-central/source/browser/base/content/browser.js#4775
+ // the code for window.todggleSideBar()..
+ let { document } = window;
+ let sidebarEle = document.getElementById('sidebar');
+ let sidebarTitle = document.getElementById('sidebar-title');
+ let sidebarBox = document.getElementById('sidebar-box');
+ let sidebarSplitter = document.getElementById('sidebar-splitter');
+ let commandID = sidebarBox.getAttribute('sidebarcommand');
+ let sidebarBroadcaster = document.getElementById(commandID);
+
+ sidebarBox.hidden = true;
+ sidebarSplitter.hidden = true;
+
+ sidebarEle.setAttribute('src', 'about:blank');
+ //sidebarEle.docShell.createAboutBlankContentViewer(null);
+
+ sidebarBroadcaster.removeAttribute('checked');
+ sidebarBox.setAttribute('sidebarcommand', '');
+ sidebarTitle.value = '';
+ sidebarBox.hidden = true;
+ sidebarSplitter.hidden = true;
+
+ // TODO: perhaps this isn't necessary if the window is not most recent?
+ window.gBrowser.selectedBrowser.focus();
+ }
+
+ return promise;
+}
+exports.hideSidebar = hideSidebar;
+
+function makeID(id) {
+ return 'jetpack-sidebar-' + id;
+}
diff --git a/addon-sdk/source/lib/sdk/ui/state.js b/addon-sdk/source/lib/sdk/ui/state.js
new file mode 100644
index 000000000..152ce696d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/state.js
@@ -0,0 +1,239 @@
+/* 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 Button module currently supports only Firefox.
+// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps
+module.metadata = {
+ 'stability': 'experimental',
+ 'engines': {
+ 'Firefox': '*',
+ 'SeaMonkey': '*',
+ 'Thunderbird': '*'
+ }
+};
+
+const { Ci } = require('chrome');
+
+const events = require('../event/utils');
+const { events: browserEvents } = require('../browser/events');
+const { events: tabEvents } = require('../tab/events');
+const { events: stateEvents } = require('./state/events');
+
+const { windows, isInteractive, getFocusedBrowser } = require('../window/utils');
+const { getActiveTab, getOwnerWindow } = require('../tabs/utils');
+
+const { ignoreWindow } = require('../private-browsing/utils');
+
+const { freeze } = Object;
+const { merge } = require('../util/object');
+const { on, off, emit } = require('../event/core');
+
+const { add, remove, has, clear, iterator } = require('../lang/weak-set');
+const { isNil } = require('../lang/type');
+
+const { viewFor } = require('../view/core');
+
+const components = new WeakMap();
+
+const ERR_UNREGISTERED = 'The state cannot be set or get. ' +
+ 'The object may be not be registered, or may already have been unloaded.';
+
+const ERR_INVALID_TARGET = 'The state cannot be set or get for this target.' +
+ 'Only window, tab and registered component are valid targets.';
+
+const isWindow = thing => thing instanceof Ci.nsIDOMWindow;
+const isTab = thing => thing.tagName && thing.tagName.toLowerCase() === 'tab';
+const isActiveTab = thing => isTab(thing) && thing === getActiveTab(getOwnerWindow(thing));
+const isEnumerable = window => !ignoreWindow(window);
+const browsers = _ =>
+ windows('navigator:browser', { includePrivate: true }).filter(isInteractive);
+const getMostRecentTab = _ => getActiveTab(getFocusedBrowser());
+
+function getStateFor(component, target) {
+ if (!isRegistered(component))
+ throw new Error(ERR_UNREGISTERED);
+
+ if (!components.has(component))
+ return null;
+
+ let states = components.get(component);
+
+ if (target) {
+ if (isTab(target) || isWindow(target) || target === component)
+ return states.get(target) || null;
+ else
+ throw new Error(ERR_INVALID_TARGET);
+ }
+
+ return null;
+}
+exports.getStateFor = getStateFor;
+
+function getDerivedStateFor(component, target) {
+ if (!isRegistered(component))
+ throw new Error(ERR_UNREGISTERED);
+
+ if (!components.has(component))
+ return null;
+
+ let states = components.get(component);
+
+ let componentState = states.get(component);
+ let windowState = null;
+ let tabState = null;
+
+ if (target) {
+ // has a target
+ if (isTab(target)) {
+ windowState = states.get(getOwnerWindow(target), null);
+
+ if (states.has(target)) {
+ // we have a tab state
+ tabState = states.get(target);
+ }
+ }
+ else if (isWindow(target) && states.has(target)) {
+ // we have a window state
+ windowState = states.get(target);
+ }
+ }
+
+ return freeze(merge({}, componentState, windowState, tabState));
+}
+exports.getDerivedStateFor = getDerivedStateFor;
+
+function setStateFor(component, target, state) {
+ if (!isRegistered(component))
+ throw new Error(ERR_UNREGISTERED);
+
+ let isComponentState = target === component;
+ let targetWindows = isWindow(target) ? [target] :
+ isActiveTab(target) ? [getOwnerWindow(target)] :
+ isComponentState ? browsers() :
+ isTab(target) ? [] :
+ null;
+
+ if (!targetWindows)
+ throw new Error(ERR_INVALID_TARGET);
+
+ // initialize the state's map
+ if (!components.has(component))
+ components.set(component, new WeakMap());
+
+ let states = components.get(component);
+
+ if (state === null && !isComponentState) // component state can't be deleted
+ states.delete(target);
+ else {
+ let base = isComponentState ? states.get(target) : null;
+ states.set(target, freeze(merge({}, base, state)));
+ }
+
+ render(component, targetWindows);
+}
+exports.setStateFor = setStateFor;
+
+function render(component, targetWindows) {
+ targetWindows = targetWindows ? [].concat(targetWindows) : browsers();
+
+ for (let window of targetWindows.filter(isEnumerable)) {
+ let tabState = getDerivedStateFor(component, getActiveTab(window));
+
+ emit(stateEvents, 'data', {
+ type: 'render',
+ target: component,
+ window: window,
+ state: tabState
+ });
+
+ }
+}
+exports.render = render;
+
+function properties(contract) {
+ let { rules } = contract;
+ let descriptor = Object.keys(rules).reduce(function(descriptor, name) {
+ descriptor[name] = {
+ get: function() { return getDerivedStateFor(this)[name] },
+ set: function(value) {
+ let changed = {};
+ changed[name] = value;
+
+ setStateFor(this, this, contract(changed));
+ }
+ }
+ return descriptor;
+ }, {});
+
+ return Object.create(Object.prototype, descriptor);
+}
+exports.properties = properties;
+
+function state(contract) {
+ return {
+ state: function state(target, state) {
+ let nativeTarget = target === 'window' ? getFocusedBrowser()
+ : target === 'tab' ? getMostRecentTab()
+ : target === this ? null
+ : viewFor(target);
+
+ if (!nativeTarget && target !== this && !isNil(target))
+ throw new Error(ERR_INVALID_TARGET);
+
+ target = nativeTarget || target;
+
+ // jquery style
+ return arguments.length < 2
+ ? getDerivedStateFor(this, target)
+ : setStateFor(this, target, contract(state))
+ }
+ }
+}
+exports.state = state;
+
+const register = (component, state) => {
+ add(components, component);
+ setStateFor(component, component, state);
+}
+exports.register = register;
+
+const unregister = component => {
+ remove(components, component);
+}
+exports.unregister = unregister;
+
+const isRegistered = component => has(components, component);
+exports.isRegistered = isRegistered;
+
+var tabSelect = events.filter(tabEvents, e => e.type === 'TabSelect');
+var tabClose = events.filter(tabEvents, e => e.type === 'TabClose');
+var windowOpen = events.filter(browserEvents, e => e.type === 'load');
+var windowClose = events.filter(browserEvents, e => e.type === 'close');
+
+var close = events.merge([tabClose, windowClose]);
+var activate = events.merge([windowOpen, tabSelect]);
+
+on(activate, 'data', ({target}) => {
+ let [window, tab] = isWindow(target)
+ ? [target, getActiveTab(target)]
+ : [getOwnerWindow(target), target];
+
+ if (ignoreWindow(window)) return;
+
+ for (let component of iterator(components)) {
+ emit(stateEvents, 'data', {
+ type: 'render',
+ target: component,
+ window: window,
+ state: getDerivedStateFor(component, tab)
+ });
+ }
+});
+
+on(close, 'data', function({target}) {
+ for (let component of iterator(components)) {
+ components.get(component).delete(target);
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/ui/state/events.js b/addon-sdk/source/lib/sdk/ui/state/events.js
new file mode 100644
index 000000000..98909656a
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/state/events.js
@@ -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',
+ 'engines': {
+ 'Firefox': '*',
+ 'SeaMonkey': '*',
+ 'Thunderbird': '*'
+ }
+};
+
+var channel = {};
+
+exports.events = channel;
diff --git a/addon-sdk/source/lib/sdk/ui/toolbar.js b/addon-sdk/source/lib/sdk/ui/toolbar.js
new file mode 100644
index 000000000..c1becab2d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/toolbar.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "experimental",
+ "engines": {
+ "Firefox": "> 28"
+ }
+};
+
+const { Toolbar } = require("./toolbar/model");
+require("./toolbar/view");
+
+exports.Toolbar = Toolbar;
diff --git a/addon-sdk/source/lib/sdk/ui/toolbar/model.js b/addon-sdk/source/lib/sdk/ui/toolbar/model.js
new file mode 100644
index 000000000..5c5428606
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/toolbar/model.js
@@ -0,0 +1,151 @@
+/* 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",
+ "engines": {
+ "Firefox": "> 28"
+ }
+};
+
+const { Class } = require("../../core/heritage");
+const { EventTarget } = require("../../event/target");
+const { off, setListeners, emit } = require("../../event/core");
+const { Reactor, foldp, merges, send } = require("../../event/utils");
+const { Disposable } = require("../../core/disposable");
+const { InputPort } = require("../../input/system");
+const { OutputPort } = require("../../output/system");
+const { identify } = require("../id");
+const { pairs, object, map, each } = require("../../util/sequence");
+const { patch, diff } = require("diffpatcher/index");
+const { contract } = require("../../util/contract");
+const { id: addonID } = require("../../self");
+
+// Input state is accumulated from the input received form the toolbar
+// view code & local output. Merging local output reflects local state
+// changes without complete roundloop.
+const input = foldp(patch, {}, new InputPort({ id: "toolbar-changed" }));
+const output = new OutputPort({ id: "toolbar-change" });
+
+// Takes toolbar title and normalizes is to an
+// identifier, also prefixes with add-on id.
+const titleToId = title =>
+ ("toolbar-" + addonID + "-" + title).
+ toLowerCase().
+ replace(/\s/g, "-").
+ replace(/[^A-Za-z0-9_\-]/g, "");
+
+const validate = contract({
+ title: {
+ is: ["string"],
+ ok: x => x.length > 0,
+ msg: "The `option.title` string must be provided"
+ },
+ items: {
+ is:["undefined", "object", "array"],
+ msg: "The `options.items` must be iterable sequence of items"
+ },
+ hidden: {
+ is: ["boolean", "undefined"],
+ msg: "The `options.hidden` must be boolean"
+ }
+});
+
+// Toolbars is a mapping between `toolbar.id` & `toolbar` instances,
+// which is used to find intstance for dispatching events.
+var toolbars = new Map();
+
+const Toolbar = Class({
+ extends: EventTarget,
+ implements: [Disposable],
+ initialize: function(params={}) {
+ const options = validate(params);
+ const id = titleToId(options.title);
+
+ if (toolbars.has(id))
+ throw Error("Toolbar with this id already exists: " + id);
+
+ // Set of the items in the toolbar isn't mutable, as a matter of fact
+ // it just defines desired set of items, actual set is under users
+ // control. Conver test to an array and freeze to make sure users won't
+ // try mess with it.
+ const items = Object.freeze(options.items ? [...options.items] : []);
+
+ const initial = {
+ id: id,
+ title: options.title,
+ // By default toolbars are visible when add-on is installed, unless
+ // add-on authors decides it should be hidden. From that point on
+ // user is in control.
+ collapsed: !!options.hidden,
+ // In terms of state only identifiers of items matter.
+ items: items.map(identify)
+ };
+
+ this.id = id;
+ this.items = items;
+
+ toolbars.set(id, this);
+ setListeners(this, params);
+
+ // Send initial state to the host so it can reflect it
+ // into a user interface.
+ send(output, object([id, initial]));
+ },
+
+ get title() {
+ const state = reactor.value[this.id];
+ return state && state.title;
+ },
+ get hidden() {
+ const state = reactor.value[this.id];
+ return state && state.collapsed;
+ },
+
+ destroy: function() {
+ send(output, object([this.id, null]));
+ },
+ // `JSON.stringify` serializes objects based of the return
+ // value of this method. For convinienc we provide this method
+ // to serialize actual state data. Note: items will also be
+ // serialized so they should probably implement `toJSON`.
+ toJSON: function() {
+ return {
+ id: this.id,
+ title: this.title,
+ hidden: this.hidden,
+ items: this.items
+ };
+ }
+});
+exports.Toolbar = Toolbar;
+identify.define(Toolbar, toolbar => toolbar.id);
+
+const dispose = toolbar => {
+ toolbars.delete(toolbar.id);
+ emit(toolbar, "detach");
+ off(toolbar);
+};
+
+const reactor = new Reactor({
+ onStep: (present, past) => {
+ const delta = diff(past, present);
+
+ each(([id, update]) => {
+ const toolbar = toolbars.get(id);
+
+ // Remove
+ if (!update)
+ dispose(toolbar);
+ // Add
+ else if (!past[id])
+ emit(toolbar, "attach");
+ // Update
+ else
+ emit(toolbar, update.collapsed ? "hide" : "show", toolbar);
+ }, pairs(delta));
+ }
+});
+reactor.run(input);
diff --git a/addon-sdk/source/lib/sdk/ui/toolbar/view.js b/addon-sdk/source/lib/sdk/ui/toolbar/view.js
new file mode 100644
index 000000000..4ef0c3d46
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/toolbar/view.js
@@ -0,0 +1,248 @@
+/* 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",
+ "engines": {
+ "Firefox": "> 28"
+ }
+};
+
+const { Cu } = require("chrome");
+const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+const { subscribe, send, Reactor, foldp, lift, merges } = require("../../event/utils");
+const { InputPort } = require("../../input/system");
+const { OutputPort } = require("../../output/system");
+const { Interactive } = require("../../input/browser");
+const { CustomizationInput } = require("../../input/customizable-ui");
+const { pairs, map, isEmpty, object,
+ each, keys, values } = require("../../util/sequence");
+const { curry, flip } = require("../../lang/functional");
+const { patch, diff } = require("diffpatcher/index");
+const prefs = require("../../preferences/service");
+const { getByOuterId } = require("../../window/utils");
+const { ignoreWindow } = require('../../private-browsing/utils');
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const PREF_ROOT = "extensions.sdk-toolbar-collapsed.";
+
+
+// There are two output ports one for publishing changes that occured
+// and the other for change requests. Later is synchronous and is only
+// consumed here. Note: it needs to be synchronous to avoid race conditions
+// when `collapsed` attribute changes are caused by user interaction and
+// toolbar is destroyed between the ticks.
+const output = new OutputPort({ id: "toolbar-changed" });
+const syncoutput = new OutputPort({ id: "toolbar-change", sync: true });
+
+// Merge disptached changes and recevied changes from models to keep state up to
+// date.
+const Toolbars = foldp(patch, {}, merges([new InputPort({ id: "toolbar-changed" }),
+ new InputPort({ id: "toolbar-change" })]));
+const State = lift((toolbars, windows, customizable) =>
+ ({windows: windows, toolbars: toolbars, customizable: customizable}),
+ Toolbars, Interactive, new CustomizationInput());
+
+// Shared event handler that makes `event.target.parent` collapsed.
+// Used as toolbar's close buttons click handler.
+const collapseToolbar = event => {
+ const toolbar = event.target.parentNode;
+ toolbar.collapsed = true;
+};
+
+const parseAttribute = x =>
+ x === "true" ? true :
+ x === "false" ? false :
+ x === "" ? null :
+ x;
+
+// Shared mutation observer that is used to observe `toolbar` node's
+// attribute mutations. Mutations are aggregated in the `delta` hash
+// and send to `ToolbarStateChanged` channel to let model know state
+// has changed.
+const attributesChanged = mutations => {
+ const delta = mutations.reduce((changes, {attributeName, target}) => {
+ const id = target.id;
+ const field = attributeName === "toolbarname" ? "title" : attributeName;
+ let change = changes[id] || (changes[id] = {});
+ change[field] = parseAttribute(target.getAttribute(attributeName));
+ return changes;
+ }, {});
+
+ // Calculate what are the updates from the current state and if there are
+ // any send them.
+ const updates = diff(reactor.value, patch(reactor.value, delta));
+
+ if (!isEmpty(pairs(updates))) {
+ // TODO: Consider sending sync to make sure that there won't be a new
+ // update doing a delete in the meantime.
+ send(syncoutput, updates);
+ }
+};
+
+
+// Utility function creates `toolbar` with a "close" button and returns
+// it back. In addition it set's up a listener and observer to communicate
+// state changes.
+const addView = curry((options, {document, window}) => {
+ if (ignoreWindow(window))
+ return;
+
+ let view = document.createElementNS(XUL_NS, "toolbar");
+ view.setAttribute("id", options.id);
+ view.setAttribute("collapsed", options.collapsed);
+ view.setAttribute("toolbarname", options.title);
+ view.setAttribute("pack", "end");
+ view.setAttribute("customizable", "false");
+ view.setAttribute("style", "padding: 2px 0; max-height: 40px;");
+ view.setAttribute("mode", "icons");
+ view.setAttribute("iconsize", "small");
+ view.setAttribute("context", "toolbar-context-menu");
+ view.setAttribute("class", "chromeclass-toolbar");
+
+ let label = document.createElementNS(XUL_NS, "label");
+ label.setAttribute("value", options.title);
+ label.setAttribute("collapsed", "true");
+ view.appendChild(label);
+
+ let closeButton = document.createElementNS(XUL_NS, "toolbarbutton");
+ closeButton.setAttribute("id", "close-" + options.id);
+ closeButton.setAttribute("class", "close-icon");
+ closeButton.setAttribute("customizable", false);
+ closeButton.addEventListener("command", collapseToolbar);
+
+ view.appendChild(closeButton);
+
+ // In order to have a close button not costumizable, aligned on the right,
+ // leaving the customizable capabilities of Australis, we need to create
+ // a toolbar inside a toolbar.
+ // This is should be a temporary hack, we should have a proper XBL for toolbar
+ // instead. See:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=982005
+ let toolbar = document.createElementNS(XUL_NS, "toolbar");
+ toolbar.setAttribute("id", "inner-" + options.id);
+ toolbar.setAttribute("defaultset", options.items.join(","));
+ toolbar.setAttribute("customizable", "true");
+ toolbar.setAttribute("style", "-moz-appearance: none; overflow: hidden");
+ toolbar.setAttribute("mode", "icons");
+ toolbar.setAttribute("iconsize", "small");
+ toolbar.setAttribute("context", "toolbar-context-menu");
+ toolbar.setAttribute("flex", "1");
+
+ view.insertBefore(toolbar, closeButton);
+
+ const observer = new document.defaultView.MutationObserver(attributesChanged);
+ observer.observe(view, { attributes: true,
+ attributeFilter: ["collapsed", "toolbarname"] });
+
+ const toolbox = document.getElementById("navigator-toolbox");
+ toolbox.appendChild(view);
+});
+const viewAdd = curry(flip(addView));
+
+const removeView = curry((id, {document}) => {
+ const view = document.getElementById(id);
+ if (view) view.remove();
+});
+
+const updateView = curry((id, {title, collapsed, isCustomizing}, {document}) => {
+ const view = document.getElementById(id);
+
+ if (!view)
+ return;
+
+ if (title)
+ view.setAttribute("toolbarname", title);
+
+ if (collapsed !== void(0))
+ view.setAttribute("collapsed", Boolean(collapsed));
+
+ if (isCustomizing !== void(0)) {
+ view.querySelector("label").collapsed = !isCustomizing;
+ view.querySelector("toolbar").style.visibility = isCustomizing
+ ? "hidden" : "visible";
+ }
+});
+
+const viewUpdate = curry(flip(updateView));
+
+// Utility function used to register toolbar into CustomizableUI.
+const registerToolbar = state => {
+ // If it's first additon register toolbar as customizableUI component.
+ CustomizableUI.registerArea("inner-" + state.id, {
+ type: CustomizableUI.TYPE_TOOLBAR,
+ legacy: true,
+ defaultPlacements: [...state.items]
+ });
+};
+// Utility function used to unregister toolbar from the CustomizableUI.
+const unregisterToolbar = CustomizableUI.unregisterArea;
+
+const reactor = new Reactor({
+ onStep: (present, past) => {
+ const delta = diff(past, present);
+
+ each(([id, update]) => {
+ // If update is `null` toolbar is removed, in such case
+ // we unregister toolbar and remove it from each window
+ // it was added to.
+ if (update === null) {
+ unregisterToolbar("inner-" + id);
+ each(removeView(id), values(past.windows));
+
+ send(output, object([id, null]));
+ }
+ else if (past.toolbars[id]) {
+ // If `collapsed` state for toolbar was updated, persist
+ // it for a future sessions.
+ if (update.collapsed !== void(0))
+ prefs.set(PREF_ROOT + id, update.collapsed);
+
+ // Reflect update in each window it was added to.
+ each(updateView(id, update), values(past.windows));
+
+ send(output, object([id, update]));
+ }
+ // Hack: Mutation observers are invoked async, which means that if
+ // client does `hide(toolbar)` & then `toolbar.destroy()` by the
+ // time we'll get update for `collapsed` toolbar will be removed.
+ // For now we check if `update.id` is present which will be undefined
+ // in such cases.
+ else if (update.id) {
+ // If it is a new toolbar we create initial state by overriding
+ // `collapsed` filed with value persisted in previous sessions.
+ const state = patch(update, {
+ collapsed: prefs.get(PREF_ROOT + id, update.collapsed),
+ });
+
+ // Register toolbar and add it each window known in the past
+ // (note that new windows if any will be handled in loop below).
+ registerToolbar(state);
+ each(addView(state), values(past.windows));
+
+ send(output, object([state.id, state]));
+ }
+ }, pairs(delta.toolbars));
+
+ // Add views to every window that was added.
+ each(window => {
+ if (window)
+ each(viewAdd(window), values(past.toolbars));
+ }, values(delta.windows));
+
+ each(([id, isCustomizing]) => {
+ each(viewUpdate(getByOuterId(id), {isCustomizing: !!isCustomizing}),
+ keys(present.toolbars));
+
+ }, pairs(delta.customizable))
+ },
+ onEnd: state => {
+ each(id => {
+ unregisterToolbar("inner-" + id);
+ each(removeView(id), values(state.windows));
+ }, keys(state.toolbars));
+ }
+});
+reactor.run(State);
diff --git a/addon-sdk/source/lib/sdk/uri/resource.js b/addon-sdk/source/lib/sdk/uri/resource.js
new file mode 100644
index 000000000..8a1dcbf2c
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/uri/resource.js
@@ -0,0 +1,37 @@
+/* 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 ioService = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+const resourceHandler = ioService.getProtocolHandler("resource").
+ QueryInterface(Ci.nsIResProtocolHandler);
+
+const URI = (uri, base=null) =>
+ ioService.newURI(uri, null, base && URI(base))
+
+const mount = (domain, uri) =>
+ resourceHandler.setSubstitution(domain, ioService.newURI(uri, null, null));
+exports.mount = mount;
+
+const unmount = (domain, uri) =>
+ resourceHandler.setSubstitution(domain, null);
+exports.unmount = unmount;
+
+const domain = 1;
+const path = 2;
+const resolve = (uri) => {
+ const match = /resource\:\/\/([^\/]+)\/{0,1}([\s\S]*)/.exec(uri);
+ const domain = match && match[1];
+ const path = match && match[2];
+ return !match ? null :
+ !resourceHandler.hasSubstitution(domain) ? null :
+ resourceHandler.resolveURI(URI(`/${path}`, `resource://${domain}/`));
+}
+exports.resolve = resolve;
diff --git a/addon-sdk/source/lib/sdk/url.js b/addon-sdk/source/lib/sdk/url.js
new file mode 100644
index 000000000..ae16ac4a8
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/url.js
@@ -0,0 +1,349 @@
+/* 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("./core/heritage");
+const base64 = require("./base64");
+var tlds = Cc["@mozilla.org/network/effective-tld-service;1"]
+ .getService(Ci.nsIEffectiveTLDService);
+
+var ios = Cc['@mozilla.org/network/io-service;1']
+ .getService(Ci.nsIIOService);
+
+var resProt = ios.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+
+var URLParser = Cc["@mozilla.org/network/url-parser;1?auth=no"]
+ .getService(Ci.nsIURLParser);
+
+const { Services } = Cu.import("resource://gre/modules/Services.jsm");
+
+function newURI(uriStr, base) {
+ try {
+ let baseURI = base ? ios.newURI(base, null, null) : null;
+ return ios.newURI(uriStr, null, baseURI);
+ }
+ catch (e) {
+ if (e.result == Cr.NS_ERROR_MALFORMED_URI) {
+ throw new Error("malformed URI: " + uriStr);
+ }
+ if (e.result == Cr.NS_ERROR_FAILURE ||
+ e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ throw new Error("invalid URI: " + uriStr);
+ }
+ }
+}
+
+function resolveResourceURI(uri) {
+ var resolved;
+ try {
+ resolved = resProt.resolveURI(uri);
+ }
+ catch (e) {
+ if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw new Error("resource does not exist: " + uri.spec);
+ }
+ }
+ return resolved;
+}
+
+var fromFilename = exports.fromFilename = function fromFilename(path) {
+ var file = Cc['@mozilla.org/file/local;1']
+ .createInstance(Ci.nsILocalFile);
+ file.initWithPath(path);
+ return ios.newFileURI(file).spec;
+};
+
+var toFilename = exports.toFilename = function toFilename(url) {
+ var uri = newURI(url);
+ if (uri.scheme == "resource")
+ uri = newURI(resolveResourceURI(uri));
+ if (uri.scheme == "chrome") {
+ var channel = ios.newChannelFromURI2(uri,
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ try {
+ channel = channel.QueryInterface(Ci.nsIFileChannel);
+ return channel.file.path;
+ }
+ catch (e) {
+ if (e.result == Cr.NS_NOINTERFACE) {
+ throw new Error("chrome url isn't on filesystem: " + url);
+ }
+ }
+ }
+ if (uri.scheme == "file") {
+ var file = uri.QueryInterface(Ci.nsIFileURL).file;
+ return file.path;
+ }
+ throw new Error("cannot map to filename: " + url);
+};
+
+function URL(url, base) {
+ if (!(this instanceof URL)) {
+ return new URL(url, base);
+ }
+
+ var uri = newURI(url, base);
+
+ var userPass = null;
+ try {
+ userPass = uri.userPass ? uri.userPass : null;
+ }
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ }
+
+ var host = null;
+ try {
+ host = uri.host;
+ }
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ }
+
+ var port = null;
+ try {
+ port = uri.port == -1 ? null : uri.port;
+ }
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ }
+
+ let fileName = "/";
+ try {
+ fileName = uri.QueryInterface(Ci.nsIURL).fileName;
+ } catch (e) {
+ if (e.result != Cr.NS_NOINTERFACE) {
+ throw e;
+ }
+ }
+
+ let uriData = [uri.path, uri.path.length, {}, {}, {}, {}, {}, {}];
+ URLParser.parsePath.apply(URLParser, uriData);
+ let [{ value: filepathPos }, { value: filepathLen },
+ { value: queryPos }, { value: queryLen },
+ { value: refPos }, { value: refLen }] = uriData.slice(2);
+
+ let hash = uri.ref ? "#" + uri.ref : "";
+ let pathname = uri.path.substr(filepathPos, filepathLen);
+ let search = uri.path.substr(queryPos, queryLen);
+ search = search ? "?" + search : "";
+
+ this.__defineGetter__("fileName", () => fileName);
+ this.__defineGetter__("scheme", () => uri.scheme);
+ this.__defineGetter__("userPass", () => userPass);
+ this.__defineGetter__("host", () => host);
+ this.__defineGetter__("hostname", () => host);
+ this.__defineGetter__("port", () => port);
+ this.__defineGetter__("path", () => uri.path);
+ this.__defineGetter__("pathname", () => pathname);
+ this.__defineGetter__("hash", () => hash);
+ this.__defineGetter__("href", () => uri.spec);
+ this.__defineGetter__("origin", () => uri.prePath);
+ this.__defineGetter__("protocol", () => uri.scheme + ":");
+ this.__defineGetter__("search", () => search);
+
+ Object.defineProperties(this, {
+ toString: {
+ value() {
+ return new String(uri.spec).toString();
+ },
+ enumerable: false
+ },
+ valueOf: {
+ value() {
+ return new String(uri.spec).valueOf();
+ },
+ enumerable: false
+ },
+ toSource: {
+ value() {
+ return new String(uri.spec).toSource();
+ },
+ enumerable: false
+ },
+ // makes more sense to flatten to string, easier to travel across JSON
+ toJSON: {
+ value() {
+ return new String(uri.spec).toString();
+ },
+ enumerable: false
+ }
+ });
+
+ return this;
+};
+
+URL.prototype = Object.create(String.prototype);
+exports.URL = URL;
+
+/**
+ * Parse and serialize a Data URL.
+ *
+ * See: http://tools.ietf.org/html/rfc2397
+ *
+ * Note: Could be extended in the future to decode / encode automatically binary
+ * data.
+ */
+const DataURL = Class({
+
+ get base64 () {
+ return "base64" in this.parameters;
+ },
+
+ set base64 (value) {
+ if (value)
+ this.parameters["base64"] = "";
+ else
+ delete this.parameters["base64"];
+ },
+ /**
+ * Initialize the Data URL object. If a uri is given, it will be parsed.
+ *
+ * @param {String} [uri] The uri to parse
+ *
+ * @throws {URIError} if the Data URL is malformed
+ */
+ initialize: function(uri) {
+ // Due to bug 751834 it is not possible document and define these
+ // properties in the prototype.
+
+ /**
+ * An hashmap that contains the parameters of the Data URL. By default is
+ * empty, that accordingly to RFC is equivalent to {"charset" : "US-ASCII"}
+ */
+ this.parameters = {};
+
+ /**
+ * The MIME type of the data. By default is empty, that accordingly to RFC
+ * is equivalent to "text/plain"
+ */
+ this.mimeType = "";
+
+ /**
+ * The string that represent the data in the Data URL
+ */
+ this.data = "";
+
+ if (typeof uri === "undefined")
+ return;
+
+ uri = String(uri);
+
+ let matches = uri.match(/^data:([^,]*),(.*)$/i);
+
+ if (!matches)
+ throw new URIError("Malformed Data URL: " + uri);
+
+ let mediaType = matches[1].trim();
+
+ this.data = decodeURIComponent(matches[2].trim());
+
+ if (!mediaType)
+ return;
+
+ let parametersList = mediaType.split(";");
+
+ this.mimeType = parametersList.shift().trim();
+
+ for (let parameter, i = 0; parameter = parametersList[i++];) {
+ let pairs = parameter.split("=");
+ let name = pairs[0].trim();
+ let value = pairs.length > 1 ? decodeURIComponent(pairs[1].trim()) : "";
+
+ this.parameters[name] = value;
+ }
+
+ if (this.base64)
+ this.data = base64.decode(this.data);
+
+ },
+
+ /**
+ * Returns the object as a valid Data URL string
+ *
+ * @returns {String} The Data URL
+ */
+ toString : function() {
+ let parametersList = [];
+
+ for (let name in this.parameters) {
+ let encodedParameter = encodeURIComponent(name);
+ let value = this.parameters[name];
+
+ if (value)
+ encodedParameter += "=" + encodeURIComponent(value);
+
+ parametersList.push(encodedParameter);
+ }
+
+ // If there is at least a parameter, add an empty string in order
+ // to start with a `;` on join call.
+ if (parametersList.length > 0)
+ parametersList.unshift("");
+
+ let data = this.base64 ? base64.encode(this.data) : this.data;
+
+ return "data:" +
+ this.mimeType +
+ parametersList.join(";") + "," +
+ encodeURIComponent(data);
+ }
+});
+
+exports.DataURL = DataURL;
+
+var getTLD = exports.getTLD = function getTLD (url) {
+ let uri = newURI(url.toString());
+ let tld = null;
+ try {
+ tld = tlds.getPublicSuffix(uri);
+ }
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS &&
+ e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
+ throw e;
+ }
+ }
+ return tld;
+};
+
+var isValidURI = exports.isValidURI = function (uri) {
+ try {
+ newURI(uri);
+ }
+ catch(e) {
+ return false;
+ }
+ return true;
+}
+
+function isLocalURL(url) {
+ if (String.indexOf(url, './') === 0)
+ return true;
+
+ try {
+ return ['resource', 'data', 'chrome'].indexOf(URL(url).scheme) > -1;
+ }
+ catch(e) {}
+
+ return false;
+}
+exports.isLocalURL = isLocalURL;
diff --git a/addon-sdk/source/lib/sdk/url/utils.js b/addon-sdk/source/lib/sdk/url/utils.js
new file mode 100644
index 000000000..aa5759204
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/url/utils.js
@@ -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 { Cc, Ci, Cr } = require("chrome");
+const IOService = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+const { isValidURI } = require("../url");
+const { method } = require("../../method/core");
+
+function newURI (uri) {
+ if (!isValidURI(uri))
+ throw new Error("malformed URI: " + uri);
+ return IOService.newURI(uri, null, null);
+}
+exports.newURI = newURI;
+
+var getURL = method('sdk/url:getURL');
+getURL.define(String, url => url);
+getURL.define(function (object) {
+ return null;
+});
+exports.getURL = getURL;
diff --git a/addon-sdk/source/lib/sdk/util/array.js b/addon-sdk/source/lib/sdk/util/array.js
new file mode 100644
index 000000000..1d61a973e
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/array.js
@@ -0,0 +1,123 @@
+/* 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"
+};
+
+/**
+ * Returns `true` if given `array` contain given `element` or `false`
+ * otherwise.
+ * @param {Array} array
+ * Target array.
+ * @param {Object|String|Number|Boolean} element
+ * Element being looked up.
+ * @returns {Boolean}
+ */
+var has = exports.has = function has(array, element) {
+ // shorter and faster equivalent of `array.indexOf(element) >= 0`
+ return !!~array.indexOf(element);
+};
+var hasAny = exports.hasAny = function hasAny(array, elements) {
+ if (arguments.length < 2)
+ return false;
+ if (!Array.isArray(elements))
+ elements = [ elements ];
+ return array.some(function (element) {
+ return has(elements, element);
+ });
+};
+
+/**
+ * Adds given `element` to the given `array` if it does not contain it yet.
+ * `true` is returned if element was added otherwise `false` is returned.
+ * @param {Array} array
+ * Target array.
+ * @param {Object|String|Number|Boolean} element
+ * Element to be added.
+ * @returns {Boolean}
+ */
+var add = exports.add = function add(array, element) {
+ var result;
+ if ((result = !has(array, element)))
+ array.push(element);
+
+ return result;
+};
+
+/**
+ * Removes first occurrence of the given `element` from the given `array`. If
+ * `array` does not contain given `element` `false` is returned otherwise
+ * `true` is returned.
+ * @param {Array} array
+ * Target array.
+ * @param {Object|String|Number|Boolean} element
+ * Element to be removed.
+ * @returns {Boolean}
+ */
+exports.remove = function remove(array, element) {
+ var result;
+ if ((result = has(array, element)))
+ array.splice(array.indexOf(element), 1);
+
+ return result;
+};
+
+/**
+ * Produces a duplicate-free version of the given `array`.
+ * @param {Array} array
+ * Source array.
+ * @returns {Array}
+ */
+function unique(array) {
+ return array.reduce(function(result, item) {
+ add(result, item);
+ return result;
+ }, []);
+};
+exports.unique = unique;
+
+/**
+ * Produce an array that contains the union: each distinct element from all
+ * of the passed-in arrays.
+ */
+function union() {
+ return unique(Array.concat.apply(null, arguments));
+};
+exports.union = union;
+
+exports.flatten = function flatten(array){
+ var flat = [];
+ for (var i = 0, l = array.length; i < l; i++) {
+ flat = flat.concat(Array.isArray(array[i]) ? flatten(array[i]) : array[i]);
+ }
+ return flat;
+};
+
+function fromIterator(iterator) {
+ let array = [];
+ if (iterator.__iterator__) {
+ for (let item of iterator)
+ array.push(item);
+ }
+ else {
+ for (let item of iterator)
+ array.push(item);
+ }
+ return array;
+}
+exports.fromIterator = fromIterator;
+
+function find(array, predicate, fallback) {
+ var index = 0;
+ var count = array.length;
+ while (index < count) {
+ var value = array[index];
+ if (predicate(value)) return value;
+ else index = index + 1;
+ }
+ return fallback;
+}
+exports.find = find;
diff --git a/addon-sdk/source/lib/sdk/util/collection.js b/addon-sdk/source/lib/sdk/util/collection.js
new file mode 100644
index 000000000..194a29470
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/collection.js
@@ -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"
+};
+
+exports.Collection = Collection;
+
+/**
+ * Adds a collection property to the given object. Setting the property to a
+ * scalar value empties the collection and adds the value. Setting it to an
+ * array empties the collection and adds all the items in the array.
+ *
+ * @param obj
+ * The property will be defined on this object.
+ * @param propName
+ * The name of the property.
+ * @param array
+ * If given, this will be used as the collection's backing array.
+ */
+exports.addCollectionProperty = function addCollProperty(obj, propName, array) {
+ array = array || [];
+ let publicIface = new Collection(array);
+
+ Object.defineProperty(obj, propName, {
+ configurable: true,
+ enumerable: true,
+
+ set: function set(itemOrItems) {
+ array.splice(0, array.length);
+ publicIface.add(itemOrItems);
+ },
+
+ get: function get() {
+ return publicIface;
+ }
+ });
+};
+
+/**
+ * A collection is ordered, like an array, but its items are unique, like a set.
+ *
+ * @param array
+ * The collection is backed by an array. If this is given, it will be
+ * used as the backing array. This way the caller can fully control the
+ * collection. Otherwise a new empty array will be used, and no one but
+ * the collection will have access to it.
+ */
+function Collection(array) {
+ array = array || [];
+
+ /**
+ * Provides iteration over the collection. Items are yielded in the order
+ * they were added.
+ */
+ this.__iterator__ = function Collection___iterator__() {
+ let items = array.slice();
+ for (let i = 0; i < items.length; i++)
+ yield items[i];
+ };
+
+ /**
+ * The number of items in the collection.
+ */
+ this.__defineGetter__("length", function Collection_get_length() {
+ return array.length;
+ });
+
+ /**
+ * Adds a single item or an array of items to the collection. Any items
+ * already contained in the collection are ignored.
+ *
+ * @param itemOrItems
+ * An item or array of items.
+ * @return The collection.
+ */
+ this.add = function Collection_add(itemOrItems) {
+ let items = toArray(itemOrItems);
+ for (let i = 0; i < items.length; i++) {
+ let item = items[i];
+ if (array.indexOf(item) < 0)
+ array.push(item);
+ }
+ return this;
+ };
+
+ /**
+ * Removes a single item or an array of items from the collection. Any items
+ * not contained in the collection are ignored.
+ *
+ * @param itemOrItems
+ * An item or array of items.
+ * @return The collection.
+ */
+ this.remove = function Collection_remove(itemOrItems) {
+ let items = toArray(itemOrItems);
+ for (let i = 0; i < items.length; i++) {
+ let idx = array.indexOf(items[i]);
+ if (idx >= 0)
+ array.splice(idx, 1);
+ }
+ return this;
+ };
+};
+
+function toArray(itemOrItems) {
+ let isArr = itemOrItems &&
+ itemOrItems.constructor &&
+ itemOrItems.constructor.name === "Array";
+ return isArr ? itemOrItems : [itemOrItems];
+}
diff --git a/addon-sdk/source/lib/sdk/util/contract.js b/addon-sdk/source/lib/sdk/util/contract.js
new file mode 100644
index 000000000..c689ea601
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/contract.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { validateOptions: valid } = require("../deprecated/api-utils");
+const method = require("method/core");
+
+// Function takes property validation rules and returns function that given
+// an `options` object will return validated / normalized options back. If
+// option(s) are invalid validator will throw exception described by rules.
+// Returned will also have contain `rules` property with a given validation
+// rules and `properties` function that can be used to generate validated
+// property getter and setters can be mixed into prototype. For more details
+// see `properties` function below.
+function contract(rules) {
+ const validator = (instance, options) => {
+ return valid(options || instance || {}, rules);
+ };
+ validator.rules = rules
+ validator.properties = function(modelFor) {
+ return properties(modelFor, rules);
+ }
+ return validator;
+}
+exports.contract = contract
+
+// Function takes `modelFor` instance state model accessor functions and
+// a property validation rules and generates object with getters and setters
+// that can be mixed into prototype. Property accessors update model for the
+// given instance. If you wish to react to property updates you can always
+// override setters to put specific logic.
+function properties(modelFor, rules) {
+ let descriptor = Object.keys(rules).reduce(function(descriptor, name) {
+ descriptor[name] = {
+ get: function() { return modelFor(this)[name] },
+ set: function(value) {
+ let change = {};
+ change[name] = value;
+ modelFor(this)[name] = valid(change, rules)[name];
+ }
+ }
+ return descriptor
+ }, {});
+ return Object.create(Object.prototype, descriptor);
+}
+exports.properties = properties;
+
+const validate = method("contract/validate");
+exports.validate = validate;
diff --git a/addon-sdk/source/lib/sdk/util/deprecate.js b/addon-sdk/source/lib/sdk/util/deprecate.js
new file mode 100644
index 000000000..40f236de5
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/deprecate.js
@@ -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": "experimental"
+};
+
+const { get, format } = require("../console/traceback");
+const { get: getPref } = require("../preferences/service");
+const PREFERENCE = "devtools.errorconsole.deprecation_warnings";
+
+function deprecateUsage(msg) {
+ // Print caller stacktrace in order to help figuring out which code
+ // does use deprecated thing
+ let stack = get().slice(2);
+
+ if (getPref(PREFERENCE))
+ console.error("DEPRECATED: " + msg + "\n" + format(stack));
+}
+exports.deprecateUsage = deprecateUsage;
+
+function deprecateFunction(fun, msg) {
+ return function deprecated() {
+ deprecateUsage(msg);
+ return fun.apply(this, arguments);
+ };
+}
+exports.deprecateFunction = deprecateFunction;
+
+function deprecateEvent(fun, msg, evtTypes) {
+ return function deprecateEvent(evtType) {
+ if (evtTypes.indexOf(evtType) >= 0)
+ deprecateUsage(msg);
+ return fun.apply(this, arguments);
+ };
+}
+exports.deprecateEvent = deprecateEvent;
diff --git a/addon-sdk/source/lib/sdk/util/dispatcher.js b/addon-sdk/source/lib/sdk/util/dispatcher.js
new file mode 100644
index 000000000..67d29dfed
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/dispatcher.js
@@ -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";
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const method = require("method/core");
+
+// Utility function that is just an enhancement over `method` to
+// allow predicate based dispatch in addition to polymorphic
+// dispatch. Unfortunately polymorphic dispatch does not quite
+// cuts it in the world of XPCOM where no types / classes exist
+// and all the XUL nodes share same type / prototype.
+// Probably this is more generic and belongs some place else, but
+// we can move it later once this will be relevant.
+var dispatcher = hint => {
+ const base = method(hint);
+ // Make a map for storing predicate, implementation mappings.
+ let implementations = new Map();
+
+ // Dispatcher function goes through `predicate, implementation`
+ // pairs to find predicate that matches first argument and
+ // returns application of arguments on the associated
+ // `implementation`. If no matching predicate is found delegates
+ // to a `base` polymorphic function.
+ let dispatch = (value, ...rest) => {
+ for (let [predicate, implementation] of implementations) {
+ if (predicate(value))
+ return implementation(value, ...rest);
+ }
+
+ return base(value, ...rest);
+ };
+
+ // Expose base API.
+ dispatch.define = base.define;
+ dispatch.implement = base.implement;
+ dispatch.toString = base.toString;
+
+ // Add a `when` function to allow extending function via
+ // predicates.
+ dispatch.when = (predicate, implementation) => {
+ if (implementations.has(predicate))
+ throw TypeError("Already implemented for the given predicate");
+ implementations.set(predicate, implementation);
+ };
+
+ return dispatch;
+};
+
+exports.dispatcher = dispatcher;
diff --git a/addon-sdk/source/lib/sdk/util/list.js b/addon-sdk/source/lib/sdk/util/list.js
new file mode 100644
index 000000000..6d7d2dea9
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/list.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Class } = require('../core/heritage');
+const listNS = require('../core/namespace').ns();
+
+const listOptions = {
+ /**
+ * List constructor can take any number of element to populate itself.
+ * @params {Object|String|Number} element
+ * @example
+ * List(1,2,3).length == 3 // true
+ */
+ initialize: function List() {
+ listNS(this).keyValueMap = [];
+
+ for (let i = 0, ii = arguments.length; i < ii; i++)
+ addListItem(this, arguments[i]);
+ },
+ /**
+ * Number of elements in this list.
+ * @type {Number}
+ */
+ get length() {
+ return listNS(this).keyValueMap.length;
+ },
+ /**
+ * Returns a string representing this list.
+ * @returns {String}
+ */
+ toString: function toString() {
+ return 'List(' + listNS(this).keyValueMap + ')';
+ },
+ /**
+ * Custom iterator providing `List`s enumeration behavior.
+ * We cant reuse `_iterator` that is defined by `Iterable` since it provides
+ * iteration in an arbitrary order.
+ * @see https://developer.mozilla.org/en/JavaScript/Reference/Statements/for...in
+ * @param {Boolean} onKeys
+ */
+ __iterator__: function __iterator__(onKeys, onKeyValue) {
+ let array = listNS(this).keyValueMap.slice(0),
+ i = -1;
+ for (let element of array)
+ yield onKeyValue ? [++i, element] : onKeys ? ++i : element;
+ },
+};
+listOptions[Symbol.iterator] = function iterator() {
+ return listNS(this).keyValueMap.slice(0)[Symbol.iterator]();
+};
+const List = Class(listOptions);
+exports.List = List;
+
+function addListItem(that, value) {
+ let list = listNS(that).keyValueMap,
+ index = list.indexOf(value);
+
+ if (-1 === index) {
+ try {
+ that[that.length] = value;
+ }
+ catch (e) {}
+ list.push(value);
+ }
+}
+exports.addListItem = addListItem;
+
+function removeListItem(that, element) {
+ let list = listNS(that).keyValueMap,
+ index = list.indexOf(element);
+
+ if (0 <= index) {
+ list.splice(index, 1);
+ try {
+ for (let length = list.length; index < length; index++)
+ that[index] = list[index];
+ that[list.length] = undefined;
+ }
+ catch(e){}
+ }
+}
+exports.removeListItem = removeListItem;
+
+exports.listNS = listNS;
diff --git a/addon-sdk/source/lib/sdk/util/match-pattern.js b/addon-sdk/source/lib/sdk/util/match-pattern.js
new file mode 100644
index 000000000..a0eb88b49
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/match-pattern.js
@@ -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";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { URL } = require('../url');
+const cache = {};
+
+function MatchPattern(pattern) {
+ if (cache[pattern]) return cache[pattern];
+
+ if (typeof pattern.test == "function") {
+ // For compatibility with -moz-document rules, we require the RegExp's
+ // global, ignoreCase, and multiline flags to be set to false.
+ if (pattern.global) {
+ throw new Error("A RegExp match pattern cannot be set to `global` " +
+ "(i.e. //g).");
+ }
+ if (pattern.multiline) {
+ throw new Error("A RegExp match pattern cannot be set to `multiline` " +
+ "(i.e. //m).");
+ }
+
+ this.regexp = pattern;
+ }
+ else {
+ let firstWildcardPosition = pattern.indexOf("*");
+ let lastWildcardPosition = pattern.lastIndexOf("*");
+ if (firstWildcardPosition != lastWildcardPosition)
+ throw new Error("There can be at most one '*' character in a wildcard.");
+
+ if (firstWildcardPosition == 0) {
+ if (pattern.length == 1)
+ this.anyWebPage = true;
+ else if (pattern[1] != ".")
+ throw new Error("Expected a *.<domain name> string, got: " + pattern);
+ else
+ this.domain = pattern.substr(2);
+ }
+ else {
+ if (pattern.indexOf(":") == -1) {
+ throw new Error("When not using *.example.org wildcard, the string " +
+ "supplied is expected to be either an exact URL to " +
+ "match or a URL prefix. The provided string ('" +
+ pattern + "') is unlikely to match any pages.");
+ }
+
+ if (firstWildcardPosition == -1)
+ this.exactURL = pattern;
+ else if (firstWildcardPosition == pattern.length - 1)
+ this.urlPrefix = pattern.substr(0, pattern.length - 1);
+ else {
+ throw new Error("The provided wildcard ('" + pattern + "') has a '*' " +
+ "in an unexpected position. It is expected to be the " +
+ "first or the last character in the wildcard.");
+ }
+ }
+ }
+
+ cache[pattern] = this;
+}
+
+MatchPattern.prototype = {
+ test: function MatchPattern_test(urlStr) {
+ try {
+ var url = URL(urlStr);
+ }
+ catch (err) {
+ return false;
+ }
+
+ // Test the URL against a RegExp pattern. For compatibility with
+ // -moz-document rules, we require the RegExp to match the entire URL,
+ // so we not only test for a match, we also make sure the matched string
+ // is the entire URL string.
+ //
+ // Assuming most URLs don't match most match patterns, we call `test` for
+ // speed when determining whether or not the URL matches, then call `exec`
+ // for the small subset that match to make sure the entire URL matches.
+ if (this.regexp && this.regexp.test(urlStr) &&
+ this.regexp.exec(urlStr)[0] == urlStr)
+ return true;
+
+ if (this.anyWebPage && /^(https?|ftp)$/.test(url.scheme))
+ return true;
+
+ if (this.exactURL && this.exactURL == urlStr)
+ return true;
+
+ // Tests the urlStr against domain and check if
+ // wildcard submitted (*.domain.com), it only allows
+ // subdomains (sub.domain.com) or from the root (http://domain.com)
+ // and reject non-matching domains (otherdomain.com)
+ // bug 856913
+ if (this.domain && url.host &&
+ (url.host === this.domain ||
+ url.host.slice(-this.domain.length - 1) === "." + this.domain))
+ return true;
+
+ if (this.urlPrefix && 0 == urlStr.indexOf(this.urlPrefix))
+ return true;
+
+ return false;
+ },
+
+ toString: () => '[object MatchPattern]'
+};
+
+exports.MatchPattern = MatchPattern;
diff --git a/addon-sdk/source/lib/sdk/util/object.js b/addon-sdk/source/lib/sdk/util/object.js
new file mode 100644
index 000000000..9d202bb51
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/object.js
@@ -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": "unstable"
+};
+
+const { flatten } = require('./array');
+
+/**
+ * Merges all the properties of all arguments into first argument. 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.
+ *
+ * Any argument given with "falsy" value - commonly `null` and `undefined` in
+ * case of objects - are skipped.
+ *
+ * @examples
+ * var a = { bar: 0, a: 'a' }
+ * var b = merge(a, { foo: 'foo', bar: 1 }, { foo: 'bar', name: 'b' });
+ * b === a // true
+ * b.a // 'a'
+ * b.foo // 'bar'
+ * b.bar // 1
+ * b.name // 'b'
+ */
+function merge(source) {
+ let descriptor = {};
+
+ // `Boolean` converts the first parameter to a boolean value. Any object is
+ // converted to `true` where `null` and `undefined` becames `false`. Therefore
+ // the `filter` method will keep only objects that are defined and not null.
+ Array.slice(arguments, 1).filter(Boolean).forEach(function onEach(properties) {
+ getOwnPropertyIdentifiers(properties).forEach(function(name) {
+ descriptor[name] = Object.getOwnPropertyDescriptor(properties, name);
+ });
+ });
+ return Object.defineProperties(source, descriptor);
+}
+exports.merge = merge;
+
+/**
+ * Returns an object that inherits from the first argument and contains all the
+ * properties from all following arguments.
+ * `extend(source1, source2, source3)` is equivalent of
+ * `merge(Object.create(source1), source2, source3)`.
+ */
+function extend(source) {
+ let rest = Array.slice(arguments, 1);
+ rest.unshift(Object.create(source));
+ return merge.apply(null, rest);
+}
+exports.extend = extend;
+
+function has(obj, key) {
+ return obj.hasOwnProperty(key);
+}
+exports.has = has;
+
+function each(obj, fn) {
+ for (let key in obj) has(obj, key) && fn(obj[key], key, obj);
+}
+exports.each = each;
+
+/**
+ * Like `merge`, except no property descriptors are manipulated, for use
+ * with platform objects. Identical to underscore's `extend`. Useful for
+ * merging XPCOM objects
+ */
+function safeMerge(source) {
+ Array.slice(arguments, 1).forEach(function onEach (obj) {
+ for (let prop in obj) source[prop] = obj[prop];
+ });
+ return source;
+}
+exports.safeMerge = safeMerge;
+
+/*
+ * Returns a copy of the object without omitted properties
+ */
+function omit(source, ...values) {
+ let copy = {};
+ let keys = flatten(values);
+ for (let prop in source)
+ if (!~keys.indexOf(prop))
+ copy[prop] = source[prop];
+ return copy;
+}
+exports.omit = omit;
+
+// get object's own property Symbols and/or Names, including nonEnumerables by default
+function getOwnPropertyIdentifiers(object, options = { names: true, symbols: true, nonEnumerables: true }) {
+ const symbols = !options.symbols ? [] :
+ Object.getOwnPropertySymbols(object);
+ const names = !options.names ? [] :
+ options.nonEnumerables ? Object.getOwnPropertyNames(object) :
+ Object.keys(object);
+ return [...names, ...symbols];
+}
+exports.getOwnPropertyIdentifiers = getOwnPropertyIdentifiers;
diff --git a/addon-sdk/source/lib/sdk/util/rules.js b/addon-sdk/source/lib/sdk/util/rules.js
new file mode 100644
index 000000000..98e3109b0
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/rules.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { Class } = require('../core/heritage');
+const { MatchPattern } = require('./match-pattern');
+const { emit } = require('../event/core');
+const { EventTarget } = require('../event/target');
+const { List, addListItem, removeListItem } = require('./list');
+
+// Should deprecate usage of EventEmitter/compose
+const Rules = Class({
+ implements: [
+ EventTarget,
+ List
+ ],
+ add: function(...rules) {
+ return [].concat(rules).forEach(function onAdd(rule) {
+ addListItem(this, rule);
+ emit(this, 'add', rule);
+ }, this);
+ },
+ remove: function(...rules) {
+ return [].concat(rules).forEach(function onRemove(rule) {
+ removeListItem(this, rule);
+ emit(this, 'remove', rule);
+ }, this);
+ },
+ get: function(rule) {
+ let found = false;
+ for (let i in this) if (this[i] === rule) found = true;
+ return found;
+ },
+ // Returns true if uri matches atleast one stored rule
+ matchesAny: function(uri) {
+ return !!filterMatches(this, uri).length;
+ },
+ toString: () => '[object Rules]'
+});
+exports.Rules = Rules;
+
+function filterMatches(instance, uri) {
+ let matches = [];
+ for (let i in instance) {
+ if (new MatchPattern(instance[i]).test(uri)) matches.push(instance[i]);
+ }
+ return matches;
+}
diff --git a/addon-sdk/source/lib/sdk/util/sequence.js b/addon-sdk/source/lib/sdk/util/sequence.js
new file mode 100644
index 000000000..28e3de255
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/sequence.js
@@ -0,0 +1,593 @@
+/* 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"
+};
+
+// Disclamer:
+// In this module we'll have some common argument / variable names
+// to hint their type or behavior.
+//
+// - `f` stands for "function" that is intended to be side effect
+// free.
+// - `p` stands for "predicate" that is function which returns logical
+// true or false and is intended to be side effect free.
+// - `x` / `y` single item of the sequence.
+// - `xs` / `ys` sequence of `x` / `y` items where `x` / `y` signifies
+// type of the items in sequence, so sequence is not of the same item.
+// - `_` used for argument(s) or variable(s) who's values are ignored.
+
+const { complement, flip, identity } = require("../lang/functional");
+const { isArray, isArguments, isMap, isSet, isGenerator,
+ isString, isBoolean, isNumber } = require("../lang/type");
+
+const Sequence = function Sequence(iterator) {
+ if (!isGenerator(iterator)) {
+ throw TypeError("Expected generator argument");
+ }
+
+ this[Symbol.iterator] = iterator;
+};
+exports.Sequence = Sequence;
+
+const polymorphic = dispatch => x =>
+ x === null ? dispatch.null(null) :
+ x === void(0) ? dispatch.void(void(0)) :
+ isArray(x) ? (dispatch.array || dispatch.indexed)(x) :
+ isString(x) ? (dispatch.string || dispatch.indexed)(x) :
+ isArguments(x) ? (dispatch.arguments || dispatch.indexed)(x) :
+ isMap(x) ? dispatch.map(x) :
+ isSet(x) ? dispatch.set(x) :
+ isNumber(x) ? dispatch.number(x) :
+ isBoolean(x) ? dispatch.boolean(x) :
+ dispatch.default(x);
+
+const nogen = function*() {};
+const empty = () => new Sequence(nogen);
+exports.empty = empty;
+
+const seq = polymorphic({
+ null: empty,
+ void: empty,
+ array: identity,
+ string: identity,
+ arguments: identity,
+ map: identity,
+ set: identity,
+ default: x => x instanceof Sequence ? x : new Sequence(x)
+});
+exports.seq = seq;
+
+// Function to cast seq to string.
+const string = (...etc) => "".concat(...etc);
+exports.string = string;
+
+// Function for casting seq to plain object.
+const object = (...pairs) => {
+ let result = {};
+ for (let [key, value] of pairs)
+ result[key] = value;
+
+ return result;
+};
+exports.object = object;
+
+// Takes `getEnumerator` function that returns `nsISimpleEnumerator`
+// and creates lazy sequence of it's items. Note that function does
+// not take `nsISimpleEnumerator` itslef because that would allow
+// single iteration, which would not be consistent with rest of the
+// lazy sequences.
+const fromEnumerator = getEnumerator => seq(function* () {
+ const enumerator = getEnumerator();
+ while (enumerator.hasMoreElements())
+ yield enumerator.getNext();
+});
+exports.fromEnumerator = fromEnumerator;
+
+// Takes `object` and returns lazy sequence of own `[key, value]`
+// pairs (does not include inherited and non enumerable keys).
+const pairs = polymorphic({
+ null: empty,
+ void: empty,
+ map: identity,
+ indexed: indexed => seq(function* () {
+ const count = indexed.length;
+ let index = 0;
+ while (index < count) {
+ yield [index, indexed[index]];
+ index = index + 1;
+ }
+ }),
+ default: object => seq(function* () {
+ for (let key of Object.keys(object))
+ yield [key, object[key]];
+ })
+});
+exports.pairs = pairs;
+
+const names = polymorphic({
+ null: empty,
+ void: empty,
+ default: object => seq(function*() {
+ for (let name of Object.getOwnPropertyNames(object)) {
+ yield name;
+ }
+ })
+});
+exports.names = names;
+
+const symbols = polymorphic({
+ null: empty,
+ void: empty,
+ default: object => seq(function* () {
+ for (let symbol of Object.getOwnPropertySymbols(object)) {
+ yield symbol;
+ }
+ })
+});
+exports.symbols = symbols;
+
+const keys = polymorphic({
+ null: empty,
+ void: empty,
+ indexed: indexed => seq(function* () {
+ const count = indexed.length;
+ let index = 0;
+ while (index < count) {
+ yield index;
+ index = index + 1;
+ }
+ }),
+ map: map => seq(function* () {
+ for (let [key, _] of map)
+ yield key;
+ }),
+ default: object => seq(function* () {
+ for (let key of Object.keys(object))
+ yield key;
+ })
+});
+exports.keys = keys;
+
+
+const values = polymorphic({
+ null: empty,
+ void: empty,
+ set: identity,
+ indexed: indexed => seq(function* () {
+ const count = indexed.length;
+ let index = 0;
+ while (index < count) {
+ yield indexed[index];
+ index = index + 1;
+ }
+ }),
+ map: map => seq(function* () {
+ for (let [_, value] of map) yield value;
+ }),
+ default: object => seq(function* () {
+ for (let key of Object.keys(object)) yield object[key];
+ })
+});
+exports.values = values;
+
+
+
+// Returns a lazy sequence of `x`, `f(x)`, `f(f(x))` etc.
+// `f` must be free of side-effects. Note that returned
+// sequence is infinite so it must be consumed partially.
+//
+// Implements clojure iterate:
+// http://clojuredocs.org/clojure_core/clojure.core/iterate
+const iterate = (f, x) => seq(function* () {
+ let state = x;
+ while (true) {
+ yield state;
+ state = f(state);
+ }
+});
+exports.iterate = iterate;
+
+// Returns a lazy sequence of the items in sequence for which `p(item)`
+// returns `true`. `p` must be free of side-effects.
+//
+// Implements clojure filter:
+// http://clojuredocs.org/clojure_core/clojure.core/filter
+const filter = (p, sequence) => seq(function* () {
+ if (sequence !== null && sequence !== void(0)) {
+ for (let item of sequence) {
+ if (p(item))
+ yield item;
+ }
+ }
+});
+exports.filter = filter;
+
+// Returns a lazy sequence consisting of the result of applying `f` to the
+// set of first items of each sequence, followed by applying f to the set
+// of second items in each sequence, until any one of the sequences is
+// exhausted. Any remaining items in other sequences are ignored. Function
+// `f` should accept number-of-sequences arguments.
+//
+// Implements clojure map:
+// http://clojuredocs.org/clojure_core/clojure.core/map
+const map = (f, ...sequences) => seq(function* () {
+ const count = sequences.length;
+ // Optimize a single sequence case
+ if (count === 1) {
+ let [sequence] = sequences;
+ if (sequence !== null && sequence !== void(0)) {
+ for (let item of sequence)
+ yield f(item);
+ }
+ }
+ else {
+ // define args array that will be recycled on each
+ // step to aggregate arguments to be passed to `f`.
+ let args = [];
+ // define inputs to contain started generators.
+ let inputs = [];
+
+ let index = 0;
+ while (index < count) {
+ inputs[index] = sequences[index][Symbol.iterator]();
+ index = index + 1;
+ }
+
+ // Run loop yielding of applying `f` to the set of
+ // items at each step until one of the `inputs` is
+ // exhausted.
+ let done = false;
+ while (!done) {
+ let index = 0;
+ let value = void(0);
+ while (index < count && !done) {
+ ({ done, value } = inputs[index].next());
+
+ // If input is not exhausted yet store value in args.
+ if (!done) {
+ args[index] = value;
+ index = index + 1;
+ }
+ }
+
+ // If none of the inputs is exhasted yet, `args` contain items
+ // from each input so we yield application of `f` over them.
+ if (!done)
+ yield f(...args);
+ }
+ }
+});
+exports.map = map;
+
+// Returns a lazy sequence of the intermediate values of the reduction (as
+// per reduce) of sequence by `f`, starting with `initial` value if provided.
+//
+// Implements clojure reductions:
+// http://clojuredocs.org/clojure_core/clojure.core/reductions
+const reductions = (...params) => {
+ const count = params.length;
+ let hasInitial = false;
+ let f, initial, source;
+ if (count === 2) {
+ [f, source] = params;
+ }
+ else if (count === 3) {
+ [f, initial, source] = params;
+ hasInitial = true;
+ }
+ else {
+ throw Error("Invoked with wrong number of arguments: " + count);
+ }
+
+ const sequence = seq(source);
+
+ return seq(function* () {
+ let started = hasInitial;
+ let result = void(0);
+
+ // If initial is present yield it.
+ if (hasInitial)
+ yield (result = initial);
+
+ // For each item of the sequence accumulate new result.
+ for (let item of sequence) {
+ // If nothing has being yield yet set result to first
+ // item and yield it.
+ if (!started) {
+ started = true;
+ yield (result = item);
+ }
+ // Otherwise accumulate new result and yield it.
+ else {
+ yield (result = f(result, item));
+ }
+ }
+
+ // If nothing has being yield yet it's empty sequence and no
+ // `initial` was provided in which case we need to yield `f()`.
+ if (!started)
+ yield f();
+ });
+};
+exports.reductions = reductions;
+
+// `f` should be a function of 2 arguments. If `initial` is not supplied,
+// returns the result of applying `f` to the first 2 items in sequence, then
+// applying `f` to that result and the 3rd item, etc. If sequence contains no
+// items, `f` must accept no arguments as well, and reduce returns the
+// result of calling f with no arguments. If sequence has only 1 item, it
+// is returned and `f` is not called. If `initial` is supplied, returns the
+// result of applying `f` to `initial` and the first item in sequence, then
+// applying `f` to that result and the 2nd item, etc. If sequence contains no
+// items, returns `initial` and `f` is not called.
+//
+// Implements clojure reduce:
+// http://clojuredocs.org/clojure_core/clojure.core/reduce
+const reduce = (...args) => {
+ const xs = reductions(...args);
+ let x;
+ for (x of xs) void(0);
+ return x;
+};
+exports.reduce = reduce;
+
+const each = (f, sequence) => {
+ for (let x of seq(sequence)) void(f(x));
+};
+exports.each = each;
+
+
+const inc = x => x + 1;
+// Returns the number of items in the sequence. `count(null)` && `count()`
+// returns `0`. Also works on strings, arrays, Maps & Sets.
+
+// Implements clojure count:
+// http://clojuredocs.org/clojure_core/clojure.core/count
+const count = polymorphic({
+ null: _ => 0,
+ void: _ => 0,
+ indexed: indexed => indexed.length,
+ map: map => map.size,
+ set: set => set.size,
+ default: xs => reduce(inc, 0, xs)
+});
+exports.count = count;
+
+// Returns `true` if sequence has no items.
+
+// Implements clojure empty?:
+// http://clojuredocs.org/clojure_core/clojure.core/empty_q
+const isEmpty = sequence => {
+ // Treat `null` and `undefined` as empty sequences.
+ if (sequence === null || sequence === void(0))
+ return true;
+
+ // If contains any item non empty so return `false`.
+ for (let _ of sequence)
+ return false;
+
+ // If has not returned yet, there was nothing to iterate
+ // so it's empty.
+ return true;
+};
+exports.isEmpty = isEmpty;
+
+const and = (a, b) => a && b;
+
+// Returns true if `p(x)` is logical `true` for every `x` in sequence, else
+// `false`.
+//
+// Implements clojure every?:
+// http://clojuredocs.org/clojure_core/clojure.core/every_q
+const isEvery = (p, sequence) => {
+ if (sequence !== null && sequence !== void(0)) {
+ for (let item of sequence) {
+ if (!p(item))
+ return false;
+ }
+ }
+ return true;
+};
+exports.isEvery = isEvery;
+
+// Returns the first logical true value of (p x) for any x in sequence,
+// else `null`.
+//
+// Implements clojure some:
+// http://clojuredocs.org/clojure_core/clojure.core/some
+const some = (p, sequence) => {
+ if (sequence !== null && sequence !== void(0)) {
+ for (let item of sequence) {
+ if (p(item))
+ return true;
+ }
+ }
+ return null;
+};
+exports.some = some;
+
+// Returns a lazy sequence of the first `n` items in sequence, or all items if
+// there are fewer than `n`.
+//
+// Implements clojure take:
+// http://clojuredocs.org/clojure_core/clojure.core/take
+const take = (n, sequence) => n <= 0 ? empty() : seq(function* () {
+ let count = n;
+ for (let item of sequence) {
+ yield item;
+ count = count - 1;
+ if (count === 0) break;
+ }
+});
+exports.take = take;
+
+// Returns a lazy sequence of successive items from sequence while
+// `p(item)` returns `true`. `p` must be free of side-effects.
+//
+// Implements clojure take-while:
+// http://clojuredocs.org/clojure_core/clojure.core/take-while
+const takeWhile = (p, sequence) => seq(function* () {
+ for (let item of sequence) {
+ if (!p(item))
+ break;
+
+ yield item;
+ }
+});
+exports.takeWhile = takeWhile;
+
+// Returns a lazy sequence of all but the first `n` items in
+// sequence.
+//
+// Implements clojure drop:
+// http://clojuredocs.org/clojure_core/clojure.core/drop
+const drop = (n, sequence) => seq(function* () {
+ if (sequence !== null && sequence !== void(0)) {
+ let count = n;
+ for (let item of sequence) {
+ if (count > 0)
+ count = count - 1;
+ else
+ yield item;
+ }
+ }
+});
+exports.drop = drop;
+
+// Returns a lazy sequence of the items in sequence starting from the
+// first item for which `p(item)` returns falsy value.
+//
+// Implements clojure drop-while:
+// http://clojuredocs.org/clojure_core/clojure.core/drop-while
+const dropWhile = (p, sequence) => seq(function* () {
+ let keep = false;
+ for (let item of sequence) {
+ keep = keep || !p(item);
+ if (keep) yield item;
+ }
+});
+exports.dropWhile = dropWhile;
+
+// Returns a lazy sequence representing the concatenation of the
+// suplied sequences.
+//
+// Implements clojure conact:
+// http://clojuredocs.org/clojure_core/clojure.core/concat
+const concat = (...sequences) => seq(function* () {
+ for (let sequence of sequences)
+ for (let item of sequence)
+ yield item;
+});
+exports.concat = concat;
+
+// Returns the first item in the sequence.
+//
+// Implements clojure first:
+// http://clojuredocs.org/clojure_core/clojure.core/first
+const first = sequence => {
+ if (sequence !== null && sequence !== void(0)) {
+ for (let item of sequence)
+ return item;
+ }
+ return null;
+};
+exports.first = first;
+
+// Returns a possibly empty sequence of the items after the first.
+//
+// Implements clojure rest:
+// http://clojuredocs.org/clojure_core/clojure.core/rest
+const rest = sequence => drop(1, sequence);
+exports.rest = rest;
+
+// Returns the value at the index. Returns `notFound` or `undefined`
+// if index is out of bounds.
+const nth = (xs, n, notFound) => {
+ if (n >= 0) {
+ if (isArray(xs) || isArguments(xs) || isString(xs)) {
+ return n < xs.length ? xs[n] : notFound;
+ }
+ else if (xs !== null && xs !== void(0)) {
+ let count = n;
+ for (let x of xs) {
+ if (count <= 0)
+ return x;
+
+ count = count - 1;
+ }
+ }
+ }
+ return notFound;
+};
+exports.nth = nth;
+
+// Return the last item in sequence, in linear time.
+// If `sequence` is an array or string or arguments
+// returns in constant time.
+// Implements clojure last:
+// http://clojuredocs.org/clojure_core/clojure.core/last
+const last = polymorphic({
+ null: _ => null,
+ void: _ => null,
+ indexed: indexed => indexed[indexed.length - 1],
+ map: xs => reduce((_, x) => x, xs),
+ set: xs => reduce((_, x) => x, xs),
+ default: xs => reduce((_, x) => x, xs)
+});
+exports.last = last;
+
+// Return a lazy sequence of all but the last `n` (default 1) items
+// from the give `xs`.
+//
+// Implements clojure drop-last:
+// http://clojuredocs.org/clojure_core/clojure.core/drop-last
+const dropLast = flip((xs, n=1) => seq(function* () {
+ let ys = [];
+ for (let x of xs) {
+ ys.push(x);
+ if (ys.length > n)
+ yield ys.shift();
+ }
+}));
+exports.dropLast = dropLast;
+
+// Returns a lazy sequence of the elements of `xs` with duplicates
+// removed
+//
+// Implements clojure distinct
+// http://clojuredocs.org/clojure_core/clojure.core/distinct
+const distinct = sequence => seq(function* () {
+ let items = new Set();
+ for (let item of sequence) {
+ if (!items.has(item)) {
+ items.add(item);
+ yield item;
+ }
+ }
+});
+exports.distinct = distinct;
+
+// Returns a lazy sequence of the items in `xs` for which
+// `p(x)` returns false. `p` must be free of side-effects.
+//
+// Implements clojure remove
+// http://clojuredocs.org/clojure_core/clojure.core/remove
+const remove = (p, xs) => filter(complement(p), xs);
+exports.remove = remove;
+
+// Returns the result of applying concat to the result of
+// `map(f, xs)`. Thus function `f` should return a sequence.
+//
+// Implements clojure mapcat
+// http://clojuredocs.org/clojure_core/clojure.core/mapcat
+const mapcat = (f, sequence) => seq(function* () {
+ const sequences = map(f, sequence);
+ for (let sequence of sequences)
+ for (let item of sequence)
+ yield item;
+});
+exports.mapcat = mapcat;
diff --git a/addon-sdk/source/lib/sdk/util/uuid.js b/addon-sdk/source/lib/sdk/util/uuid.js
new file mode 100644
index 000000000..6d0f2de53
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/uuid.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { Cc, Ci, components: { ID: parseUUID } } = require('chrome');
+const { generateUUID } = Cc['@mozilla.org/uuid-generator;1'].
+ getService(Ci.nsIUUIDGenerator);
+
+// Returns `uuid`. If `id` is passed then it's parsed to `uuid` and returned
+// if not then new one is generated.
+exports.uuid = function uuid(id) {
+ return id ? parseUUID(id) : generateUUID();
+};
diff --git a/addon-sdk/source/lib/sdk/view/core.js b/addon-sdk/source/lib/sdk/view/core.js
new file mode 100644
index 000000000..5e82e9b5d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/view/core.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+var { Ci } = require("chrome");
+var method = require("../../method/core");
+
+// Returns DOM node associated with a view for
+// the given `value`. If `value` has no view associated
+// it returns `null`. You can implement this method for
+// this type to define what the result should be for it.
+var getNodeView = method("getNodeView");
+getNodeView.define(x =>
+ x instanceof Ci.nsIDOMNode ? x :
+ x instanceof Ci.nsIDOMWindow ? x :
+ null);
+exports.getNodeView = getNodeView;
+exports.viewFor = getNodeView;
+
+var getActiveView = method("getActiveView");
+exports.getActiveView = getActiveView;
diff --git a/addon-sdk/source/lib/sdk/webextension.js b/addon-sdk/source/lib/sdk/webextension.js
new file mode 100644
index 000000000..d1c4385e2
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/webextension.js
@@ -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": "experimental"
+};
+
+let webExtension;
+let waitForWebExtensionAPI;
+
+module.exports = {
+ initFromBootstrapAddonParam(data) {
+ if (webExtension) {
+ throw new Error("'sdk/webextension' module has been already initialized");
+ }
+
+ webExtension = data.webExtension;
+ },
+
+ startup() {
+ if (!webExtension) {
+ return Promise.reject(new Error(
+ "'sdk/webextension' module is currently disabled. " +
+ "('hasEmbeddedWebExtension' option is missing or set to false)"
+ ));
+ }
+
+ // NOTE: calling `startup` more than once raises an "Embedded Extension already started"
+ // error, but given that SDK addons are going to have access to the startup method through
+ // an SDK module that can be required in any part of the addon, it will be nicer if any
+ // additional startup calls return the startup promise instead of raising an exception,
+ // so that the SDK addon can access the API object in the other addon modules without the
+ // need to manually pass this promise around.
+ if (!waitForWebExtensionAPI) {
+ waitForWebExtensionAPI = webExtension.startup();
+ }
+
+ return waitForWebExtensionAPI;
+ }
+};
diff --git a/addon-sdk/source/lib/sdk/window/browser.js b/addon-sdk/source/lib/sdk/window/browser.js
new file mode 100644
index 000000000..380b5a486
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/window/browser.js
@@ -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 { windowNS } = require('./namespace');
+const { on, off, once } = require('../event/core');
+const { method } = require('../lang/functional');
+const { getWindowTitle } = require('./utils');
+const unload = require('../system/unload');
+const { EventTarget } = require('../event/target');
+const { isPrivate } = require('../private-browsing/utils');
+const { isWindowPrivate, isFocused } = require('../window/utils');
+const { viewFor } = require('../view/core');
+
+const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec, consider using require("sdk/tabs") instead';
+
+const BrowserWindow = Class({
+ initialize: function initialize(options) {
+ EventTarget.prototype.initialize.call(this, options);
+ windowNS(this).window = options.window;
+ },
+ activate: function activate() {
+ // TODO
+ return null;
+ },
+ close: function() {
+ throw new Error(ERR_FENNEC_MSG);
+ return null;
+ },
+ get title() {
+ return getWindowTitle(windowNS(this).window);
+ },
+ // NOTE: Fennec only has one window, which is assumed below
+ // TODO: remove assumption below
+ // NOTE: tabs requires windows
+ get tabs() {
+ return require('../tabs');
+ },
+ get activeTab() {
+ return require('../tabs').activeTab;
+ },
+ on: method(on),
+ removeListener: method(off),
+ once: method(once)
+});
+exports.BrowserWindow = BrowserWindow;
+
+const getWindowView = window => windowNS(window).window;
+
+viewFor.define(BrowserWindow, getWindowView);
+isPrivate.define(BrowserWindow, (window) => isWindowPrivate(viewFor(window).window));
+isFocused.define(BrowserWindow, (window) => isFocused(viewFor(window).window));
diff --git a/addon-sdk/source/lib/sdk/window/events.js b/addon-sdk/source/lib/sdk/window/events.js
new file mode 100644
index 000000000..b1d3a1f3e
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/window/events.js
@@ -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": "unstable"
+};
+
+const { Ci, Cu } = require("chrome");
+const { observe } = require("../event/chrome");
+const { open } = require("../event/dom");
+const { windows } = require("../window/utils");
+const { filter, merge, map, expand } = require("../event/utils");
+
+function documentMatches(weakWindow, event) {
+ let window = weakWindow.get();
+ return window && event.target === window.document;
+}
+
+function makeStrictDocumentFilter(window) {
+ // Note: Do not define a closure within this function. Otherwise
+ // you may leak the window argument.
+ let weak = Cu.getWeakReference(window);
+ return documentMatches.bind(null, weak);
+}
+
+function toEventWithDefaultViewTarget({type, target}) {
+ return { type: type, target: target.defaultView }
+}
+
+// Function registers single shot event listeners for relevant window events
+// that forward events to exported event stream.
+function eventsFor(window) {
+ // NOTE: Do no use pass a closure from this function into a stream
+ // transform function. You will capture the window in the
+ // closure and leak the window until the event stream is
+ // completely closed.
+ let interactive = open(window, "DOMContentLoaded", { capture: true });
+ let complete = open(window, "load", { capture: true });
+ let states = merge([interactive, complete]);
+ let changes = filter(states, makeStrictDocumentFilter(window));
+ return map(changes, toEventWithDefaultViewTarget);
+}
+
+// Create our event channels. We do this in a separate function to
+// minimize the chance of leaking intermediate objects on the global.
+function makeEvents() {
+ // In addition to observing windows that are open we also observe windows
+ // that are already already opened in case they're in process of loading.
+ var opened = windows(null, { includePrivate: true });
+ var currentEvents = merge(opened.map(eventsFor));
+
+ // Register system event listeners for top level window open / close.
+ function rename({type, target, data}) {
+ return { type: rename[type], target: target, data: data }
+ }
+ rename.domwindowopened = "open";
+ rename.domwindowclosed = "close";
+
+ var openEvents = map(observe("domwindowopened"), rename);
+ var closeEvents = map(observe("domwindowclosed"), rename);
+ var futureEvents = expand(openEvents, ({target}) => eventsFor(target));
+
+ return merge([currentEvents, futureEvents, openEvents, closeEvents]);
+}
+
+exports.events = makeEvents();
diff --git a/addon-sdk/source/lib/sdk/window/helpers.js b/addon-sdk/source/lib/sdk/window/helpers.js
new file mode 100644
index 000000000..56cfcaba7
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/window/helpers.js
@@ -0,0 +1,81 @@
+/* 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 { defer, all } = require('../core/promise');
+const events = require('../system/events');
+const { open: openWindow, onFocus, getToplevelWindow,
+ isInteractive, isStartupFinished, getOuterId } = require('./utils');
+const { Ci } = require("chrome");
+
+function open(uri, options) {
+ return promise(openWindow.apply(null, arguments), 'load').then(focus);
+}
+exports.open = open;
+
+function close(window) {
+ let deferred = defer();
+ let toplevelWindow = getToplevelWindow(window);
+ let outerId = getOuterId(toplevelWindow);
+ events.on("outer-window-destroyed", function onclose({subject}) {
+ let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ if (id == outerId) {
+ events.off("outer-window-destroyed", onclose);
+ deferred.resolve();
+ }
+ }, true);
+ window.close();
+ return deferred.promise;
+}
+exports.close = close;
+
+function focus(window) {
+ let p = onFocus(window);
+ window.focus();
+ return p;
+}
+exports.focus = focus;
+
+function ready(window) {
+ let { promise: result, resolve } = defer();
+
+ if (isInteractive(window))
+ resolve(window);
+ else
+ resolve(promise(window, 'DOMContentLoaded'));
+
+ return result;
+}
+exports.ready = ready;
+
+function startup(window) {
+ let { promise: result, resolve } = defer();
+
+ if (isStartupFinished(window)) {
+ resolve(window);
+ } else {
+ events.on("browser-delayed-startup-finished", function listener({subject}) {
+ if (subject === window) {
+ events.off("browser-delayed-startup-finished", listener);
+ resolve(window);
+ }
+ });
+ }
+
+ return result;
+}
+exports.startup = startup;
+
+function promise(target, evt, capture) {
+ let deferred = defer();
+ capture = !!capture;
+
+ target.addEventListener(evt, function eventHandler() {
+ target.removeEventListener(evt, eventHandler, capture);
+ deferred.resolve(target);
+ }, capture);
+
+ return deferred.promise;
+}
+exports.promise = promise;
diff --git a/addon-sdk/source/lib/sdk/window/namespace.js b/addon-sdk/source/lib/sdk/window/namespace.js
new file mode 100644
index 000000000..b486f888d
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/window/namespace.js
@@ -0,0 +1,6 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+exports.windowNS = require('../core/namespace').ns();
diff --git a/addon-sdk/source/lib/sdk/window/utils.js b/addon-sdk/source/lib/sdk/window/utils.js
new file mode 100644
index 000000000..db91a0fed
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/window/utils.js
@@ -0,0 +1,460 @@
+/* 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 array = require('../util/array');
+const { defer } = require('sdk/core/promise');
+const { dispatcher } = require("../util/dispatcher");
+
+const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1'].
+ getService(Ci.nsIWindowWatcher);
+const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
+ getService(Ci.nsIAppShellService);
+const WM = Cc['@mozilla.org/appshell/window-mediator;1'].
+ getService(Ci.nsIWindowMediator);
+const io = Cc['@mozilla.org/network/io-service;1'].
+ getService(Ci.nsIIOService);
+const FM = Cc["@mozilla.org/focus-manager;1"].
+ getService(Ci.nsIFocusManager);
+
+const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+
+const prefs = require("../preferences/service");
+const BROWSER = 'navigator:browser',
+ URI_BROWSER = prefs.get('browser.chromeURL', null),
+ NAME = '_blank',
+ FEATURES = 'chrome,all,dialog=no,non-private';
+
+function isWindowPrivate(win) {
+ if (!win)
+ return false;
+
+ // if the pbService is undefined, the PrivateBrowsingUtils.jsm is available,
+ // and the app is Firefox, then assume per-window private browsing is
+ // enabled.
+ try {
+ return win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext)
+ .usePrivateBrowsing;
+ }
+ catch(e) {}
+
+ // Sometimes the input is not a nsIDOMWindow.. but it is still a winodw.
+ try {
+ return !!win.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing;
+ }
+ catch (e) {}
+
+ return false;
+}
+exports.isWindowPrivate = isWindowPrivate;
+
+function getMostRecentBrowserWindow() {
+ return getMostRecentWindow(BROWSER);
+}
+exports.getMostRecentBrowserWindow = getMostRecentBrowserWindow;
+
+function getHiddenWindow() {
+ return appShellService.hiddenDOMWindow;
+}
+exports.getHiddenWindow = getHiddenWindow;
+
+function getMostRecentWindow(type) {
+ return WM.getMostRecentWindow(type);
+}
+exports.getMostRecentWindow = getMostRecentWindow;
+
+/**
+ * Returns the ID of the window's current inner window.
+ */
+function getInnerId(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+};
+exports.getInnerId = getInnerId;
+
+/**
+ * Returns the ID of the window's outer window.
+ */
+function getOuterId(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+};
+exports.getOuterId = getOuterId;
+
+/**
+ * Returns window by the outer window id.
+ */
+const getByOuterId = WM.getOuterWindowWithId;
+exports.getByOuterId = getByOuterId;
+
+const getByInnerId = WM.getCurrentInnerWindowWithId;
+exports.getByInnerId = getByInnerId;
+
+/**
+ * Returns `nsIXULWindow` for the given `nsIDOMWindow`.
+ */
+function getXULWindow(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShellTreeItem).
+ treeOwner.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIXULWindow);
+};
+exports.getXULWindow = getXULWindow;
+
+function getDOMWindow(xulWindow) {
+ return xulWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindow);
+}
+exports.getDOMWindow = getDOMWindow;
+
+/**
+ * Returns `nsIBaseWindow` for the given `nsIDOMWindow`.
+ */
+function getBaseWindow(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShell).
+ QueryInterface(Ci.nsIDocShellTreeItem).
+ treeOwner.
+ QueryInterface(Ci.nsIBaseWindow);
+}
+exports.getBaseWindow = getBaseWindow;
+
+/**
+ * Returns the `nsIDOMWindow` toplevel window for any child/inner window
+ */
+function getToplevelWindow(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+}
+exports.getToplevelWindow = getToplevelWindow;
+
+function getWindowDocShell(window) {
+ return window.gBrowser.docShell;
+}
+exports.getWindowDocShell = getWindowDocShell;
+
+function getWindowLoadingContext(window) {
+ return getWindowDocShell(window).
+ QueryInterface(Ci.nsILoadContext);
+}
+exports.getWindowLoadingContext = getWindowLoadingContext;
+
+const isTopLevel = window => window && getToplevelWindow(window) === window;
+exports.isTopLevel = isTopLevel;
+
+/**
+ * Takes hash of options and serializes it to a features string that
+ * can be used passed to `window.open`. For more details on features string see:
+ * https://developer.mozilla.org/en/DOM/window.open#Position_and_size_features
+ */
+function serializeFeatures(options) {
+ return Object.keys(options).reduce(function(result, name) {
+ let value = options[name];
+
+ // the chrome and private features are special
+ if ((name == 'private' || name == 'chrome' || name == 'all'))
+ return result + ((value === true) ? ',' + name : '');
+
+ return result + ',' + name + '=' +
+ (value === true ? 'yes' : value === false ? 'no' : value);
+ }, '').substr(1);
+}
+
+/**
+ * Opens a top level window and returns it's `nsIDOMWindow` representation.
+ * @params {String} uri
+ * URI of the document to be loaded into window.
+ * @params {nsIDOMWindow} options.parent
+ * Used as parent for the created window.
+ * @params {String} options.name
+ * Optional name that is assigned to the window.
+ * @params {Object} options.features
+ * Map of key, values like: `{ width: 10, height: 15, chrome: true, private: true }`.
+ */
+function open(uri, options) {
+ uri = uri || URI_BROWSER;
+ options = options || {};
+
+ if (!uri)
+ throw new Error('browser.chromeURL is undefined, please provide an explicit uri');
+
+ if (['chrome', 'resource', 'data'].indexOf(io.newURI(uri, null, null).scheme) < 0)
+ throw new Error('only chrome, resource and data uris are allowed');
+
+ let newWindow = windowWatcher.
+ openWindow(options.parent || null,
+ uri,
+ options.name || null,
+ options.features ? serializeFeatures(options.features) : null,
+ options.args || null);
+
+ return newWindow;
+}
+exports.open = open;
+
+function onFocus(window) {
+ let { resolve, promise } = defer();
+
+ if (isFocused(window)) {
+ resolve(window);
+ }
+ else {
+ window.addEventListener("focus", function focusListener() {
+ window.removeEventListener("focus", focusListener, true);
+ resolve(window);
+ }, true);
+ }
+
+ return promise;
+}
+exports.onFocus = onFocus;
+
+var isFocused = dispatcher("window-isFocused");
+isFocused.when(x => x instanceof Ci.nsIDOMWindow, (window) => {
+ const FM = Cc["@mozilla.org/focus-manager;1"].
+ getService(Ci.nsIFocusManager);
+
+ let childTargetWindow = {};
+ FM.getFocusedElementForWindow(window, true, childTargetWindow);
+ childTargetWindow = childTargetWindow.value;
+
+ let focusedChildWindow = {};
+ if (FM.activeWindow) {
+ FM.getFocusedElementForWindow(FM.activeWindow, true, focusedChildWindow);
+ focusedChildWindow = focusedChildWindow.value;
+ }
+
+ return (focusedChildWindow === childTargetWindow);
+});
+exports.isFocused = isFocused;
+
+/**
+ * Opens a top level window and returns it's `nsIDOMWindow` representation.
+ * Same as `open` but with more features
+ * @param {Object} options
+ *
+ */
+function openDialog(options) {
+ options = options || {};
+
+ let features = options.features || FEATURES;
+ let featureAry = features.toLowerCase().split(',');
+
+ if (!!options.private) {
+ // add private flag if private window is desired
+ if (!array.has(featureAry, 'private')) {
+ featureAry.push('private');
+ }
+
+ // remove the non-private flag ig a private window is desired
+ let nonPrivateIndex = featureAry.indexOf('non-private');
+ if (nonPrivateIndex >= 0) {
+ featureAry.splice(nonPrivateIndex, 1);
+ }
+
+ features = featureAry.join(',');
+ }
+
+ let browser = getMostRecentBrowserWindow();
+
+ // if there is no browser then do nothing
+ if (!browser)
+ return undefined;
+
+ let newWindow = browser.openDialog.apply(
+ browser,
+ array.flatten([
+ options.url || URI_BROWSER,
+ options.name || NAME,
+ features,
+ options.args || null
+ ])
+ );
+
+ return newWindow;
+}
+exports.openDialog = openDialog;
+
+/**
+ * Returns an array of all currently opened windows.
+ * Note that these windows may still be loading.
+ */
+function windows(type, options) {
+ options = options || {};
+ let list = [];
+ let winEnum = WM.getEnumerator(type);
+ while (winEnum.hasMoreElements()) {
+ let window = winEnum.getNext().QueryInterface(Ci.nsIDOMWindow);
+ // Only add non-private windows when pb permission isn't set,
+ // unless an option forces the addition of them.
+ if (!window.closed && (options.includePrivate || !isWindowPrivate(window))) {
+ list.push(window);
+ }
+ }
+ return list;
+}
+exports.windows = windows;
+
+/**
+ * Check if the given window is interactive.
+ * i.e. if its "DOMContentLoaded" event has already been fired.
+ * @params {nsIDOMWindow} window
+ */
+const isInteractive = window =>
+ window.document.readyState === "interactive" ||
+ isDocumentLoaded(window) ||
+ // XUL documents stays '"uninitialized"' until it's `readyState` becomes
+ // `"complete"`.
+ isXULDocumentWindow(window) && window.document.readyState === "interactive";
+exports.isInteractive = isInteractive;
+
+/**
+ * Check if the given browser window has finished the startup.
+ * @params {nsIDOMWindow} window
+ */
+const isStartupFinished = (window) =>
+ isBrowser(window) &&
+ window.gBrowserInit &&
+ window.gBrowserInit.delayedStartupFinished;
+
+exports.isStartupFinished = isStartupFinished;
+
+const isXULDocumentWindow = ({document}) =>
+ document.documentElement &&
+ document.documentElement.namespaceURI === XUL_NS;
+
+/**
+ * Check if the given window is completely loaded.
+ * i.e. if its "load" event has already been fired and all possible DOM content
+ * is done loading (the whole DOM document, images content, ...)
+ * @params {nsIDOMWindow} window
+ */
+function isDocumentLoaded(window) {
+ return window.document.readyState == "complete";
+}
+exports.isDocumentLoaded = isDocumentLoaded;
+
+function isBrowser(window) {
+ try {
+ return window.document.documentElement.getAttribute("windowtype") === BROWSER;
+ }
+ catch (e) {}
+ return false;
+};
+exports.isBrowser = isBrowser;
+
+function getWindowTitle(window) {
+ return window && window.document ? window.document.title : null;
+}
+exports.getWindowTitle = getWindowTitle;
+
+function isXULBrowser(window) {
+ return !!(isBrowser(window) && window.XULBrowserWindow);
+}
+exports.isXULBrowser = isXULBrowser;
+
+/**
+ * Returns the most recent focused window
+ */
+function getFocusedWindow() {
+ let window = WM.getMostRecentWindow(BROWSER);
+
+ return window ? window.document.commandDispatcher.focusedWindow : null;
+}
+exports.getFocusedWindow = getFocusedWindow;
+
+/**
+ * Returns the focused browser window if any, or the most recent one.
+ * Opening new window, updates most recent window, but focus window
+ * changes later; so most recent window and focused window are not always
+ * the same.
+ */
+function getFocusedBrowser() {
+ let window = FM.activeWindow;
+ return isBrowser(window) ? window : getMostRecentBrowserWindow()
+}
+exports.getFocusedBrowser = getFocusedBrowser;
+
+/**
+ * Returns the focused element in the most recent focused window
+ */
+function getFocusedElement() {
+ let window = WM.getMostRecentWindow(BROWSER);
+
+ return window ? window.document.commandDispatcher.focusedElement : null;
+}
+exports.getFocusedElement = getFocusedElement;
+
+function getFrames(window) {
+ return Array.slice(window.frames).reduce(function(frames, frame) {
+ return frames.concat(frame, getFrames(frame));
+ }, []);
+}
+exports.getFrames = getFrames;
+
+function getScreenPixelsPerCSSPixel(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).screenPixelsPerCSSPixel;
+}
+exports.getScreenPixelsPerCSSPixel = getScreenPixelsPerCSSPixel;
+
+function getOwnerBrowserWindow(node) {
+ /**
+ Takes DOM node and returns browser window that contains it.
+ **/
+ let window = getToplevelWindow(node.ownerDocument.defaultView);
+ // If anchored window is browser then it's target browser window.
+ return isBrowser(window) ? window : null;
+}
+exports.getOwnerBrowserWindow = getOwnerBrowserWindow;
+
+function getParentWindow(window) {
+ try {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem).parent
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ }
+ catch (e) {}
+ return null;
+}
+exports.getParentWindow = getParentWindow;
+
+
+function getParentFrame(window) {
+ try {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem).parent
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ }
+ catch (e) {}
+ return null;
+}
+exports.getParentWindow = getParentWindow;
+
+// The element in which the window is embedded, or `null`
+// if the window is top-level. Similar to `window.frameElement`
+// but can cross chrome-content boundries.
+const getFrameElement = target =>
+ (target instanceof Ci.nsIDOMDocument ? target.defaultView : target).
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).
+ containerElement;
+exports.getFrameElement = getFrameElement;
diff --git a/addon-sdk/source/lib/sdk/windows.js b/addon-sdk/source/lib/sdk/windows.js
new file mode 100644
index 000000000..06dbe70b2
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/windows.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ 'stability': 'stable'
+};
+
+const { isBrowser } = require('./window/utils');
+const { modelFor } = require('./model/core');
+const { viewFor } = require('./view/core');
+
+
+if (require('./system/xul-app').is('Fennec')) {
+ module.exports = require('./windows/fennec');
+}
+else {
+ module.exports = require('./windows/firefox');
+}
+
+
+const browsers = module.exports.browserWindows;
+
+//
+modelFor.when(isBrowser, view => {
+ for (let model of browsers) {
+ if (viewFor(model) === view)
+ return model;
+ }
+ return null;
+});
diff --git a/addon-sdk/source/lib/sdk/windows/fennec.js b/addon-sdk/source/lib/sdk/windows/fennec.js
new file mode 100644
index 000000000..3c3b6c313
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/windows/fennec.js
@@ -0,0 +1,83 @@
+/* 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 { BrowserWindow } = require('../window/browser');
+const { WindowTracker } = require('../deprecated/window-utils');
+const { isBrowser, getMostRecentBrowserWindow } = require('../window/utils');
+const { windowNS } = require('../window/namespace');
+const { on, off, once, emit } = require('../event/core');
+const { method } = require('../lang/functional');
+const { EventTarget } = require('../event/target');
+const { List, addListItem } = require('../util/list');
+
+const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec, consider using require("sdk/tabs") instead';
+
+// NOTE: On Fennec there is only one window.
+
+var BrowserWindows = Class({
+ implements: [ List ],
+ extends: EventTarget,
+ initialize: function() {
+ List.prototype.initialize.apply(this);
+ },
+ get activeWindow() {
+ let window = getMostRecentBrowserWindow();
+ return window ? getBrowserWindow({window: window}) : null;
+ },
+ open: function open(options) {
+ throw new Error(ERR_FENNEC_MSG);
+ return null;
+ }
+});
+const browserWindows = exports.browserWindows = BrowserWindows();
+
+
+/**
+ * Gets a `BrowserWindow` for the given `chromeWindow` if previously
+ * registered, `null` otherwise.
+ */
+function getRegisteredWindow(chromeWindow) {
+ for (let window of browserWindows) {
+ if (chromeWindow === windowNS(window).window)
+ return window;
+ }
+
+ return null;
+}
+
+/**
+ * Gets a `BrowserWindow` for the provided window options obj
+ * @params {Object} options
+ * Options that are passed to the the `BrowserWindow`
+ * @returns {BrowserWindow}
+ */
+function getBrowserWindow(options) {
+ let window = null;
+
+ // if we have a BrowserWindow already then use it
+ if ('window' in options)
+ window = getRegisteredWindow(options.window);
+ if (window)
+ return window;
+
+ // we don't have a BrowserWindow yet, so create one
+ window = BrowserWindow(options);
+ addListItem(browserWindows, window);
+ return window;
+}
+
+WindowTracker({
+ onTrack: function onTrack(chromeWindow) {
+ if (!isBrowser(chromeWindow)) return;
+ let window = getBrowserWindow({ window: chromeWindow });
+ emit(browserWindows, 'open', window);
+ },
+ onUntrack: function onUntrack(chromeWindow) {
+ if (!isBrowser(chromeWindow)) return;
+ let window = getBrowserWindow({ window: chromeWindow });
+ emit(browserWindows, 'close', window);
+ }
+});
diff --git a/addon-sdk/source/lib/sdk/windows/firefox.js b/addon-sdk/source/lib/sdk/windows/firefox.js
new file mode 100644
index 000000000..1eb1d8488
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/windows/firefox.js
@@ -0,0 +1,224 @@
+/* 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 { observer } = require('./observer');
+const { isBrowser, getMostRecentBrowserWindow, windows, open, getInnerId,
+ getWindowTitle, getToplevelWindow, isFocused, isWindowPrivate } = require('../window/utils');
+const { List, addListItem, removeListItem } = require('../util/list');
+const { viewFor } = require('../view/core');
+const { modelFor } = require('../model/core');
+const { emit, emitOnObject, setListeners } = require('../event/core');
+const { once } = require('../dom/events');
+const { EventTarget } = require('../event/target');
+const { getSelectedTab } = require('../tabs/utils');
+const { Cc, Ci } = require('chrome');
+const { Options } = require('../tabs/common');
+const system = require('../system/events');
+const { ignoreWindow, isPrivate, isWindowPBSupported } = require('../private-browsing/utils');
+const { data, isPrivateBrowsingSupported } = require('../self');
+const { setImmediate } = require('../timers');
+
+const supportPrivateWindows = isPrivateBrowsingSupported && isWindowPBSupported;
+
+const modelsFor = new WeakMap();
+const viewsFor = new WeakMap();
+
+const Window = Class({
+ implements: [EventTarget],
+ initialize: function(domWindow) {
+ modelsFor.set(domWindow, this);
+ viewsFor.set(this, domWindow);
+ },
+
+ get title() {
+ return getWindowTitle(viewsFor.get(this));
+ },
+
+ activate: function() {
+ viewsFor.get(this).focus();
+ },
+
+ close: function(callback) {
+ let domWindow = viewsFor.get(this);
+
+ if (callback) {
+ // We want to catch the close event immediately after the close events are
+ // emitted everywhere but without letting the event loop spin. Registering
+ // for the same events as windowEventListener but afterwards does this
+ let listener = (event, closedWin) => {
+ if (event != "close" || closedWin != domWindow)
+ return;
+
+ observer.off("*", listener);
+ callback();
+ }
+
+ observer.on("*", listener);
+ }
+
+ domWindow.close();
+ }
+});
+
+const windowTabs = new WeakMap();
+
+const BrowserWindow = Class({
+ extends: Window,
+
+ get tabs() {
+ let tabs = windowTabs.get(this);
+ if (tabs)
+ return tabs;
+
+ return new WindowTabs(this);
+ }
+});
+
+const WindowTabs = Class({
+ implements: [EventTarget],
+ extends: List,
+ initialize: function(window) {
+ List.prototype.initialize.call(this);
+ windowTabs.set(window, this);
+ viewsFor.set(this, viewsFor.get(window));
+
+ // Make sure the tabs module has loaded and found all existing tabs
+ const tabs = require('../tabs');
+
+ for (let tab of tabs) {
+ if (tab.window == window)
+ addListItem(this, tab);
+ }
+ },
+
+ get activeTab() {
+ return modelFor(getSelectedTab(viewsFor.get(this)));
+ },
+
+ open: function(options) {
+ options = Options(options);
+
+ let domWindow = viewsFor.get(this);
+ let { Tab } = require('../tabs/tab-firefox');
+
+ // The capturing listener will see the TabOpen event before
+ // sdk/tabs/observer giving us time to set up the tab and listeners before
+ // the real open event is fired
+ let listener = event => {
+ new Tab(event.target, options);
+ };
+
+ once(domWindow, "TabOpen", listener, true);
+ domWindow.gBrowser.addTab(options.url);
+ }
+});
+
+const BrowserWindows = Class({
+ implements: [EventTarget],
+ extends: List,
+ initialize: function() {
+ List.prototype.initialize.call(this);
+ },
+
+ get activeWindow() {
+ let domWindow = getMostRecentBrowserWindow();
+ if (ignoreWindow(domWindow))
+ return null;
+ return modelsFor.get(domWindow);
+ },
+
+ open: function(options) {
+ if (typeof options == "string")
+ options = { url: options };
+
+ let { url, isPrivate } = options;
+ if (url)
+ url = data.url(url);
+
+ let args = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ args.data = url;
+
+ let features = {
+ chrome: true,
+ all: true,
+ dialog: false
+ };
+ features.private = supportPrivateWindows && isPrivate;
+
+ let domWindow = open(null, {
+ parent: null,
+ name: "_blank",
+ features,
+ args
+ })
+
+ let window = makeNewWindow(domWindow, true);
+ setListeners(window, options);
+ return window;
+ }
+});
+
+const browserWindows = new BrowserWindows();
+exports.browserWindows = browserWindows;
+
+function windowEmit(window, event, ...args) {
+ if (window instanceof BrowserWindow && (event == "open" || event == "close"))
+ emitOnObject(window, event, browserWindows, window, ...args);
+ else
+ emit(window, event, window, ...args);
+
+ if (window instanceof BrowserWindow)
+ emit(browserWindows, event, window, ...args);
+}
+
+function makeNewWindow(domWindow, browserHint = false) {
+ if (browserHint || isBrowser(domWindow))
+ return new BrowserWindow(domWindow);
+ else
+ return new Window(domWindow);
+}
+
+for (let domWindow of windows(null, {includePrivate: supportPrivateWindows})) {
+ let window = makeNewWindow(domWindow);
+ if (window instanceof BrowserWindow)
+ addListItem(browserWindows, window);
+}
+
+var windowEventListener = (event, domWindow, ...args) => {
+ let toplevelWindow = getToplevelWindow(domWindow);
+
+ if (ignoreWindow(toplevelWindow))
+ return;
+
+ let window = modelsFor.get(toplevelWindow);
+ if (!window)
+ window = makeNewWindow(toplevelWindow);
+
+ if (isBrowser(toplevelWindow)) {
+ if (event == "open")
+ addListItem(browserWindows, window);
+ else if (event == "close")
+ removeListItem(browserWindows, window);
+ }
+
+ windowEmit(window, event, ...args);
+
+ // The window object shouldn't be reachable after closed
+ if (event == "close") {
+ viewsFor.delete(window);
+ modelsFor.delete(toplevelWindow);
+ }
+};
+observer.on("*", windowEventListener);
+
+viewFor.define(BrowserWindow, window => {
+ return viewsFor.get(window);
+})
+
+const isBrowserWindow = (x) => x instanceof BrowserWindow;
+isPrivate.when(isBrowserWindow, (w) => isWindowPrivate(viewsFor.get(w)));
+isFocused.when(isBrowserWindow, (w) => isFocused(viewsFor.get(w)));
diff --git a/addon-sdk/source/lib/sdk/windows/observer.js b/addon-sdk/source/lib/sdk/windows/observer.js
new file mode 100644
index 000000000..5ba2535f1
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/windows/observer.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { EventTarget } = require("../event/target");
+const { emit } = require("../event/core");
+const { WindowTracker, windowIterator } = require("../deprecated/window-utils");
+const { DOMEventAssembler } = require("../deprecated/events/assembler");
+const { Class } = require("../core/heritage");
+const { Cu } = require("chrome");
+
+// Event emitter objects used to register listeners and emit events on them
+// when they occur.
+const Observer = Class({
+ initialize() {
+ // Using `WindowTracker` to track window events.
+ WindowTracker({
+ onTrack: chromeWindow => {
+ emit(this, "open", chromeWindow);
+ this.observe(chromeWindow);
+ },
+ onUntrack: chromeWindow => {
+ emit(this, "close", chromeWindow);
+ this.ignore(chromeWindow);
+ }
+ });
+ },
+ implements: [EventTarget, DOMEventAssembler],
+ /**
+ * Events that are supported and emitted by the module.
+ */
+ supportedEventsTypes: [ "activate", "deactivate" ],
+ /**
+ * 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) {
+ // Ignore events from windows in the child process as they can't be top-level
+ if (Cu.isCrossProcessWrapper(event.target))
+ return;
+ emit(this, event.type, event.target, event);
+ }
+});
+
+exports.observer = new Observer();
diff --git a/addon-sdk/source/lib/sdk/windows/tabs-fennec.js b/addon-sdk/source/lib/sdk/windows/tabs-fennec.js
new file mode 100644
index 000000000..0ef5ec9f5
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/windows/tabs-fennec.js
@@ -0,0 +1,172 @@
+/* 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 { Tab } = require('../tabs/tab');
+const { browserWindows } = require('./fennec');
+const { windowNS } = require('../window/namespace');
+const { tabsNS, tabNS } = require('../tabs/namespace');
+const { openTab, getTabs, getSelectedTab, getTabForBrowser: getRawTabForBrowser,
+ getTabContentWindow } = require('../tabs/utils');
+const { Options } = require('../tabs/common');
+const { getTabForBrowser, getTabForRawTab } = require('../tabs/helpers');
+const { on, once, off, emit } = require('../event/core');
+const { method } = require('../lang/functional');
+const { EVENTS } = require('../tabs/events');
+const { EventTarget } = require('../event/target');
+const { when: unload } = require('../system/unload');
+const { windowIterator } = require('../deprecated/window-utils');
+const { List, addListItem, removeListItem } = require('../util/list');
+const { isPrivateBrowsingSupported, data } = require('../self');
+const { isTabPBSupported, ignoreWindow } = require('../private-browsing/utils');
+
+const mainWindow = windowNS(browserWindows.activeWindow).window;
+
+const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec';
+
+const supportPrivateTabs = isPrivateBrowsingSupported && isTabPBSupported;
+
+const Tabs = Class({
+ implements: [ List ],
+ extends: EventTarget,
+ initialize: function initialize(options) {
+ let tabsInternals = tabsNS(this);
+ let window = tabsNS(this).window = options.window || mainWindow;
+
+ EventTarget.prototype.initialize.call(this, options);
+ List.prototype.initialize.apply(this, getTabs(window).map(Tab));
+
+ // TabOpen event
+ window.BrowserApp.deck.addEventListener(EVENTS.open.dom, onTabOpen, false);
+
+ // TabSelect
+ window.BrowserApp.deck.addEventListener(EVENTS.activate.dom, onTabSelect, false);
+ },
+ get activeTab() {
+ return getTabForRawTab(getSelectedTab(tabsNS(this).window));
+ },
+ open: function(options) {
+ options = Options(options);
+ let activeWin = browserWindows.activeWindow;
+
+ if (options.isPinned) {
+ console.error(ERR_FENNEC_MSG); // TODO
+ }
+
+ let url = options.url ? data.url(options.url) : options.url;
+ let rawTab = openTab(windowNS(activeWin).window, url, {
+ inBackground: options.inBackground,
+ isPrivate: supportPrivateTabs && options.isPrivate
+ });
+
+ // by now the tab has been created
+ let tab = getTabForRawTab(rawTab);
+
+ if (options.onClose)
+ tab.on('close', options.onClose);
+
+ if (options.onOpen) {
+ // NOTE: on Fennec this will be true
+ if (tabNS(tab).opened)
+ options.onOpen(tab);
+
+ tab.on('open', options.onOpen);
+ }
+
+ if (options.onReady)
+ tab.on('ready', options.onReady);
+
+ if (options.onLoad)
+ tab.on('load', options.onLoad);
+
+ if (options.onPageShow)
+ tab.on('pageshow', options.onPageShow);
+
+ if (options.onActivate)
+ tab.on('activate', options.onActivate);
+
+ return tab;
+ }
+});
+var gTabs = exports.tabs = Tabs(mainWindow);
+
+function tabsUnloader(event, window) {
+ window = window || (event && event.target);
+ if (!(window && window.BrowserApp))
+ return;
+ window.BrowserApp.deck.removeEventListener(EVENTS.open.dom, onTabOpen, false);
+ window.BrowserApp.deck.removeEventListener(EVENTS.activate.dom, onTabSelect, false);
+}
+
+// unload handler
+unload(function() {
+ for (let window in windowIterator()) {
+ tabsUnloader(null, window);
+ }
+});
+
+function addTab(tab) {
+ addListItem(gTabs, tab);
+ return tab;
+}
+
+function removeTab(tab) {
+ removeListItem(gTabs, tab);
+ return tab;
+}
+
+// TabOpen
+function onTabOpen(event) {
+ let browser = event.target;
+
+ // Eventually ignore private tabs
+ if (ignoreWindow(browser.contentWindow))
+ return;
+
+ let tab = getTabForBrowser(browser);
+ if (tab === null) {
+ let rawTab = getRawTabForBrowser(browser);
+
+ // create a Tab instance for this new tab
+ tab = addTab(Tab(rawTab));
+ }
+
+ tabNS(tab).opened = true;
+
+ tab.on('ready', () => emit(gTabs, 'ready', tab));
+ tab.once('close', onTabClose);
+
+ tab.on('pageshow', (_tab, persisted) =>
+ emit(gTabs, 'pageshow', tab, persisted));
+
+ emit(tab, 'open', tab);
+ emit(gTabs, 'open', tab);
+}
+
+// TabSelect
+function onTabSelect(event) {
+ let browser = event.target;
+
+ // Eventually ignore private tabs
+ if (ignoreWindow(browser.contentWindow))
+ return;
+
+ // Set value whenever new tab becomes active.
+ let tab = getTabForBrowser(browser);
+ emit(tab, 'activate', tab);
+ emit(gTabs, 'activate', tab);
+
+ for (let t of gTabs) {
+ if (t === tab) continue;
+ emit(t, 'deactivate', t);
+ emit(gTabs, 'deactivate', t);
+ }
+}
+
+// TabClose
+function onTabClose(tab) {
+ removeTab(tab);
+ emit(gTabs, EVENTS.close.name, tab);
+}
diff --git a/addon-sdk/source/lib/sdk/worker/utils.js b/addon-sdk/source/lib/sdk/worker/utils.js
new file mode 100644
index 000000000..fca19be63
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/worker/utils.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+module.metadata = {
+ 'stability': 'deprecated'
+};
+
+const {
+ requiresAddonGlobal, attach, detach, destroy, WorkerHost
+} = require('../content/utils');
+
+exports.WorkerHost = WorkerHost;
+exports.detach = detach;
+exports.attach = attach;
+exports.destroy = destroy;
+exports.requiresAddonGlobal = requiresAddonGlobal;
diff --git a/addon-sdk/source/lib/sdk/zip/utils.js b/addon-sdk/source/lib/sdk/zip/utils.js
new file mode 100644
index 000000000..e600380cb
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/zip/utils.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Cc, Ci } = require("chrome");
+
+function getZipReader(aFile) {
+ return new Promise(resolve => {
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ zipReader.open(aFile);
+ resolve(zipReader);
+ });
+};
+exports.getZipReader = getZipReader;