diff options
Diffstat (limited to 'tools/addon-sdk-1.12/lib/sdk')
127 files changed, 25396 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.12/lib/sdk/addon-page.js b/tools/addon-sdk-1.12/lib/sdk/addon-page.js new file mode 100644 index 0000000..6440b25 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/addon-page.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 { WindowTracker } = require('./deprecated/window-utils'); +const { isXULBrowser } = require('./window/utils'); +const { add, remove } = require('./util/array'); +const { getTabs, closeTab, getURI } = require('./tabs/utils'); +const { data } = require('./self'); + +const addonURL = data.url('index.html'); + +WindowTracker({ + onTrack: function onTrack(window) { + if (isXULBrowser(window)) + add(window.XULBrowserWindow.inContentWhitelist, addonURL); + }, + onUntrack: function onUntrack(window) { + if (isXULBrowser(window)) + getTabs(window).filter(tabFilter).forEach(untrackTab.bind(null, window)); + } +}); + +function tabFilter(tab) { + return getURI(tab) === addonURL; +} + +function untrackTab(window, tab) { + // Note: `onUntrack` will be called for all windows on add-on unloads, + // so we want to clean them up from these URLs. + remove(window.XULBrowserWindow.inContentWhitelist, addonURL); + closeTab(tab); +} diff --git a/tools/addon-sdk-1.12/lib/sdk/addon/installer.js b/tools/addon-sdk-1.12/lib/sdk/addon/installer.js new file mode 100644 index 0000000..f72b21e --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/addon/installer.js @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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) { + console.log("failed"); + aInstall.removeListener(listener); + reject(aInstall.error); + }, + onDownloadFailed: function(aInstall) { + this.onInstallFailed(aInstall); + } + }; + + // Order AddonManager to install the addon + AddonManager.getInstallForFile(file, function(install) { + install.addListener(listener); + install.install(); + }); + + 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 + AddonManager.getAddonByID(addonId, function (addon) { + addon.uninstall(); + }); + + return promise; +}; + +exports.disable = function disable(addonId) { + let { promise, resolve, reject } = defer(); + + AddonManager.getAddonByID(addonId, function (addon) { + addon.userDisabled = true; + resolve(); + }); + + return promise; +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/addon/runner.js b/tools/addon-sdk-1.12/lib/sdk/addon/runner.js new file mode 100644 index 0000000..0552de2 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/addon/runner.js @@ -0,0 +1,153 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 } = require('chrome'); +const { descriptor, Sandbox, evaluate, main, resolveURI } = require('toolkit/loader'); +const { once } = require('../system/events'); +const { exit, env, staticArgs, name } = require('../system'); +const { when: unload } = require('../system/unload'); +const { loadReason } = require('../self'); +const { rootURI } = require("@loader/options"); +const globals = require('../system/globals'); + +const NAME2TOPIC = { + 'Firefox': 'sessionstore-windows-restored', + 'Fennec': 'sessionstore-windows-restored', + 'SeaMonkey': 'sessionstore-windows-restored', + 'Thunderbird': 'mail-startup-done', + '*': 'final-ui-startup' +}; + +// Gets the topic that fit best as application startup event, in according with +// the current application (e.g. Firefox, Fennec, Thunderbird...) +const APP_STARTUP = NAME2TOPIC[name] || NAME2TOPIC['*']; + +// 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 wait(reason, options) { + once(APP_STARTUP, function() { + startup(null, options); + }); +} + +function startup(reason, options) { + if (reason === 'startup') + return wait(reason, options); + + // Inject globals ASAP in order to have console API working ASAP + Object.defineProperties(options.loader.globals, descriptor(globals)); + + // 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) { + 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 + if (data) + definePseudo(options.loader, '@l10n/data', data); + run(options); + }); +} + +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 !== 'test-harness/run-tests') + require('../l10n/html').enable(); + } + catch(error) { + console.exception(error); + } + // Initialize inline options localization, without preventing addon to be + // run in case of error + try { + require('../l10n/prefs'); + } + 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 + setDefaultPrefs(options.prefsURI); + + // 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 + }); + } + } 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/tools/addon-sdk-1.12/lib/sdk/base64.js b/tools/addon-sdk-1.12/lib/sdk/base64.js new file mode 100644 index 0000000..42fea57 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/base64.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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"); + +// If an object is not given as second argument, the JavaScript Module scope is +// returned, so we can obtain from it the `atob` and `btoa` functions +const { 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'"); +} + +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))) + + return btoa(data); +} diff --git a/tools/addon-sdk-1.12/lib/sdk/clipboard.js b/tools/addon-sdk-1.12/lib/sdk/clipboard.js new file mode 100644 index 0000000..58a3d7e --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/clipboard.js @@ -0,0 +1,333 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 { DataURL } = require("./url"); +const errors = require("./deprecated/errors"); +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" } +]; + +let clipboardService = Cc["@mozilla.org/widget/clipboard;1"]. + getService(Ci.nsIClipboard); + +let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + +let 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 if e.name === "URIError") { + // Not a valid Data URL + } + } + + 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) + let (str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString), + 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 each (var flavor in 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 each (let flavorMap in 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 each (let flavorMap in 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/tools/addon-sdk-1.12/lib/sdk/console/plain-text.js b/tools/addon-sdk-1.12/lib/sdk/console/plain-text.js new file mode 100644 index 0000000..9e3b748 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/console/plain-text.js @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 self = require("../self"); + +function stringify(arg) { + try { + return String(arg); + } + catch(ex) { + return "<toString() error>"; + } +} + +function stringifyArgs(args) { + return Array.map(args, stringify).join(" "); +} + +function message(print, level, args, name) { + print(level + ": " + name + ": " + + stringifyArgs(args) + "\n", level); +} + +var Console = exports.PlainTextConsole = function PlainTextConsole(print) { + if (!print) + print = dump; + if (print === dump) { + // If we're just using dump(), auto-enable preferences so + // that the developer actually sees the console output. + var prefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + prefs.setBoolPref("browser.dom.window.dump.enabled", true); + } + this.print = print; + + // 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); +}; + +Console.prototype = { + log: function log() { + message(this.print, "info", arguments, self.name); + }, + + info: function info() { + message(this.print, "info", arguments, self.name); + }, + + warn: function warn() { + message(this.print, "warning", arguments, self.name); + }, + + error: function error() { + message(this.print, "error", arguments, self.name); + }, + + debug: function debug() { + message(this.print, "debug", arguments, self.name); + }, + + exception: function exception(e) { + var fullString = ("An exception occurred.\n" + + require("./traceback").format(e) + "\n" + e); + this.error(fullString); + }, + + trace: function trace() { + var traceback = require("./traceback"); + var stack = traceback.get(); + stack.splice(-1, 1); + message(this.print, "info", [traceback.format(stack)], self.name); + } +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/console/traceback.js b/tools/addon-sdk-1.12/lib/sdk/console/traceback.js new file mode 100644 index 0000000..6b23be1 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/console/traceback.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"; + +module.metadata = { + "stability": "experimental" +}; + +const { Cc, Ci, components } = require("chrome"); +const { readURISync } = require("../net/url"); + +// Undo the auto-parentification of URLs done in bug 418356. +function deParentifyURL(url) { + return url ? url.split(" -> ").slice(-1)[0] : 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 errorStackToJSON(stack) { + var lines = stack.split("\n"); + + var frames = []; + lines.forEach( + function(line) { + if (!line) + return; + var atIndex = line.indexOf("@"); + var colonIndex = line.lastIndexOf(":"); + var filename = deParentifyURL(line.slice(atIndex + 1, colonIndex)); + var lineNo = parseInt(line.slice(colonIndex + 1)); + var funcSig = line.slice(0, atIndex); + var endFuncName = funcSig.indexOf("("); + // Bug 751149: FF15 changed function signature + // Instead of: runTest([object Object]) + // We now have: runTest + var funcName = endFuncName != -1 + ? funcSig.slice(0, endFuncName) + : funcSig; + frames.unshift({filename: filename, + funcName: funcName, + lineNo: lineNo}); + }); + + return frames; +}; + +function nsIStackFramesToJSON(frame) { + var stack = []; + + while (frame) { + if (frame.filename) { + var filename = deParentifyURL(frame.filename); + stack.splice(0, 0, {filename: filename, + lineNo: frame.lineNumber, + funcName: 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 errorStackToJSON(e.stack); + if (e.fileName && typeof(e.lineNumber == "number")) + return [{filename: deParentifyURL(e.fileName), + lineNo: e.lineNumber, + funcName: 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.splice(-1, 1); + } + + 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.lineNo || frame.funcName)) + return; + + lines.push(' File "' + frame.filename + '", line ' + + frame.lineNo + ', in ' + frame.funcName); + var sourceLine = safeGetFileLine(frame.filename, frame.lineNo); + if (sourceLine) + lines.push(' ' + sourceLine.trim()); + }); + + return lines.join("\n"); +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/content/content-proxy.js b/tools/addon-sdk-1.12/lib/sdk/content/content-proxy.js new file mode 100644 index 0000000..76df7f6 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/content/content-proxy.js @@ -0,0 +1,870 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* Trick the linker in order to avoid error on `Components.interfaces` usage. + We are tricking the linker with `require('./content-proxy.js')` from + worjer.js in order to ensure shipping this file! But then the linker think + that this file is going to be used as a CommonJS module where we forbid usage + of `Components`. +*/ +let Ci = Components['interfaces']; + +/** + * Access key that allows privileged code to unwrap proxy wrappers through + * valueOf: + * let xpcWrapper = proxyWrapper.valueOf(UNWRAP_ACCESS_KEY); + * This key should only be used by proxy unit test. + */ + const UNWRAP_ACCESS_KEY = {}; + + + /** + * Returns a closure that wraps arguments before calling the given function, + * which can be given to native functions that accept a function, such that when + * the closure is called, the given function is called with wrapped arguments. + * + * @param fun {Function} + * the function for which to create a closure wrapping its arguments + * @param obj {Object} + * target object from which `fun` comes from + * (optional, for debugging purpose) + * @param name {String} + * name of the attribute from which `fun` is binded on `obj` + * (optional, for debugging purpose) + * + * Example: + * function contentScriptListener(event) {} + * let wrapper = ContentScriptFunctionWrapper(contentScriptListener); + * xray.addEventListener("...", wrapper, false); + * -> Allow to `event` to be wrapped + */ +function ContentScriptFunctionWrapper(fun, obj, name) { + if ("___proxy" in fun && typeof fun.___proxy == "function") + return fun.___proxy; + + let wrappedFun = function () { + let args = []; + for (let i = 0, l = arguments.length; i < l; i++) + args.push(wrap(arguments[i])); + + //console.log("Called from native :"+obj+"."+name); + //console.log(">args "+arguments.length); + //console.log(fun); + + // Native code can execute this callback with `this` being the wrapped + // function. For example, window.mozRequestAnimationFrame. + if (this == wrappedFun) + return fun.apply(fun, args); + + return fun.apply(wrap(this), args); + }; + + Object.defineProperty(fun, "___proxy", {value : wrappedFun, + writable : false, + enumerable : false, + configurable : false}); + + return wrappedFun; +} + +/** + * Returns a closure that unwraps arguments before calling the `fun` function, + * which can be used to build a wrapper for a native function that accepts + * wrapped arguments, since native function only accept unwrapped arguments. + * + * @param fun {Function} + * the function to wrap + * @param originalObject {Object} + * target object from which `fun` comes from + * (optional, for debugging purpose) + * @param name {String} + * name of the attribute from which `fun` is binded on `originalObject` + * (optional, for debugging purpose) + * + * Example: + * wrapper.appendChild = NativeFunctionWrapper(xray.appendChild, xray); + * wrapper.appendChild(anotherWrapper); + * -> Allow to call xray.appendChild with unwrapped version of anotherWrapper + */ +function NativeFunctionWrapper(fun, originalObject, name) { + return function () { + let args = []; + let obj = this && typeof this.valueOf == "function" ? + this.valueOf(UNWRAP_ACCESS_KEY) : this; + + for (let i = 0, l = arguments.length; i < l; i++) + args.push( unwrap(arguments[i], obj, name) ); + + //if (name != "toString") + //console.log(">>calling native ["+(name?name:'#closure#')+"]: \n"+fun.apply+"\n"+obj+"\n("+args.join(', ')+")\nthis :"+obj+"from:"+originalObject+"\n"); + + // Need to use Function.prototype.apply.apply because XMLHttpRequest + // is a function (typeof return 'function') and fun.apply is null :/ + let unwrapResult = Function.prototype.apply.apply(fun, [obj, args]); + let result = wrap(unwrapResult, obj, name); + + //console.log("<< "+rr+" -> "+r); + + return result; + }; +} + +/* + * Unwrap a JS value that comes from the content script. + * Mainly converts proxy wrapper to XPCNativeWrapper. + */ +function unwrap(value, obj, name) { + //console.log("unwrap : "+value+" ("+name+")"); + if (!value) + return value; + let type = typeof value; + + // In case of proxy, unwrap them recursively + // (it should not be recursive, just in case of) + if (["object", "function"].indexOf(type) !== -1 && + "__isWrappedProxy" in value) { + while("__isWrappedProxy" in value) + value = value.valueOf(UNWRAP_ACCESS_KEY); + return value; + } + + // In case of functions we need to return a wrapper that converts native + // arguments applied to this function into proxies. + if (type == "function") + return ContentScriptFunctionWrapper(value, obj, name); + + // We must wrap objects coming from content script too, as they may have + // a function that will be called by a native method. + // For example: + // addEventListener(..., { handleEvent: function(event) {} }, ...); + if (type == "object") + return ContentScriptObjectWrapper(value); + + if (["string", "number", "boolean"].indexOf(type) !== -1) + return value; + //console.log("return non-wrapped to native : "+typeof value+" -- "+value); + return value; +} + +/** + * Returns an XrayWrapper proxy object that allow to wrap any of its function + * though `ContentScriptFunctionWrapper`. These proxies are given to + * XrayWrappers in order to automatically wrap values when they call a method + * of these proxies. So that they are only used internaly and content script, + * nor web page have ever access to them. As a conclusion, we can consider + * this code as being safe regarding web pages overload. + * + * + * @param obj {Object} + * object coming from content script context to wrap + * + * Example: + * let myListener = { handleEvent: function (event) {} }; + * node.addEventListener("click", myListener, false); + * `event` has to be wrapped, so handleEvent has to be wrapped using + * `ContentScriptFunctionWrapper` function. + * In order to do so, we build this new kind of proxies. + */ +function ContentScriptObjectWrapper(obj) { + if ("___proxy" in obj && typeof obj.___proxy == "object") + return obj.___proxy; + + function valueOf(key) { + if (key === UNWRAP_ACCESS_KEY) + return obj; + return this; + } + + let proxy = Proxy.create({ + // Fundamental traps + getPropertyDescriptor: function(name) { + return Object.getOwnPropertyDescriptor(obj, name); + }, + defineProperty: function(name, desc) { + return Object.defineProperty(obj, name, desc); + }, + getOwnPropertyNames: function () { + return Object.getOwnPropertyNames(obj); + }, + delete: function(name) { + return delete obj[name]; + }, + // derived traps + has: function(name) { + return name === "__isXrayWrapperProxy" || + name in obj; + }, + hasOwn: function(name) { + return Object.prototype.hasOwnProperty.call(obj, name); + }, + get: function(receiver, name) { + if (name == "valueOf") + return valueOf; + let value = obj[name]; + if (!value) + return value; + + return unwrap(value); + }, + set: function(receiver, name, val) { + obj[name] = val; + return true; + }, + enumerate: function() { + var result = []; + for each (let name in obj) { + result.push(name); + }; + return result; + }, + keys: function() { + return Object.keys(obj); + } + }); + + Object.defineProperty(obj, "___proxy", {value : proxy, + writable : false, + enumerable : false, + configurable : false}); + + return proxy; +} + +// List of all existing typed arrays. +// Can be found here: +// http://mxr.mozilla.org/mozilla-central/source/js/src/jsapi.cpp#1790 +const typedArraysCtor = [ + ArrayBuffer, + Int8Array, + Uint8Array, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + Uint8ClampedArray +]; + +/* + * Wrap a JS value coming from the document by building a proxy wrapper. + */ +function wrap(value, obj, name, debug) { + if (!value) + return value; + let type = typeof value; + if (type == "object") { + // Bug 671016: Typed arrays don't need to be proxified. + // We avoid checking the whole constructor list on all objects + // by doing this check only on non-extensible objects: + if (!Object.isExtensible(value) && + typedArraysCtor.indexOf(value.constructor) !== -1) + return value; + + // Bug 715755: do not proxify COW wrappers + // These wrappers throw an exception when trying to access + // any attribute that is not in a white list + try { + ("nonExistantAttribute" in value); + } + catch(e) { + if (e.message.indexOf("Permission denied to access property") !== -1) + return value; + } + + // We may have a XrayWrapper proxy. + // For example: + // let myListener = { handleEvent: function () {} }; + // node.addEventListener("click", myListener, false); + // When native code want to call handleEvent, + // we go though ContentScriptFunctionWrapper that calls `wrap(this)` + // `this` is the XrayWrapper proxy of myListener. + // We return this object without building a CS proxy as it is already + // a value coming from the CS. + if ("__isXrayWrapperProxy" in value) + return value.valueOf(UNWRAP_ACCESS_KEY); + + // Unwrap object before wrapping it. + // It should not happen with CS proxy objects. + while("__isWrappedProxy" in value) { + value = value.valueOf(UNWRAP_ACCESS_KEY); + } + + if (XPCNativeWrapper.unwrap(value) !== value) + return getProxyForObject(value); + // In case of Event, HTMLCollection or NodeList or ??? + // XPCNativeWrapper.unwrap(value) === value + // but it's still a XrayWrapper so let's build a proxy + return getProxyForObject(value); + } + if (type == "function") { + if (XPCNativeWrapper.unwrap(value) !== value + || (typeof value.toString === "function" && + value.toString().match(/\[native code\]/))) { + return getProxyForFunction(value, NativeFunctionWrapper(value, obj, name)); + } + return value; + } + if (type == "string") + return value; + if (type == "number") + return value; + if (type == "boolean") + return value; + //console.log("return non-wrapped to wrapped : "+value); + return value; +} + +/* + * Wrap an object from the document to a proxy wrapper + */ +function getProxyForObject(obj) { + if (typeof obj != "object") { + let msg = "tried to proxify something other than an object: " + typeof obj; + console.warn(msg); + throw msg; + } + if ("__isWrappedProxy" in obj) { + return obj; + } + // Check if there is a proxy cached on this wrapper, + // but take care of prototype ___proxy attribute inheritance! + if (obj && obj.___proxy && obj.___proxy.valueOf(UNWRAP_ACCESS_KEY) === obj) { + return obj.___proxy; + } + + let proxy = Proxy.create(handlerMaker(obj)); + + Object.defineProperty(obj, "___proxy", {value : proxy, + writable : false, + enumerable : false, + configurable : false}); + return proxy; +} + +/* + * Wrap a function from the document to a proxy wrapper + */ +function getProxyForFunction(fun, callTrap) { + if (typeof fun != "function") { + let msg = "tried to proxify something other than a function: " + typeof fun; + console.warn(msg); + throw msg; + } + if ("__isWrappedProxy" in fun) + return obj; + if ("___proxy" in fun) + return fun.___proxy; + + let proxy = Proxy.createFunction(handlerMaker(fun), callTrap); + + Object.defineProperty(fun, "___proxy", {value : proxy, + writable : false, + enumerable : false, + configurable : false}); + + return proxy; +} + +/* + * Check if a DOM attribute name is an event name. + */ +function isEventName(id) { + if (id.indexOf("on") != 0 || id.length == 2) + return false; + // Taken from: + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#7616 + switch (id[2]) { + case 'a' : + return (id == "onabort" || + id == "onafterscriptexecute" || + id == "onafterprint"); + case 'b' : + return (id == "onbeforeunload" || + id == "onbeforescriptexecute" || + id == "onblur" || + id == "onbeforeprint"); + case 'c' : + return (id == "onchange" || + id == "onclick" || + id == "oncontextmenu" || + id == "oncopy" || + id == "oncut" || + id == "oncanplay" || + id == "oncanplaythrough"); + case 'd' : + return (id == "ondblclick" || + id == "ondrag" || + id == "ondragend" || + id == "ondragenter" || + id == "ondragleave" || + id == "ondragover" || + id == "ondragstart" || + id == "ondrop" || + id == "ondurationchange"); + case 'e' : + return (id == "onerror" || + id == "onemptied" || + id == "onended"); + case 'f' : + return id == "onfocus"; + case 'h' : + return id == "onhashchange"; + case 'i' : + return (id == "oninput" || + id == "oninvalid"); + case 'k' : + return (id == "onkeydown" || + id == "onkeypress" || + id == "onkeyup"); + case 'l' : + return (id == "onload" || + id == "onloadeddata" || + id == "onloadedmetadata" || + id == "onloadstart"); + case 'm' : + return (id == "onmousemove" || + id == "onmouseout" || + id == "onmouseover" || + id == "onmouseup" || + id == "onmousedown" || + id == "onmessage"); + case 'p' : + return (id == "onpaint" || + id == "onpageshow" || + id == "onpagehide" || + id == "onpaste" || + id == "onpopstate" || + id == "onpause" || + id == "onplay" || + id == "onplaying" || + id == "onprogress"); + case 'r' : + return (id == "onreadystatechange" || + id == "onreset" || + id == "onresize" || + id == "onratechange"); + case 's' : + return (id == "onscroll" || + id == "onselect" || + id == "onsubmit" || + id == "onseeked" || + id == "onseeking" || + id == "onstalled" || + id == "onsuspend"); + case 't': + return id == "ontimeupdate" + /* + // TODO: Make it work for mobile version + || + (nsDOMTouchEvent::PrefEnabled() && + (id == "ontouchstart" || + id == "ontouchend" || + id == "ontouchmove" || + id == "ontouchenter" || + id == "ontouchleave" || + id == "ontouchcancel"))*/; + + case 'u' : + return id == "onunload"; + case 'v': + return id == "onvolumechange"; + case 'w': + return id == "onwaiting"; + } + + return false; +} + +// XrayWrappers miss some attributes. +// Here is a list of functions that return a value when it detects a miss: +const NODES_INDEXED_BY_NAME = ["IMG", "FORM", "APPLET", "EMBED", "OBJECT"]; +const xRayWrappersMissFixes = [ + + // Fix bug with XPCNativeWrapper on HTMLCollection + // We can only access array item once, then it's undefined :o + function (obj, name) { + let i = parseInt(name); + if (obj.toString().match(/HTMLCollection|NodeList/) && + i >= 0 && i < obj.length) { + return wrap(XPCNativeWrapper(obj.wrappedJSObject[name]), obj, name); + } + return null; + }, + + // Trap access to document["form name"] + // that may refer to an existing form node + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#9285 + function (obj, name) { + if ("nodeType" in obj && obj.nodeType == 9) { + let node = obj.wrappedJSObject[name]; + // List of supported tag: + // http://mxr.mozilla.org/mozilla-central/source/content/html/content/src/nsGenericHTMLElement.cpp#1267 + if (node && NODES_INDEXED_BY_NAME.indexOf(node.tagName) != -1) + return wrap(XPCNativeWrapper(node)); + } + return null; + }, + + // Trap access to window["frame name"] and window.frames[i] + // that refer to an (i)frame internal window object + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#6824 + function (obj, name) { + if (typeof obj == "object" && "document" in obj) { + // Ensure that we are on a window object + try { + obj.QueryInterface(Ci.nsIDOMWindow); + } + catch(e) { + return null; + } + + // Integer case: + let i = parseInt(name); + if (i >= 0 && i in obj) { + return wrap(XPCNativeWrapper(obj[i])); + } + + // String name case: + if (name in obj.wrappedJSObject) { + let win = obj.wrappedJSObject[name]; + let nodes = obj.document.getElementsByName(name); + for (let i = 0, l = nodes.length; i < l; i++) { + let node = nodes[i]; + if ("contentWindow" in node && node.contentWindow == win) + return wrap(node.contentWindow); + } + } + } + return null; + }, + + // Trap access to form["node name"] + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#9477 + function (obj, name) { + if (typeof obj == "object" && "tagName" in obj && obj.tagName == "FORM") { + let match = obj.wrappedJSObject[name]; + let nodes = obj.ownerDocument.getElementsByName(name); + for (let i = 0, l = nodes.length; i < l; i++) { + let node = nodes[i]; + if (node == match) + return wrap(node); + } + } + return null; + } + +]; + +// XrayWrappers have some buggy methods. +// Here is the list of them with functions returning some replacement +// for a given object `obj`: +const xRayWrappersMethodsFixes = { + // postMessage method is checking the Javascript global + // and it expects it to be a window object. + // But in our case, the global object is our sandbox global object. + // See nsGlobalWindow::CallerInnerWindow(): + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsGlobalWindow.cpp#5893 + // nsCOMPtr<nsPIDOMWindow> win = do_QueryWrappedNative(wrapper); + // win is null + postMessage: function (obj) { + // Ensure that we are on a window object + try { + obj.QueryInterface(Ci.nsIDOMWindow); + } + catch(e) { + return null; + } + + // Create a wrapper that is going to call `postMessage` through `eval` + let f = function postMessage(message, targetOrigin) { + let jscode = "this.postMessage("; + if (typeof message != "string") + jscode += JSON.stringify(message); + else + jscode += "'" + message.toString().replace(/['\\]/g,"\\$&") + "'"; + + targetOrigin = targetOrigin.toString().replace(/['\\]/g,"\\$&"); + + jscode += ", '" + targetOrigin + "')"; + return this.wrappedJSObject.eval(jscode); + }; + return getProxyForFunction(f, NativeFunctionWrapper(f)); + }, + + // Fix mozMatchesSelector uses that is broken on XrayWrappers + // when we use document.documentElement.mozMatchesSelector.call(node, expr) + // It's only working if we call mozMatchesSelector on the node itself. + // SEE BUG 658909: mozMatchesSelector returns incorrect results with XrayWrappers + mozMatchesSelector: function (obj) { + // Ensure that we are on an object to expose this buggy method + try { + // Bug 707576 removed nsIDOMNSElement. + // Can be simplified as soon as Firefox 11 become the minversion + obj.QueryInterface("nsIDOMElement" in Ci ? Ci.nsIDOMElement : + Ci.nsIDOMNSElement); + } + catch(e) { + return null; + } + // We can't use `wrap` function as `f` is not a native function, + // so wrap it manually: + let f = function mozMatchesSelector(selectors) { + return this.mozMatchesSelector(selectors); + }; + + return getProxyForFunction(f, NativeFunctionWrapper(f)); + }, + + // Bug 679054: History API doesn't work with Proxy objects. We have to pass + // regular JS objects on `pushState` and `replaceState` methods. + // In addition, the first argument has to come from the same compartment. + pushState: function (obj) { + // Ensure that we are on an object that expose History API + try { + obj.QueryInterface(Ci.nsIDOMHistory); + } + catch(e) { + return null; + } + let f = function fix() { + // Call native method with JSON objects + // (need to convert `arguments` to an array via `slice`) + return this.pushState.apply(this, JSON.parse(JSON.stringify(Array.slice(arguments)))); + }; + + return getProxyForFunction(f, NativeFunctionWrapper(f)); + }, + replaceState: function (obj) { + // Ensure that we are on an object that expose History API + try { + obj.QueryInterface(Ci.nsIDOMHistory); + } + catch(e) { + return null; + } + let f = function fix() { + // Call native method with JSON objects + // (need to convert `arguments` to an array via `slice`) + return this.replaceState.apply(this, JSON.parse(JSON.stringify(Array.slice(arguments)))); + }; + + return getProxyForFunction(f, NativeFunctionWrapper(f)); + }, + + // Bug 769006: nsIDOMMutationObserver.observe fails with proxy as options + // attributes + observe: function observe(obj) { + // Ensure that we are on a DOMMutation object + try { + // nsIDOMMutationObserver starts with FF14 + if ("nsIDOMMutationObserver" in Ci) + obj.QueryInterface(Ci.nsIDOMMutationObserver); + else + return null; + } + catch(e) { + return null; + } + return function nsIDOMMutationObserverObserveFix(target, options) { + // Gets native/unwrapped this + let self = this && typeof this.valueOf == "function" ? + this.valueOf(UNWRAP_ACCESS_KEY) : this; + // Unwrap the xraywrapper target out of JS proxy + let targetXray = unwrap(target); + // But do not wrap `options` through ContentScriptObjectWrapper + let result = wrap(self.observe(targetXray, options)); + // Finally wrap result into JS proxies + return wrap(result); + }; + } +}; + +/* + * Generate handler for proxy wrapper + */ +function handlerMaker(obj) { + // Overloaded attributes dictionary + let overload = {}; + // Expando attributes dictionary (i.e. onclick, onfocus, on* ...) + let expando = {}; + // Cache of methods overloaded to fix XrayWrapper bug + let methodFixes = {}; + return { + // Fundamental traps + getPropertyDescriptor: function(name) { + return Object.getOwnPropertyDescriptor(obj, name); + }, + defineProperty: function(name, desc) { + return Object.defineProperty(obj, name, desc); + }, + getOwnPropertyNames: function () { + return Object.getOwnPropertyNames(obj); + }, + delete: function(name) { + delete expando[name]; + delete overload[name]; + return delete obj[name]; + }, + + // derived traps + has: function(name) { + if (name == "___proxy") return false; + if (isEventName(name)) { + // XrayWrappers throw exception when we try to access expando attributes + // even on "name in wrapper". So avoid doing it! + return name in expando; + } + return name in obj || name in overload || name == "__isWrappedProxy" || + undefined !== this.get(null, name); + }, + hasOwn: function(name) { + return Object.prototype.hasOwnProperty.call(obj, name); + }, + get: function(receiver, name) { + if (name == "___proxy") + return undefined; + + // Overload toString in order to avoid returning "[XrayWrapper [object HTMLElement]]" + // or "[object Function]" for function's Proxy + if (name == "toString") { + // Bug 714778: we should not pass obj.wrappedJSObject.toString + // in order to avoid sharing its proxy between two contents scripts. + // (not that `unwrappedObj` can be equal to `obj` when `obj` isn't + // an xraywrapper) + let unwrappedObj = XPCNativeWrapper.unwrap(obj); + return wrap(function () { + return unwrappedObj.toString.call( + this.valueOf(UNWRAP_ACCESS_KEY), arguments); + }, obj, name); + } + + // Offer a way to retrieve XrayWrapper from a proxified node through `valueOf` + if (name == "valueOf") + return function (key) { + if (key === UNWRAP_ACCESS_KEY) + return obj; + return this; + }; + + // Return overloaded value if there is one. + // It allows to overload native methods like addEventListener that + // are not saved, even on the wrapper itself. + // (And avoid some methods like toSource from being returned here! [__proto__ test]) + if (name in overload && + overload[name] != Object.getPrototypeOf(overload)[name] && + name != "__proto__") { + return overload[name]; + } + + // Catch exceptions thrown by XrayWrappers when we try to access on* + // attributes like onclick, onfocus, ... + if (isEventName(name)) { + //console.log("expando:"+obj+" - "+obj.nodeType); + return name in expando ? expando[name].original : undefined; + } + + // Overload some XrayWrappers method in order to fix its bugs + if (name in methodFixes && + methodFixes[name] != Object.getPrototypeOf(methodFixes)[name] && + name != "__proto__") + return methodFixes[name]; + if (Object.keys(xRayWrappersMethodsFixes).indexOf(name) !== -1) { + let fix = xRayWrappersMethodsFixes[name](obj); + if (fix) + return methodFixes[name] = fix; + } + + let o = obj[name]; + + // XrayWrapper miss some attributes, try to catch these and return a value + if (!o) { + for each(let atttributeFixer in xRayWrappersMissFixes) { + let fix = atttributeFixer(obj, name); + if (fix) + return fix; + } + } + + // Generic case + return wrap(o, obj, name); + + }, + + set: function(receiver, name, val) { + + if (isEventName(name)) { + //console.log("SET on* attribute : " + name + " / " + val + "/" + obj); + let shortName = name.replace(/^on/,""); + + // Unregister previously set listener + if (expando[name]) { + obj.removeEventListener(shortName, expando[name], true); + delete expando[name]; + } + + // Only accept functions + if (typeof val != "function") + return false; + + // Register a new listener + let original = val; + val = ContentScriptFunctionWrapper(val); + expando[name] = val; + val.original = original; + obj.addEventListener(name.replace(/^on/, ""), val, true); + return true; + } + + obj[name] = val; + + // Handle native method not overloaded on XrayWrappers: + // obj.addEventListener = val; -> obj.addEventlistener = native method + // And, XPCNativeWrapper bug where nested values appear to be wrapped: + // obj.customNestedAttribute = val -> obj.customNestedAttribute !== val + // obj.customNestedAttribute = "waive wrapper something" + // SEE BUG 658560: Fix identity problem with CrossOriginWrappers + // TODO: check that DOM can't be updated by the document itself and so overloaded value becomes wrong + // but I think such behavior is limited to primitive type + if ((typeof val == "function" || typeof val == "object") && name) { + overload[name] = val; + } + + return true; + }, + + enumerate: function() { + var result = []; + for each (let name in Object.keys(obj)) { + result.push(name); + }; + return result; + }, + + keys: function() { + return Object.keys(obj); + } + }; +}; + + +/* + * Wrap an object from the document to a proxy wrapper. + */ +function create(object) { + if ("wrappedJSObject" in object) + object = object.wrappedJSObject; + let xpcWrapper = XPCNativeWrapper(object); + // If we can't build an XPCNativeWrapper, it doesn't make sense to build + // a proxy. All proxy code is based on having such wrapper that store + // different JS attributes set. + // (we can't build XPCNativeWrapper when object is from the same + // principal/domain) + if (object === xpcWrapper) { + return object; + } + return getProxyForObject(xpcWrapper); +} diff --git a/tools/addon-sdk-1.12/lib/sdk/content/content-worker.js b/tools/addon-sdk-1.12/lib/sdk/content/content-worker.js new file mode 100644 index 0000000..8cf9c5c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/content/content-worker.js @@ -0,0 +1,308 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 ContentWorker = 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 each (let callback in listeners[name]) { + results.push(callback.apply(null, args)); + } + return results; + } + function hasListenerFor(name) { + if (!(name in listeners)) + return false; + return listeners[name].length > 0; + } + return { + eventEmitter: eventEmitter, + emit: onEvent, + hasListenerFor: hasListenerFor + }; + }, + + /** + * 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) { + function onEvent() { + // Convert to real array + let args = Array.slice(arguments); + // JSON.stringify is buggy with cross-sandbox values, + // it may return "{}" on functions. Use a replacer to match them correctly. + function replacer(k, v) { + return typeof v === "function" ? undefined : v; + } + let str = JSON.stringify(args, replacer); + emitToChrome(str); + } + + let { eventEmitter, emit, hasListenerFor } = + 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); + }, + hasListenerFor: hasListenerFor + }; + }, + + 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") + }); + }, + + 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 on a symbiont 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); + 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); + } + } + _timers[id] = timer; + return id; + } + + 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) { + + 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 + }); + + // Deprecated use of on/postMessage from globals + exports.postMessage = function deprecatedPostMessage() { + console.error("DEPRECATED: The global `postMessage()` function in " + + "content scripts is deprecated in favor of the " + + "`self.postMessage()` function, which works the same. " + + "Replace calls to `postMessage()` with calls to " + + "`self.postMessage()`." + + "For more info on `self.on`, see " + + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); + return self.postMessage.apply(null, arguments); + }; + exports.on = function deprecatedOn() { + console.error("DEPRECATED: The global `on()` function in content " + + "scripts is deprecated in favor of the `self.on()` " + + "function, which works the same. Replace calls to `on()` " + + "with calls to `self.on()`" + + "For more info on `self.on`, see " + + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); + return self.on.apply(null, arguments); + }; + + // Deprecated use of `onMessage` from globals + let onMessage = null; + Object.defineProperty(exports, "onMessage", { + get: function () onMessage, + set: function (v) { + if (onMessage) + self.removeListener("message", onMessage); + console.error("DEPRECATED: The global `onMessage` function in content" + + "scripts is deprecated in favor of the `self.on()` " + + "function. Replace `onMessage = function (data){}` " + + "definitions with calls to `self.on('message', " + + "function (data){})`. " + + "For more info on `self.on`, see " + + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); + onMessage = v; + if (typeof onMessage == "function") + self.on("message", onMessage); + } + }); + }, + + injectOptions: function (exports, options) { + Object.defineProperty( exports.self, "options", { value: JSON.parse( options ) }); + }, + + inject: function (exports, chromeAPI, emitToChrome, options) { + let { pipe, onChromeEvent, hasListenerFor } = + ContentWorker.createPipe(emitToChrome); + + ContentWorker.injectConsole(exports, pipe); + ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console); + ContentWorker.injectMessageAPI(exports, pipe); + if ( options !== undefined ) { + ContentWorker.injectOptions(exports, options); + } + + Object.freeze( exports.self ); + + return { + emitToContent: onChromeEvent, + hasListenerFor: hasListenerFor + }; + } +}); diff --git a/tools/addon-sdk-1.12/lib/sdk/content/content.js b/tools/addon-sdk-1.12/lib/sdk/content/content.js new file mode 100644 index 0000000..4bffd45 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/content/content.js @@ -0,0 +1,15 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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" +}; + +exports.Loader = require('./loader').Loader; +exports.Symbiont = require('./symbiont').Symbiont; +exports.Worker = require('./worker').Worker; + diff --git a/tools/addon-sdk-1.12/lib/sdk/content/loader.js b/tools/addon-sdk-1.12/lib/sdk/content/loader.js new file mode 100644 index 0000000..b01675c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/content/loader.js @@ -0,0 +1,204 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 { EventEmitter } = require('../deprecated/events'); +const { validateOptions } = require('../deprecated/api-utils'); +const { URL } = require('../url'); +const file = require('../io/file'); + +const LOCAL_URI_SCHEMES = ['resource', 'data']; + +// Returns `null` if `value` is `null` or `undefined`, otherwise `value`. +function ensureNull(value) { + return value == null ? null : value; +} + +// map of property validations +const valid = { + contentURL: { + ok: function (value) { + try { + URL(value); + } + catch(e) { + return false; + } + return true; + }, + msg: 'The `contentURL` option must be a valid URL.' + }, + contentScriptFile: { + is: ['undefined', 'null', 'string', 'array'], + map: ensureNull, + ok: function(value) { + if (value === null) + return true; + + value = [].concat(value); + + // Make sure every item is a local file URL. + return value.every(function (item) { + try { + return ~LOCAL_URI_SCHEMES.indexOf(URL(item).scheme); + } + catch(e) { + return false; + } + }); + + }, + msg: 'The `contentScriptFile` option must be a local URL or an array of URLs.' + }, + contentScript: { + is: ['undefined', 'null', 'string', 'array'], + map: ensureNull, + ok: function(value) { + return !Array.isArray(value) || value.every( + function(item) { return typeof item === 'string' } + ); + }, + msg: 'The `contentScript` option must be a string or an array of strings.' + }, + contentScriptWhen: { + is: ['string'], + ok: function(value) { return ~['start', 'ready', 'end'].indexOf(value) }, + map: function(value) { + return value || 'end'; + }, + msg: 'The `contentScriptWhen` option must be either "start", "ready" or "end".' + }, + contentScriptOptions: { + ok: function(value) { + if ( value === undefined ) { return true; } + try { JSON.parse( JSON.stringify( value ) ); } catch(e) { return false; } + return true; + }, + map: function(value) 'undefined' === getTypeOf(value) ? null : 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) validateOptions( + { $: suspect }, + { $: validation } +).$ + +function Allow(script) ({ + get script() script, + set script(value) script = !!value +}) + +/** + * Trait is intended to be used in some composition. It provides set of core + * properties and bounded validations to them. Trait is useful for all the + * compositions providing high level APIs for interaction with content. + * Property changes emit `"propertyChange"` events on instances. + */ +const Loader = EventEmitter.compose({ + /** + * Permissions for the content, with the following keys: + * @property {Object} [allow = { script: true }] + * @property {Boolean} [allow.script = true] + * Whether or not to execute script in the content. Defaults to true. + */ + get allow() this._allow || (this._allow = Allow(true)), + set allow(value) this.allow.script = value && value.script, + _allow: null, + /** + * The content to load. Either a string of HTML or a URL. + * @type {String} + */ + get contentURL() this._contentURL, + set contentURL(value) { + value = validate(value, valid.contentURL); + if (this._contentURL != value) { + this._emit('propertyChange', { + contentURL: this._contentURL = value + }); + } + }, + _contentURL: null, + /** + * When to load the content scripts. + * Possible values are "end" (default), which loads them once all page + * contents have been loaded, "ready", which loads them once DOM nodes are + * ready (ie like DOMContentLoaded event), and "start", which loads them once + * the `window` object for the page has been created, but before any scripts + * specified by the page have been loaded. + * Property change emits `propertyChange` event on instance with this key + * and new value. + * @type {'start'|'ready'|'end'} + */ + get contentScriptWhen() this._contentScriptWhen, + set contentScriptWhen(value) { + value = validate(value, valid.contentScriptWhen); + if (value !== this._contentScriptWhen) { + this._emit('propertyChange', { + contentScriptWhen: this._contentScriptWhen = value + }); + } + }, + _contentScriptWhen: 'end', + /** + * Options avalaible from the content script as `self.options`. + * The value of options can be of any type (object, array, string, etc.) + * but only jsonable values will be available as frozen objects from the + * content script. + * Property change emits `propertyChange` event on instance with this key + * and new value. + * @type {Object} + */ + get contentScriptOptions() this._contentScriptOptions, + set contentScriptOptions(value) this._contentScriptOptions = value, + _contentScriptOptions: null, + /** + * The URLs of content scripts. + * Property change emits `propertyChange` event on instance with this key + * and new value. + * @type {String[]} + */ + get contentScriptFile() this._contentScriptFile, + set contentScriptFile(value) { + value = validate(value, valid.contentScriptFile); + if (value != this._contentScriptFile) { + this._emit('propertyChange', { + contentScriptFile: this._contentScriptFile = value + }); + } + }, + _contentScriptFile: null, + /** + * The texts of content script. + * Property change emits `propertyChange` event on instance with this key + * and new value. + * @type {String|undefined} + */ + get contentScript() this._contentScript, + set contentScript(value) { + value = validate(value, valid.contentScript); + if (value != this._contentScript) { + this._emit('propertyChange', { + contentScript: this._contentScript = value + }); + } + }, + _contentScript: null +}); +exports.Loader = Loader; diff --git a/tools/addon-sdk-1.12/lib/sdk/content/symbiont.js b/tools/addon-sdk-1.12/lib/sdk/content/symbiont.js new file mode 100644 index 0000000..186e9d3 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/content/symbiont.js @@ -0,0 +1,197 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { Worker } = require('./worker'); +const { Loader } = require('./loader'); +const hiddenFrames = require('../frame/hidden-frame'); +const observers = require('../deprecated/observer-service'); +const unload = require('../system/unload'); + +const assetsURI = require('../self').data.url(); + +/** + * This trait is layered on top of `Worker` and in contrast to symbiont + * Worker constructor requires `content` option that represents content + * that will be loaded in the provided frame, if frame is not provided + * Worker will create hidden one. + */ +const Symbiont = Worker.resolve({ + constructor: '_initWorker', + destroy: '_workerDestroy' + }).compose(Loader, { + + /** + * The constructor requires all the options that are required by + * `require('content').Worker` with the difference that the `frame` option + * is optional. If `frame` is not provided, `contentURL` is expected. + * @param {Object} options + * @param {String} options.contentURL + * URL of a content to load into `this._frame` and create worker for. + * @param {Element} [options.frame] + * iframe element that is used to load `options.contentURL` into. + * if frame is not provided hidden iframe will be created. + */ + constructor: function Symbiont(options) { + options = options || {}; + + if ('contentURL' in options) + this.contentURL = options.contentURL; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + if ('contentScriptOptions' in options) + this.contentScriptOptions = options.contentScriptOptions; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('allow' in options) + this.allow = options.allow; + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + if ('frame' in options) { + this._initFrame(options.frame); + } + else { + let self = this; + this._hiddenFrame = hiddenFrames.HiddenFrame({ + onReady: function onFrame() { + self._initFrame(this.element); + }, + onUnload: function onUnload() { + // Bug 751211: Remove reference to _frame when hidden frame is + // automatically removed on unload, otherwise we are going to face + // "dead object" exception + self.destroy(); + } + }); + hiddenFrames.add(this._hiddenFrame); + } + + unload.ensure(this._public, "destroy"); + }, + + destroy: function destroy() { + this._workerDestroy(); + this._unregisterListener(); + this._frame = null; + if (this._hiddenFrame) { + hiddenFrames.remove(this._hiddenFrame); + this._hiddenFrame = null; + } + }, + + /** + * XUL iframe or browser elements with attribute `type` being `content`. + * Used to create `ContentSymbiont` from. + * @type {nsIFrame|nsIBrowser} + */ + _frame: null, + + /** + * Listener to the `'frameReady"` event (emitted when `iframe` is ready). + * Removes listener, sets right permissions to the frame and loads content. + */ + _initFrame: function _initFrame(frame) { + if (this._loadListener) + this._unregisterListener(); + + this._frame = frame; + frame.docShell.allowJavascript = this.allow.script; + frame.setAttribute("src", this._contentURL); + + // Inject `addon` object in document if we load a document from + // one of our addon folder and if no content script are defined. bug 612726 + let isDataResource = + typeof this._contentURL == "string" && + this._contentURL.indexOf(assetsURI) == 0; + let hasContentScript = + (Array.isArray(this.contentScript) ? this.contentScript.length > 0 + : !!this.contentScript) || + (Array.isArray(this.contentScriptFile) ? this.contentScriptFile.length > 0 + : !!this.contentScriptFile); + // If we have to inject `addon` we have to do it before document + // script execution, so during `start`: + this._injectInDocument = isDataResource && !hasContentScript; + if (this._injectInDocument) + this.contentScriptWhen = "start"; + + if ((frame.contentDocument.readyState == "complete" || + (frame.contentDocument.readyState == "interactive" && + this.contentScriptWhen != 'end' )) && + frame.contentDocument.location == this._contentURL) { + // In some cases src doesn't change and document is already ready + // (for ex: when the user moves a widget while customizing toolbars.) + this._onInit(); + return; + } + + let self = this; + + if ('start' == this.contentScriptWhen) { + this._loadEvent = 'start'; + observers.add('document-element-inserted', + this._loadListener = function onStart(doc) { + + let window = doc.defaultView; + if (window && window == frame.contentWindow) { + self._unregisterListener(); + self._onInit(); + } + + }); + return; + } + + let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded'; + let self = this; + this._loadEvent = eventName; + frame.addEventListener(eventName, + this._loadListener = function _onReady(event) { + + if (event.target != frame.contentDocument) + return; + self._unregisterListener(); + + self._onInit(); + + }, true); + + }, + + /** + * Unregister listener that watchs for document being ready to be injected. + * This listener is registered in `Symbiont._initFrame`. + */ + _unregisterListener: function _unregisterListener() { + if (!this._loadListener) + return; + if (this._loadEvent == "start") { + observers.remove('document-element-inserted', this._loadListener); + } + else { + this._frame.removeEventListener(this._loadEvent, this._loadListener, + true); + } + this._loadListener = null; + }, + + /** + * Called by Symbiont itself when the frame is ready to load + * content scripts according to contentScriptWhen. Overloaded by Panel. + */ + _onInit: function () { + this._initWorker({ window: this._frame.contentWindow }); + } + +}); +exports.Symbiont = Symbiont; diff --git a/tools/addon-sdk-1.12/lib/sdk/content/thumbnail.js b/tools/addon-sdk-1.12/lib/sdk/content/thumbnail.js new file mode 100644 index 0000000..9e57274 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/content/thumbnail.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' +}; + +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() +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/content/worker.js b/tools/addon-sdk-1.12/lib/sdk/content/worker.js new file mode 100644 index 0000000..b2f204c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/content/worker.js @@ -0,0 +1,582 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { Trait } = require('../deprecated/traits'); +const { EventEmitter, EventEmitterTrait } = require('../deprecated/events'); +const { Ci, Cu, Cc } = require('chrome'); +const timer = require('../timers'); +const { URL } = require('../url'); +const unload = require('../system/unload'); +const observers = require('../deprecated/observer-service'); +const { Cortex } = require('../deprecated/cortex'); +const { sandbox, evaluate, load } = require("../loader/sandbox"); +const { merge } = require('../util/object'); +const xulApp = require("../system/xul-app"); +const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion, + "17.0a2", "*"); +const { getTabForWindow } = require('../tabs/helpers'); + +/* Trick the linker in order to ensure shipping these files in the XPI. + require('./content-proxy.js'); + require('./content-worker.js'); + Then, retrieve URL of these files in the XPI: +*/ +let prefix = module.uri.split('worker.js')[0]; +const CONTENT_PROXY_URL = prefix + 'content-proxy.js'; +const CONTENT_WORKER_URL = prefix + 'content-worker.js'; + +const JS_VERSION = '1.8'; + +const ERR_DESTROYED = + "Couldn't find the worker to receive this message. " + + "The script may not be initialized yet, or may already have been unloaded."; + +const ERR_FROZEN = "The page is currently hidden and can no longer be used " + + "until it is visible again."; + +/** + * This key is not exported and should only be used for proxy tests. + * The following `PRIVATE_KEY` is used in addon module scope in order to tell + * Worker API to expose `UNWRAP_ACCESS_KEY` in content script. + * This key allows test-content-proxy.js to unwrap proxy with valueOf: + * let xpcWrapper = proxyWrapper.valueOf(UNWRAP_ACCESS_KEY); + */ +const PRIVATE_KEY = {}; + + +const WorkerSandbox = EventEmitter.compose({ + + /** + * Emit a message to the worker content sandbox + */ + emit: function emit() { + // First ensure having a regular array + // (otherwise, `arguments` would be mapped to an object by `stringify`) + let array = Array.slice(arguments); + // JSON.stringify is buggy with cross-sandbox values, + // it may return "{}" on functions. Use a replacer to match them correctly. + function replacer(k, v) { + return typeof v === "function" ? undefined : v; + } + // Ensure having an asynchronous behavior + let self = this; + timer.setTimeout(function () { + self._emitToContent(JSON.stringify(array, replacer)); + }, 0); + }, + + /** + * Synchronous version of `emit`. + * /!\ Should only be used when it is strictly mandatory /!\ + * Doesn't ensure passing only JSON values. + * Mainly used by context-menu in order to avoid breaking it. + */ + emitSync: function emitSync() { + let args = Array.slice(arguments); + // Bug 732716: Ensure wrapping xrays sent to the content script + // otherwise it will have access to raw xraywrappers and content script + // will assume it is an user object coming from the content script sandbox + if ("_wrap" in this) + args = args.map(this._wrap); + return this._emitToContent(args); + }, + + /** + * Tells if content script has at least one listener registered for one event, + * through `self.on('xxx', ...)`. + * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API. + */ + hasListenerFor: function hasListenerFor(name) { + return this._hasListenerFor(name); + }, + + /** + * Method called by the worker sandbox when it needs to send a message + */ + _onContentEvent: function onContentEvent(args) { + // As `emit`, we ensure having an asynchronous behavior + let self = this; + timer.setTimeout(function () { + // We emit event to chrome/addon listeners + self._emit.apply(self, JSON.parse(args)); + }, 0); + }, + + /** + * Configures sandbox and loads content scripts into it. + * @param {Worker} worker + * content worker + */ + constructor: function WorkerSandbox(worker) { + this._addonWorker = worker; + + // Ensure that `emit` has always the right `this` + this.emit = this.emit.bind(this); + this.emitSync = this.emitSync.bind(this); + + // We receive a wrapped window, that may be an xraywrapper if it's content + let window = worker._window; + let proto = window; + + // Instantiate trusted code in another Sandbox in order to prevent content + // script from messing with standard classes used by proxy and API code. + let apiSandbox = sandbox(window, { wantXrays: true }); + + // Build content proxies only if the document has a non-system principal + // And only on old firefox versions that doesn't ship bug 738244 + if (USE_JS_PROXIES && XPCNativeWrapper.unwrap(window) !== window) { + apiSandbox.console = console; + // Execute the proxy code + load(apiSandbox, CONTENT_PROXY_URL); + // Get a reference of the window's proxy + proto = apiSandbox.create(window); + // Keep a reference to `wrap` function for `emitSync` usage + this._wrap = apiSandbox.wrap; + } + + // Create the sandbox and bind it to window in order for content scripts to + // have access to all standard globals (window, document, ...) + let content = this._sandbox = sandbox(window, { + sandboxPrototype: proto, + wantXrays: true + }); + // We have to ensure that window.top and window.parent are the exact same + // object than window object, i.e. the sandbox global object. But not + // always, in case of iframes, top and parent are another window object. + let top = window.top === window ? content : content.top; + let parent = window.parent === window ? content : content.parent; + merge(content, { + // We need "this === window === top" to be true in toplevel scope: + get window() content, + get top() top, + get parent() parent, + // Use the Greasemonkey naming convention to provide access to the + // unwrapped window object so the content script can access document + // JavaScript values. + // NOTE: this functionality is experimental and may change or go away + // at any time! + get unsafeWindow() window.wrappedJSObject + }); + + // Load trusted code that will inject content script API. + // We need to expose JS objects defined in same principal in order to + // avoid having any kind of wrapper. + load(apiSandbox, CONTENT_WORKER_URL); + + // prepare a clean `self.options` + let options = 'contentScriptOptions' in worker ? + JSON.stringify( worker.contentScriptOptions ) : + undefined; + + // Then call `inject` method and communicate with this script + // by trading two methods that allow to send events to the other side: + // - `onEvent` called by content script + // - `result.emitToContent` called by addon script + // Bug 758203: We have to explicitely define `__exposedProps__` in order + // to allow access to these chrome object attributes from this sandbox with + // content priviledges + // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers + let chromeAPI = { + timers: { + setTimeout: timer.setTimeout, + setInterval: timer.setInterval, + clearTimeout: timer.clearTimeout, + clearInterval: timer.clearInterval, + __exposedProps__: { + setTimeout: 'r', + setInterval: 'r', + clearTimeout: 'r', + clearInterval: 'r' + } + }, + __exposedProps__: { + timers: 'r' + } + }; + let onEvent = this._onContentEvent.bind(this); + // `ContentWorker` is defined in CONTENT_WORKER_URL file + let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options); + this._emitToContent = result.emitToContent; + this._hasListenerFor = result.hasListenerFor; + + // Handle messages send by this script: + let self = this; + // console.xxx calls + this.on("console", function consoleListener(kind) { + console[kind].apply(console, Array.slice(arguments, 1)); + }); + + // self.postMessage calls + this.on("message", function postMessage(data) { + // destroyed? + if (self._addonWorker) + self._addonWorker._emit('message', data); + }); + + // self.port.emit calls + this.on("event", function portEmit(name, args) { + // destroyed? + if (self._addonWorker) + self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments); + }); + + // Internal feature that is only used by SDK tests: + // Expose unlock key to content script context. + // See `PRIVATE_KEY` definition for more information. + if (apiSandbox && worker._expose_key) + content.UNWRAP_ACCESS_KEY = apiSandbox.UNWRAP_ACCESS_KEY; + + // Inject `addon` global into target document if document is trusted, + // `addon` in document is equivalent to `self` in content script. + if (worker._injectInDocument) { + let win = window.wrappedJSObject ? window.wrappedJSObject : window; + Object.defineProperty(win, "addon", { + value: content.self + } + ); + } + + // The order of `contentScriptFile` and `contentScript` evaluation is + // intentional, so programs can load libraries like jQuery from script URLs + // and use them in scripts. + let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile + : null, + contentScript = ('contentScript' in worker) ? worker.contentScript : null; + + if (contentScriptFile) { + if (Array.isArray(contentScriptFile)) + this._importScripts.apply(this, contentScriptFile); + else + this._importScripts(contentScriptFile); + } + if (contentScript) { + this._evaluate( + Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript + ); + } + }, + destroy: function destroy() { + this.emitSync("detach"); + this._sandbox = null; + this._addonWorker = null; + this._wrap = null; + }, + + /** + * JavaScript sandbox where all the content scripts are evaluated. + * {Sandbox} + */ + _sandbox: null, + + /** + * Reference to the addon side of the worker. + * @type {Worker} + */ + _addonWorker: null, + + /** + * Evaluates code in the sandbox. + * @param {String} code + * JavaScript source to evaluate. + * @param {String} [filename='javascript:' + code] + * Name of the file + */ + _evaluate: function(code, filename) { + try { + evaluate(this._sandbox, code, filename || 'javascript:' + code); + } + catch(e) { + this._addonWorker._emit('error', e); + } + }, + /** + * Imports scripts to the sandbox by reading files under urls and + * evaluating its source. If exception occurs during evaluation + * `"error"` event is emitted on the worker. + * This is actually an analog to the `importScript` method in web + * workers but in our case it's not exposed even though content + * scripts may be able to do it synchronously since IO operation + * takes place in the UI process. + */ + _importScripts: function _importScripts(url) { + let urls = Array.slice(arguments, 0); + for each (let contentScriptFile in urls) { + try { + let uri = URL(contentScriptFile); + if (uri.scheme === 'resource') + load(this._sandbox, String(uri)); + else + throw Error("Unsupported `contentScriptFile` url: " + String(uri)); + } + catch(e) { + this._addonWorker._emit('error', e); + } + } + } +}); + +/** + * Message-passing facility for communication between code running + * in the content and add-on process. + * @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/content/worker + */ +const Worker = EventEmitter.compose({ + on: Trait.required, + _removeAllListeners: Trait.required, + + /** + * Sends a message to the worker's global scope. Method takes single + * argument, which represents data to be sent to the worker. The data may + * be any primitive type value or `JSON`. Call of this method asynchronously + * emits `message` event with data value in the global scope of this + * symbiont. + * + * `message` event listeners can be set either by calling + * `self.on` with a first argument string `"message"` or by + * implementing `onMessage` function in the global scope of this worker. + * @param {Number|String|JSON} data + */ + postMessage: function postMessage(data) { + if (!this._contentWorker) + throw new Error(ERR_DESTROYED); + if (this._frozen) + throw new Error(ERR_FROZEN); + + this._contentWorker.emit("message", data); + }, + + /** + * EventEmitter, that behaves (calls listeners) asynchronously. + * A way to send customized messages to / from the worker. + * Events from in the worker can be observed / emitted via + * worker.on / worker.emit. + */ + get port() { + // We generate dynamically this attribute as it needs to be accessible + // before Worker.constructor gets called. (For ex: Panel) + + // create an event emitter that receive and send events from/to the worker + let self = this; + this._port = EventEmitterTrait.create({ + emit: function () self._emitEventToContent(Array.slice(arguments)) + }); + + // expose wrapped port, that exposes only public properties: + // We need to destroy this getter in order to be able to set the + // final value. We need to update only public port attribute as we never + // try to access port attribute from private API. + delete this._public.port; + this._public.port = Cortex(this._port); + // Replicate public port to the private object + delete this.port; + this.port = this._public.port; + + return this._port; + }, + + /** + * Same object than this.port but private API. + * Allow access to _emit, in order to send event to port. + */ + _port: null, + + /** + * Emit a custom event to the content script, + * i.e. emit this event on `self.port` + */ + _emitEventToContent: function _emitEventToContent(args) { + // We need to save events that are emitted before the worker is + // initialized + if (!this._inited) { + this._earlyEvents.push(args); + return; + } + + if (this._frozen) + throw new Error(ERR_FROZEN); + + // We throw exception when the worker has been destroyed + if (!this._contentWorker) { + throw new Error(ERR_DESTROYED); + } + + // Forward the event to the WorkerSandbox object + this._contentWorker.emit.apply(null, ["event"].concat(args)); + }, + + // Is worker connected to the content worker sandbox ? + _inited: false, + + // Is worker being frozen? i.e related document is frozen in bfcache. + // Content script should not be reachable if frozen. + _frozen: true, + + // List of custom events fired before worker is initialized + get _earlyEvents() { + delete this._earlyEvents; + this._earlyEvents = []; + return this._earlyEvents; + }, + + constructor: function Worker(options) { + options = options || {}; + + if ('window' in options) + this._window = options.window; + 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 ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + if ('onDetach' in options) + this.on('detach', options.onDetach); + + // Internal feature that is only used by SDK unit tests. + // See `PRIVATE_KEY` definition for more information. + if ('exposeUnlockKey' in options && options.exposeUnlockKey === PRIVATE_KEY) + this._expose_key = true; + + // Track document unload to destroy this worker. + // We can't watch for unload event on page's window object as it + // prevents bfcache from working: + // https://developer.mozilla.org/En/Working_with_BFCache + this._windowID = this._window. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils). + currentInnerWindowID; + observers.add("inner-window-destroyed", + this._documentUnload = this._documentUnload.bind(this)); + + // Listen to pagehide event in order to freeze the content script + // while the document is frozen in bfcache: + this._window.addEventListener("pageshow", + this._pageShow = this._pageShow.bind(this), + true); + this._window.addEventListener("pagehide", + this._pageHide = this._pageHide.bind(this), + true); + + unload.ensure(this._public, "destroy"); + + // Ensure that worker._port is initialized for contentWorker to be able + // to send use event during WorkerSandbox(this) + this.port; + + // will set this._contentWorker pointing to the private API: + this._contentWorker = WorkerSandbox(this); + + // Mainly enable worker.port.emit to send event to the content worker + this._inited = true; + this._frozen = false; + + // Flush all events that have been fired before the worker is initialized. + this._earlyEvents.forEach((function (args) this._emitEventToContent(args)). + bind(this)); + }, + + _documentUnload: function _documentUnload(subject, topic, data) { + let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (innerWinID != this._windowID) return false; + this._workerCleanup(); + return true; + }, + + _pageShow: function _pageShow() { + this._contentWorker.emitSync("pageshow"); + this._emit("pageshow"); + this._frozen = false; + }, + + _pageHide: function _pageHide() { + this._contentWorker.emitSync("pagehide"); + this._emit("pagehide"); + this._frozen = true; + }, + + get url() { + // this._window will be null after detach + return this._window ? this._window.document.location.href : null; + }, + + get tab() { + // this._window will be null after detach + if (this._window) + return getTabForWindow(this._window); + return null; + }, + + /** + * Tells content worker to unload itself and + * removes all the references from itself. + */ + destroy: function destroy() { + this._workerCleanup(); + this._removeAllListeners(); + }, + + /** + * Remove all internal references to the attached document + * Tells _port to unload itself and removes all the references from itself. + */ + _workerCleanup: function _workerCleanup() { + // maybe unloaded before content side is created + // As Symbiont call worker.constructor on document load + if (this._contentWorker) + this._contentWorker.destroy(); + this._contentWorker = null; + if (this._window) { + this._window.removeEventListener("pageshow", this._pageShow, true); + this._window.removeEventListener("pagehide", this._pageHide, true); + } + this._window = null; + // This method may be called multiple times, + // avoid dispatching `detach` event more than once + if (this._windowID) { + this._windowID = null; + observers.remove("inner-window-destroyed", this._documentUnload); + this._earlyEvents.slice(0, this._earlyEvents.length); + this._emit("detach"); + } + }, + + /** + * Receive an event from the content script that need to be sent to + * worker.port. Provide a way for composed object to catch all events. + */ + _onContentScriptEvent: function _onContentScriptEvent() { + this._port._emit.apply(this._port, arguments); + }, + + /** + * Reference to the content side of the worker. + * @type {WorkerGlobalScope} + */ + _contentWorker: null, + + /** + * Reference to the window that is accessible from + * the content scripts. + * @type {Object} + */ + _window: null, + + /** + * Flag to enable `addon` object injection in document. (bug 612726) + * @type {Boolean} + */ + _injectInDocument: false +}); +exports.Worker = Worker; diff --git a/tools/addon-sdk-1.12/lib/sdk/context-menu.js b/tools/addon-sdk-1.12/lib/sdk/context-menu.js new file mode 100644 index 0000000..9d20f0b --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/context-menu.js @@ -0,0 +1,1494 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 {Ci} = require("chrome"); + +if (!require("./system/xul-app").is("Firefox")) { + throw new Error([ + "The context-menu module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +const apiUtils = require("./deprecated/api-utils"); +const collection = require("./util/collection"); +const { Worker } = require("./content/worker"); +const { URL } = require("./url"); +const { MatchPattern } = require("./page-mod/match-pattern"); +const { EventEmitterTrait: EventEmitter } = require("./deprecated/events"); +const observerServ = require("./deprecated/observer-service"); +const jpSelf = require("./self"); +const { WindowTracker } = require("./deprecated/window-utils"); +const { getInnerId, isBrowser } = require("./window/utils"); +const { Trait } = require("./deprecated/light-traits"); +const { Cortex } = require("./deprecated/cortex"); +const timer = require("./timers"); + +// All user items we add have this class name. +const ITEM_CLASS = "jetpack-context-menu-item"; + +// Items in the top-level context menu also have this class. +const TOPLEVEL_ITEM_CLASS = "jetpack-context-menu-item-toplevel"; + +// Items in the overflow submenu also have this class. +const OVERFLOW_ITEM_CLASS = "jetpack-context-menu-item-overflow"; + +// The ID of the menu separator that separates standard context menu items from +// our user items. +const SEPARATOR_ID = "jetpack-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 this. +const OVERFLOW_MENU_LABEL = "Add-ons"; + +// The ID of the overflow sub-xul:menu. +const OVERFLOW_MENU_ID = "jetpack-content-menu-overflow-menu"; + +// The ID of the overflow submenu's xul:menupopup. +const OVERFLOW_POPUP_ID = "jetpack-content-menu-overflow-popup"; + +// 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, +]; + +// This is used to access private properties of Item and Menu instances. +const PRIVATE_PROPS_KEY = { + valueOf: function valueOf() "private properties key" +}; + +// Used as an internal ID for items and as part of a public ID for item DOM +// elements. Careful: This number is not necessarily unique to any one instance +// of the module. For each module instance, when the first item is created this +// number will be 0, when the second is created it will be 1, and so on. +let nextItemID = 0; + +// The number of items that haven't finished initializing yet. See +// AIT__finishActiveItemInit(). +let numItemsWithUnfinishedInit = 0; + +exports.Item = Item; +exports.Menu = Menu; +exports.Separator = Separator; + + +// A word about traits and privates. `this` inside of traits methods is an +// object private to the implementation. It should never be publicly leaked. +// We use Cortex in the exported menu item constructors to create public +// reflections of the private objects that hide private properties -- those +// prefixed with an underscore. Public reflections are attached to the private +// objects via the `_public` property. +// +// All item objects passed into the implementation by the client will be public +// reflections, not private objects. Likewise, all item objects passed out of +// the implementation to the client must be public, not private. Mixing up +// public and private is bad and easy to do, so not only are private objects +// restricted to the implementation, but as much as possible we try to restrict +// them to the Item, Menu, and Separator traits and constructors. Everybody +// else in the implementation should expect to be passed public reflections, and +// they must specifically request private objects via privateItem(). + +// Item, Menu, and Separator are composed of this trait. +const ItemBaseTrait = Trait({ + + _initBase: function IBT__initBase(opts, optRules, optsToNotSet) { + this._optRules = optRules; + for (let optName in optRules) + if (optsToNotSet.indexOf(optName) < 0) + this[optName] = opts[optName]; + optsToNotSet.forEach(function (opt) validateOpt(opts[opt], optRules[opt])); + this._isInited = true; + + this._id = nextItemID++; + this._parentMenu = null; + + // This makes the private properties accessible to anyone with access to + // PRIVATE_PROPS_KEY. Barring loader tricks, only this file has has access + // to it, so only this file has access to the private properties. + const self = this; + this.valueOf = function IBT_valueOf(key) { + return key === PRIVATE_PROPS_KEY ? self : self._public; + }; + }, + + destroy: function IBT_destroy() { + if (this._wasDestroyed) + return; + if (this.parentMenu) + this.parentMenu.removeItem(this._public); + else if (!(this instanceof Separator) && this._hasFinishedInit) + browserManager.removeTopLevelItem(this._public); + browserManager.unregisterItem(this._public); + this._wasDestroyed = true; + }, + + get parentMenu() { + return this._parentMenu; + }, + + set parentMenu(val) { + throw new Error("The 'parentMenu' property is not intended to be set. " + + "Use menu.addItem(item) instead."); + }, + + set _isTopLevel(val) { + if (val) + this._workerReg = new WorkerRegistry(this._public); + else { + this._workerReg.destroy(); + delete this._workerReg; + } + }, + + get _topLevelItem() { + let topLevelItem = this._public; + let parentMenu = this.parentMenu; + while (parentMenu) { + topLevelItem = parentMenu; + parentMenu = parentMenu.parentMenu; + } + return topLevelItem; + } +}); + +// Item and Menu are composed of this trait. +const ActiveItemTrait = Trait.compose(ItemBaseTrait, EventEmitter, Trait({ + + _initActiveItem: function AIT__initActiveItem(opts, optRules, optsToNotSet) { + this._initBase(opts, optRules, + optsToNotSet.concat(["onMessage", "context"])); + + if ("onMessage" in opts) + this.on("message", opts.onMessage); + + // When a URL context is removed (by calling context.remove(urlContext)), we + // may need to create workers for windows containing pages that the item now + // matches. Likewise, when a URL context is added, we need to destroy + // workers for windows containing pages that the item now does not match. + // + // collection doesn't provide a way to listen for removals. utils/registry + // does, but it doesn't allow its elements to be enumerated. So as a hack, + // use a collection for item.context and replace its add and remove methods. + collection.addCollectionProperty(this, "context"); + if (opts.context) + this.context.add(opts.context); + + const self = this; + + let add = this.context.add; + this.context.add = function itemContextAdd() { + let args = Array.slice(arguments); + add.apply(self.context, args); + if (self._workerReg && args.some(function (a) a instanceof URLContext)) + self._workerReg.destroyUnneededWorkers(); + }; + + let remove = this.context.remove; + this.context.remove = function itemContextRemove() { + let args = Array.slice(arguments); + remove.apply(self.context, args); + if (self._workerReg && args.some(function (a) a instanceof URLContext)) + self._workerReg.createNeededWorkers(); + }; + }, + + // Workers are only created for top-level menu items. When a top-level item + // is later added to a Menu, its workers are destroyed. Well, all items start + // out as top-level because there is, unfortunately, no contextMenu.add(). So + // when an item is created and immediately added to a Menu, workers for it are + // needlessly created and destroyed. The point of this timeout is to avoid + // that. Items that are created and added to Menus in the same turn of the + // event loop won't have workers created for them. + _finishActiveItemInit: function AIT__finishActiveItemInit() { + numItemsWithUnfinishedInit++; + const self = this; + timer.setTimeout(function AIT__finishActiveItemInitTimeout() { + if (!self.parentMenu && !self._wasDestroyed) + browserManager.addTopLevelItem(self._public); + self._hasFinishedInit = true; + numItemsWithUnfinishedInit--; + }, 0); + }, + + get label() { + return this._label; + }, + + set label(val) { + this._label = validateOpt(val, this._optRules.label); + if (this._isInited) + browserManager.setItemLabel(this, this._label); + return this._label; + }, + + get image() { + return this._image; + }, + + set image(val) { + this._image = validateOpt(val, this._optRules.image); + if (this._isInited) + browserManager.setItemImage(this, this._image); + return this._image; + }, + + get contentScript() { + return this._contentScript; + }, + + set contentScript(val) { + this._contentScript = validateOpt(val, this._optRules.contentScript); + return this._contentScript; + }, + + get contentScriptFile() { + return this._contentScriptFile; + }, + + set contentScriptFile(val) { + this._contentScriptFile = + validateOpt(val, this._optRules.contentScriptFile); + return this._contentScriptFile; + } +})); + +// Item is composed of this trait. +const ItemTrait = Trait.compose(ActiveItemTrait, Trait({ + + _initItem: function IT__initItem(opts, optRules) { + this._initActiveItem(opts, optRules, []); + }, + + get data() { + return this._data; + }, + + set data(val) { + this._data = validateOpt(val, this._optRules.data); + if (this._isInited) + browserManager.setItemData(this, this._data); + return this._data; + }, + + toString: function IT_toString() { + return '[object Item "' + this.label + '"]'; + } +})); + +// The exported Item constructor. +function Item(options) { + let optRules = optionsRules(); + optRules.data = { + map: function (v) v.toString(), + is: ["string", "undefined"] + }; + + let item = ItemTrait.create(Item.prototype); + item._initItem(options, optRules); + + item._public = Cortex(item); + browserManager.registerItem(item._public); + item._finishActiveItemInit(); + + return item._public; +} + +// Menu is composed of this trait. +const MenuTrait = Trait.compose( + ActiveItemTrait.resolve({ destroy: "_destroyThisItem" }), + Trait({ + + _initMenu: function MT__initMenu(opts, optRules, optsToNotSet) { + this._items = []; + this._initActiveItem(opts, optRules, optsToNotSet); + }, + + destroy: function MT_destroy() { + while (this.items.length) + this.items[0].destroy(); + this._destroyThisItem(); + }, + + get items() { + return this._items; + }, + + set items(val) { + let newItems = validateOpt(val, this._optRules.items); + while (this.items.length) + this.items[0].destroy(); + newItems.forEach(function (i) this.addItem(i), this); + return newItems; + }, + + addItem: function MT_addItem(item) { + // First, remove the item from its current parent. + let privates = privateItem(item); + if (item.parentMenu) + item.parentMenu.removeItem(item); + else if (!(item instanceof Separator) && privates._hasFinishedInit) + browserManager.removeTopLevelItem(item); + + // Now add the item to this menu. + this._items.push(item); + privates._parentMenu = this._public; + browserManager.addItemToMenu(item, this._public); + }, + + removeItem: function MT_removeItem(item) { + let idx = this._items.indexOf(item); + if (idx < 0) + return; + this._items.splice(idx, 1); + privateItem(item)._parentMenu = null; + browserManager.removeItemFromMenu(item, this._public); + }, + + toString: function MT_toString() { + return '[object Menu "' + this.label + '"]'; + } +})); + +// The exported Menu constructor. +function Menu(options) { + let optRules = optionsRules(); + optRules.items = { + is: ["array"], + ok: function (v) { + return v.every(function (item) { + return (item instanceof Item) || + (item instanceof Menu) || + (item instanceof Separator); + }); + }, + msg: "items must be an array, and each element in the array must be an " + + "Item, Menu, or Separator." + }; + + let menu = MenuTrait.create(Menu.prototype); + + // We can't rely on _initBase to set the `items` property, because the menu + // needs to be registered with and added to the browserManager before any + // child items are added to it. + menu._initMenu(options, optRules, ["items"]); + + menu._public = Cortex(menu); + browserManager.registerItem(menu._public); + menu.items = options.items; + menu._finishActiveItemInit(); + + return menu._public; +} + +// The exported Separator constructor. +function Separator() { + let sep = ItemBaseTrait.create(Separator.prototype); + sep._initBase({}, {}, []); + + sep._public = Cortex(sep); + browserManager.registerItem(sep._public); + sep._hasFinishedInit = true; + return sep._public; +} + + +function Context() {} + +function PageContext() { + this.isCurrent = function PageContext_isCurrent(popupNode) { + let win = popupNode.ownerDocument.defaultView; + if (win && !win.getSelection().isCollapsed) + return false; + + let cursor = popupNode; + while (cursor && !(cursor instanceof Ci.nsIDOMHTMLHtmlElement)) { + if (NON_PAGE_CONTEXT_ELTS.some(function (iface) cursor instanceof iface)) + return false; + cursor = cursor.parentNode; + } + return true; + }; +} + +PageContext.prototype = new Context(); + +function SelectorContext(selector) { + let opts = apiUtils.validateOptions({ selector: selector }, { + selector: { + is: ["string"], + msg: "selector must be a string." + } + }); + + this.adjustPopupNode = function SelectorContext_adjustPopupNode(node) { + return closestMatchingAncestor(node); + }; + + this.isCurrent = function SelectorContext_isCurrent(popupNode) { + return !!closestMatchingAncestor(popupNode); + }; + + // Returns node if it matches selector, or the closest ancestor of node that + // matches, or null if node and none of its ancestors matches. + function closestMatchingAncestor(node) { + let cursor = node; + while (cursor) { + if (cursor.mozMatchesSelector(selector)) + return cursor; + if (cursor instanceof Ci.nsIDOMHTMLHtmlElement) + break; + cursor = cursor.parentNode; + } + return null; + } +} + +SelectorContext.prototype = new Context(); + +function SelectionContext() { + this.isCurrent = function SelectionContext_isCurrent(popupNode) { + let win = popupNode.ownerDocument.defaultView; + if (!win) + return false; + + let hasSelection = !win.getSelection().isCollapsed; + if (!hasSelection) { + // window.getSelection doesn't return a selection for text selected in a + // form field (see bug 85686), so before returning false we want to check + // if the popupNode is a text field. + let { selectionStart, selectionEnd } = popupNode; + hasSelection = !isNaN(selectionStart) && + !isNaN(selectionEnd) && + selectionStart !== selectionEnd; + } + return hasSelection; + }; +} + +SelectionContext.prototype = new Context(); + +function URLContext(patterns) { + let opts = apiUtils.validateOptions({ patterns: patterns }, { + patterns: { + map: function (v) apiUtils.getTypeOf(v) === "array" ? v : [v], + ok: function (v) v.every(function (p) typeof(p) === "string"), + msg: "patterns must be a string or an array of strings." + } + }); + try { + patterns = opts.patterns.map(function (p) new MatchPattern(p)); + } + catch (err) { + console.error("Error creating URLContext match pattern:"); + throw err; + } + + const self = this; + + this.isCurrent = function URLContext_isCurrent(popupNode) { + return self.isCurrentForURL(popupNode.ownerDocument.URL); + }; + + this.isCurrentForURL = function URLContext_isCurrentForURL(url) { + return patterns.some(function (p) p.test(url)); + }; +} + +URLContext.prototype = new Context(); + +exports.PageContext = apiUtils.publicConstructor(PageContext); +exports.SelectorContext = apiUtils.publicConstructor(SelectorContext); +exports.SelectionContext = apiUtils.publicConstructor(SelectionContext); +exports.URLContext = apiUtils.publicConstructor(URLContext); + + +// Returns a version of opt validated against the given rule. +function validateOpt(opt, rule) { + return apiUtils.validateOptions({ opt: opt }, { opt: rule }).opt; +} + +// Returns rules for apiUtils.validateOptions() common to Item and Menu. +function optionsRules() { + return { + context: { + is: ["undefined", "object", "array"], + ok: function (v) { + if (!v) + return true; + let arr = apiUtils.getTypeOf(v) === "array" ? v : [v]; + return arr.every(function (o) o instanceof Context); + }, + msg: "The 'context' option must be a Context object or an array of " + + "Context objects." + }, + label: { + map: function (v) v.toString(), + is: ["string"], + ok: function (v) !!v, + msg: "The item must have a non-empty string label." + }, + image: { + map: function (v) v.toString(), + is: ["string", "undefined", "null"] + }, + contentScript: { + is: ["string", "array", "undefined"], + ok: function (v) { + return apiUtils.getTypeOf(v) !== "array" || + v.every(function (s) typeof(s) === "string"); + } + }, + contentScriptFile: { + is: ["string", "array", "undefined"], + ok: function (v) { + if (!v) + return true; + let arr = apiUtils.getTypeOf(v) === "array" ? v : [v]; + try { + return arr.every(function (s) { + return apiUtils.getTypeOf(s) === "string" && + URL(s).scheme === 'resource'; + }); + } + catch (err) {} + return false; + }, + msg: "The 'contentScriptFile' option must be a local file URL or " + + "an array of local file URLs." + }, + onMessage: { + is: ["function", "undefined"] + } + }; +} + +// Does a binary search on elts, a NodeList, and returns the DOM element +// before which an item with targetLabel should be inserted. null is returned +// if the new item should be inserted at the end. +function insertionPoint(targetLabel, elts) { + let from = 0; + let to = elts.length - 1; + + while (from <= to) { + let i = Math.floor((from + to) / 2); + let comp = targetLabel.localeCompare(elts[i].getAttribute("label")); + if (comp < 0) + to = i - 1; + else if (comp > 0) + from = i + 1; + else + return elts[i]; + } + return elts[from] || null; +} + +// Builds an ID suitable for a DOM element from the given item ID. +// isInOverflowSubtree should be true if the returned element will be inserted +// into the DOM subtree rooted at the overflow menu. +function domEltIDFromItemID(itemID, isInOverflowSubtree) { + let suffix = isInOverflowSubtree ? "-overflow" : ""; + return jpSelf.id + "-context-menu-item-" + itemID + suffix; +} + +// Parses the item ID out of the given DOM element ID and returns it. If the +// element's ID is malformed or it indicates that the element was not created by +// the instance of the module calling this function, returns -1. +function itemIDFromDOMEltID(domEltID) { + let match = /^(.+?)-context-menu-item-([0-9]+)[-a-z]*$/.exec(domEltID); + return !match || match[1] !== jpSelf.id ? -1 : match[2]; +} + +// Returns the private version of the given public reflection. +function privateItem(publicItem) { + return publicItem.valueOf(PRIVATE_PROPS_KEY); +} + + +// A type of Worker tailored to our uses. +const ContextMenuWorker = Worker.compose({ + destroy: Worker.required, + + // Returns true if any context listeners are defined in the worker's port. + anyContextListeners: function CMW_anyContextListeners() { + return this._contentWorker.hasListenerFor("context"); + }, + + // Returns the first string or truthy value returned by a context listener in + // the worker's port. If none return a string or truthy value or if there are + // no context listeners, returns false. popupNode is the node that was + // context-clicked. + isAnyContextCurrent: function CMW_isAnyContextCurrent(popupNode) { + let results = this._contentWorker.emitSync("context", popupNode); + for (let i = 0; i < results.length; i++) { + let val = results[i]; + if (typeof val === "string" || val) + return val; + } + return false; + }, + + // 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 CMW_fireClick(popupNode, clickedItemData) { + this._contentWorker.emitSync("click", popupNode, clickedItemData); + } +}); + + +// This class creates and stores content workers for pairs of menu items and +// content windows. Use one instance for each item. Not all pairs need a +// worker: if an item has a URL context that does not match a window's page, +// then no worker is created for the pair. +function WorkerRegistry(item) { + this.item = item; + + // inner window ID => { win, worker } + this.winWorkers = {}; + + // inner window ID => content window + this.winsWithoutWorkers = {}; +} + +WorkerRegistry.prototype = { + + // Registers a content window, creating a worker for it if it needs one. + registerContentWin: function WR_registerContentWin(win) { + let innerWinID = getInnerId(win); + if ((innerWinID in this.winWorkers) || + (innerWinID in this.winsWithoutWorkers)) + return; + if (this._doesURLNeedWorker(win.document.URL)) + this.winWorkers[innerWinID] = { win: win, worker: this._makeWorker(win) }; + else + this.winsWithoutWorkers[innerWinID] = win; + }, + + // Unregisters a content window, destroying its related worker if it has one. + unregisterContentWin: function WR_unregisterContentWin(innerWinID) { + if (innerWinID in this.winWorkers) { + let winWorker = this.winWorkers[innerWinID]; + winWorker.worker.destroy(); + delete winWorker.worker; + delete winWorker.win; + delete this.winWorkers[innerWinID]; + } + else + delete this.winsWithoutWorkers[innerWinID]; + }, + + // Creates a worker for each window that needs a worker but doesn't have one. + createNeededWorkers: function WR_createNeededWorkers() { + for (let innerWinID in this.winsWithoutWorkers) { + let win = this.winsWithoutWorkers[innerWinID] + delete this.winsWithoutWorkers[innerWinID]; + this.registerContentWin(win); + } + }, + + // Destroys the worker for each window that has a worker but doesn't need it. + destroyUnneededWorkers: function WR_destroyUnneededWorkers() { + for (let innerWinID in this.winWorkers) { + let winWorker = this.winWorkers[innerWinID]; + if (!this._doesURLNeedWorker(winWorker.win.document.URL)) { + this.unregisterContentWin(innerWinID); + this.winsWithoutWorkers[innerWinID] = winWorker.win; + } + } + }, + + // Returns the worker for the item-window pair or null if none exists. + find: function WR_find(contentWin) { + let innerWinID = getInnerId(contentWin); + return (innerWinID in this.winWorkers) ? + this.winWorkers[innerWinID].worker : + null; + }, + + // Unregisters all content windows from the registry, which destroys all + // workers. + destroy: function WR_destroy() { + for (let innerWinID in this.winWorkers) + this.unregisterContentWin(innerWinID); + for (let innerWinID in this.winsWithoutWorkers) + this.unregisterContentWin(innerWinID); + }, + + // Returns false if the item has a URL context that does not match the given + // URL. + _doesURLNeedWorker: function WR__doesURLNeedWorker(url) { + for (let ctxt in this.item.context) + if ((ctxt instanceof URLContext) && !ctxt.isCurrentForURL(url)) + return false; + return true; + }, + + _makeWorker: function WR__makeWorker(win) { + let worker = ContextMenuWorker({ + window: win, + contentScript: this.item.contentScript, + contentScriptFile: this.item.contentScriptFile, + onError: function (err) console.exception(err) + }); + let item = this.item; + worker.on("message", function workerOnMessage(msg) { + try { + privateItem(item)._emitOnObject(item, "message", msg); + } + catch (err) { + console.exception(err); + } + }); + return worker; + } +}; + + +// Mirrors state across all browser windows. Also responsible for detecting +// all content window loads and unloads. +let browserManager = { + topLevelItems: [], + browserWins: [], + + // inner window ID => content window + contentWins: {}, + + // Call this when a new item is created, top-level or not. + registerItem: function BM_registerItem(item) { + this.browserWins.forEach(function (w) w.registerItem(item)); + }, + + // Call this when an item is destroyed and won't be used again, top-level or + // not. + unregisterItem: function BM_unregisterItem(item) { + this.browserWins.forEach(function (w) w.unregisterItem(item)); + }, + + addTopLevelItem: function BM_addTopLevelItem(item) { + this.topLevelItems.push(item); + this.browserWins.forEach(function (w) w.addTopLevelItem(item)); + + // Create the item's worker registry and register all currently loaded + // content windows with it. + let privates = privateItem(item); + privates._isTopLevel = true; + for each (let win in this.contentWins) + privates._workerReg.registerContentWin(win); + }, + + removeTopLevelItem: function BM_removeTopLevelItem(item) { + let idx = this.topLevelItems.indexOf(item); + if (idx < 0) + throw new Error("Internal error: item not in top-level menu: " + item); + this.topLevelItems.splice(idx, 1); + this.browserWins.forEach(function (w) w.removeTopLevelItem(item)); + privateItem(item)._isTopLevel = false; + }, + + addItemToMenu: function BM_addItemToMenu(item, parentMenu) { + this.browserWins.forEach(function (w) w.addItemToMenu(item, parentMenu)); + }, + + removeItemFromMenu: function BM_removeItemFromMenu(item, parentMenu) { + this.browserWins.forEach(function (w) w.removeItemFromMenu(item, + parentMenu)); + }, + + setItemLabel: function BM_setItemLabel(item, label) { + this.browserWins.forEach(function (w) w.setItemLabel(item, label)); + }, + + setItemImage: function BM_setItemImage(item, imageURL) { + this.browserWins.forEach(function (w) w.setItemImage(item, imageURL)); + }, + + setItemData: function BM_setItemData(item, data) { + this.browserWins.forEach(function (w) w.setItemData(item, data)); + }, + + // Note that calling this method will cause onTrack to be called immediately + // for each currently open browser window. + init: function BM_init() { + require("./system/unload").ensure(this); + let windowTracker = WindowTracker(this); + + // Register content windows on content-document-global-created and + // unregister them on inner-window-destroyed. For rationale, see bug 667957 + // for the former and bug 642004 for the latter. + observerServ.add("content-document-global-created", + this._onDocGlobalCreated, this); + observerServ.add("inner-window-destroyed", + this._onInnerWinDestroyed, this); + }, + + _onDocGlobalCreated: function BM__onDocGlobalCreated(contentWin) { + let doc = contentWin.document; + if (doc.readyState == "loading") { + const self = this; + doc.addEventListener("readystatechange", function onReadyStateChange(e) { + if (e.target != doc || doc.readyState != "complete") + return; + doc.removeEventListener("readystatechange", onReadyStateChange, false); + self._registerContentWin(contentWin); + }, false); + } + else if (doc.readyState == "complete") + this._registerContentWin(contentWin); + }, + + _onInnerWinDestroyed: function BM__onInnerWinDestroyed(subj) { + this._unregisterContentWin( + subj.QueryInterface(Ci.nsISupportsPRUint64).data); + }, + + // Stores the given content window with the manager and registers it with each + // top-level item's worker registry. + _registerContentWin: function BM__registerContentWin(win) { + let innerID = getInnerId(win); + + // It's an error to call this method for the same window more than once, but + // we allow it in one case: when onTrack races _onDocGlobalCreated. (See + // the comment in onTrack.) Make sure the window is registered only once. + if (innerID in this.contentWins) + return; + + this.contentWins[innerID] = win; + this.topLevelItems.forEach(function (item) { + privateItem(item)._workerReg.registerContentWin(win); + }); + }, + + // Removes the given content window from the manager and unregisters it from + // each top-level item's worker registry. + _unregisterContentWin: function BM__unregisterContentWin(innerID) { + delete this.contentWins[innerID]; + this.topLevelItems.forEach(function (item) { + privateItem(item)._workerReg.unregisterContentWin(innerID); + }); + }, + + unload: function BM_unload() { + // The window tracker is unloaded at the same time this method is called, + // which causes onUntrack to be called for each open browser window, so + // there's no need to clean up browser windows here. + + while (this.topLevelItems.length) { + let item = this.topLevelItems[0]; + this.removeTopLevelItem(item); + this.unregisterItem(item); + } + delete this.contentWins; + }, + + // Registers a browser window with the manager. This is a WindowTracker + // callback. Note that this is called in two cases: for each newly opened + // chrome window, and for each chrome window that is open when the loader + // loads this module. + onTrack: function BM_onTrack(window) { + if (!isBrowser(window)) + return; + + let browserWin = new BrowserWindow(window); + this.browserWins.push(browserWin); + + // Register all loaded content windows in the browser window. Be sure to + // include frames and iframes. If onTrack is called as a result of a new + // browser window being opened, as opposed to the module being loaded, then + // this will race the content-document-global-created notification. That's + // OK, since _registerContentWin will not register the same content window + // more than once. + window.gBrowser.browsers.forEach(function (browser) { + let topContentWin = browser.contentWindow; + let allContentWins = Array.slice(topContentWin.frames); + allContentWins.push(topContentWin); + allContentWins.forEach(function (contentWin) { + if (contentWin.document.readyState == "complete") + this._registerContentWin(contentWin); + }, this); + }, this); + + // Add all top-level items and, recursively, their child items to the new + // browser window. + function addItemTree(item, parentMenu) { + browserWin.registerItem(item); + if (parentMenu) + browserWin.addItemToMenu(item, parentMenu); + else + browserWin.addTopLevelItem(item); + if (item instanceof Menu) + item.items.forEach(function (subitem) addItemTree(subitem, item)); + } + this.topLevelItems.forEach(function (item) addItemTree(item, null)); + }, + + // Unregisters a browser window from the manager. This is a WindowTracker + // callback. Note that this is called in two cases: for each newly closed + // chrome window, and for each chrome window that is open when this module is + // unloaded. + onUntrack: function BM_onUntrack(window) { + if (!isBrowser(window)) + return; + + // Remove the window from the window list. + let idx = 0; + for (; idx < this.browserWins.length; idx++) + if (this.browserWins[idx].window == window) + break; + if (idx == this.browserWins.length) + throw new Error("Internal error: browser window not found"); + let browserWin = this.browserWins.splice(idx, 1)[0]; + + // Remove all top-level items from the window. + this.topLevelItems.forEach(function (i) browserWin.removeTopLevelItem(i)); + browserWin.destroy(); + } +}; + + +// Responsible for creating and managing context menu item DOM elements for a +// browser window. Also responsible for providing a description of the window's +// current context and determining whether an item matches the current context. +// +// TODO: If other apps besides Firefox want to support the context menu in +// whatever way is appropriate for them, plugging in a substitute for or an +// adapter to this class should be the way to do it. Make it easy for them. +// See bug 560716. +function BrowserWindow(window) { + this.window = window; + this.doc = window.document; + + let popupDOMElt = this.doc.getElementById("contentAreaContextMenu"); + if (!popupDOMElt) + throw new Error("Internal error: Context menu popup not found."); + this.contextMenuPopup = new ContextMenuPopup(popupDOMElt, this); + + // item ID => { item, domElt, overflowDOMElt, popup, overflowPopup } + // item may or may not be top-level. domElt is the item's DOM element + // contained in the subtree rooted in the top-level context menu. + // overflowDOMElt is the item's DOM element contained in the subtree rooted in + // the overflow submenu. popup and overflowPopup are only defined if the item + // is a Menu; they're the Popup instances containing the Menu's child items, + // with the aforementioned distinction between top-level and overflow + // subtrees. + this.items = {}; +} + +BrowserWindow.prototype = { + + // Creates and stores DOM elements for the given item, top-level or not. + registerItem: function BW_registerItem(item) { + // this.items[id] is referenced by _makeMenu, so it needs to be defined + // before _makeDOMElt is called. + let props = { item: item }; + this.items[privateItem(item)._id] = props; + props.domElt = this._makeDOMElt(item, false); + props.overflowDOMElt = this._makeDOMElt(item, true); + }, + + // Removes the given item's DOM elements from the store. + unregisterItem: function BW_unregisterItem(item) { + delete this.items[privateItem(item)._id]; + }, + + addTopLevelItem: function BW_addTopLevelItem(item) { + this.contextMenuPopup.addItem(item); + }, + + removeTopLevelItem: function BW_removeTopLevelItem(item) { + this.contextMenuPopup.removeItem(item); + }, + + addItemToMenu: function BW_addItemToMenu(item, parentMenu) { + let { popup, overflowPopup } = this.items[privateItem(parentMenu)._id]; + popup.addItem(item); + overflowPopup.addItem(item); + }, + + removeItemFromMenu: function BW_removeItemFromMenu(item, parentMenu) { + let { popup, overflowPopup } = this.items[privateItem(parentMenu)._id]; + popup.removeItem(item); + overflowPopup.removeItem(item); + }, + + setItemLabel: function BW_setItemLabel(item, label) { + let privates = privateItem(item); + let { domElt, overflowDOMElt } = this.items[privates._id]; + this._setDOMEltLabel(domElt, label); + this._setDOMEltLabel(overflowDOMElt, label); + if (!item.parentMenu && privates._hasFinishedInit) + this.contextMenuPopup.itemLabelDidChange(item); + }, + + _setDOMEltLabel: function BW__setDOMEltLabel(domElt, label) { + domElt.setAttribute("label", label); + }, + + setItemImage: function BW_setItemImage(item, imageURL) { + let { domElt, overflowDOMElt } = this.items[privateItem(item)._id]; + let isMenu = item instanceof Menu; + this._setDOMEltImage(domElt, imageURL, isMenu); + this._setDOMEltImage(overflowDOMElt, imageURL, isMenu); + }, + + _setDOMEltImage: function BW__setDOMEltImage(domElt, imageURL, isMenu) { + if (!imageURL) { + domElt.removeAttribute("image"); + domElt.classList.remove("menu-iconic"); + domElt.classList.remove("menuitem-iconic"); + } + else { + domElt.setAttribute("image", imageURL); + domElt.classList.add(isMenu ? "menu-iconic" : "menuitem-iconic"); + } + }, + + setItemData: function BW_setItemData(item, data) { + let { domElt, overflowDOMElt } = this.items[privateItem(item)._id]; + this._setDOMEltData(domElt, data); + this._setDOMEltData(overflowDOMElt, data); + }, + + _setDOMEltData: function BW__setDOMEltData(domElt, data) { + domElt.setAttribute("value", data); + }, + + // The context specified for a top-level item may not match exactly the real + // context that triggers it. For example, if the user context-clicks a span + // inside an anchor, we want items that specify an anchor context to be + // triggered, but the real context will indicate that the span was clicked, + // not the anchor. Where the real context and an item's context conflict, + // clients should be given the item's context, and this method can be used to + // make such adjustments. Returns an adjusted popupNode. + adjustPopupNode: function BW_adjustPopupNode(popupNode, topLevelItem) { + for (let ctxt in topLevelItem.context) { + if (typeof(ctxt.adjustPopupNode) === "function") { + let ctxtNode = ctxt.adjustPopupNode(popupNode); + if (ctxtNode) { + popupNode = ctxtNode; + break; + } + } + } + return popupNode; + }, + + // Returns true if all of item's contexts are current in the window. + areAllContextsCurrent: function BW_areAllContextsCurrent(item, popupNode) { + let win = popupNode.ownerDocument.defaultView; + let worker = privateItem(item)._workerReg.find(win); + + // If the worker for the item-window pair doesn't exist (e.g., because the + // page hasn't loaded yet), we can't really make a good decision since the + // content script may have a context listener. So just don't show the item + // at all. + if (!worker) + return false; + + // If there are no contexts given at all, the page context applies. + let hasContentContext = worker.anyContextListeners(); + if (!hasContentContext && !item.context.length) + return new PageContext().isCurrent(popupNode); + + // Otherwise, determine if all given contexts are current. Evaluate the + // declarative contexts first and the worker's context listeners last. That + // way the listener might be able to avoid some work. + let curr = true; + for (let ctxt in item.context) { + curr = curr && ctxt.isCurrent(popupNode); + if (!curr) + return false; + } + return !hasContentContext || worker.isAnyContextCurrent(popupNode); + }, + + // Returns the node the user context-clicked to invoke the context menu. + // For Gecko 2.0 and later, triggerNode is this node; + // if it's falsey, document.popupNode is used. + capturePopupNode: function BW_capturePopupNode(triggerNode) { + var popupNode = triggerNode || this.doc.popupNode; + if (!popupNode) + console.warn("popupNode is null."); + return popupNode; + }, + + destroy: function BW_destroy() { + this.contextMenuPopup.destroy(); + delete this.window; + delete this.doc; + delete this.items; + }, + + // Emits a click event in the port of the content worker related to given top- + // level item and popupNode's content window. Listeners will be passed + // popupNode and clickedItemData. + fireClick: function BW_fireClick(topLevelItem, popupNode, clickedItemData) { + let win = popupNode.ownerDocument.defaultView; + let worker = privateItem(topLevelItem)._workerReg.find(win); + if (worker) + worker.fireClick(popupNode, clickedItemData); + }, + + _makeDOMElt: function BW__makeDOMElt(item, isInOverflowSubtree) { + let elt = item instanceof Item ? this._makeMenuitem(item) : + item instanceof Menu ? this._makeMenu(item, isInOverflowSubtree) : + item instanceof Separator ? this._makeSeparator() : + null; + if (!elt) + throw new Error("Internal error: can't make element, unknown item type"); + + elt.id = domEltIDFromItemID(privateItem(item)._id, isInOverflowSubtree); + elt.classList.add(ITEM_CLASS); + return elt; + }, + + // Returns a new xul:menu representing the menu. + _makeMenu: function BW__makeMenu(menu, isInOverflowSubtree) { + let menuElt = this.doc.createElement("menu"); + this._setDOMEltLabel(menuElt, menu.label); + if (menu.image) + this._setDOMEltImage(menuElt, menu.image, true); + let popupElt = this.doc.createElement("menupopup"); + menuElt.appendChild(popupElt); + + let popup = new Popup(popupElt, this, isInOverflowSubtree); + let props = this.items[privateItem(menu)._id]; + if (isInOverflowSubtree) + props.overflowPopup = popup; + else + props.popup = popup; + + return menuElt; + }, + + // Returns a new xul:menuitem representing the item. + _makeMenuitem: function BW__makeMenuitem(item) { + let elt = this.doc.createElement("menuitem"); + this._setDOMEltLabel(elt, item.label); + if (item.image) + this._setDOMEltImage(elt, item.image, false); + if (item.data) + this._setDOMEltData(elt, item.data); + return elt; + }, + + // Returns a new xul:menuseparator. + _makeSeparator: function BW__makeSeparator() { + return this.doc.createElement("menuseparator"); + } +}; + + +// Responsible for adding DOM elements to and removing them from poupDOMElt. +function Popup(popupDOMElt, browserWin, isInOverflowSubtree) { + this.popupDOMElt = popupDOMElt; + this.browserWin = browserWin; + this.isInOverflowSubtree = isInOverflowSubtree; +} + +Popup.prototype = { + + addItem: function Popup_addItem(item) { + let props = this.browserWin.items[privateItem(item)._id]; + let elt = this.isInOverflowSubtree ? props.overflowDOMElt : props.domElt; + this.popupDOMElt.appendChild(elt); + }, + + removeItem: function Popup_removeItem(item) { + let props = this.browserWin.items[privateItem(item)._id]; + let elt = this.isInOverflowSubtree ? props.overflowDOMElt : props.domElt; + this.popupDOMElt.removeChild(elt); + } +}; + + +// Represents a browser window's context menu popup. Responsible for hiding and +// showing items according to the browser window's current context and for +// handling item clicks. +function ContextMenuPopup(popupDOMElt, browserWin) { + this.popupDOMElt = popupDOMElt; + this.browserWin = browserWin; + this.doc = popupDOMElt.ownerDocument; + + // item ID => item + // Calling this variable "topLevelItems" is redundant, since Popup and + // ContextMenuPopup are only responsible for their child items, not all their + // descendant items. But calling it "items" might encourage one to believe + // otherwise, so topLevelItems it is. + this.topLevelItems = {}; + + popupDOMElt.addEventListener("popupshowing", this, false); + popupDOMElt.addEventListener("command", this, false); +} + +ContextMenuPopup.prototype = { + + addItem: function CMP_addItem(item) { + this._ensureStaticEltsExist(); + let itemID = privateItem(item)._id; + this.topLevelItems[itemID] = item; + let props = this.browserWin.items[itemID]; + props.domElt.classList.add(TOPLEVEL_ITEM_CLASS); + props.overflowDOMElt.classList.add(OVERFLOW_ITEM_CLASS); + this._insertItemInSortedOrder(item); + }, + + removeItem: function CMP_removeItem(item) { + let itemID = privateItem(item)._id; + delete this.topLevelItems[itemID]; + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + domElt.classList.remove(TOPLEVEL_ITEM_CLASS); + overflowDOMElt.classList.remove(OVERFLOW_ITEM_CLASS); + this.popupDOMElt.removeChild(domElt); + this._overflowPopup.removeChild(overflowDOMElt); + }, + + // Call this after the item's label changes. This re-inserts the item into + // the popup so that it remains in sorted order. + itemLabelDidChange: function CMP_itemLabelDidChange(item) { + let itemID = privateItem(item)._id; + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + this.popupDOMElt.removeChild(domElt); + this._overflowPopup.removeChild(overflowDOMElt); + this._insertItemInSortedOrder(item); + }, + + destroy: function CMP_destroy() { + // If there are no more items from any instance of the module, remove the + // separator and overflow submenu, if they exist. + let elts = this._topLevelElts; + if (!elts.length) { + let submenu = this._overflowMenu; + if (submenu) + this.popupDOMElt.removeChild(submenu); + + let sep = this._separator; + if (sep) + this.popupDOMElt.removeChild(sep); + } + + this.popupDOMElt.removeEventListener("popupshowing", this, false); + this.popupDOMElt.removeEventListener("command", this, false); + }, + + handleEvent: function CMP_handleEvent(event) { + try { + if (event.type === "command") + this._handleClick(event.target); + else if (event.type === "popupshowing" && + event.target === this.popupDOMElt) + this._handlePopupShowing(); + } + catch (err) { + console.exception(err); + } + }, + + // command events bubble to the context menu's top-level xul:menupopup and are + // caught here. + _handleClick: function CMP__handleClick(clickedDOMElt) { + if (!clickedDOMElt.classList.contains(ITEM_CLASS)) + return; + let itemID = itemIDFromDOMEltID(clickedDOMElt.id); + if (itemID < 0) + return; + let { item, domElt, overflowDOMElt } = this.browserWin.items[itemID]; + + // Bail if the DOM element was not created by this module instance. In + // real-world add-ons, the itemID < 0 check above is sufficient, but for the + // unit test the JID never changes, making this necessary. + if (clickedDOMElt != domElt && clickedDOMElt != overflowDOMElt) + return; + + let topLevelItem = privateItem(item)._topLevelItem; + let popupNode = this.browserWin.adjustPopupNode(this._getPopupNode(), + topLevelItem); + this.browserWin.fireClick(topLevelItem, popupNode, item.data); + }, + + _getPopupNode: function CMP__getPopupNode() { + // popupDOMElt.triggerNode was added in Gecko 2.0 by bug 383930. The || is + // to avoid a Spidermonkey strict warning on earlier versions. + let triggerNode = this.popupDOMElt.triggerNode || undefined; + return this.browserWin.capturePopupNode(triggerNode); + }, + + // popupshowing is used to show top-level items that match the browser + // window's current context and hide items that don't. Each module instance + // is responsible for showing and hiding the items it owns. + _handlePopupShowing: function CMP__handlePopupShowing() { + + // If there are items queued up to finish initializing, let them go first. + // Otherwise the overflow submenu and menu separator may end up in an + // inappropriate state when those items are later added to the menu. + if (numItemsWithUnfinishedInit) { + const self = this; + timer.setTimeout(function popupShowingTryAgain() { + self._handlePopupShowing(); + }, 0); + return; + } + + let popupNode = this._getPopupNode(); + // Show and hide items. Set a "jetpackContextCurrent" property on the + // DOM elements to signal which of our items match the current context. + for (let itemID in this.topLevelItems) { + let item = this.topLevelItems[itemID] + let areContextsCurr = + this.browserWin.areAllContextsCurrent(item, popupNode); + + // Change the item's label if the return value was a string. + if (typeof(areContextsCurr) === "string") { + item.label = areContextsCurr; + areContextsCurr = true; + } + + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + domElt.jetpackContextCurrent = areContextsCurr; + domElt.hidden = !areContextsCurr; + overflowDOMElt.jetpackContextCurrent = areContextsCurr; + overflowDOMElt.hidden = !areContextsCurr; + } + + // Get the total number of items that match the current context. It's a + // little tricky: There may be other instances of this module loaded, + // each hiding and showing their items. So we can't base this number on + // only our items, or on the hidden state of items. That's why we set + // the jetpackContextCurrent property above. The last instance to run + // will leave the menupopup in the correct state. + let elts = this._topLevelElts; + let numShown = Array.reduce(elts, function (total, elt) { + return total + (elt.jetpackContextCurrent ? 1 : 0); + }, 0); + + // If too many items are shown, show the submenu and hide the top-level + // items. Otherwise, hide the submenu and show the top-level items. + let overflow = numShown > this._overflowThreshold; + if (overflow) + Array.forEach(elts, function (e) e.hidden = true); + + let submenu = this._overflowMenu; + if (submenu) + submenu.hidden = !overflow; + + // If no items are shown, hide the menu separator. + let sep = this._separator; + if (sep) + sep.hidden = numShown === 0; + }, + + // Adds the menu separator and overflow submenu if they don't exist. + _ensureStaticEltsExist: function CMP__ensureStaticEltsExist() { + let sep = this._separator; + if (!sep) { + sep = this._makeSeparator(); + this.popupDOMElt.appendChild(sep); + } + + let submenu = this._overflowMenu; + if (!submenu) { + submenu = this._makeOverflowMenu(); + submenu.hidden = true; + this.popupDOMElt.insertBefore(submenu, sep.nextSibling); + } + }, + + // Inserts the given item's DOM element into the popup in sorted order. + _insertItemInSortedOrder: function CMP__insertItemInSortedOrder(item) { + let props = this.browserWin.items[privateItem(item)._id]; + this.popupDOMElt.insertBefore( + props.domElt, insertionPoint(item.label, this._topLevelElts)); + this._overflowPopup.insertBefore( + props.overflowDOMElt, insertionPoint(item.label, this._overflowElts)); + }, + + // Creates and returns the xul:menu that's shown when too many items are added + // to the popup. + _makeOverflowMenu: function CMP__makeOverflowMenu() { + let submenu = this.doc.createElement("menu"); + submenu.id = OVERFLOW_MENU_ID; + submenu.setAttribute("label", OVERFLOW_MENU_LABEL); + let popup = this.doc.createElement("menupopup"); + popup.id = OVERFLOW_POPUP_ID; + submenu.appendChild(popup); + return submenu; + }, + + // Creates and returns the xul:menuseparator that separates the standard + // context menu items from our items. + _makeSeparator: function CMP__makeSeparator() { + let elt = this.doc.createElement("menuseparator"); + elt.id = SEPARATOR_ID; + return elt; + }, + + // Returns the item elements contained in the overflow menu, a NodeList. + get _overflowElts() { + return this._overflowPopup.getElementsByClassName(OVERFLOW_ITEM_CLASS); + }, + + // Returns the overflow xul:menu. + get _overflowMenu() { + return this.doc.getElementById(OVERFLOW_MENU_ID); + }, + + // Returns the overflow xul:menupopup. + get _overflowPopup() { + return this.doc.getElementById(OVERFLOW_POPUP_ID); + }, + + // Returns the OVERFLOW_THRESH_PREF pref value if it exists or + // OVERFLOW_THRESH_DEFAULT if it doesn't. + get _overflowThreshold() { + let prefs = require("./preferences/service"); + return prefs.get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); + }, + + // Returns the xul:menuseparator. + get _separator() { + return this.doc.getElementById(SEPARATOR_ID); + }, + + // Returns the item elements contained in the top-level menu, a NodeList. + get _topLevelElts() { + return this.popupDOMElt.getElementsByClassName(TOPLEVEL_ITEM_CLASS); + } +}; + + +// Init the browserManager only after setting prototypes and such above, because +// it will cause browserManager.onTrack to be called immediately if there are +// open windows. +browserManager.init(); diff --git a/tools/addon-sdk-1.12/lib/sdk/core/heritage.js b/tools/addon-sdk-1.12/lib/sdk/core/heritage.js new file mode 100644 index 0000000..1c62e6c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/core/heritage.js @@ -0,0 +1,146 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 = Object.getOwnPropertyNames; +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; + }, {}); +} + +/** + * 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 freeze(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); + // 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)); + + // Note: we use reflection `apply` in the constructor instead of method + // call since later may be overridden. + function constructor() { + return apply(prototype.constructor, create(prototype), arguments); + } + constructor.prototype = prototype; + return freeze(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/tools/addon-sdk-1.12/lib/sdk/core/namespace.js b/tools/addon-sdk-1.12/lib/sdk/core/namespace.js new file mode 100644 index 0000000..3ceb73b --- /dev/null +++ b/tools/addon-sdk-1.12/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/tools/addon-sdk-1.12/lib/sdk/core/promise.js b/tools/addon-sdk-1.12/lib/sdk/core/promise.js new file mode 100644 index 0000000..2506c80 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/core/promise.js @@ -0,0 +1,230 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/*jshint asi: true undef: true es5: true node: true browser: true devel: true + forin: true latedef: false */ +/*global define: true, Cu: true, __URI__: true */ +;(function(id, factory) { // Module boilerplate :( + if (typeof(define) === 'function') { // RequireJS + define(factory); + } else if (typeof(require) === 'function') { // CommonJS + factory.call(this, require, exports, module); + } else if (~String(this).indexOf('BackstagePass')) { // JSM + factory(function require(uri) { + var imports = {}; + this['Components'].utils.import(uri, imports); + return imports; + }, this, { uri: __URI__, id: id }); + this.EXPORTED_SYMBOLS = Object.keys(this); + } else { // Browser or alike + var globals = this + factory(function require(id) { + return globals[id]; + }, (globals[id] = {}), { uri: document.location.href + '#' + id, id: id }); + } +}).call(this, 'loader', function(require, exports, module) { + +'use strict'; + +module.metadata = { + "stability": "unstable" +}; + +function resolution(value) { + /** + Returns non-standard compliant (`then` does not returns a promise) promise + that resolves to a given `value`. Used just internally only. + **/ + return { then: function then(resolve) { resolve(value) } } +} + +function rejection(reason) { + /** + Returns non-standard compliant promise (`then` does not returns a promise) + that rejects with a given `reason`. This is used internally only. + **/ + return { then: function then(resolve, reject) { reject(reason) } } +} + +function attempt(f) { + /** + Returns wrapper function that delegates to `f`. If `f` throws then captures + error and returns promise that rejects with a thrown error. Otherwise returns + return value. (Internal utility) + **/ + return function effort(options) { + try { return f(options) } + catch(error) { return rejection(error) } + } +} + +function isPromise(value) { + /** + Returns true if given `value` is promise. Value is assumed to be promise if + it implements `then` method. + **/ + return value && typeof(value.then) === 'function' +} + +function defer(prototype) { + /** + Returns object containing following properties: + - `promise` Eventual value representation implementing CommonJS [Promises/A] + (http://wiki.commonjs.org/wiki/Promises/A) API. + - `resolve` Single shot function that resolves returned `promise` with a given + `value` argument. + - `reject` Single shot function that rejects returned `promise` with a given + `reason` argument. + + Given `prototype` argument is used as a prototype of the returned `promise` + allowing one to implement additional API. If prototype is not passed then + it falls back to `Object.prototype`. + + ## Examples + + // Simple usage. + var deferred = defer() + deferred.promise.then(console.log, console.error) + deferred.resolve(value) + + // Advanced usage + var prototype = { + get: function get(name) { + return this.then(function(value) { + return value[name]; + }) + } + } + + var foo = defer(prototype) + deferred.promise.get('name').then(console.log) + deferred.resolve({ name: 'Foo' }) + //=> 'Foo' + */ + var pending = [], result + prototype = (prototype || prototype === null) ? prototype : Object.prototype + + // Create an object implementing promise API. + var promise = Object.create(prototype, { + then: { value: function then(resolve, reject) { + // create a new deferred using a same `prototype`. + var deferred = defer(prototype) + // If `resolve / reject` callbacks are not provided. + resolve = resolve ? attempt(resolve) : resolution + reject = reject ? attempt(reject) : rejection + + // Create a listeners for a enclosed promise resolution / rejection that + // delegate to an actual callbacks and resolve / reject returned promise. + function resolved(value) { deferred.resolve(resolve(value)) } + function rejected(reason) { deferred.resolve(reject(reason)) } + + // If promise is pending register listeners. Otherwise forward them to + // resulting resolution. + if (pending) pending.push([ resolved, rejected ]) + else result.then(resolved, rejected) + + return deferred.promise + }} + }) + + var deferred = { + promise: promise, + resolve: function resolve(value) { + /** + Resolves associated `promise` to a given `value`, unless it's already + resolved or rejected. + **/ + if (pending) { + // store resolution `value` as a promise (`value` itself may be a + // promise), so that all subsequent listeners can be forwarded to it, + // which either resolves immediately or forwards if `value` is + // a promise. + result = isPromise(value) ? value : resolution(value) + // forward all pending observers. + while (pending.length) result.then.apply(result, pending.shift()) + // mark promise as resolved. + pending = null + } + }, + reject: function reject(reason) { + /** + Rejects associated `promise` with a given `reason`, unless it's already + resolved / rejected. + **/ + deferred.resolve(rejection(reason)) + } + } + + return deferred +} +exports.defer = defer + +function resolve(value, prototype) { + /** + Returns a promise resolved to a given `value`. Optionally second `prototype` + arguments my be provided to be used as a prototype for a returned promise. + **/ + var deferred = defer(prototype) + deferred.resolve(value) + return deferred.promise +} +exports.resolve = resolve + +function reject(reason, prototype) { + /** + Returns a promise that is rejected with a given `reason`. Optionally second + `prototype` arguments my be provided to be used as a prototype for a returned + promise. + **/ + var deferred = defer(prototype) + deferred.reject(reason) + return deferred.promise +} +exports.reject = reject + +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(function(value) { + return 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() { + // create array of [ f, this, args... ] + return concat.apply([ f, this ], arguments). + // 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 + +}) diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/api-utils.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/api-utils.js new file mode 100644 index 0000000..521ad6e --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/api-utils.js @@ -0,0 +1,158 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 memory = require("./memory"); +// The possible return values of getTypeOf. +const VALID_TYPES = [ + "array", + "boolean", + "function", + "null", + "number", + "object", + "string", + "undefined", +]; + +/** + * Returns a function C that creates instances of privateCtor. C may be called + * with or without the new keyword. The prototype of each instance returned + * from C is C.prototype, and C.prototype is an object whose prototype is + * privateCtor.prototype. Instances returned from C will therefore be instances + * of both C and privateCtor. Additionally, the constructor of each instance + * returned from C is C. + * + * @param privateCtor + * A constructor. + * @return A function that makes new instances of privateCtor. + */ +exports.publicConstructor = function publicConstructor(privateCtor) { + function PublicCtor() { + let obj = { constructor: PublicCtor, __proto__: PublicCtor.prototype }; + memory.track(obj, privateCtor.name); + privateCtor.apply(obj, arguments); + return obj; + } + PublicCtor.prototype = { __proto__: privateCtor.prototype }; + return PublicCtor; +}; + +/** + * Returns a validated options dictionary given some requirements. If any of + * the requirements are not met, an exception is thrown. + * + * @param options + * An object, the options dictionary to validate. It's not modified. + * If it's null or otherwise falsey, an empty object is assumed. + * @param requirements + * An object whose keys are the expected keys in options. Any key in + * 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 and null are identified by the special type names + * "array" and "null"; "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 = {}; + let mapThrew = false; + + for (let key in requirements) { + 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) { + mapThrew = true; + } + } + if (req.is) { + // Sanity check the caller's type names. + req.is.forEach(function (typ) { + if (VALID_TYPES.indexOf(typ) < 0) { + let msg = 'Internal error: invalid requirement type "' + typ + '".'; + throw new Error(msg); + } + }); + if (req.is.indexOf(getTypeOf(optsVal)) < 0) + throw requirementError(key, req); + } + if (req.ok && !req.ok(optsVal)) + throw requirementError(key, req); + + if (keyInOpts || (req.map && !mapThrew)) + 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 and null are identified by "array" and +// "null", not "object". +let getTypeOf = exports.getTypeOf = function getTypeOf(val) { + let typ = typeof(val); + if (typ === "object") { + if (!val) + return "null"; + if (Array.isArray(val)) + return "array"; + } + return typ; +} + +// Returns a new Error with a nice message. +function requirementError(key, requirement) { + 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."; + } + return new Error(msg); +} diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/app-strings.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/app-strings.js new file mode 100644 index 0000000..e09f79e --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/app-strings.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "deprecated" +}; + +const {Cc,Ci} = require("chrome"); +const apiUtils = require("./api-utils"); + +/** + * A bundle of strings. + * + * @param url {String} + * the URL of the string bundle + */ +exports.StringBundle = apiUtils.publicConstructor(function StringBundle(url) { + + let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(url); + + this.__defineGetter__("url", function () url); + + /** + * Get a string from the bundle. + * + * @param name {String} + * the name of the string to get + * @param args {array} [optional] + * an array of arguments that replace occurrences of %S in the string + * + * @returns {String} the value of the string + */ + this.get = function strings_get(name, args) { + try { + if (args) + return stringBundle.formatStringFromName(name, args, args.length); + else + return stringBundle.GetStringFromName(name); + } + catch(ex) { + // f.e. "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) + // [nsIStringBundle.GetStringFromName]" + throw new Error("String '" + name + "' could not be retrieved from the " + + "bundle due to an unknown error (it doesn't exist?)."); + } + }, + + /** + * Iterate the strings in the bundle. + * + */ + apiUtils.addIterator( + this, + function keysValsGen() { + let enumerator = stringBundle.getSimpleEnumeration(); + while (enumerator.hasMoreElements()) { + let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + yield [elem.key, elem.value]; + } + } + ); +}); diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/cortex.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/cortex.js new file mode 100644 index 0000000..8eef91c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/cortex.js @@ -0,0 +1,113 @@ +/* vim:set ts=2 sw=2 sts=2 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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" +}; + +// `var` is being used in the module in order to make it reusable in +// environments in which `let` and `const` is not yet supported. + +// Returns `object`'s property value, where `name` is a name of the property. +function get(object, name) { + return object[name]; +} + +// Assigns `value` to the `object`'s property, where `name` is the name of the +// property. +function set(object, name, value) { + return object[name] = value; +} + +/** + * Given an `object` containing a property with the given `name`, create + * a property descriptor that can be used to define alias/proxy properties + * on other objects. A change in the value of an alias will propagate + * to the aliased property and vice versa. + */ +function createAliasProperty(object, name) { + // Getting own property descriptor of an `object` for the given `name` as + // we are going to create proxy analog. + var property = Object.getOwnPropertyDescriptor(object, name); + var descriptor = { + configurable: property.configurable, + enumerable: property.enumerable, + alias: true + }; + + // If the original property has a getter and/or setter, bind a + // corresponding getter/setter in the alias descriptor to the original + // object, so the `this` object in the getter/setter is the original object + // rather than the alias. + if ("get" in property && property.get) + descriptor.get = property.get.bind(object); + if ("set" in property && property.set) + descriptor.set = property.set.bind(object); + + // If original property was a value property. + if ("value" in property) { + // If original property is a method using it's `object` bounded copy. + if (typeof property.value === "function") { + descriptor.value = property.value.bind(object); + // Also preserving writability of the original property. + descriptor.writable = property.writable; + } + + // If the original property was just a data property, we create proxy + // accessors using our custom get/set functions to propagate changes to the + // original `object` and vice versa. + else { + descriptor.get = get.bind(null, object, name); + descriptor.set = set.bind(null, object, name); + } + } + return descriptor; +} + +// Defines property on `object` object with a name `alias` if given if not +// defaults to `name` that represents an alias of `source[name]`. If aliased +// property was an assessor or a method `this` pseudo-variable will be `source` +// when invoked. If aliased property was a data property changes on any of the +// aliases will propagate to the `source[name]` and also other way round. +function defineAlias(source, target, name, alias) { + return Object.defineProperty(target, alias || name, + createAliasProperty(source, name)); +} + +/** + * Function takes any `object` and returns a proxy for its own public + * properties. By default properties are considered to be public if they don't + * start with `"_"`, but default behavior can be overridden if needed, by + * passing array of public property `names` as a second argument. By default + * returned object will be direct decedent of the given `object`'s prototype, + * but this can be overridden by passing third optional argument, that will be + * used as `prototype` instead. + * @param {Object} object + * Object to create cortex for. + * @param {String[]} [names] + * Optional array of public property names. + * @param {Object} [prototype] + * Optional argument that will be used as `prototype` of the returned object, + * if not provided `Object.getPrototypeOf(object)` is used instead. + */ +exports.Cortex = function Cortex(object, names, prototype) { + // Creating a cortex object from the given `prototype`, if one was not + // provided then `prototype` of a given `object` is used. This allows + // consumer to define expected behavior `instanceof`. In common case + // `prototype` argument can be omitted to preserve same behavior of + // `instanceof` as on original `object`. + var cortex = Object.create(prototype || Object.getPrototypeOf(object)); + // Creating alias properties on the `cortex` object for all the own + // properties of the original `object` that are contained in `names` array. + // If `names` array is not provided then all the properties that don't + // start with `"_"` are aliased. + Object.getOwnPropertyNames(object).forEach(function (name) { + if ((!names && "_" !== name.charAt(0)) || (names && ~names.indexOf(name))) + defineAlias(object, cortex, name); + }); + return cortex; +} diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/errors.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/errors.js new file mode 100644 index 0000000..b640d05 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/errors.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "deprecated" +}; + +function logToConsole(e) { + console.exception(e); +} + +var catchAndLog = exports.catchAndLog = function(callback, + defaultResponse, + logException) { + if (!logException) + logException = logToConsole; + + return function() { + try { + return callback.apply(this, arguments); + } catch (e) { + logException(e); + return defaultResponse; + } + }; +}; + +exports.catchAndLogProps = function catchAndLogProps(object, + props, + defaultResponse, + logException) { + if (typeof(props) == "string") + props = [props]; + props.forEach( + function(property) { + object[property] = catchAndLog(object[property], + defaultResponse, + logException); + }); +}; + +/** + * Catch and return an exception while calling the callback. If the callback + * doesn't throw, return the return value of the callback in a way that makes it + * possible to distinguish between a return value and an exception. + * + * This function is useful when you need to pass the result of a call across + * a process boundary (across which exceptions don't propagate). It probably + * doesn't need to be factored out into this module, since it is only used by + * a single caller, but putting it here works around bug 625560. + */ +exports.catchAndReturn = function(callback) { + return function() { + try { + return { returnValue: callback.apply(this, arguments) }; + } + catch (exception) { + return { exception: exception }; + } + }; +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/events.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/events.js new file mode 100644 index 0000000..0c28a1a --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/events.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"; + +module.metadata = { + "stability": "deprecated" +}; + +const ERROR_TYPE = 'error', + UNCAUGHT_ERROR = 'An error event was dispatched for which there was' + + ' no listener.', + BAD_LISTENER = 'The event listener must be a function.'; +/** + * This object is used to create an `EventEmitter` that, useful for composing + * objects that emit events. It implements an interface like `EventTarget` from + * DOM Level 2, which is implemented by Node objects in implementations that + * support the DOM Event Model. + * @see http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget + * @see http://nodejs.org/api.html#EventEmitter + * @see http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/events/EventDispatcher.html + */ +const eventEmitter = { + /** + * 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: function on(type, listener) { + if ('function' !== typeof listener) + throw new Error(BAD_LISTENER); + let listeners = this._listeners(type); + if (0 > listeners.indexOf(listener)) + listeners.push(listener); + // Use of `_public` is required by the legacy traits code that will go away + // once bug-637633 is fixed. + return this._public || this; + }, + + /** + * 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: function once(type, listener) { + this.on(type, function selfRemovableListener() { + this.removeListener(type, selfRemovableListener); + listener.apply(this, arguments); + }); + }, + + /** + * Unregister `listener` for the specified event type. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ + removeListener: function removeListener(type, listener) { + if ('function' !== typeof listener) + throw new Error(BAD_LISTENER); + let listeners = this._listeners(type), + index = listeners.indexOf(listener); + if (0 <= index) + listeners.splice(index, 1); + // Use of `_public` is required by the legacy traits code, that will go away + // once bug-637633 is fixed. + return this._public || this; + }, + + /** + * Hash of listeners on this EventEmitter. + */ + _events: null, + + /** + * Returns an array of listeners for the specified event `type`. This array + * can be manipulated, e.g. to remove listeners. + * @param {String} type + * The type of event. + */ + _listeners: function listeners(type) { + let events = this._events || (this._events = {}); + return events[type] || (events[type] = []); + }, + + /** + * Execute each of the listeners in order with the supplied arguments. + * Returns `true` if listener for this event was called, `false` if there are + * no listeners for this event `type`. + * + * 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 propagate to a + * caller of this method. + * + * **It's recommended to have a default 'error' listener in all the complete + * composition that in worst case may dump errors to the console.** + * + * @param {String} type + * The type of event. + * @params {Object|Number|String|Boolean} + * Arguments that will be passed to listeners. + * @returns {Boolean} + */ + _emit: function _emit(type, event) { + let args = Array.slice(arguments); + // Use of `_public` is required by the legacy traits code that will go away + // once bug-637633 is fixed. + args.unshift(this._public || this); + return this._emitOnObject.apply(this, args); + }, + + /** + * A version of _emit that lets you specify the object on which listeners are + * called. This is a hack that is sometimes necessary when such an object + * (exports, for example) cannot be an EventEmitter for some reason, but other + * object(s) managing events for the object are EventEmitters. Once bug + * 577782 is fixed, this method shouldn't be necessary. + * + * @param {object} targetObj + * The object on which listeners will be called. + * @param {string} type + * The event name. + * @param {value} event + * The first argument to pass to listeners. + * @param {value} ... + * More arguments to pass to listeners. + * @returns {boolean} + */ + _emitOnObject: function _emitOnObject(targetObj, type, event /* , ... */) { + let listeners = this._listeners(type).slice(0); + // If there is no 'error' event listener then throw. + if (type === ERROR_TYPE && !listeners.length) + console.exception(event); + if (!listeners.length) + return false; + let params = Array.slice(arguments, 2); + for each (let listener in listeners) { + try { + listener.apply(targetObj, params); + } catch(e) { + // Bug 726967: Ignore exceptions being throws while notifying the error + // in order to avoid infinite loops. + if (type !== ERROR_TYPE) + this._emit(ERROR_TYPE, e); + else + console.exception("Exception in error event listener " + e); + } + } + return true; + }, + + /** + * Removes all the event listeners for the specified event `type`. + * @param {String} type + * The type of event. + */ + _removeAllListeners: function _removeAllListeners(type) { + if (typeof type == "undefined") { + this._events = null; + return this; + } + + this._listeners(type).splice(0); + return this; + } +}; +exports.EventEmitter = require("./traits").Trait.compose(eventEmitter); +exports.EventEmitterTrait = require('./light-traits').Trait(eventEmitter); diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/events/assembler.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/events/assembler.js new file mode 100644 index 0000000..fcde1e3 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/events/assembler.js @@ -0,0 +1,53 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 { Trait } = require("../light-traits"); +const { removeListener, on } = require("../../dom/events"); + +/** + * Trait may be used for building objects / composing traits that wish to handle + * multiple dom events from multiple event targets in one place. 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 = Trait({ + /** + * 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: Trait.required, + /** + * Array of supported event names. + * @type {String[]} + */ + supportedEventsTypes: Trait.required, + /** + * 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/tools/addon-sdk-1.12/lib/sdk/deprecated/light-traits.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/light-traits.js new file mode 100644 index 0000000..b18dccb --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/light-traits.js @@ -0,0 +1,600 @@ +/* vim:ts=2:sts=2:sw=2: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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" +}; + +// `var` is being used in the module in order to make it reusable in +// environments in which `let` is not yet supported. + +// Shortcut to `Object.prototype.hasOwnProperty.call`. +// owns(object, name) would be the same as +// Object.prototype.hasOwnProperty.call(object, name); +var owns = Function.prototype.call.bind(Object.prototype.hasOwnProperty); + +/** + * Whether or not given property descriptors are equivalent. They are + * equivalent either if both are marked as 'conflict' or 'required' property + * or if all the properties of descriptors are equal. + * @param {Object} actual + * @param {Object} expected + */ +function equivalentDescriptors(actual, expected) { + return (actual.conflict && expected.conflict) || + (actual.required && expected.required) || + equalDescriptors(actual, expected); +} +/** + * Whether or not given property descriptors define equal properties. + */ +function equalDescriptors(actual, expected) { + return actual.get === expected.get && + actual.set === expected.set && + actual.value === expected.value && + !!actual.enumerable === !!expected.enumerable && + !!actual.configurable === !!expected.configurable && + !!actual.writable === !!expected.writable; +} + +// Utilities that throwing exceptions for a properties that are marked +// as "required" or "conflict" properties. +function throwConflictPropertyError(name) { + throw new Error("Remaining conflicting property: `" + name + "`"); +} +function throwRequiredPropertyError(name) { + throw new Error("Missing required property: `" + name + "`"); +} + +/** + * Generates custom **required** property descriptor. Descriptor contains + * non-standard property `required` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function RequiredPropertyDescriptor(name) { + // Creating function by binding first argument to a property `name` on the + // `throwConflictPropertyError` function. Created function is used as a + // getter & setter of the created property descriptor. This way we ensure + // that we throw exception late (on property access) if object with + // `required` property was instantiated using built-in `Object.create`. + var accessor = throwRequiredPropertyError.bind(null, name); + return { get: accessor, set: accessor, required: true }; +} + +/** + * Generates custom **conflicting** property descriptor. Descriptor contains + * non-standard property `conflict` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function ConflictPropertyDescriptor(name) { + // For details see `RequiredPropertyDescriptor` since idea is same. + var accessor = throwConflictPropertyError.bind(null, name); + return { get: accessor, set: accessor, conflict: true }; +} + +/** + * Tests if property is marked as `required` property. + */ +function isRequiredProperty(object, name) { + return !!object[name].required; +} + +/** + * Tests if property is marked as `conflict` property. + */ +function isConflictProperty(object, name) { + return !!object[name].conflict; +} + +/** + * Function tests whether or not method of the `source` object with a given + * `name` is inherited from `Object.prototype`. + */ +function isBuiltInMethod(name, source) { + var target = Object.prototype[name]; + + // If methods are equal then we know it's `true`. + return target == source || + // If `source` object comes form a different sandbox `==` will evaluate + // to `false`, in that case we check if functions names and sources match. + (String(target) === String(source) && target.name === source.name); +} + +/** + * Function overrides `toString` and `constructor` methods of a given `target` + * object with a same-named methods of a given `source` if methods of `target` + * object are inherited / copied from `Object.prototype`. + * @see create + */ +function overrideBuiltInMethods(target, source) { + if (isBuiltInMethod("toString", target.toString)) { + Object.defineProperty(target, "toString", { + value: source.toString, + configurable: true, + enumerable: false + }); + } + + if (isBuiltInMethod("constructor", target.constructor)) { + Object.defineProperty(target, "constructor", { + value: source.constructor, + configurable: true, + enumerable: false + }); + } +} + +/** + * Composes new trait with the same own properties as the original trait, + * except that all property names appearing in the first argument are replaced + * by "required" property descriptors. + * @param {String[]} keys + * Array of strings property names. + * @param {Object} trait + * A trait some properties of which should be excluded. + * @returns {Object} + * @example + * var newTrait = exclude(["name", ...], trait) + */ +function exclude(names, trait) { + var map = {}; + + Object.keys(trait).forEach(function(name) { + + // If property is not excluded (the array of names does not contain it), + // or it is a "required" property, copy it to the property descriptor `map` + // that will be used for creation of resulting trait. + if (!~names.indexOf(name) || isRequiredProperty(trait, name)) + map[name] = { value: trait[name], enumerable: true }; + + // For all the `names` in the exclude name array we create required + // property descriptors and copy them to the `map`. + else + map[name] = { value: RequiredPropertyDescriptor(name), enumerable: true }; + }); + + return Object.create(Trait.prototype, map); +} + +/** + * Composes new instance of `Trait` with a properties of a given `trait`, + * except that all properties whose name is an own property of `renames` will + * be renamed to `renames[name]` and a `"required"` property for name will be + * added instead. + * + * For each renamed property, a required property is generated. If + * the `renames` map two properties to the same name, a conflict is generated. + * If the `renames` map a property to an existing unrenamed property, a + * conflict is generated. + * + * @param {Object} renames + * An object whose own properties serve as a mapping from old names to new + * names. + * @param {Object} trait + * A new trait with renamed properties. + * @returns {Object} + * @example + * + * // Return trait with `bar` property equal to `trait.foo` and with + * // `foo` and `baz` "required" properties. + * var renamedTrait = rename({ foo: "bar", baz: null }), trait); + * + * // t1 and t2 are equivalent traits + * var t1 = rename({a: "b"}, t); + * var t2 = compose(exclude(["a"], t), { a: { required: true }, b: t[a] }); + */ +function rename(renames, trait) { + var map = {}; + + // Loop over all the properties of the given `trait` and copy them to a + // property descriptor `map` that will be used for the creation of the + // resulting trait. Also, rename properties in the `map` as specified by + // `renames`. + Object.keys(trait).forEach(function(name) { + var alias; + + // If the property is in the `renames` map, and it isn't a "required" + // property (which should never need to be aliased because "required" + // properties never conflict), then we must try to rename it. + if (owns(renames, name) && !isRequiredProperty(trait, name)) { + alias = renames[name]; + + // If the `map` already has the `alias`, and it isn't a "required" + // property, that means the `alias` conflicts with an existing name for a + // provided trait (that can happen if >=2 properties are aliased to the + // same name). In this case we mark it as a conflicting property. + // Otherwise, everything is fine, and we copy property with an `alias` + // name. + if (owns(map, alias) && !map[alias].value.required) { + map[alias] = { + value: ConflictPropertyDescriptor(alias), + enumerable: true + }; + } + else { + map[alias] = { + value: trait[name], + enumerable: true + }; + } + + // Regardless of whether or not the rename was successful, we check to + // see if the original `name` exists in the map (such a property + // could exist if previous another property was aliased to this `name`). + // If it isn't, we mark it as "required", to make sure the caller + // provides another value for the old name, which methods of the trait + // might continue to reference. + if (!owns(map, name)) { + map[name] = { + value: RequiredPropertyDescriptor(name), + enumerable: true + }; + } + } + + // Otherwise, either the property isn't in the `renames` map (thus the + // caller is not trying to rename it) or it is a "required" property. + // Either way, we don't have to alias the property, we just have to copy it + // to the map. + else { + // The property isn't in the map yet, so we copy it over. + if (!owns(map, name)) { + map[name] = { value: trait[name], enumerable: true }; + } + + // The property is already in the map (that means another property was + // aliased with this `name`, which creates a conflict if the property is + // not marked as "required"), so we have to mark it as a "conflict" + // property. + else if (!isRequiredProperty(trait, name)) { + map[name] = { + value: ConflictPropertyDescriptor(name), + enumerable: true + }; + } + } + }); + return Object.create(Trait.prototype, map); +} + +/** + * Composes new resolved trait, with all the same properties as the original + * `trait`, except that all properties whose name is an own property of + * `resolutions` will be renamed to `resolutions[name]`. + * + * If `resolutions[name]` is `null`, the value is mapped to a property + * descriptor that is marked as a "required" property. + */ +function resolve(resolutions, trait) { + var renames = {}; + var exclusions = []; + + // Go through each mapping in `resolutions` object and distribute it either + // to `renames` or `exclusions`. + Object.keys(resolutions).forEach(function(name) { + + // If `resolutions[name]` is a truthy value then it's a mapping old -> new + // so we copy it to `renames` map. + if (resolutions[name]) + renames[name] = resolutions[name]; + + // Otherwise it's not a mapping but an exclusion instead in which case we + // add it to the `exclusions` array. + else + exclusions.push(name); + }); + + // First `exclude` **then** `rename` and order is important since + // `exclude` and `rename` are not associative. + return rename(renames, exclude(exclusions, trait)); +} + +/** + * Create a Trait (a custom property descriptor map) that represents the given + * `object`'s own properties. Property descriptor map is a "custom", because it + * inherits from `Trait.prototype` and it's property descriptors may contain + * two attributes that is not part of the ES5 specification: + * + * - "required" (this property must be provided by another trait + * before an instance of this trait can be created) + * - "conflict" (when the trait is composed with another trait, + * a unique value for this property is provided by two or more traits) + * + * Data properties bound to the `Trait.required` singleton exported by + * this module will be marked as "required" properties. + * + * @param {Object} object + * Map of properties to compose trait from. + * @returns {Trait} + * Trait / Property descriptor map containing all the own properties of the + * given argument. + */ +function trait(object) { + var map; + var trait = object; + + if (!(object instanceof Trait)) { + // If the passed `object` is not already an instance of `Trait`, we create + // a property descriptor `map` containing descriptors for the own properties + // of the given `object`. `map` is then used to create a `Trait` instance + // after all properties are mapped. Note that we can't create a trait and + // then just copy properties into it since that will fail for inherited + // read-only properties. + map = {}; + + // Each own property of the given `object` is mapped to a data property + // whose value is a property descriptor. + Object.keys(object).forEach(function (name) { + + // If property of an `object` is equal to a `Trait.required`, it means + // that it was marked as "required" property, in which case we map it + // to "required" property. + if (Trait.required == + Object.getOwnPropertyDescriptor(object, name).value) { + map[name] = { + value: RequiredPropertyDescriptor(name), + enumerable: true + }; + } + // Otherwise property is mapped to it's property descriptor. + else { + map[name] = { + value: Object.getOwnPropertyDescriptor(object, name), + enumerable: true + }; + } + }); + + trait = Object.create(Trait.prototype, map); + } + return trait; +} + +/** + * Compose a property descriptor map that inherits from `Trait.prototype` and + * contains property descriptors for all the own properties of the passed + * traits. + * + * If two or more traits have own properties with the same name, the returned + * trait will contain a "conflict" property for that name. Composition is a + * commutative and associative operation, and the order of its arguments is + * irrelevant. + */ +function compose(trait1, trait2/*, ...*/) { + // Create a new property descriptor `map` to which all the own properties + // of the passed traits are copied. This map will be used to create a `Trait` + // instance that will be the result of this composition. + var map = {}; + + // Properties of each passed trait are copied to the composition. + Array.prototype.forEach.call(arguments, function(trait) { + // Copying each property of the given trait. + Object.keys(trait).forEach(function(name) { + + // If `map` already owns a property with the `name` and it is not + // marked "required". + if (owns(map, name) && !map[name].value.required) { + + // If the source trait's property with the `name` is marked as + // "required", we do nothing, as the requirement was already resolved + // by a property in the `map` (because it already contains a + // non-required property with that `name`). But if properties are just + // different, we have a name clash and we substitute it with a property + // that is marked "conflict". + if (!isRequiredProperty(trait, name) && + !equivalentDescriptors(map[name].value, trait[name]) + ) { + map[name] = { + value: ConflictPropertyDescriptor(name), + enumerable: true + }; + } + } + + // Otherwise, the `map` does not have an own property with the `name`, or + // it is marked "required". Either way, the trait's property is copied to + // the map (if the property of the `map` is marked "required", it is going + // to be resolved by the property that is being copied). + else { + map[name] = { value: trait[name], enumerable: true }; + } + }); + }); + + return Object.create(Trait.prototype, map); +} + +/** + * `defineProperties` is like `Object.defineProperties`, except that it + * ensures that: + * - An exception is thrown if any property in a given `properties` map + * is marked as "required" property and same named property is not + * found in a given `prototype`. + * - An exception is thrown if any property in a given `properties` map + * is marked as "conflict" property. + * @param {Object} object + * Object to define properties on. + * @param {Object} properties + * Properties descriptor map. + * @returns {Object} + * `object` that was passed as a first argument. + */ +function defineProperties(object, properties) { + + // Create a map into which we will copy each verified property from the given + // `properties` description map. We use it to verify that none of the + // provided properties is marked as a "conflict" property and that all + // "required" properties are resolved by a property of an `object`, so we + // can throw an exception before mutating object if that isn't the case. + var verifiedProperties = {}; + + // Coping each property from a given `properties` descriptor map to a + // verified map of property descriptors. + Object.keys(properties).forEach(function(name) { + + // If property is marked as "required" property and we don't have a same + // named property in a given `object` we throw an exception. If `object` + // has same named property just skip this property since required property + // is was inherited and there for requirement was satisfied. + if (isRequiredProperty(properties, name)) { + if (!(name in object)) + throwRequiredPropertyError(name); + } + + // If property is marked as "conflict" property we throw an exception. + else if (isConflictProperty(properties, name)) { + throwConflictPropertyError(name); + } + + // If property is not marked neither as "required" nor "conflict" property + // we copy it to verified properties map. + else { + verifiedProperties[name] = properties[name]; + } + }); + + // If no exceptions were thrown yet, we know that our verified property + // descriptor map has no properties marked as "conflict" or "required", + // so we just delegate to the built-in `Object.defineProperties`. + return Object.defineProperties(object, verifiedProperties); +} + +/** + * `create` is like `Object.create`, except that it ensures that: + * - An exception is thrown if any property in a given `properties` map + * is marked as "required" property and same named property is not + * found in a given `prototype`. + * - An exception is thrown if any property in a given `properties` map + * is marked as "conflict" property. + * @param {Object} prototype + * prototype of the composed object + * @param {Object} properties + * Properties descriptor map. + * @returns {Object} + * An object that inherits form a given `prototype` and implements all the + * properties defined by a given `properties` descriptor map. + */ +function create(prototype, properties) { + + // Creating an instance of the given `prototype`. + var object = Object.create(prototype); + + // Overriding `toString`, `constructor` methods if they are just inherited + // from `Object.prototype` with a same named methods of the `Trait.prototype` + // that will have more relevant behavior. + overrideBuiltInMethods(object, Trait.prototype); + + // Trying to define given `properties` on the `object`. We use our custom + // `defineProperties` function instead of build-in `Object.defineProperties` + // that behaves exactly the same, except that it will throw if any + // property in the given `properties` descriptor is marked as "required" or + // "conflict" property. + return defineProperties(object, properties); +} + +/** + * Composes new trait. If two or more traits have own properties with the + * same name, the new trait will contain a "conflict" property for that name. + * "compose" is a commutative and associative operation, and the order of its + * arguments is not significant. + * + * **Note:** Use `Trait.compose` instead of calling this function with more + * than one argument. The multiple-argument functionality is strictly for + * backward compatibility. + * + * @params {Object} trait + * Takes traits as an arguments + * @returns {Object} + * New trait containing the combined own properties of all the traits. + * @example + * var newTrait = compose(trait_1, trait_2, ..., trait_N) + */ +function Trait(trait1, trait2) { + + // If the function was called with one argument, the argument should be + // an object whose properties are mapped to property descriptors on a new + // instance of Trait, so we delegate to the trait function. + // If the function was called with more than one argument, those arguments + // should be instances of Trait or plain property descriptor maps + // whose properties should be mixed into a new instance of Trait, + // so we delegate to the compose function. + + return trait2 === undefined ? trait(trait1) : compose.apply(null, arguments); +} + +Object.freeze(Object.defineProperties(Trait.prototype, { + toString: { + value: function toString() { + return "[object " + this.constructor.name + "]"; + } + }, + + /** + * `create` is like `Object.create`, except that it ensures that: + * - An exception is thrown if this trait defines a property that is + * marked as required property and same named property is not + * found in a given `prototype`. + * - An exception is thrown if this trait contains property that is + * marked as "conflict" property. + * @param {Object} + * prototype of the compared object + * @returns {Object} + * An object with all of the properties described by the trait. + */ + create: { + value: function createTrait(prototype) { + return create(undefined === prototype ? Object.prototype : prototype, + this); + }, + enumerable: true + }, + + /** + * Composes a new resolved trait, with all the same properties as the original + * trait, except that all properties whose name is an own property of + * `resolutions` will be renamed to the value of `resolutions[name]`. If + * `resolutions[name]` is `null`, the property is marked as "required". + * @param {Object} resolutions + * An object whose own properties serve as a mapping from old names to new + * names, or to `null` if the property should be excluded. + * @returns {Object} + * New trait with the same own properties as the original trait but renamed. + */ + resolve: { + value: function resolveTrait(resolutions) { + return resolve(resolutions, this); + }, + enumerable: true + } +})); + +/** + * @see compose + */ +Trait.compose = Object.freeze(compose); +Object.freeze(compose.prototype); + +/** + * Constant singleton, representing placeholder for required properties. + * @type {Object} + */ +Trait.required = Object.freeze(Object.create(Object.prototype, { + toString: { + value: Object.freeze(function toString() { + return "<Trait.required>"; + }) + } +})); +Object.freeze(Trait.required.toString.prototype); + +exports.Trait = Object.freeze(Trait); diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/list.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/list.js new file mode 100644 index 0000000..5de69e7 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/list.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"; + +module.metadata = { + "stability": "experimental" +}; + +const { Trait } = require('../deprecated/traits'); + +/** + * @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/list + */ +const Iterable = Trait.compose({ + /** + * Hash map of key-values to iterate over. + * Note: That this property can be a getter if you need dynamic behavior. + * @type {Object} + */ + _keyValueMap: Trait.required, + /** + * Custom iterator providing `Iterable`s enumeration behavior. + * @param {Boolean} onKeys + */ + __iterator__: function __iterator__(onKeys, onKeyValue) { + let map = this._keyValueMap; + for (let key in map) + yield onKeyValue ? [key, map[key]] : onKeys ? key : map[key]; + } +}); +exports.Iterable = Iterable; + +/** + * An ordered collection (also known as a sequence) disallowing duplicate + * elements. List is composed out of `Iterable` there for it provides custom + * enumeration behavior that is similar to array (enumerates only on the + * elements of the list). List is a base trait and is meant to be a part of + * composition, since all of it's API is private except length property. + */ +const List = Trait.resolve({ toString: null }).compose({ + _keyValueMap: null, + /** + * List constructor can take any number of element to populate itself. + * @params {Object|String|Number} element + * @example + * List(1,2,3).length == 3 // true + */ + constructor: function List() { + this._keyValueMap = []; + for (let i = 0, ii = arguments.length; i < ii; i++) + this._add(arguments[i]); + }, + /** + * Number of elements in this list. + * @type {Number} + */ + get length() this._keyValueMap.length, + /** + * Returns a string representing this list. + * @returns {String} + */ + toString: function toString() 'List(' + this._keyValueMap + ')', + /** + * Returns `true` if this list contains the specified `element`. + * @param {Object|Number|String} element + * @returns {Boolean} + */ + _has: function _has(element) 0 <= this._keyValueMap.indexOf(element), + /** + * Appends the specified `element` to the end of this list, if it doesn't + * contains it. Ignores the call if `element` is already contained. + * @param {Object|Number|String} element + */ + _add: function _add(element) { + let list = this._keyValueMap, + index = list.indexOf(element); + if (0 > index) + list.push(this._public[list.length] = element); + }, + /** + * Removes specified `element` from this list, if it contains it. + * Ignores the call if `element` is not contained. + * @param {Object|Number|String} element + */ + _remove: function _remove(element) { + let list = this._keyValueMap, + index = list.indexOf(element); + if (0 <= index) { + delete this._public[list.length - 1]; + list.splice(index, 1); + for (let length = list.length; index < length; index++) + this._public[index] = list[index]; + } + }, + /** + * Removes all of the elements from this list. + */ + _clear: function _clear() { + for (let i = 0, ii = this._keyValueMap.length; i < ii; i ++) + delete this._public[i]; + this._keyValueMap.splice(0); + }, + /** + * 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 = this._keyValueMap.slice(0), + i = -1; + for each(let element in array) + yield onKeyValue ? [++i, element] : onKeys ? ++i : element; + } +}); +exports.List = List; diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/memory.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/memory.js new file mode 100644 index 0000000..70bcffc --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/memory.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"; + +module.metadata = { + "stability": "deprecated" +}; + +const {Cc,Ci,Cu,components} = require("chrome"); +var trackedObjects = {}; + +var Compacter = { + INTERVAL: 5000, + notify: function(timer) { + var newTrackedObjects = {}; + for (let name in trackedObjects) { + var oldBin = trackedObjects[name]; + var newBin = []; + var strongRefs = []; + for (var i = 0; i < oldBin.length; i++) { + var strongRef = oldBin[i].weakref.get(); + if (strongRef && strongRefs.indexOf(strongRef) == -1) { + strongRefs.push(strongRef); + newBin.push(oldBin[i]); + } + } + if (newBin.length) + newTrackedObjects[name] = newBin; + } + trackedObjects = newTrackedObjects; + } +}; + +var timer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + +timer.initWithCallback(Compacter, + Compacter.INTERVAL, + Ci.nsITimer.TYPE_REPEATING_SLACK); + +var track = exports.track = function track(object, bin, stackFrameNumber) { + var frame = components.stack.caller; + var weakref = Cu.getWeakReference(object); + if (!bin && 'constructor' in object) + bin = object.constructor.name; + if (bin == "Object") + bin = frame.name; + if (!bin) + bin = "generic"; + if (!(bin in trackedObjects)) + trackedObjects[bin] = []; + + if (stackFrameNumber > 0) + for (var i = 0; i < stackFrameNumber; i++) + frame = frame.caller; + + trackedObjects[bin].push({weakref: weakref, + created: new Date(), + filename: frame.filename, + lineNo: frame.lineNumber, + bin: bin}); +}; + +var getBins = exports.getBins = function getBins() { + var names = []; + for (let name in trackedObjects) + names.push(name); + return names; +}; + +var getObjects = exports.getObjects = function getObjects(bin) { + function getLiveObjectsInBin(bin, array) { + for (var i = 0; i < bin.length; i++) { + var object = bin[i].weakref.get(); + if (object) + array.push(bin[i]); + } + } + + var results = []; + if (bin) { + if (bin in trackedObjects) + getLiveObjectsInBin(trackedObjects[bin], results); + } else + for (let name in trackedObjects) + getLiveObjectsInBin(trackedObjects[name], results); + return results; +}; + +var gc = exports.gc = function gc() { + // Components.utils.forceGC() doesn't currently perform + // cycle collection, which means that e.g. DOM elements + // won't be collected by it. Fortunately, there are + // other ways... + + var window = Cc["@mozilla.org/appshell/appShellService;1"] + .getService(Ci.nsIAppShellService) + .hiddenDOMWindow; + var test_utils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + test_utils.garbageCollect(); + Compacter.notify(); + + // Not sure why, but sometimes it appears that we don't get + // them all with just one CC, so let's do it again. + test_utils.garbageCollect(); +}; + +require("../system/unload").when( + function() { + trackedObjects = {}; + if (timer) { + timer.cancel(); + timer = null; + } + }); diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/observer-service.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/observer-service.js new file mode 100644 index 0000000..6a25109 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/observer-service.js @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "deprecated" +}; + +const { Cc, Ci } = require("chrome"); +const { when: unload } = require("../system/unload"); +const { ns } = require("../core/namespace"); +const { on, off, emit, once } = require("../system/events"); +const { id } = require("../self"); + +const subscribers = ns(); +const cache = []; + +/** + * Topics specifically available to Jetpack-generated extensions. + * + * Using these predefined consts instead of the platform strings is good: + * - allows us to scope topics specifically for Jetpacks + * - addons aren't dependent on strings nor behavior of core platform topics + * - the core platform topics are not clearly named + * + */ +exports.topics = { + /** + * A topic indicating that the application is in a state usable + * by add-ons. + */ + APPLICATION_READY: id + "_APPLICATION_READY" +}; + +function Listener(callback, target) { + return function listener({ subject, data }) { + callback.call(target || callback, subject, data); + } +} + +/** + * Register the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic to observe + * + * @param callback {Object} + * the callback; an Object that implements nsIObserver or a Function + * that gets called when the notification occurs + * + * @param target {Object} [optional] + * the object to use as |this| when calling a Function callback + * + * @returns the observer + */ +function add(topic, callback, target) { + let listeners = subscribers(callback); + if (!(topic in listeners)) { + let listener = Listener(callback, target); + listeners[topic] = listener; + + // Cache callback unless it's already cached. + if (!~cache.indexOf(callback)) + cache.push(callback); + + on(topic, listener); + } +}; +exports.add = add; + +/** + * Unregister the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic being observed + * + * @param callback {Object} + * the callback doing the observing + * + * @param target {Object} [optional] + * the object being used as |this| when calling a Function callback + */ +function remove(topic, callback, target) { + let listeners = subscribers(callback); + if (topic in listeners) { + let listener = listeners[topic]; + delete listeners[topic]; + + // If no more observers are registered and callback is still in cache + // then remove it. + let index = cache.indexOf(callback); + if (~index && !Object.keys(listeners).length) + cache.splice(index, 1) + + off(topic, listener); + } +}; +exports.remove = remove; + +/** + * Notify observers about something. + * + * @param topic {String} + * the topic to notify observers about + * + * @param subject {Object} [optional] + * some information about the topic; can be any JS object or primitive + * + * @param data {String} [optional] [deprecated] + * some more information about the topic; deprecated as the subject + * is sufficient to pass all needed information to the JS observers + * that this module targets; if you have multiple values to pass to + * the observer, wrap them in an object and pass them via the subject + * parameter (i.e.: { foo: 1, bar: "some string", baz: myObject }) + */ +function notify(topic, subject, data) { + emit(topic, { + subject: subject === undefined ? null : subject, + data: data === undefined ? null : data + }); +} +exports.notify = notify; + +unload(function() { + // Make a copy of cache first, since cache will be changing as we + // iterate through it. + cache.slice().forEach(function(callback) { + Object.keys(subscribers(callback)).forEach(function(topic) { + remove(topic, callback); + }); + }); +}) diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/tab-browser.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/tab-browser.js new file mode 100644 index 0000000..068471c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/tab-browser.js @@ -0,0 +1,731 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.metadata = { + 'stability': 'deprecated' +}; + +const {Cc,Ci,Cu} = require('chrome'); +var NetUtil = {}; +Cu.import('resource://gre/modules/NetUtil.jsm', NetUtil); +NetUtil = NetUtil.NetUtil; +const errors = require('./errors'); +const windowUtils = require('./window-utils'); +const apiUtils = require('./api-utils'); +const collection = require('../util/collection'); +const { getMostRecentBrowserWindow } = require('../window/utils'); +const { getSelectedTab } = require('../tabs/utils'); + +// TODO: The hard-coding of app-specific info here isn't very nice; +// ideally such app-specific info should be more decoupled, and the +// module should be extensible, allowing for support of new apps at +// runtime, perhaps by inspecting supported packages (e.g. via +// dynamically-named modules or package-defined extension points). + +if (!require("../system/xul-app").is("Firefox")) { + throw new Error([ + "The tab-browser module currently supports only Firefox. In the future ", + "it will support other applications. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +function onBrowserLoad(callback, event) { + if (event.target && event.target.defaultView == this) { + this.removeEventListener("load", onBrowserLoad, true); + try { + require("../timers").setTimeout(function () { + callback(event); + }, 10); + } catch (e) { console.exception(e); } + } +} + +// Utility function to open a new browser window. +function openBrowserWindow(callback, url) { + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + let urlString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + urlString.data = url; + let window = ww.openWindow(null, "chrome://browser/content/browser.xul", + "_blank", "chrome,all,dialog=no", urlString); + if (callback) + window.addEventListener("load", onBrowserLoad.bind(window, callback), true); + + return window; +} + +// Open a URL in a new tab +exports.addTab = function addTab(url, options) { + if (!options) + options = {}; + options.url = url; + + options = apiUtils.validateOptions(options, { + // TODO: take URL object instead of string (bug 564524) + url: { + is: ["string"], + ok: function (v) !!v, + msg: "The url parameter must have be a non-empty string." + }, + inNewWindow: { + is: ["undefined", "null", "boolean"] + }, + inBackground: { + is: ["undefined", "null", "boolean"] + }, + onLoad: { + is: ["undefined", "null", "function"] + }, + isPinned: { + is: ["undefined", "boolean"] + } + }); + + var win = getMostRecentBrowserWindow(); + if (!win || options.inNewWindow) { + openBrowserWindow(function(e) { + if(options.isPinned) { + //get the active tab in the recently created window + let mainWindow = e.target.defaultView; + mainWindow.gBrowser.pinTab(getSelectedTab(mainWindow)); + } + require("./errors").catchAndLog(function(e) options.onLoad(e))(e); + }, options.url); + } + else { + let tab = win.gBrowser.addTab(options.url); + if (!options.inBackground) + win.gBrowser.selectedTab = tab; + if (options.onLoad) { + let tabBrowser = win.gBrowser.getBrowserForTab(tab); + tabBrowser.addEventListener("load", function onLoad(e) { + if (e.target.defaultView.content.location == "about:blank") + return; + // remove event handler from addTab - don't want notified + // for subsequent loads in same tab. + tabBrowser.removeEventListener("load", onLoad, true); + require("./errors").catchAndLog(function(e) options.onLoad(e))(e); + }, true); + } + } +} + +// Iterate over a window's tabbrowsers +function tabBrowserIterator(window) { + var browsers = window.document.querySelectorAll("tabbrowser"); + for (var i = 0; i < browsers.length; i++) + yield browsers[i]; +} + +// Iterate over a tabbrowser's tabs +function tabIterator(tabbrowser) { + var tabs = tabbrowser.tabContainer; + for (var i = 0; i < tabs.children.length; i++) { + yield tabs.children[i]; + } +} + +// Tracker for all tabbrowsers across all windows, +// or a single tabbrowser if the window is given. +function Tracker(delegate, window) { + this._delegate = delegate; + this._browsers = []; + this._window = window; + this._windowTracker = windowUtils.WindowTracker(this); + + require("../system/unload").ensure(this); +} +Tracker.prototype = { + __iterator__: function __iterator__() { + for (var i = 0; i < this._browsers.length; i++) + yield this._browsers[i]; + }, + get: function get(index) { + return this._browsers[index]; + }, + onTrack: function onTrack(window) { + if (this._window && window != this._window) + return; + + for (let browser in tabBrowserIterator(window)) + this._browsers.push(browser); + if (this._delegate) + for (let browser in tabBrowserIterator(window)) + this._delegate.onTrack(browser); + }, + onUntrack: function onUntrack(window) { + if (this._window && window != this._window) + return; + + for (let browser in tabBrowserIterator(window)) { + let index = this._browsers.indexOf(browser); + if (index != -1) + this._browsers.splice(index, 1); + else + console.error("internal error: browser tab not found"); + } + if (this._delegate) + for (let browser in tabBrowserIterator(window)) + this._delegate.onUntrack(browser); + }, + get length() { + return this._browsers.length; + }, + unload: function unload() { + this._windowTracker.unload(); + } +}; +exports.Tracker = apiUtils.publicConstructor(Tracker); + +// Tracker for all tabs across all windows, +// or a single window if it's given. +function TabTracker(delegate, window) { + this._delegate = delegate; + this._tabs = []; + this._tracker = new Tracker(this, window); + require("../system/unload").ensure(this); +} +TabTracker.prototype = { + _TAB_EVENTS: ["TabOpen", "TabClose"], + _safeTrackTab: function safeTrackTab(tab) { + this._tabs.push(tab); + try { + this._delegate.onTrack(tab); + } catch (e) { + console.exception(e); + } + }, + _safeUntrackTab: function safeUntrackTab(tab) { + var index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + this._tabs.splice(index, 1); + try { + this._delegate.onUntrack(tab); + } catch (e) { + console.exception(e); + } + }, + handleEvent: function handleEvent(event) { + switch (event.type) { + case "TabOpen": + this._safeTrackTab(event.target); + break; + case "TabClose": + this._safeUntrackTab(event.target); + break; + default: + throw new Error("internal error: unknown event type: " + + event.type); + } + }, + onTrack: function onTrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeTrackTab(tab); + var self = this; + this._TAB_EVENTS.forEach( + function(eventName) { + tabbrowser.tabContainer.addEventListener(eventName, self, true); + }); + }, + onUntrack: function onUntrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeUntrackTab(tab); + var self = this; + this._TAB_EVENTS.forEach( + function(eventName) { + tabbrowser.tabContainer.removeEventListener(eventName, self, true); + }); + }, + unload: function unload() { + this._tracker.unload(); + } +}; +exports.TabTracker = apiUtils.publicConstructor(TabTracker); + +exports.whenContentLoaded = function whenContentLoaded(callback) { + var cb = require("./errors").catchAndLog(function eventHandler(event) { + if (event.target && event.target.defaultView) + callback(event.target.defaultView); + }); + + var tracker = new Tracker({ + onTrack: function(tabBrowser) { + tabBrowser.addEventListener("DOMContentLoaded", cb, false); + }, + onUntrack: function(tabBrowser) { + tabBrowser.removeEventListener("DOMContentLoaded", cb, false); + } + }); + + return tracker; +}; + +Object.defineProperty(exports, 'activeTab', { + get: function() { + return getSelectedTab(getMostRecentBrowserWindow()); + } +}); + +/******************* TabModule *********************/ + +// Supported tab events +const events = [ + "onActivate", + "onDeactivate", + "onOpen", + "onClose", + "onReady", + "onLoad", + "onPaint" +]; +exports.tabEvents = events; + +/** + * TabModule + * + * Constructor for a module that implements the tabs API + */ +let TabModule = exports.TabModule = function TabModule(window) { + let self = this; + /** + * Tab + * + * Safe object representing a tab. + */ + let tabConstructor = apiUtils.publicConstructor(function(element) { + if (!element) + throw new Error("no tab element."); + let win = element.ownerDocument.defaultView; + if (!win) + throw new Error("element has no window."); + if (window && win != window) + throw new Error("module's window and element's window don't match."); + let browser = win.gBrowser.getBrowserForTab(element); + + this.__defineGetter__("title", function() browser.contentDocument.title); + this.__defineGetter__("location", function() browser.contentDocument.location); + this.__defineSetter__("location", function(val) browser.contentDocument.location = val); + this.__defineGetter__("contentWindow", function() browser.contentWindow); + this.__defineGetter__("contentDocument", function() browser.contentDocument); + this.__defineGetter__("favicon", function() { + let pageURI = NetUtil.newURI(browser.contentDocument.location); + let fs = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + let faviconURL; + try { + let faviconURI = fs.getFaviconForPage(pageURI); + faviconURL = fs.getFaviconDataAsDataURL(faviconURI); + } catch(ex) { + let data = getChromeURLContents("chrome://mozapps/skin/places/defaultFavicon.png"); + let encoded = browser.contentWindow.btoa(data); + faviconURL = "data:image/png;base64," + encoded; + } + return faviconURL; + }); + this.__defineGetter__("style", function() null); // TODO + this.__defineGetter__("index", function() win.gBrowser.getBrowserIndexForDocument(browser.contentDocument)); + this.__defineGetter__("thumbnail", function() getThumbnailCanvasForTab(element, browser.contentWindow)); + + this.close = function() win.gBrowser.removeTab(element); + this.move = function(index) { + win.gBrowser.moveTabTo(element, index); + }; + + this.__defineGetter__("isPinned", function() element.pinned); + this.pin = function() win.gBrowser.pinTab(element); + this.unpin = function() win.gBrowser.unpinTab(element); + + // Set up the event handlers + let tab = this; + events.filter(function(e) e != "onOpen").forEach(function(e) { + // create a collection for each event + collection.addCollectionProperty(tab, e); + // make tabs setter for each event, for adding via property assignment + tab.__defineSetter__(e, function(val) tab[e].add(val)); + }); + + // listen for events, filtered on this tab + eventsTabDelegate.addTabDelegate(this); + }); + + /** + * tabs.activeTab + */ + this.__defineGetter__("activeTab", function() { + try { + return window ? tabConstructor(getSelectedTab(window)) + : tabConstructor(exports.activeTab); + } + catch (e) { } + return null; + }); + this.__defineSetter__("activeTab", function(tab) { + let [tabElement, win] = getElementAndWindowForTab(tab, window); + if (tabElement) { + // set as active tab + win.gBrowser.selectedTab = tabElement; + // focus the window + win.focus(); + } + }); + + this.open = function TM_open(options) { + open(options, tabConstructor, window); + } + + // Set up the event handlers + events.forEach(function(eventHandler) { + // create a collection for each event + collection.addCollectionProperty(self, eventHandler); + // make tabs setter for each event, for adding via property assignment + self.__defineSetter__(eventHandler, function(val) self[eventHandler].add(val)); + }); + + // Tracker that listens for tab events, and proxies + // them to registered event listeners. + let eventsTabDelegate = { + selectedTab: null, + tabs: [], + addTabDelegate: function TETT_addTabDelegate(tabObj) { + this.tabs.push(tabObj); + }, + pushTabEvent: function TETT_pushTabEvent(event, tab) { + for (let callback in self[event]) { + require("./errors").catchAndLog(function(tab) { + callback(new tabConstructor(tab)); + })(tab); + } + + if (event != "onOpen") { + this.tabs.forEach(function(tabObj) { + if (tabObj[event].length) { + let [tabEl,] = getElementAndWindowForTab(tabObj, window); + if (tabEl == tab) { + for (let callback in tabObj[event]) + require("./errors").catchAndLog(function() callback())(); + } + } + // if being closed, remove the tab object from the cache + // of tabs to notify about events. + if (event == "onClose") + this.tabs.splice(this.tabs.indexOf(tabObj), 1); + }, this); + } + }, + unload: function() { + this.selectedTab = null; + this.tabs.splice(0); + } + }; + require("../system/unload").ensure(eventsTabDelegate); + + let eventsTabTracker = new ModuleTabTracker({ + onTrack: function TETT_onTrack(tab) { + eventsTabDelegate.pushTabEvent("onOpen", tab); + }, + onUntrack: function TETT_onUntrack(tab) { + eventsTabDelegate.pushTabEvent("onClose", tab); + }, + onSelect: function TETT_onSelect(tab) { + if (eventsTabDelegate.selectedTab) + eventsTabDelegate.pushTabEvent("onDeactivate", tab); + + eventsTabDelegate.selectedTab = new tabConstructor(tab); + + eventsTabDelegate.pushTabEvent("onActivate", tab); + }, + onReady: function TETT_onReady(tab) { + eventsTabDelegate.pushTabEvent("onReady", tab); + }, + onLoad: function TETT_onLoad(tab) { + eventsTabDelegate.pushTabEvent("onLoad", tab); + }, + onPaint: function TETT_onPaint(tab) { + eventsTabDelegate.pushTabEvent("onPaint", tab); + } + }, window); + require("../system/unload").ensure(eventsTabTracker); + + // Iterator for all tabs + this.__iterator__ = function tabsIterator() { + for (let i = 0; i < eventsTabTracker._tabs.length; i++) + yield tabConstructor(eventsTabTracker._tabs[i]); + } + + this.__defineGetter__("length", function() eventsTabTracker._tabs.length); + + // Cleanup when unloaded + this.unload = function TM_unload() { + // Unregister tabs event listeners + events.forEach(function(e) self[e] = []); + } + require("../system/unload").ensure(this); + +} // End of TabModule constructor + +/** + * tabs.open - open a URL in a new tab + */ +function open(options, tabConstructor, window) { + if (typeof options === "string") + options = { url: options }; + + options = apiUtils.validateOptions(options, { + url: { + is: ["string"] + }, + inNewWindow: { + is: ["undefined", "boolean"] + }, + inBackground: { + is: ["undefined", "boolean"] + }, + isPinned: { + is: ["undefined", "boolean"] + }, + onOpen: { + is: ["undefined", "function"] + } + }); + + if (window) + options.inNewWindow = false; + + let win = window || require("./window-utils").activeBrowserWindow; + + if (!win || options.inNewWindow) + openURLInNewWindow(options, tabConstructor); + else + openURLInNewTab(options, win, tabConstructor); +} + +function openURLInNewWindow(options, tabConstructor) { + let addTabOptions = { + inNewWindow: true + }; + if (options.onOpen) { + addTabOptions.onLoad = function(e) { + let win = e.target.defaultView; + let tabEl = win.gBrowser.tabContainer.childNodes[0]; + let tabBrowser = win.gBrowser.getBrowserForTab(tabEl); + tabBrowser.addEventListener("load", function onLoad(e) { + tabBrowser.removeEventListener("load", onLoad, true); + let tab = tabConstructor(tabEl); + require("./errors").catchAndLog(function(e) options.onOpen(e))(tab); + }, true); + }; + } + if (options.isPinned) { + addTabOptions.isPinned = true; + } + exports.addTab(options.url.toString(), addTabOptions); +} + +function openURLInNewTab(options, window, tabConstructor) { + window.focus(); + let tabEl = window.gBrowser.addTab(options.url.toString()); + if (!options.inBackground) + window.gBrowser.selectedTab = tabEl; + if (options.isPinned) + window.gBrowser.pinTab(tabEl); + if (options.onOpen) { + let tabBrowser = window.gBrowser.getBrowserForTab(tabEl); + tabBrowser.addEventListener("load", function onLoad(e) { + // remove event handler from addTab - don't want to be notified + // for subsequent loads in same tab. + tabBrowser.removeEventListener("load", onLoad, true); + let tab = tabConstructor(tabEl); + require("../timers").setTimeout(function() { + require("./errors").catchAndLog(function(tab) options.onOpen(tab))(tab); + }, 10); + }, true); + } +} + +function getElementAndWindowForTab(tabObj, window) { + // iterate over open windows, or use single window if provided + let windowIterator = window ? function() { yield window; } + : require("./window-utils").windowIterator; + for (let win in windowIterator()) { + if (win.gBrowser) { + // find the tab element at tab.index + let index = win.gBrowser.getBrowserIndexForDocument(tabObj.contentDocument); + if (index > -1) + return [win.gBrowser.tabContainer.getItemAtIndex(index), win]; + } + } + return [null, null]; +} + +// Tracker for all tabs across all windows +// This is tab-browser.TabTracker, but with +// support for additional events added. +function ModuleTabTracker(delegate, window) { + this._delegate = delegate; + this._tabs = []; + this._tracker = new Tracker(this, window); + require("../system/unload").ensure(this); +} +ModuleTabTracker.prototype = { + _TAB_EVENTS: ["TabOpen", "TabClose", "TabSelect", "DOMContentLoaded", + "load", "MozAfterPaint"], + _safeTrackTab: function safeTrackTab(tab) { + tab.addEventListener("load", this, false); + tab.linkedBrowser.addEventListener("MozAfterPaint", this, false); + this._tabs.push(tab); + try { + this._delegate.onTrack(tab); + } catch (e) { + console.exception(e); + } + }, + _safeUntrackTab: function safeUntrackTab(tab) { + tab.removeEventListener("load", this, false); + tab.linkedBrowser.removeEventListener("MozAfterPaint", this, false); + var index = this._tabs.indexOf(tab); + if (index == -1) + throw new Error("internal error: tab not found"); + this._tabs.splice(index, 1); + try { + this._delegate.onUntrack(tab); + } catch (e) { + console.exception(e); + } + }, + _safeSelectTab: function safeSelectTab(tab) { + var index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onSelect) + this._delegate.onSelect(tab); + } catch (e) { + console.exception(e); + } + }, + _safeDOMContentLoaded: function safeDOMContentLoaded(event) { + let tabBrowser = event.currentTarget; + let tabBrowserIndex = tabBrowser.getBrowserIndexForDocument(event.target); + // TODO: I'm seeing this when loading data url images + if (tabBrowserIndex == -1) + return; + let tab = tabBrowser.tabContainer.getItemAtIndex(tabBrowserIndex); + let index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onReady) + this._delegate.onReady(tab); + } catch (e) { + console.exception(e); + } + }, + _safeLoad: function safeLoad(event) { + let tab = event.target; + let index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onLoad) + this._delegate.onLoad(tab); + } catch (e) { + console.exception(e); + } + }, + _safeMozAfterPaint: function safeMozAfterPaint(event) { + let win = event.currentTarget.ownerDocument.defaultView; + let tabIndex = win.gBrowser.getBrowserIndexForDocument(event.target.document); + if (tabIndex == -1) + return; + let tab = win.gBrowser.tabContainer.getItemAtIndex(tabIndex); + let index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onPaint) + this._delegate.onPaint(tab); + } catch (e) { + console.exception(e); + } + }, + handleEvent: function handleEvent(event) { + switch (event.type) { + case "TabOpen": + this._safeTrackTab(event.target); + break; + case "TabClose": + this._safeUntrackTab(event.target); + break; + case "TabSelect": + this._safeSelectTab(event.target); + break; + case "DOMContentLoaded": + this._safeDOMContentLoaded(event); + break; + case "load": + this._safeLoad(event); + break; + case "MozAfterPaint": + this._safeMozAfterPaint(event); + break; + default: + throw new Error("internal error: unknown event type: " + + event.type); + } + }, + onTrack: function onTrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeTrackTab(tab); + tabbrowser.tabContainer.addEventListener("TabOpen", this, false); + tabbrowser.tabContainer.addEventListener("TabClose", this, false); + tabbrowser.tabContainer.addEventListener("TabSelect", this, false); + tabbrowser.ownerDocument.defaultView.gBrowser.addEventListener("DOMContentLoaded", this, false); + }, + onUntrack: function onUntrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeUntrackTab(tab); + tabbrowser.tabContainer.removeEventListener("TabOpen", this, false); + tabbrowser.tabContainer.removeEventListener("TabClose", this, false); + tabbrowser.tabContainer.removeEventListener("TabSelect", this, false); + tabbrowser.ownerDocument.defaultView.gBrowser.removeEventListener("DOMContentLoaded", this, false); + }, + unload: function unload() { + this._tracker.unload(); + } +}; + +// Utility to get a thumbnail canvas from a tab object +function getThumbnailCanvasForTab(tabEl, window) { + var thumbnail = window.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + thumbnail.mozOpaque = true; + window = tabEl.linkedBrowser.contentWindow; + thumbnail.width = Math.ceil(window.screen.availWidth / 5.75); + var aspectRatio = 0.5625; // 16:9 + thumbnail.height = Math.round(thumbnail.width * aspectRatio); + var ctx = thumbnail.getContext("2d"); + var snippetWidth = window.innerWidth * .6; + var scale = thumbnail.width / snippetWidth; + ctx.scale(scale, scale); + ctx.drawWindow(window, window.scrollX, window.scrollY, snippetWidth, snippetWidth * aspectRatio, "rgb(255,255,255)"); + return thumbnail; +} + +// Utility to return the contents of the target of a chrome URL +function getChromeURLContents(chromeURL) { + let io = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let channel = io.newChannel(chromeURL, null, null); + let input = channel.open(); + let stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(input); + let str = stream.readBytes(input.available()); + stream.close(); + input.close(); + return str; +} diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/traits.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/traits.js new file mode 100644 index 0000000..30ae342 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/traits.js @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 { + compose: _compose, + override: _override, + resolve: _resolve, + trait: _trait, + //create: _create, + required, +} = require('./traits/core'); + +const defineProperties = Object.defineProperties, + freeze = Object.freeze, + create = Object.create; + +/** + * Work around bug 608959 by defining the _create function here instead of + * importing it from traits/core. For docs on this function, see the create + * function in that module. + * + * FIXME: remove this workaround in favor of importing the function once that + * bug has been fixed. + */ +function _create(proto, trait) { + let properties = {}, + keys = Object.getOwnPropertyNames(trait); + for each(let key in keys) { + let descriptor = trait[key]; + if (descriptor.required && + !Object.prototype.hasOwnProperty.call(proto, key)) + throw new Error('Missing required property: ' + key); + else if (descriptor.conflict) + throw new Error('Remaining conflicting property: ' + key); + else + properties[key] = descriptor; + } + return Object.create(proto, properties); +} + +/** + * Placeholder for `Trait.prototype` + */ +let TraitProto = Object.prototype; + +function Get(key) this[key] +function Set(key, value) this[key] = value + +/** + * Creates anonymous trait descriptor from the passed argument, unless argument + * is a trait constructor. In later case trait's already existing properties + * descriptor is returned. + * This is module's internal function and is used as a gateway to a trait's + * internal properties descriptor. + * @param {Function} $ + * Composed trait's constructor. + * @returns {Object} + * Private trait property of the composition. + */ +function TraitDescriptor(object) + ( + 'function' == typeof object && + (object.prototype == TraitProto || object.prototype instanceof Trait) + ) ? object._trait(TraitDescriptor) : _trait(object) + +function Public(instance, trait) { + let result = {}, + keys = Object.getOwnPropertyNames(trait); + for each (let key in keys) { + if ('_' === key.charAt(0) && '__iterator__' !== key ) + continue; + let property = trait[key], + descriptor = { + configurable: property.configurable, + enumerable: property.enumerable + }; + if (property.get) + descriptor.get = property.get.bind(instance); + if (property.set) + descriptor.set = property.set.bind(instance); + if ('value' in property) { + let value = property.value; + if ('function' === typeof value) { + descriptor.value = property.value.bind(instance); + descriptor.writable = property.writable; + } else { + descriptor.get = Get.bind(instance, key); + descriptor.set = Set.bind(instance, key); + } + } + result[key] = descriptor; + } + return result; +} + +/** + * This is private function that composes new trait with privates. + */ +function Composition(trait) { + function Trait() { + let self = _create({}, trait); + self._public = create(Trait.prototype, Public(self, trait)); + delete self._public.constructor; + if (Object === self.constructor) + self.constructor = Trait; + else + return self.constructor.apply(self, arguments) || self._public; + return self._public; + } + defineProperties(Trait, { + prototype: { value: freeze(create(TraitProto, { + constructor: { value: constructor, writable: true } + }))}, // writable is `true` to avoid getters in custom ES5 + displayName: { value: (trait.constructor || constructor).name }, + compose: { value: compose, enumerable: true }, + override: { value: override, enumerable: true }, + resolve: { value: resolve, enumerable: true }, + required: { value: required, enumerable: true }, + _trait: { value: function _trait(caller) + caller === TraitDescriptor ? trait : undefined + } + }); + return freeze(Trait); +} + +/** + * Composes new trait out of itself and traits / property maps passed as an + * arguments. If two or more traits / property maps have properties with the + * same name, the new trait will contain a "conflict" property for that name. + * This is a commutative and associative operation, and the order of its + * arguments is not significant. + * @params {Object|Function} + * List of Traits or property maps to create traits from. + * @returns {Function} + * New trait containing the combined properties of all the traits. + */ +function compose() { + let traits = Array.slice(arguments, 0); + traits.push(this); + return Composition(_compose.apply(null, traits.map(TraitDescriptor))); +} + +/** + * Composes a new trait with all of the combined properties of `this` and the + * argument traits. In contrast to `compose`, `override` immediately resolves + * all conflicts resulting from this composition by overriding the properties of + * later traits. Trait priority is from left to right. I.e. the properties of + * the leftmost trait are never overridden. + * @params {Object} trait + * @returns {Object} + */ +function override() { + let traits = Array.slice(arguments, 0); + traits.push(this); + return Composition(_override.apply(null, traits.map(TraitDescriptor))); +} + +/** + * Composes new resolved trait, with all the same properties as this + * trait, except that all properties whose name is an own property of + * `resolutions` will be renamed to `resolutions[name]`. If it is + * `resolutions[name]` is `null` value is changed into a required property + * descriptor. + */ +function resolve(resolutions) + Composition(_resolve(resolutions, TraitDescriptor(this))) + +/** + * Base Trait, that all the traits are composed of. + */ +const Trait = Composition({ + /** + * Internal property holding public API of this instance. + */ + _public: { value: null, configurable: true, writable: true }, + toString: { value: function() '[object ' + this.constructor.name + ']' } +}); +TraitProto = Trait.prototype; +exports.Trait = Trait; + diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/traits/core.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/traits/core.js new file mode 100644 index 0000000..c08c38f --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/traits/core.js @@ -0,0 +1,322 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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" +}; + +// Design inspired by: http://www.traitsjs.org/ + +// shortcuts +const getOwnPropertyNames = Object.getOwnPropertyNames, + getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor, + hasOwn = Object.prototype.hasOwnProperty, + _create = Object.create; + +function doPropertiesMatch(object1, object2, name) { + // If `object1` has property with the given `name` + return name in object1 ? + // then `object2` should have it with the same value. + name in object2 && object1[name] === object2[name] : + // otherwise `object2` should not have property with the given `name`. + !(name in object2); +} + +/** + * Compares two trait custom property descriptors if they are the same. If + * both are `conflict` or all the properties of descriptor are equal returned + * value will be `true`, otherwise it will be `false`. + * @param {Object} desc1 + * @param {Object} desc2 + */ +function areSame(desc1, desc2) { + return ('conflict' in desc1 && desc1.conflict && + 'conflict' in desc2 && desc2.conflict) || + (doPropertiesMatch(desc1, desc2, 'get') && + doPropertiesMatch(desc1, desc2, 'set') && + doPropertiesMatch(desc1, desc2, 'value') && + doPropertiesMatch(desc1, desc2, 'enumerable') && + doPropertiesMatch(desc1, desc2, 'required') && + doPropertiesMatch(desc1, desc2, 'conflict')); +} + +/** + * Converts array to an object whose own property names represent + * values of array. + * @param {String[]} names + * @returns {Object} + * @example + * Map(['foo', ...]) => { foo: true, ...} + */ +function Map(names) { + let map = {}; + for each (let name in names) + map[name] = true; + return map; +} + + +const ERR_CONFLICT = 'Remaining conflicting property: ', + ERR_REQUIRED = 'Missing required property: '; +/** + * Constant singleton, representing placeholder for required properties. + * @type {Object} + */ +const required = { toString: function()'<Trait.required>' }; +exports.required = required; + +/** + * Generates custom **required** property descriptor. Descriptor contains + * non-standard property `required` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function Required(name) { + function required() { throw new Error(ERR_REQUIRED + name) } + return { + get: required, + set: required, + required: true + }; +} + +/** + * Generates custom **conflicting** property descriptor. Descriptor contains + * non-standard property `conflict` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function Conflict(name) { + function conflict() { throw new Error(ERR_CONFLICT + name) } + return { + get: conflict, + set: conflict, + conflict: true + }; +} + +/** + * Function generates custom properties descriptor of the `object`s own + * properties. All the inherited properties are going to be ignored. + * Properties with values matching `required` singleton will be marked as + * 'required' properties. + * @param {Object} object + * Set of properties to generate trait from. + * @returns {Object} + * Properties descriptor of all of the `object`'s own properties. + */ +function trait(properties) { + let result = {}, + keys = getOwnPropertyNames(properties); + for each (let key in keys) { + let descriptor = getOwnPropertyDescriptor(properties, key); + result[key] = (required === descriptor.value) ? Required(key) : descriptor; + } + return result; +} +exports.Trait = exports.trait = trait; + +/** + * Composes new trait. If two or more traits have own properties with the + * same name, the new trait will contain a 'conflict' property for that name. + * 'compose' is a commutative and associative operation, and the order of its + * arguments is not significant. + * + * @params {Object} trait + * Takes traits as an arguments + * @returns {Object} + * New trait containing the combined own properties of all the traits. + * @example + * var newTrait = compose(trait_1, trait_2, ..., trait_N); + */ +function compose(trait1, trait2) { + let traits = Array.slice(arguments, 0), + result = {}; + for each (let trait in traits) { + let keys = getOwnPropertyNames(trait); + for each (let key in keys) { + let descriptor = trait[key]; + // if property already exists and it's not a requirement + if (hasOwn.call(result, key) && !result[key].required) { + if (descriptor.required) + continue; + if (!areSame(descriptor, result[key])) + result[key] = Conflict(key); + } else { + result[key] = descriptor; + } + } + } + return result; +} +exports.compose = compose; + +/** + * Composes new trait with the same own properties as the original trait, + * except that all property names appearing in the first argument are replaced + * by 'required' property descriptors. + * @param {String[]} keys + * Array of strings property names. + * @param {Object} trait + * A trait some properties of which should be excluded. + * @returns {Object} + * @example + * var newTrait = exclude(['name', ...], trait) + */ +function exclude(keys, trait) { + let exclusions = Map(keys), + result = {}; + + keys = getOwnPropertyNames(trait); + + for each (let key in keys) { + if (!hasOwn.call(exclusions, key) || trait[key].required) + result[key] = trait[key]; + else + result[key] = Required(key); + } + return result; +} + +/** + * Composes a new trait with all of the combined properties of the argument + * traits. In contrast to `compose`, `override` immediately resolves all + * conflicts resulting from this composition by overriding the properties of + * later traits. Trait priority is from left to right. I.e. the properties of + * the leftmost trait are never overridden. + * @params {Object} trait + * @returns {Object} + * @examples + * // override is associative: + * override(t1,t2,t3) + * // is equivalent to + * override(t1, override(t2, t3)) + * // or + * to override(override(t1, t2), t3) + * + * // override is not commutative: + * override(t1,t2) + * // is not equivalent to + * override(t2,t1) + */ +function override() { + let traits = Array.slice(arguments, 0), + result = {}; + for each (let trait in traits) { + let keys = getOwnPropertyNames(trait); + for each(let key in keys) { + let descriptor = trait[key]; + if (!hasOwn.call(result, key) || result[key].required) + result[key] = descriptor; + } + } + return result; +} +exports.override = override; + +/** + * Composes a new trait with the same properties as the original trait, except + * that all properties whose name is an own property of map will be renamed to + * map[name], and a 'required' property for name will be added instead. + * @param {Object} map + * An object whose own properties serve as a mapping from old names to new + * names. + * @param {Object} trait + * A trait object + * @returns {Object} + * @example + * var newTrait = rename(map, trait); + */ +function rename(map, trait) { + let result = {}, + keys = getOwnPropertyNames(trait); + for each(let key in keys) { + // must be renamed & it's not requirement + if (hasOwn.call(map, key) && !trait[key].required) { + let alias = map[key]; + if (hasOwn.call(result, alias) && !result[alias].required) + result[alias] = Conflict(alias); + else + result[alias] = trait[key]; + if (!hasOwn.call(result, key)) + result[key] = Required(key); + } else { // must not be renamed or its a requirement + // property is not in result trait yet + if (!hasOwn.call(result, key)) + result[key] = trait[key]; + // property is already in resulted trait & it's not requirement + else if (!trait[key].required) + result[key] = Conflict(key); + } + } + return result; +} + +/** +* Composes new resolved trait, with all the same properties as the original +* trait, except that all properties whose name is an own property of +* resolutions will be renamed to `resolutions[name]`. If it is +* `resolutions[name]` is `null` value is changed into a required property +* descriptor. +* function can be implemented as `rename(map,exclude(exclusions, trait))` +* where map is the subset of mappings from oldName to newName and exclusions +* is an array of all the keys that map to `null`. +* Note: it's important to **first** `exclude`, **then** `rename`, since +* `exclude` and rename are not associative. +* @param {Object} resolutions +* An object whose own properties serve as a mapping from old names to new +* names, or to `null` if the property should be excluded. +* @param {Object} trait +* A trait object +* @returns {Object} +* Resolved trait with the same own properties as the original trait. +*/ +function resolve(resolutions, trait) { + let renames = {}, + exclusions = [], + keys = getOwnPropertyNames(resolutions); + for each (let key in keys) { // pre-process renamed and excluded properties + if (resolutions[key]) // old name -> new name + renames[key] = resolutions[key]; + else // name -> undefined + exclusions.push(key); + } + return rename(renames, exclude(exclusions, trait)); +} +exports.resolve = resolve; + +/** + * `create` is like `Object.create`, except that it ensures that: + * - an exception is thrown if 'trait' still contains required properties + * - an exception is thrown if 'trait' still contains conflicting + * properties + * @param {Object} + * prototype of the completed object + * @param {Object} trait + * trait object to be turned into a complete object + * @returns {Object} + * An object with all of the properties described by the trait. + */ +function create(proto, trait) { + let properties = {}, + keys = getOwnPropertyNames(trait); + for each(let key in keys) { + let descriptor = trait[key]; + if (descriptor.required && !hasOwn.call(proto, key)) + throw new Error(ERR_REQUIRED + key); + else if (descriptor.conflict) + throw new Error(ERR_CONFLICT + key); + else + properties[key] = descriptor; + } + return _create(proto, properties); +} +exports.create = create; + diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/unit-test-finder.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/unit-test-finder.js new file mode 100644 index 0000000..880f554 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/unit-test-finder.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": "deprecated" +}; + +const file = require("../io/file"); +const memory = require('./memory'); +const suites = require('@test/options').allTestModules; + + +const NOT_TESTS = ['setup', 'teardown']; + +var TestFinder = exports.TestFinder = function TestFinder(options) { + memory.track(this); + this.filter = options.filter; + this.testInProcess = options.testInProcess === false ? false : true; + this.testOutOfProcess = options.testOutOfProcess === true ? true : false; +}; + +TestFinder.prototype = { + findTests: function findTests(cb) { + var self = this; + var tests = []; + var filter; + // A filter string is {fileNameRegex}[:{testNameRegex}] - ie, a colon + // optionally separates a regex for the test fileName from a regex for the + // testName. + if (this.filter) { + var colonPos = this.filter.indexOf(':'); + var filterFileRegex, filterNameRegex; + if (colonPos === -1) { + filterFileRegex = new RegExp(self.filter); + } else { + filterFileRegex = new RegExp(self.filter.substr(0, colonPos)); + filterNameRegex = new RegExp(self.filter.substr(colonPos + 1)); + } + // This function will first be called with just the filename; if + // it returns true the module will be loaded then the function + // called again with both the filename and the testname. + filter = function(filename, testname) { + return filterFileRegex.test(filename) && + ((testname && filterNameRegex) ? filterNameRegex.test(testname) + : true); + }; + } else + filter = function() {return true}; + + suites.forEach( + function(suite) { + var module = require(suite); + if (self.testInProcess) + for each (let name in Object.keys(module).sort()) { + if(NOT_TESTS.indexOf(name) === -1 && filter(suite, name)) { + tests.push({ + setup: module.setup, + teardown: module.teardown, + testFunction: module[name], + name: suite + "." + name + }); + } + } + }); + + cb(tests); + } +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/unit-test.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/unit-test.js new file mode 100644 index 0000000..6a8ac1b --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/unit-test.js @@ -0,0 +1,451 @@ +/* vim:st=2:sts=2:sw=2: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 memory = require('./memory'); +var timer = require("../timers"); + +exports.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( + function (tests) { + runner.startMany({tests: tests, + stopOnError: options.stopOnError, + onDone: options.onDone}); + }); +}; + +var TestRunner = exports.TestRunner = function TestRunner(options) { + if (options) { + this.fs = options.fs; + } + this.console = (options && "console" in options) ? options.console : console; + memory.track(this); + this.passed = 0; + this.failed = 0; + this.testRunSummary = []; + this.expectFailNesting = 0; +}; + +TestRunner.prototype = { + toString: function toString() "[object TestRunner]", + + DEFAULT_PAUSE_TIMEOUT: 10000, + PAUSE_DELAY: 500, + + _logTestFailed: function _logTestFailed(why) { + if (!(why in this.test.errors)) + this.test.errors[why] = 0; + this.test.errors[why]++; + if (!this.testFailureLogged) { + this.console.error("TEST FAILED: " + this.test.name + " (" + why + ")"); + this.testFailureLogged = true; + } + }, + + pass: function pass(message) { + if(!this.expectFailure) { + this.console.info("pass:", message); + this.passed++; + this.test.passed++; + } + else { + this.expectFailure = false; + this.fail('Failure Expected: ' + message); + } + }, + + fail: function fail(message) { + if(!this.expectFailure) { + this._logTestFailed("failure"); + this.console.error("fail:", message); + this.console.trace(); + this.failed++; + this.test.failed++; + } + else { + this.expectFailure = false; + this.pass(message); + } + }, + + expectFail: function(callback) { + this.expectFailure = true; + callback(); + this.expectFailure = false; + }, + + exception: function exception(e) { + this._logTestFailed("exception"); + 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) { + this.isDone = true; + 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"); + this.failed++; + this.test.failed++; + } + + this.testRunSummary.push({ + name: this.test.name, + passed: this.test.passed, + failed: this.test.failed, + errors: [error for (error in this.test.errors)].join(", ") + }); + + if (this.onDone !== null) { + var onDone = this.onDone; + var self = this; + this.onDone = null; + timer.setTimeout(function() { onDone(self); }, 0); + } + } + }, + + // 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 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 callback = null; + 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 (callback && !stopIt) + callback(); + finished = true; + }, + 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); + finished = true; + return; + } + } + appliedArgs.push(a); + } + + // Finally call assertion function with current assertion values + assertionMethod.apply(mock, appliedArgs); + } + loop(); + this.waitUntilCallback = loop; + + // Return an object with `then` method, to offer a way to execute + // some code when the assertion passed or failed + return { + then: function (c) { + callback = c; + + // In case of immediate positive result, we need to execute callback + // immediately here: + if (finished) + callback(); + } + }; + }, + + waitUntilDone: function waitUntilDone(ms) { + if (ms === undefined) + ms = this.DEFAULT_PAUSE_TIMEOUT; + + var self = this; + + function tiredOfWaiting() { + self._logTestFailed("timed out"); + 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) { + var test = options.tests.shift(); + if (options.stopOnError && self.test && self.test.failed) { + self.console.error("aborted: test failed and --stop-on-error was specified"); + options.onDone(self); + } else if (test) { + self.start({test: test, onDone: runNextTest}); + } else { + options.onDone(self); + } + } + runNextTest(this); + }, + + start: function start(options) { + this.test = options.test; + this.test.passed = 0; + this.test.failed = 0; + this.test.errors = {}; + + this.isDone = false; + this.onDone = options.onDone; + this.waitTimeout = null; + this.testFailureLogged = false; + + try { + 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(); + } +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/deprecated/window-utils.js b/tools/addon-sdk-1.12/lib/sdk/deprecated/window-utils.js new file mode 100644 index 0000000..66fcfba --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/deprecated/window-utils.js @@ -0,0 +1,201 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 { EventEmitter } = require('../deprecated/events'); +const { Trait } = require('../deprecated/traits'); +const { when } = require('../system/unload'); +const { getInnerId, getOuterId, windows, isDocumentLoaded, isBrowser, + getMostRecentBrowserWindow } = require('../window/utils'); +const errors = require('../deprecated/errors'); +const { deprecateFunction } = require('../util/deprecate'); + +const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1']. + getService(Ci.nsIWindowWatcher); +const appShellService = Cc['@mozilla.org/appshell/appShellService;1']. + getService(Ci.nsIAppShellService); + +/** + * 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 = windows().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 each (let window in windowIterator()) { + if (isBrowser(window)) + yield window; + } +} +exports.browserWindowIterator = browserWindowIterator; + +function WindowTracker(delegate) { + if (!(this instanceof WindowTracker)) { + return new WindowTracker(delegate); + } + + this._delegate = delegate; + this._loadingWindows = []; + + for each (let window in windows()) + this._regWindow(window); + windowWatcher.registerNotification(this); + + require('../system/unload').ensure(this); + + return this; +}; + +WindowTracker.prototype = { + _regLoadingWindow: function _regLoadingWindow(window) { + this._loadingWindows.push(window); + window.addEventListener('load', this, true); + }, + + _unregLoadingWindow: function _unregLoadingWindow(window) { + var index = this._loadingWindows.indexOf(window); + + if (index != -1) { + this._loadingWindows.splice(index, 1); + window.removeEventListener('load', this, true); + } + }, + + _regWindow: function _regWindow(window) { + 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); + for each (let window in windows()) + this._unregWindow(window); + }, + + handleEvent: errors.catchAndLog(function handleEvent(event) { + if (event.type == 'load' && event.target) { + var window = event.target.defaultView; + if (window) + this._regWindow(window); + } + }), + + observe: errors.catchAndLog(function observe(subject, topic, data) { + var window = subject.QueryInterface(Ci.nsIDOMWindow); + if (topic == 'domwindowopened') + this._regWindow(window); + else + this._unregWindow(window); + }) +}; +exports.WindowTracker = WindowTracker; + +const WindowTrackerTrait = Trait.compose({ + _onTrack: Trait.required, + _onUntrack: Trait.required, + constructor: function WindowTrackerTrait() { + WindowTracker({ + onTrack: this._onTrack.bind(this), + onUntrack: this._onUntrack.bind(this) + }); + } +}); +exports.WindowTrackerTrait = WindowTrackerTrait; + +var gDocsToClose = []; + +function onDocUnload(event) { + var index = gDocsToClose.indexOf(event.target); + if (index == -1) + throw new Error('internal error: unloading document not found'); + var document = gDocsToClose.splice(index, 1)[0]; + // Just in case, let's remove the event listener too. + document.defaultView.removeEventListener('unload', onDocUnload, false); +} + +onDocUnload = require('./errors').catchAndLog(onDocUnload); + +exports.closeOnUnload = function closeOnUnload(window) { + window.addEventListener('unload', onDocUnload, false); + gDocsToClose.push(window.document); +}; + +Object.defineProperties(exports, { + activeWindow: { + enumerable: true, + get: function() { + return Cc['@mozilla.org/appshell/window-mediator;1'] + .getService(Ci.nsIWindowMediator) + .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("window/utils").getInnerId instead' +); + +exports.getOuterId = deprecateFunction(getOuterId, + 'require("window-utils").getOuterId is deprecated, ' + + 'please use require("window/utils").getOuterId instead' +); + +exports.isBrowser = deprecateFunction(isBrowser, + 'require("window-utils").isBrowser is deprecated, ' + + 'please use require("window/utils").isBrowser instead' +); + +exports.hiddenWindow = appShellService.hiddenDOMWindow; + +when( + function() { + gDocsToClose.slice().forEach( + function(doc) { doc.defaultView.close(); }); + }); diff --git a/tools/addon-sdk-1.12/lib/sdk/dom/events.js b/tools/addon-sdk-1.12/lib/sdk/dom/events.js new file mode 100644 index 0000000..3f2d21c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/dom/events.js @@ -0,0 +1,140 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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" +}; + +// 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) { + // `capture` defaults to `false`. + capture = capture || false; + 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) { + on(element, type, function selfRemovableListener(event) { + removeListener(element, type, selfRemovableListener, capture); + listener.apply(this, arguments); + }, capture); +} +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) { + 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 }) { + category = category || "UIEvents"; + initializer = initializer || getInitializerName(category); + let document = element.ownerDocument; + let event = document.createEvent(category); + event[initializer].apply(event, [type].concat(settings)); + element.dispatchEvent(event); +}; +exports.emit = emit; diff --git a/tools/addon-sdk-1.12/lib/sdk/dom/events/keys.js b/tools/addon-sdk-1.12/lib/sdk/dom/events/keys.js new file mode 100644 index 0000000..01e4a0d --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/dom/events/keys.js @@ -0,0 +1,64 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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/tools/addon-sdk-1.12/lib/sdk/event/core.js b/tools/addon-sdk-1.12/lib/sdk/event/core.js new file mode 100644 index 0000000..e63e3b9 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/event/core.js @@ -0,0 +1,152 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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(); + +// 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) { + 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; + +/** + * 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) { + on(target, type, function observer() { + off(target, type, observer); + listener.apply(target, arguments); + }); +} +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} message + * First argument that will be passed to listeners. + * @params {Object|Number|String|Boolean} ... + * More arguments that will be passed to listeners. + */ +function emit(target, type, message /*, ...*/) { + for each (let item in emit.lazy.apply(emit.lazy, arguments)) { + // We just iterate, iterator take care of emitting events. + } +} + +/** + * This is very experimental feature that you should not use unless absolutely + * need it. Also it may be removed at any point without any further notice. + * + * Creates lazy iterator of return values of listeners. You can think of it + * as lazy array of return values of listeners for the `emit` with the given + * arguments. + */ +emit.lazy = function lazy(target, type, message /*, ...*/) { + let args = Array.slice(arguments, 2) + let listeners = observers(target, type).slice() + while (listeners.length) { + try { + yield listeners.shift().apply(target, 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' && observers(target, 'error').length) + emit(target, 'error', error); + else + console.exception(error); + } + } +} +exports.emit = emit; + +/** + * 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) { + 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(function(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; diff --git a/tools/addon-sdk-1.12/lib/sdk/event/target.js b/tools/addon-sdk-1.12/lib/sdk/event/target.js new file mode 100644 index 0000000..b8906c0 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/event/target.js @@ -0,0 +1,80 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 } = require('./core'); +const { method } = require('../lang/functional'); +const { Class } = require('../core/heritage'); + +const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/; + +/** + * `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. + */ + initialize: function initialize(options) { + options = options || {}; + // Go through each property and registers event listeners for those + // that have a name matching following pattern (`onEventType`). + Object.keys(options).forEach(function onEach(key) { + let match = EVENT_TYPE_PATTERN.exec(key); + let type = match && match[1].toLowerCase(); + let listener = options[key]; + + if (type && typeof(listener) === 'function') + this.on(type, listener); + }, this); + }, + /** + * 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: 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: 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); + } +}); +exports.EventTarget = EventTarget; diff --git a/tools/addon-sdk-1.12/lib/sdk/frame/hidden-frame.js b/tools/addon-sdk-1.12/lib/sdk/frame/hidden-frame.js new file mode 100644 index 0000000..1bf51de --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/frame/hidden-frame.js @@ -0,0 +1,181 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 errors = require("../deprecated/errors"); +const apiUtils = require("../deprecated/api-utils"); +const timer = require("../timers"); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +let hostFrame, hostDocument, hiddenWindow, isHostFrameReady = false; + +if (!require("../system/xul-app").isOneOf(["Firefox", "Fennec", "Thunderbird"])) { + throw new Error([ + "The hidden-frame module currently supports only Firefox and Thunderbird. ", + "In the future, we would like it to support other applications, however. ", + "Please see https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more ", + "information." + ].join("")); +} + +let appShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); +hiddenWindow = appShellService.hiddenDOMWindow; + +if (!hiddenWindow) { + throw new Error([ + "The hidden-frame module needs an app that supports a hidden window. ", + "We would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more information." + ].join("")); +} + +// Check if we can use the hidden window itself to host our iframes. +// If it's not a suitable host, the hostFrame will be lazily created +// by the first HiddenFrame instance. +if (hiddenWindow.location.protocol == "chrome:" && + (hiddenWindow.document.contentType == "application/vnd.mozilla.xul+xml" || + hiddenWindow.document.contentType == "application/xhtml+xml")) { + hostFrame = hiddenWindow; + hostDocument = hiddenWindow.document; + isHostFrameReady = true; +} + +function setHostFrameReady() { + hostDocument = hostFrame.contentDocument; + hostFrame.removeEventListener("DOMContentLoaded", setHostFrameReady, false); + isHostFrameReady = true; +} + +// This cache is used to access friend properties between functions +// without exposing them on the public API. +let cache = []; + +exports.HiddenFrame = apiUtils.publicConstructor(HiddenFrame); + +function HiddenFrame(options) { + options = options || {}; + let self = this; + let validOptions = apiUtils.validateOptions(options, { + onReady: { + is: ["undefined", "function", "array"], + ok: function(v) { + if (apiUtils.getTypeOf(v) === "array") { + // make sure every item is a function + return v.every(function (item) typeof(item) === "function") + } + return true; + } + }, + onUnload: { + is: ["undefined", "function"] + } + }); + + for (let key in validOptions) { + let val = validOptions[key]; + if (typeof(val) != "undefined") + options[key] = val; + } + + require("../util/collection").addCollectionProperty(this, "onReady"); + if (options.onReady) + this.onReady.add(options.onReady); + if (options.onUnload) + this.onUnload = options.onUnload; + + if (!hostFrame) { + hostFrame = hiddenWindow.document.createElement("iframe"); + + // ugly ugly hack. This is the most lightweight chrome:// file I could find on the tree + // This hack should be removed by proper platform support on bug 565388 + hostFrame.setAttribute("src", "chrome://global/content/mozilla.xhtml"); + hostFrame.addEventListener("DOMContentLoaded", setHostFrameReady, false); + + hiddenWindow.document.body.appendChild(hostFrame); + } + + this.toString = function toString() "[object Frame]"; +} + +exports.add = function JP_SDK_Frame_add(frame) { + if (!(frame instanceof HiddenFrame)) + throw new Error("The object to be added must be a HiddenFrame."); + + // This instance was already added. + if (cache.filter(function (v) v.frame === frame)[0]) + return frame; + + function createElement() { + hostFrame.removeEventListener("DOMContentLoaded", createElement, false); + + let element = hostDocument.createElementNS(XUL_NS, "iframe"); + + element.setAttribute("type", "content"); + hostDocument.documentElement.appendChild(element); + + /* Public API: hiddenFrame.element */ + frame.__defineGetter__("element", function () element); + + // Notify consumers that the frame is ready. + function onReadyListener(event) { + element.removeEventListener("DOMContentLoaded", onReadyListener, false); + if (event.target == element.contentDocument) { + for (let handler in frame.onReady) + errors.catchAndLog(function () handler.call(frame))(); + } + } + element.addEventListener("DOMContentLoaded", onReadyListener, false); + + cache.push({ + frame: frame, + element: element, + unload: function unload() { + // Call before removing to let a chance to avoid "dead object" exception + if (typeof frame.onUnload === "function") + frame.onUnload(); + hostDocument.documentElement.removeChild(element); + } + }); + } + + /* Begin element construction or schedule it for later */ + if (isHostFrameReady) { + createElement(); + } else { + hostFrame.addEventListener("DOMContentLoaded", createElement, false); + } + + return frame; +} + +exports.remove = function remove(frame) { + if (!(frame instanceof HiddenFrame)) + throw new Error("The object to be removed must be a HiddenFrame."); + + let entry = cache.filter(function (v) v.frame === frame)[0]; + if (!entry) + return; + + // Remove from cache before calling in order to avoid loop + cache.splice(cache.indexOf(entry), 1); + entry.unload(); +} + +require("../system/unload").when(function () { + for each (let entry in cache.slice()) + exports.remove(entry.frame); + + if (hostFrame && hostFrame !== hiddenWindow) + hiddenWindow.document.body.removeChild(hostFrame); +}); diff --git a/tools/addon-sdk-1.12/lib/sdk/frame/utils.js b/tools/addon-sdk-1.12/lib/sdk/frame/utils.js new file mode 100644 index 0000000..ad4ca7c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/frame/utils.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": "experimental" +}; + +const XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + +/** + * 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(document, options) { + options = options || {}; + let remote = 'remote' in options && options.remote === true; + + let frame = document.createElementNS(XUL, 'browser'); + // 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'); + + // Load in separate process if `options.remote` is `true`. + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1347 + if (remote) { + // 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'); + } + + document.documentElement.appendChild(frame); + + // If browser is remote it won't have a `docShell`. + if (!remote) { + let docShell = frame.docShell; + docShell.allowAuth = options.allowAuth || false; + docShell.allowJavascript = options.allowJavascript || false; + docShell.allowPlugins = options.allowPlugins || false; + } + + return frame; +} +exports.create = create; diff --git a/tools/addon-sdk-1.12/lib/sdk/hotkeys.js b/tools/addon-sdk-1.12/lib/sdk/hotkeys.js new file mode 100644 index 0000000..3491d2c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/hotkeys.js @@ -0,0 +1,41 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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; + 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/tools/addon-sdk-1.12/lib/sdk/io/byte-streams.js b/tools/addon-sdk-1.12/lib/sdk/io/byte-streams.js new file mode 100644 index 0000000..27b1802 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/io/byte-streams.js @@ -0,0 +1,106 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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/tools/addon-sdk-1.12/lib/sdk/io/data.js b/tools/addon-sdk-1.12/lib/sdk/io/data.js new file mode 100644 index 0000000..9dd7246 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/io/data.js @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 base64 = require("../base64"); + +const IOService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm"); +const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); +const { deprecateFunction, deprecatedUsage } = require("../util/deprecate"); + +const PNG_B64 = "data:image/png;base64,"; +const DEF_FAVICON_URI = "chrome://mozapps/skin/places/defaultFavicon.png"; +let DEF_FAVICON = null; + +/** + * Takes URI of the page and returns associated favicon URI. + * If page under passed uri has no favicon then base64 encoded data URI of + * default faveicon is returned. + * @param {String} uri + * @returns {String} + */ +exports.getFaviconURIForLocation = function getFaviconURIForLocation(uri) { + let pageURI = NetUtil.newURI(uri); + try { + return FaviconService.getFaviconDataAsDataURL( + FaviconService.getFaviconForPage(pageURI)); + } + catch(e) { + if (!DEF_FAVICON) { + DEF_FAVICON = PNG_B64 + + base64.encode(getChromeURIContent(DEF_FAVICON_URI)); + } + return DEF_FAVICON; + } +} + +/** + * Takes chrome URI and returns content under that URI. + * @param {String} chromeURI + * @returns {String} + */ +function getChromeURIContent(chromeURI) { + let channel = IOService.newChannel(chromeURI, null, null); + let input = channel.open(); + let stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(input); + let content = stream.readBytes(input.available()); + stream.close(); + input.close(); + return content; +} +exports.getChromeURIContent = function deprecated_getChromeURIContent() { + deprecatedUsage( + 'getChromeURIContent is deprecated, ' + + 'please use require("sdk/net/url").readURI instead.' + ); +}; + +/** + * Creates a base-64 encoded ASCII string from a string of binary data. + */ +exports.base64Encode = deprecateFunction(base64.encode, + 'base64Encode is deprecated, ' + + 'please use require("sdk/base64").encode instead.' +); +/** + * Decodes a string of data which has been encoded using base-64 encoding. + */ +exports.base64Decode = deprecateFunction(base64.decode, + 'base64Dencode is deprecated, ' + + 'please use require("sdk/base64").decode instead.' +); diff --git a/tools/addon-sdk-1.12/lib/sdk/io/file.js b/tools/addon-sdk-1.12/lib/sdk/io/file.js new file mode 100644 index 0000000..735edea --- /dev/null +++ b/tools/addon-sdk-1.12/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": "experimental" +}; + +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 = parseInt("0644"); // 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, parseInt("0755")); // 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/tools/addon-sdk-1.12/lib/sdk/io/text-streams.js b/tools/addon-sdk-1.12/lib/sdk/io/text-streams.js new file mode 100644 index 0000000..0aa469d --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/io/text-streams.js @@ -0,0 +1,244 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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"); +var NetUtil = {}; +Cu.import("resource://gre/modules/NetUtil.jsm", NetUtil); +NetUtil = NetUtil.NetUtil; + +// 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"; + +exports.TextReader = TextReader; +exports.TextWriter = TextWriter; + +/** + * 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) { + const self = this; + 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; + }; +} + +/** + * 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) { + const self = this; + 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, function (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(self, err); + } + catch (exc) { + console.exception(exc); + } + } + }); + }; +} + +// 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) { + const self = this; + this.rawStream = rawStream; + this.opened = true; + + /** + * True iff the stream is closed. + */ + stream.__defineGetter__("closed", function stream_closed() { + return !self.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 = 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() { + // 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/tools/addon-sdk-1.12/lib/sdk/keyboard/hotkeys.js b/tools/addon-sdk-1.12/lib/sdk/keyboard/hotkeys.js new file mode 100644 index 0000000..2d963ab --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/keyboard/hotkeys.js @@ -0,0 +1,111 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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://mxr.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/tools/addon-sdk-1.12/lib/sdk/keyboard/observer.js b/tools/addon-sdk-1.12/lib/sdk/keyboard/observer.js new file mode 100644 index 0000000..26c0a83 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/keyboard/observer.js @@ -0,0 +1,58 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { Trait } = require("../deprecated/light-traits"); +const { EventEmitterTrait: EventEmitter } = require("../deprecated/events"); +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 = Trait.compose(DOMEventAssembler, EventEmitter).create({ + /** + * Method is implemented by `EventEmitter` and is used just for emitting + * events on registered listeners. + */ + _emit: Trait.required, + /** + * 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: function handleEvent(event) { + this._emit(event.type, event, event.target.ownerDocument.defaultView); + } +}); + +// Adding each opened window to a list of observed windows. +windowObserver.on("open", function onOpen(window) { + if (isBrowser(window)) + observer.observe(window); +}); +// Removing each closed window form the list of observed windows. +windowObserver.on("close", function onClose(window) { + if (isBrowser(window)) + observer.ignore(window); +}); + +// Making observer aware of already opened windows. +for each (let window in browserWindowIterator()) + observer.observe(window); + +exports.observer = observer; diff --git a/tools/addon-sdk-1.12/lib/sdk/keyboard/utils.js b/tools/addon-sdk-1.12/lib/sdk/keyboard/utils.js new file mode 100644 index 0000000..a4a8f76 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/keyboard/utils.js @@ -0,0 +1,190 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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://mxr.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/tools/addon-sdk-1.12/lib/sdk/l10n.js b/tools/addon-sdk-1.12/lib/sdk/l10n.js new file mode 100644 index 0000000..40f219c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/l10n.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"; + +module.metadata = { + "stability": "stable" +}; + +const core = require("./l10n/core"); +const { getRulesForLocale } = require("./l10n/plural-rules"); + +// Retrieve the plural mapping function +let pluralMappingFunction = getRulesForLocale(core.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"); + + // Get translation from big hashmap or default to hard coded string: + let localized = core.get(k) || 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 = arguments; + + 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; + localized = localized.replace(/%(\d*)(s|d)/g, function (v, n) { + let rv = args[n != "" ? n : offset]; + offset++; + return rv; + }); + + return localized; +} diff --git a/tools/addon-sdk-1.12/lib/sdk/l10n/core.js b/tools/addon-sdk-1.12/lib/sdk/l10n/core.js new file mode 100644 index 0000000..50472e9 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/l10n/core.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Following pseudo module is set by `api-utils/addon/runner` and its load +// method needs to be called before loading `core` module. But it may have +// failed, so that this pseudo won't be available + +module.metadata = { + "stability": "unstable" +}; + + +let hash = {}, bestMatchingLocale = null; +try { + let data = require("@l10n/data"); + hash = data.hash; + bestMatchingLocale = data.bestMatchingLocale; +} +catch(e) {} + +// 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() + : null; +} diff --git a/tools/addon-sdk-1.12/lib/sdk/l10n/html.js b/tools/addon-sdk-1.12/lib/sdk/l10n/html.js new file mode 100644 index 0000000..1a36415 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/l10n/html.js @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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": "unstable" +}; + +const { Ci } = require("chrome"); +const events = require("../system/events"); +const core = require("./core"); + +const assetsURI = require('../self').data.url(); + +// 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; + } +} +exports.translateElement = translateElement; + +function onDocumentReady2Translate(event) { + let document = event.target; + document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate, + false); + + translateElement(document); + + // Finally display document when we finished replacing all text content + document.documentElement.style.visibility = "visible"; +} + +function onContentWindow(event) { + let document = event.subject; + + // 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; + + // First hide content of the document in order to have content blinking + // between untranslated and translated states + // TODO: use result of bug 737003 discussion in order to avoid any conflict + // with document CSS + document.documentElement.style.visibility = "hidden"; + + // 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) { + events.on(ON_CONTENT, onContentWindow); + enabled = true; + } +} +exports.enable = enable; + +function disable() { + if (enabled) { + events.off(ON_CONTENT, onContentWindow); + enabled = false; + } +} +exports.disable = disable; + +require("api-utils/unload").when(disable); diff --git a/tools/addon-sdk-1.12/lib/sdk/l10n/loader.js b/tools/addon-sdk-1.12/lib/sdk/l10n/loader.js new file mode 100644 index 0000000..9af6c5c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/l10n/loader.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": "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/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/tools/addon-sdk-1.12/lib/sdk/l10n/locale.js b/tools/addon-sdk-1.12/lib/sdk/l10n/locale.js new file mode 100644 index 0000000..0e59a64 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/l10n/locale.js @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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"; +exports.getPreferedLocales = function getPreferedLocales() { + let locales = []; + + function addLocale(locale) { + 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.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 each(let locale in 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; +} + +/** + * 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://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/XPIProvider.jsm + */ +exports.findClosestLocale = function findClosestLocale(aLocales, aMatchLocales) { + + aMatchLocales = aMatchLocales || exports.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 each (let locale in aMatchLocales) { + let lparts = locale.split("-"); + for each (let localized in 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/tools/addon-sdk-1.12/lib/sdk/l10n/plural-rules.js b/tools/addon-sdk-1.12/lib/sdk/l10n/plural-rules.js new file mode 100644 index 0000000..22e5b8b --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/l10n/plural-rules.js @@ -0,0 +1,403 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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) list.indexOf(n) !== -1; +function isBetween(n, start, end) 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/tools/addon-sdk-1.12/lib/sdk/l10n/prefs.js b/tools/addon-sdk-1.12/lib/sdk/l10n/prefs.js new file mode 100644 index 0000000..016077c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/l10n/prefs.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const observers = require("../deprecated/observer-service"); +const core = require("./core"); +const { id: jetpackId} = require('../self'); + +const OPTIONS_DISPLAYED = "addon-options-displayed"; + +function onOptionsDisplayed(document, addonId) { + if (addonId !== jetpackId) + return; + let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' + + 'button[data-jetpack-id="' + jetpackId + '"][pref-name]'; + let nodes = document.querySelectorAll(query); + for (let node of nodes) { + let name = node.getAttribute("pref-name"); + if (node.tagName == "setting") { + 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); + } + } +} + +observers.add(OPTIONS_DISPLAYED, onOptionsDisplayed); diff --git a/tools/addon-sdk-1.12/lib/sdk/lang/functional.js b/tools/addon-sdk-1.12/lib/sdk/lang/functional.js new file mode 100644 index 0000000..3595a27 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/lang/functional.js @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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: Most 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 { setTimeout } = require("../timers"); + +/** + * Takes `lambda` function and returns a method. When returned method is + * invoked it calls wrapped `lambda` and passes `this` as a first argument + * and given argument as rest. + */ +function method(lambda) { + return function method() { + return lambda.apply(null, [this].concat(Array.slice(arguments))); + } +} +exports.method = method; + +/** + * 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 `setTimeout(function() { ... }, 0)`, 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. + */ +function defer(f) { + return function deferred() + setTimeout(invoke, 0, f, arguments, this); +} +exports.defer = defer; +// Exporting `remit` alias as `defer` may conflict with promises. +exports.remit = defer; + +/** + * 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. + */ +function invoke(callee, params, self) callee.apply(self, params); +exports.invoke = invoke; + +/** + * Curries a function with the arguments given. + * + * @param {Function} fn + * The function to curry + * + * @returns The function curried + */ +function curry(fn) { + if (typeof fn !== "function") + throw new TypeError(String(fn) + " is not a function"); + + let args = Array.slice(arguments, 1); + + return function() fn.apply(this, args.concat(Array.slice(arguments))); +} +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() { + let lambdas = Array.slice(arguments); + return function composed() { + let args = Array.slice(arguments), 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' + */ +function wrap(f, wrapper) { + return function wrapped() + wrapper.apply(this, [ f ].concat(Array.slice(arguments))) +}; +exports.wrap = wrap; + +/** + * Returns the same value that is used as the argument. In math: f(x) = x + */ +function 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. + */ +function memoize(f, hasher) { + let memo = Object.create(null); + hasher = hasher || identity; + return function memoizer() { + let key = hasher.apply(this, arguments); + return key in memo ? memo[key] : (memo[key] = f.apply(this, arguments)); + }; +} +exports.memoize = memoize; + +/** + * 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. + */ +function delay(f, ms) { + let args = Array.slice(arguments, 2); + setTimeout(function(context) { return f.apply(context, args); }, ms, this); +}; +exports.delay = delay; + +/** + * 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. + */ +function once(f) { + let ran = false, cache; + return function() ran ? cache : (ran = true, cache = f.apply(this, arguments)) +}; +exports.once = once; +// export cache as once will may be conflicting with event once a lot. +exports.cache = once; diff --git a/tools/addon-sdk-1.12/lib/sdk/lang/type.js b/tools/addon-sdk-1.12/lib/sdk/lang/type.js new file mode 100644 index 0000000..a184edf --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/lang/type.js @@ -0,0 +1,344 @@ +/* vim:ts=2:sts=2:sw=2: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 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; + +/** + * Returns true if `value` is an Array. + * @examples + * isArray([1, 2, 3]) // true + * isArray({ 0: 'foo', length: 1 }) // false + */ +var isArray = Array.isArray || function isArray(value) { + Object.prototype.toString.call(value) === "[object Array]"; +} +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) { + Object.prototype.toString.call(value) === "[object Arguments]"; +} +exports.isArguments = isArguments; + +/** + * 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 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/tools/addon-sdk-1.12/lib/sdk/loader/cuddlefish.js b/tools/addon-sdk-1.12/lib/sdk/loader/cuddlefish.js new file mode 100644 index 0000000..cac05a9 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/loader/cuddlefish.js @@ -0,0 +1,78 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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"); +// We need to keep a reference to the sandbox in order to unload it in +// bootstrap.js +const loaderSandbox = loadSandbox(loaderURI); +const loaderModule = loaderSandbox.exports; + +const { override } = 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, + 'addon-sdk/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.id + ' located at ' + requirer.uri + + ' has no authority to load: ' + id, requirer.uri); + + uri = requirement; + } + // If requirer is off manifest than it's a system module and we allow it + // to go off manifest. + else { + uri = id; + } + return uri; + } + }); + + 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/tools/addon-sdk-1.12/lib/sdk/loader/sandbox.js b/tools/addon-sdk-1.12/lib/sdk/loader/sandbox.js new file mode 100644 index 0000000..3c085aa --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/loader/sandbox.js @@ -0,0 +1,50 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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); + +/** + * 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) { + return Cu.Sandbox(target || systemPrincipal, options || {}); +} +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;
\ No newline at end of file diff --git a/tools/addon-sdk-1.12/lib/sdk/net/url.js b/tools/addon-sdk-1.12/lib/sdk/net/url.js new file mode 100644 index 0000000..5992e0d --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/net/url.js @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { Cu, components } = require("chrome"); +const { defer } = require("../core/promise"); +const { merge } = require("../util/object"); + +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); + +/** + * Open a channel synchronously for the URI given, with an optional charset, and + * returns a resolved promise if succeed; rejected promise otherwise. + */ +function readSync(uri, charset) { + let { promise, resolve, reject } = defer(); + + try { + resolve(readURISync(uri, charset)); + } + catch (e) { + reject("Failed to read: '" + uri + "' (Error Code: " + e.result + ")"); + } + + return promise; +} + +/** + * Open a channel synchronously for the URI given, with an optional charset, and + * returns a promise. + */ +function readAsync(uri, charset) { + let channel = NetUtil.newChannel(uri, charset, null); + + let { promise, resolve, reject } = defer(); + + 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 + ")"); + } + }); + + return promise; +} + +/** + * Reads a URI and returns a promise. If the `sync` option is set to `true`, the + * promise will be resolved synchronously. + * + * @param uri {string} The URI to read + * @param [options] {object} This parameter can have any or all of the following + * fields: `sync`, `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', { + * sync: true, + * charset: 'US-ASCII' + }); + */ +function readURI(uri, options) { + options = merge({ + charset: "UTF-8", + sync: false + }, options); + + return options.sync + ? readSync(uri, options.charset) + : readAsync(uri, options.charset); +} + +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, charset, null); + let stream = channel.open(); + + let count = stream.available(); + let data = NetUtil.readInputStreamToString(stream, count, { charset : charset }); + + stream.close(); + + return data; +} + +exports.readURISync = readURISync; diff --git a/tools/addon-sdk-1.12/lib/sdk/net/xhr.js b/tools/addon-sdk-1.12/lib/sdk/net/xhr.js new file mode 100644 index 0000000..b4fc1ef --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/net/xhr.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": "unstable" +}; + +const { Cc, Ci } = require("chrome"); +const memory = require('../deprecated/memory'); +const { when: unload } = require("../system/unload"); + +// ## Implementation Notes ## +// +// Making `XMLHttpRequest` objects available to Jetpack code involves a +// few key principles universal to all low-level module implementations: +// +// * **Unloadability**. A Jetpack-based extension using this module can be +// asked to unload itself at any time, e.g. because the user decides to +// uninstall or disable the extension. This means we need to keep track of +// all in-progress reqests and abort them on unload. +// +// * **Developer-Ergonomic Tracebacks**. Whenever an exception is raised +// by a Jetpack-based extension, we want it to be logged in a +// place that is specific to that extension--so that a developer +// can distinguish it from an error on a web page or in another +// extension, for instance. We also want it to be logged with a +// full stack traceback, which the Mozilla platform doesn't usually +// do. +// +// Because of this, we don't actually want to give the Mozilla +// platform's "real" XHR implementation to clients, but instead provide +// a simple wrapper that trivially delegates to the implementation in +// all cases except where callbacks are involved: whenever Mozilla +// platform code calls into the extension, such as during the XHR's +// `onreadystatechange` callback, we want to wrap the client's callback +// in a try-catch clause that traps any exceptions raised by the +// callback and logs them via console.exception() instead of allowing +// them to propagate back into Mozilla platform code. + +// This is a private list of all active requests, so we know what to +// abort if we're asked to unload. +var requests = []; + +// Events on XHRs that we should listen for, so we know when to remove +// a request from our private list. +const TERMINATE_EVENTS = ["load", "error", "abort"]; + +// Read-only properties of XMLHttpRequest objects that we want to +// directly delegate to. +const READ_ONLY_PROPS = ["readyState", "responseText", "responseXML", + "status", "statusText"]; + +// Methods of XMLHttpRequest that we want to directly delegate to. +const DELEGATED_METHODS = ["abort", "getAllResponseHeaders", + "getResponseHeader", "overrideMimeType", + "send", "sendAsBinary", "setRequestHeader", + "open"]; + +var getRequestCount = exports.getRequestCount = function getRequestCount() { + return requests.length; +}; + +var XMLHttpRequest = exports.XMLHttpRequest = function XMLHttpRequest() { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + // For the sake of simplicity, don't tie this request to any UI. + req.mozBackgroundRequest = true; + + memory.track(req, "XMLHttpRequest"); + + this._req = req; + this._orsc = null; + + requests.push(this); + + var self = this; + + this._boundCleanup = function _boundCleanup() { + self._cleanup(); + }; + + TERMINATE_EVENTS.forEach( + function(name) { + self._req.addEventListener(name, self._boundCleanup, false); + }); +}; + +XMLHttpRequest.prototype = { + _cleanup: function _cleanup() { + this.onreadystatechange = null; + var index = requests.indexOf(this); + if (index != -1) { + var self = this; + TERMINATE_EVENTS.forEach( + function(name) { + self._req.removeEventListener(name, self._boundCleanup, false); + }); + requests.splice(index, 1); + } + }, + _unload: function _unload() { + this._req.abort(); + this._cleanup(); + }, + addEventListener: function addEventListener() { + throw new Error("not implemented"); + }, + removeEventListener: function removeEventListener() { + throw new Error("not implemented"); + }, + set upload(newValue) { + throw new Error("not implemented"); + }, + get onreadystatechange() { + return this._orsc; + }, + set onreadystatechange(cb) { + this._orsc = cb; + if (cb) { + var self = this; + this._req.onreadystatechange = function() { + try { + self._orsc.apply(self, arguments); + } catch (e) { + console.exception(e); + } + }; + } else + this._req.onreadystatechange = null; + } +}; + +READ_ONLY_PROPS.forEach( + function(name) { + XMLHttpRequest.prototype.__defineGetter__( + name, + function() { + return this._req[name]; + }); + }); + +DELEGATED_METHODS.forEach( + function(name) { + XMLHttpRequest.prototype[name] = function() { + return this._req[name].apply(this._req, arguments); + }; + }); + +unload(function() { + requests.slice().forEach(function(request) { request._unload(); }); +}); diff --git a/tools/addon-sdk-1.12/lib/sdk/notifications.js b/tools/addon-sdk-1.12/lib/sdk/notifications.js new file mode 100644 index 0000000..909552f --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/notifications.js @@ -0,0 +1,83 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 errors = require("./deprecated/errors"); + +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: function notificationClickObserved(subject, topic, data) { + if (topic === "alertclickcallback") + errors.catchAndLog(valOpts.onClick).call(exports, valOpts.data); + } + }; + function notifyWithOpts(notifyFn) { + notifyFn(valOpts.iconURL, valOpts.title, valOpts.text, !!clickObserver, + valOpts.data, clickObserver); + } + 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 " + valOpts.iconURL + + " does not exist. A default icon will be used instead."); + delete valOpts.iconURL; + notifyWithOpts(notify); + } + catch (err) { + notifyWithOpts(notifyUsingConsole); + } +}; + +function notifyUsingConsole(iconURL, title, text) { + title = title ? "[" + title + "]" : ""; + text = text || ""; + let str = [title, text].filter(function (s) s).join(" "); + console.log(str); +} + +function validateOptions(options) { + return apiUtils.validateOptions(options, { + data: { + is: ["string", "undefined"] + }, + iconURL: { + is: ["string", "undefined"] + }, + onClick: { + is: ["function", "undefined"] + }, + text: { + is: ["string", "undefined"] + }, + title: { + is: ["string", "undefined"] + } + }); +} diff --git a/tools/addon-sdk-1.12/lib/sdk/page-mod.js b/tools/addon-sdk-1.12/lib/sdk/page-mod.js new file mode 100644 index 0000000..8bd4e5f --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/page-mod.js @@ -0,0 +1,380 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "stable" +}; + +const observers = require('./deprecated/observer-service'); +const { Loader, validationAttributes } = require('./content/loader'); +const { Worker } = require('./content/worker'); +const { EventEmitter } = require('./deprecated/events'); +const { List } = require('./deprecated/list'); +const { Registry } = require('./util/registry'); +const { MatchPattern } = require('./page-mod/match-pattern'); +const { validateOptions : validate } = require('./deprecated/api-utils'); +const { Cc, Ci } = require('chrome'); +const { merge } = require('./util/object'); +const { readURISync } = require('./net/url'); +const { windowIterator } = require('./deprecated/window-utils'); +const { isBrowser, getFrames } = require('./window/utils'); +const { getTabs, getTabContentWindow, getTabForContentWindow, + getURI: getTabURI } = require('./tabs/utils'); +const { has, hasAny } = require('./util/array'); + +const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]. + getService(Ci.nsIStyleSheetService); + +const USER_SHEET = styleSheetService.USER_SHEET; + +const io = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); + +// Valid values for `attachTo` option +const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame']; + +// contentStyle* / contentScript* are sharing the same validation constraints, +// so they can be mostly reused, except for the messages. +const validStyleOptions = { + contentStyle: merge(Object.create(validationAttributes.contentScript), { + msg: 'The `contentStyle` option must be a string or an array of strings.' + }), + contentStyleFile: merge(Object.create(validationAttributes.contentScriptFile), { + msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' + }) +}; + +// rules registry +const RULES = {}; + +const Rules = EventEmitter.resolve({ toString: null }).compose(List, { + add: function() Array.slice(arguments).forEach(function onAdd(rule) { + if (this._has(rule)) + return; + // registering rule to the rules registry + if (!(rule in RULES)) + RULES[rule] = new MatchPattern(rule); + this._add(rule); + this._emit('add', rule); + }.bind(this)), + remove: function() Array.slice(arguments).forEach(function onRemove(rule) { + if (!this._has(rule)) + return; + this._remove(rule); + this._emit('remove', rule); + }.bind(this)), +}); + +/** + * PageMod constructor (exported below). + * @constructor + */ +const PageMod = Loader.compose(EventEmitter, { + on: EventEmitter.required, + _listeners: EventEmitter.required, + attachTo: [], + contentScript: Loader.required, + contentScriptFile: Loader.required, + contentScriptWhen: Loader.required, + contentScriptOptions: Loader.required, + include: null, + constructor: function PageMod(options) { + this._onContent = this._onContent.bind(this); + options = options || {}; + + let { contentStyle, contentStyleFile } = validate(options, validStyleOptions); + + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScriptOptions' in options) + this.contentScriptOptions = options.contentScriptOptions; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + if ('onAttach' in options) + this.on('attach', options.onAttach); + if ('onError' in options) + this.on('error', options.onError); + if ('attachTo' in options) { + if (typeof options.attachTo == 'string') + this.attachTo = [options.attachTo]; + else if (Array.isArray(options.attachTo)) + this.attachTo = options.attachTo; + else + throw new Error('The `attachTo` option must be a string or an array ' + + 'of strings.'); + + let isValidAttachToItem = function isValidAttachToItem(item) { + return typeof item === 'string' && + VALID_ATTACHTO_OPTIONS.indexOf(item) !== -1; + } + if (!this.attachTo.every(isValidAttachToItem)) + throw new Error('The `attachTo` option valid accept only following ' + + 'values: '+ VALID_ATTACHTO_OPTIONS.join(', ')); + if (!hasAny(this.attachTo, ["top", "frame"])) + throw new Error('The `attachTo` option must always contain at least' + + ' `top` or `frame` value'); + } + else { + this.attachTo = ["top", "frame"]; + } + + let include = options.include; + let rules = this.include = Rules(); + rules.on('add', this._onRuleAdd = this._onRuleAdd.bind(this)); + rules.on('remove', this._onRuleRemove = this._onRuleRemove.bind(this)); + + if (Array.isArray(include)) + rules.add.apply(null, include); + else + rules.add(include); + + let styleRules = ""; + + if (contentStyleFile) + styleRules = [].concat(contentStyleFile).map(readURISync).join(""); + + if (contentStyle) + styleRules += [].concat(contentStyle).join(""); + + if (styleRules) { + this._onRuleUpdate = this._onRuleUpdate.bind(this); + + this._styleRules = styleRules; + + this._registerStyleSheet(); + rules.on('add', this._onRuleUpdate); + rules.on('remove', this._onRuleUpdate); + } + + this.on('error', this._onUncaughtError = this._onUncaughtError.bind(this)); + pageModManager.add(this._public); + + this._loadingWindows = []; + + // `_applyOnExistingDocuments` has to be called after `pageModManager.add()` + // otherwise its calls to `_onContent` method won't do anything. + if ('attachTo' in options && has(options.attachTo, 'existing')) + this._applyOnExistingDocuments(); + }, + + destroy: function destroy() { + + this._unregisterStyleSheet(); + + this.include.removeListener('add', this._onRuleUpdate); + this.include.removeListener('remove', this._onRuleUpdate); + + for each (let rule in this.include) + this.include.remove(rule); + pageModManager.remove(this._public); + this._loadingWindows = []; + + }, + + _loadingWindows: [], + + _applyOnExistingDocuments: function _applyOnExistingDocuments() { + let mod = this; + // Returns true if the tab match one rule + function isMatchingURI(uri) { + // Use Array.some as `include` isn't a native array + return Array.some(mod.include, function (rule) { + return RULES[rule].test(uri); + }); + } + let tabs = getAllTabs().filter(function (tab) { + return isMatchingURI(getTabURI(tab)); + }); + + tabs.forEach(function (tab) { + // Fake a newly created document + let window = getTabContentWindow(tab); + if (has(mod.attachTo, "top")) + mod._onContent(window); + if (has(mod.attachTo, "frame")) + getFrames(window).forEach(mod._onContent); + }); + }, + + _onContent: function _onContent(window) { + // not registered yet + if (!pageModManager.has(this)) + return; + + let isTopDocument = window.top === window; + // Is a top level document and `top` is not set, ignore + if (isTopDocument && !has(this.attachTo, "top")) + return; + // Is a frame document and `frame` is not set, ignore + if (!isTopDocument && !has(this.attachTo, "frame")) + return; + + // Immediatly evaluate content script if the document state is already + // matching contentScriptWhen expectations + let state = window.document.readyState; + if ('start' === this.contentScriptWhen || + // Is `load` event already dispatched? + 'complete' === state || + // Is DOMContentLoaded already dispatched and waiting for it? + ('ready' === this.contentScriptWhen && state === 'interactive') ) { + this._createWorker(window); + return; + } + + let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded'; + let self = this; + window.addEventListener(eventName, function onReady(event) { + if (event.target.defaultView != window) + return; + window.removeEventListener(eventName, onReady, true); + + self._createWorker(window); + }, true); + }, + _createWorker: function _createWorker(window) { + let worker = Worker({ + window: window, + contentScript: this.contentScript, + contentScriptFile: this.contentScriptFile, + contentScriptOptions: this.contentScriptOptions, + onError: this._onUncaughtError + }); + this._emit('attach', worker); + let self = this; + worker.once('detach', function detach() { + worker.destroy(); + }); + }, + _onRuleAdd: function _onRuleAdd(url) { + pageModManager.on(url, this._onContent); + }, + _onRuleRemove: function _onRuleRemove(url) { + pageModManager.off(url, this._onContent); + }, + _onUncaughtError: function _onUncaughtError(e) { + if (this._listeners('error').length == 1) + console.exception(e); + }, + _onRuleUpdate: function _onRuleUpdate(){ + this._registerStyleSheet(); + }, + + _registerStyleSheet : function _registerStyleSheet() { + let rules = this.include; + let styleRules = this._styleRules; + + let documentRules = []; + + this._unregisterStyleSheet(); + + for each (let rule in rules) { + let pattern = RULES[rule]; + + if (!pattern) + continue; + + if (pattern.regexp) + documentRules.push("regexp(\"" + pattern.regexp.source + "\")"); + else if (pattern.exactURL) + documentRules.push("url(" + pattern.exactURL + ")"); + else if (pattern.domain) + documentRules.push("domain(" + pattern.domain + ")"); + else if (pattern.urlPrefix) + documentRules.push("url-prefix(" + pattern.urlPrefix + ")"); + else if (pattern.anyWebPage) { + documentRules.push("regexp(\"^(https?|ftp)://.*?\")"); + break; + } + } + + let uri = "data:text/css;charset=utf-8,"; + if (documentRules.length > 0) + uri += encodeURIComponent("@-moz-document " + + documentRules.join(",") + " {" + styleRules + "}"); + else + uri += encodeURIComponent(styleRules); + + this._registeredStyleURI = io.newURI(uri, null, null); + + styleSheetService.loadAndRegisterSheet( + this._registeredStyleURI, + USER_SHEET + ); + }, + + _unregisterStyleSheet : function () { + let uri = this._registeredStyleURI; + + if (uri && styleSheetService.sheetRegistered(uri, USER_SHEET)) + styleSheetService.unregisterSheet(uri, USER_SHEET); + + this._registeredStyleURI = null; + } +}); +exports.PageMod = function(options) PageMod(options) +exports.PageMod.prototype = PageMod.prototype; + +const PageModManager = Registry.resolve({ + constructor: '_init', + _destructor: '_registryDestructor' +}).compose({ + constructor: function PageModRegistry(constructor) { + this._init(PageMod); + observers.add( + 'document-element-inserted', + this._onContentWindow = this._onContentWindow.bind(this) + ); + }, + _destructor: function _destructor() { + observers.remove('document-element-inserted', this._onContentWindow); + this._removeAllListeners(); + for (let rule in RULES) { + delete RULES[rule]; + } + + // We need to do some cleaning er PageMods, like unregistering any + // `contentStyle*` + this._registry.forEach(function(pageMod) { + pageMod.destroy(); + }); + + this._registryDestructor(); + }, + _onContentWindow: function _onContentWindow(document) { + let window = document.defaultView; + // XML documents don't have windows, and we don't yet support them. + if (!window) + return; + // We apply only on documents in tabs of Firefox + if (!getTabForContentWindow(window)) + return; + + for (let rule in RULES) + if (RULES[rule].test(document.URL)) + this._emit(rule, window); + }, + off: function off(topic, listener) { + this.removeListener(topic, listener); + if (!this._listeners(topic).length) + delete RULES[topic]; + } +}); +const pageModManager = PageModManager(); + +// Returns all tabs on all currently opened windows +function getAllTabs() { + let tabs = []; + // Iterate over all chrome windows + for (let window in windowIterator()) { + if (!isBrowser(window)) + continue; + tabs = tabs.concat(getTabs(window)); + } + return tabs; +} diff --git a/tools/addon-sdk-1.12/lib/sdk/page-mod/match-pattern.js b/tools/addon-sdk-1.12/lib/sdk/page-mod/match-pattern.js new file mode 100644 index 0000000..5c42e66 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/page-mod/match-pattern.js @@ -0,0 +1,108 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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"); + +exports.MatchPattern = MatchPattern; + +function MatchPattern(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.ignoreCase) { + throw new Error("A RegExp match pattern cannot be set to `ignoreCase` " + + "(i.e. //i)."); + } + 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."); + } + } + } +} + +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; + if (this.domain && url.host && + url.host.slice(-this.domain.length) == this.domain) + return true; + if (this.urlPrefix && 0 == urlStr.indexOf(this.urlPrefix)) + return true; + + return false; + } + +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/page-worker.js b/tools/addon-sdk-1.12/lib/sdk/page-worker.js new file mode 100644 index 0000000..f667e4d --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/page-worker.js @@ -0,0 +1,71 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 { Symbiont } = require("./content/symbiont"); +const { Trait } = require("./deprecated/traits"); + +if (!require("./system/xul-app").isOneOf(["Firefox", "Thunderbird"])) { + throw new Error([ + "The page-worker module currently supports only Firefox and Thunderbird. ", + "In the future, we would like it to support other applications, however. ", + "Please see https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more ", + "information." + ].join("")); +} + +const Page = Trait.compose( + Symbiont.resolve({ + constructor: '_initSymbiont' + }), + { + _frame: Trait.required, + _initFrame: Trait.required, + postMessage: Symbiont.required, + on: Symbiont.required, + destroy: Symbiont.required, + + constructor: function Page(options) { + options = options || {}; + + this.contentURL = 'contentURL' in options ? options.contentURL + : 'about:blank'; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + 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 ('allow' in options) + this.allow = options.allow; + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + + this.on('propertyChange', this._onChange.bind(this)); + + this._initSymbiont(); + }, + + _onChange: function _onChange(e) { + if ('contentURL' in e && this._frame) { + // Cleanup the worker before injecting the content script in the new + // document + this._workerCleanup(); + this._initFrame(this._frame); + } + } + } +); +exports.Page = function(options) Page(options); +exports.Page.prototype = Page.prototype; diff --git a/tools/addon-sdk-1.12/lib/sdk/panel.js b/tools/addon-sdk-1.12/lib/sdk/panel.js new file mode 100644 index 0000000..3ce6284 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/panel.js @@ -0,0 +1,403 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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" +}; + +if (!require("./system/xul-app").is("Firefox")) { + throw new Error([ + "The panel module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps ", + "for more information." + ].join("")); +} + +const { Cc, Ci } = require("chrome"); + +const { validateOptions: valid } = require('./deprecated/api-utils'); +const { Symbiont } = require('./content/content'); +const { EventEmitter } = require('./deprecated/events'); +const timer = require('./timers'); +const runtime = require('./system/runtime'); +const { getMostRecentBrowserWindow } = require('./window/utils'); + +const windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + ON_SHOW = 'popupshown', + ON_HIDE = 'popuphidden', + validNumber = { is: ['number', 'undefined', 'null'] }; + +/** + * Emits show and hide events. + */ +const Panel = Symbiont.resolve({ + constructor: '_init', + _onInit: '_onSymbiontInit', + destroy: '_symbiontDestructor', + _documentUnload: '_workerDocumentUnload' +}).compose({ + _frame: Symbiont.required, + _init: Symbiont.required, + _onSymbiontInit: Symbiont.required, + _symbiontDestructor: Symbiont.required, + _emit: Symbiont.required, + on: Symbiont.required, + removeListener: Symbiont.required, + + _inited: false, + + /** + * If set to `true` frame loaders between xul panel frame and + * hidden frame are swapped. If set to `false` frame loaders are + * set back to normal. Setting the value that was already set will + * have no effect. + */ + set _frameLoadersSwapped(value) { + if (this.__frameLoadersSwapped == value) return; + this._frame.QueryInterface(Ci.nsIFrameLoaderOwner) + .swapFrameLoaders(this._viewFrame); + this.__frameLoadersSwapped = value; + }, + __frameLoadersSwapped: false, + + constructor: function Panel(options) { + this._onShow = this._onShow.bind(this); + this._onHide = this._onHide.bind(this); + this.on('inited', this._onSymbiontInit.bind(this)); + this.on('propertyChange', this._onChange.bind(this)); + + options = options || {}; + if ('onShow' in options) + this.on('show', options.onShow); + if ('onHide' in options) + this.on('hide', options.onHide); + if ('width' in options) + this.width = options.width; + if ('height' in options) + this.height = options.height; + if ('contentURL' in options) + this.contentURL = options.contentURL; + + this._init(options); + }, + _destructor: function _destructor() { + this.hide(); + this._removeAllListeners('show'); + this._removeAllListeners('hide'); + this._removeAllListeners('propertyChange'); + this._removeAllListeners('inited'); + // defer cleanup to be performed after panel gets hidden + this._xulPanel = null; + this._symbiontDestructor(this); + this._removeAllListeners(); + }, + destroy: function destroy() { + this._destructor(); + }, + /* Public API: Panel.width */ + get width() this._width, + set width(value) + this._width = valid({ $: value }, { $: validNumber }).$ || this._width, + _width: 320, + /* Public API: Panel.height */ + get height() this._height, + set height(value) + this._height = valid({ $: value }, { $: validNumber }).$ || this._height, + _height: 240, + + /* Public API: Panel.isShowing */ + get isShowing() !!this._xulPanel && this._xulPanel.state == "open", + + /* Public API: Panel.show */ + show: function show(anchor) { + anchor = anchor || null; + let document = getWindow(anchor).document; + let xulPanel = this._xulPanel; + if (!xulPanel) { + xulPanel = this._xulPanel = document.createElementNS(XUL_NS, 'panel'); + xulPanel.setAttribute("type", "arrow"); + + // One anonymous node has a big padding that doesn't work well with + // Jetpack, as we would like to display an iframe that completely fills + // the panel. + // -> Use a XBL wrapper with inner stylesheet to remove this padding. + let css = ".panel-inner-arrowcontent, .panel-arrowcontent {padding: 0;}"; + let originalXBL = "chrome://global/content/bindings/popup.xml#arrowpanel"; + let binding = + '<bindings xmlns="http://www.mozilla.org/xbl">' + + '<binding id="id" extends="' + originalXBL + '">' + + '<resources>' + + '<stylesheet src="data:text/css;charset=utf-8,' + + document.defaultView.encodeURIComponent(css) + '"/>' + + '</resources>' + + '</binding>' + + '</bindings>'; + xulPanel.style.MozBinding = 'url("data:text/xml;charset=utf-8,' + + document.defaultView.encodeURIComponent(binding) + '")'; + + let frame = document.createElementNS(XUL_NS, 'iframe'); + frame.setAttribute('type', 'content'); + frame.setAttribute('flex', '1'); + frame.setAttribute('transparent', 'transparent'); + if (runtime.OS === "Darwin") { + frame.style.borderRadius = "6px"; + frame.style.padding = "1px"; + } + + // Load an empty document in order to have an immediatly loaded iframe, + // so swapFrameLoaders is going to work without having to wait for load. + frame.setAttribute("src","data:;charset=utf-8,"); + + xulPanel.appendChild(frame); + document.getElementById("mainPopupSet").appendChild(xulPanel); + } + let { width, height } = this, x, y, position; + + if (!anchor) { + // Open the popup in the middle of the window. + x = document.documentElement.clientWidth / 2 - width / 2; + y = document.documentElement.clientHeight / 2 - height / 2; + position = null; + } + else { + // Open the popup by the anchor. + let rect = anchor.getBoundingClientRect(); + + let window = anchor.ownerDocument.defaultView; + + let zoom = window.mozScreenPixelsPerCSSPixel; + 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"; + position = 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) + xulPanel.setAttribute("flip","both"); + } + + // Resize the iframe instead of using panel.sizeTo + // because sizeTo doesn't work with arrow panels + xulPanel.firstChild.style.width = width + "px"; + xulPanel.firstChild.style.height = height + "px"; + + // Wait for the XBL binding to be constructed + function waitForBinding() { + if (!xulPanel.openPopup) { + timer.setTimeout(waitForBinding, 50); + return; + } + xulPanel.openPopup(anchor, position, x, y); + } + waitForBinding(); + + return this._public; + }, + /* Public API: Panel.hide */ + hide: function hide() { + // The popuphiding handler takes care of swapping back the frame loaders + // and removing the XUL panel from the application window, we just have to + // trigger it by hiding the popup. + // XXX Sometimes I get "TypeError: xulPanel.hidePopup is not a function" + // when quitting the host application while a panel is visible. To suppress + // them, this now checks for "hidePopup" in xulPanel before calling it. + // It's not clear if there's an actual issue or the error is just normal. + let xulPanel = this._xulPanel; + if (xulPanel && "hidePopup" in xulPanel) + xulPanel.hidePopup(); + return this._public; + }, + + /* Public API: Panel.resize */ + resize: function resize(width, height) { + this.width = width; + this.height = height; + // Resize the iframe instead of using panel.sizeTo + // because sizeTo doesn't work with arrow panels + let xulPanel = this._xulPanel; + if (xulPanel) { + xulPanel.firstChild.style.width = width + "px"; + xulPanel.firstChild.style.height = height + "px"; + } + }, + + // While the panel is visible, this is the XUL <panel> we use to display it. + // Otherwise, it's null. + get _xulPanel() this.__xulPanel, + set _xulPanel(value) { + let xulPanel = this.__xulPanel; + if (value === xulPanel) return; + if (xulPanel) { + xulPanel.removeEventListener(ON_HIDE, this._onHide, false); + xulPanel.removeEventListener(ON_SHOW, this._onShow, false); + xulPanel.parentNode.removeChild(xulPanel); + } + if (value) { + value.addEventListener(ON_HIDE, this._onHide, false); + value.addEventListener(ON_SHOW, this._onShow, false); + } + this.__xulPanel = value; + }, + __xulPanel: null, + get _viewFrame() this.__xulPanel.children[0], + /** + * When the XUL panel becomes hidden, we swap frame loaders back to move + * the content of the panel to the hidden frame & remove panel element. + */ + _onHide: function _onHide() { + try { + this._frameLoadersSwapped = false; + this._xulPanel = null; + this._emit('hide'); + } catch(e) { + this._emit('error', e); + } + }, + + /** + * Retrieve computed text color style in order to apply to the iframe + * document. As MacOS background is dark gray, we need to use skin's + * text color. + */ + _applyStyleToDocument: function _applyStyleToDocument() { + try { + let win = this._xulPanel.ownerDocument.defaultView; + let node = win.document.getAnonymousElementByAttribute( + this._xulPanel, "class", "panel-arrowcontent"); + if (!node) { + // Before bug 764755, anonymous content was different: + // TODO: Remove this when targeting FF16+ + node = win.document.getAnonymousElementByAttribute( + this._xulPanel, "class", "panel-inner-arrowcontent"); + } + let textColor = win.getComputedStyle(node).getPropertyValue("color"); + let doc = this._xulPanel.firstChild.contentDocument; + let style = doc.createElement("style"); + style.textContent = "body { color: " + textColor + "; }"; + let container = doc.head ? doc.head : doc.documentElement; + + if (container.firstChild) + container.insertBefore(style, container.firstChild); + else + container.appendChild(style); + } + catch(e) { + console.error("Unable to apply panel style"); + console.exception(e); + } + }, + + /** + * When the XUL panel becomes shown, we swap frame loaders between panel + * frame and hidden frame to preserve state of the content dom. + */ + _onShow: function _onShow() { + try { + if (!this._inited) { // defer if not initialized yet + this.on('inited', this._onShow.bind(this)); + } else { + this._frameLoadersSwapped = true; + this._applyStyleToDocument(); + this._emit('show'); + } + } catch(e) { + this._emit('error', e); + } + }, + /** + * Notification that panel was fully initialized. + */ + _onInit: function _onInit() { + this._inited = true; + + // Avoid panel document from resizing the browser window + // New platform capability added through bug 635673 + if ("allowWindowControl" in this._frame.docShell) + this._frame.docShell.allowWindowControl = false; + + // perform all deferred tasks like initSymbiont, show, hide ... + // TODO: We're publicly exposing a private event here; this + // 'inited' event should really be made private, somehow. + this._emit('inited'); + }, + + // Catch document unload event in order to rebind load event listener with + // Symbiont._initFrame if Worker._documentUnload destroyed the worker + _documentUnload: function(subject, topic, data) { + if (this._workerDocumentUnload(subject, topic, data)) { + this._initFrame(this._frame); + return true; + } + return false; + }, + + _onChange: function _onChange(e) { + if ('contentURL' in e && this._frame) { + // Cleanup the worker before injecting the content script in the new + // document + this._workerCleanup(); + this._initFrame(this._frame); + } + } +}); +exports.Panel = function(options) Panel(options) +exports.Panel.prototype = Panel.prototype; + +function getWindow(anchor) { + let window; + + if (anchor) { + let anchorWindow = anchor.ownerDocument.defaultView.top; + let anchorDocument = anchorWindow.document; + + let enumerator = windowMediator.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + let enumWindow = enumerator.getNext(); + + // Check if the anchor is in this browser window. + if (enumWindow == anchorWindow) { + window = anchorWindow; + break; + } + + // Check if the anchor is in a browser tab in this browser window. + let browser = enumWindow.gBrowser.getBrowserForDocument(anchorDocument); + if (browser) { + window = enumWindow; + break; + } + + // Look in other subdocuments (sidebar, etc.)? + } + } + + // If we didn't find the anchor's window (or we have no anchor), + // return the most recent browser window. + if (!window) + window = getMostRecentBrowserWindow(); + + return window; +} + diff --git a/tools/addon-sdk-1.12/lib/sdk/passwords.js b/tools/addon-sdk-1.12/lib/sdk/passwords.js new file mode 100644 index 0000000..7aeb22a --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/passwords.js @@ -0,0 +1,62 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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/tools/addon-sdk-1.12/lib/sdk/passwords/utils.js b/tools/addon-sdk-1.12/lib/sdk/passwords/utils.js new file mode 100644 index 0000000..8cc2230 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/passwords/utils.js @@ -0,0 +1,105 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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) + Object.keys(this).every(function(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) 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/tools/addon-sdk-1.12/lib/sdk/platform/xpcom.js b/tools/addon-sdk-1.12/lib/sdk/platform/xpcom.js new file mode 100644 index 0000000..279d234 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/platform/xpcom.js @@ -0,0 +1,229 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "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(function(id) iid.equals(Ci[id])) || + component.implements.some(function($) 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) 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() 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() this.component +}); +exports.Service = Service; + +function isRegistered({ id }) 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://addons.mozilla.org/en-US/developers/docs/sdk/latest/packages/api-utils/xpcom.html"); + } + + 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) 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) factoryByID(Cm.contractIDToCID(contract)) +exports.factoryByContract = factoryByContract; diff --git a/tools/addon-sdk-1.12/lib/sdk/preferences/event-target.js b/tools/addon-sdk-1.12/lib/sdk/preferences/event-target.js new file mode 100644 index 0000000..fe9e07a --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/preferences/event-target.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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/tools/addon-sdk-1.12/lib/sdk/preferences/service.js b/tools/addon-sdk-1.12/lib/sdk/preferences/service.js new file mode 100644 index 0000000..4303fc1 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/preferences/service.js @@ -0,0 +1,174 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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); + +function Branch(branchName) { + function getPrefKeys() { + return keys(branchName).map(function(key) { + return key.replace(branchName, ""); + }); + } + + return Proxy.create({ + get: function(receiver, pref) { + return get(branchName + pref); + }, + set: function(receiver, pref, val) { + set(branchName + pref, val); + }, + delete: function(pref) { + reset(branchName + pref); + return true; + }, + has: function hasPrefKey(pref) { + return has(branchName + pref) + }, + getPropertyDescriptor: function(name) { + return { + value: get(branchName + name) + }; + }, + enumerate: getPrefKeys, + keys: getPrefKeys + }, Branch.prototype); +} + +function get(name, defaultValue) { + switch (prefSvc.getPrefType(name)) { + case Ci.nsIPrefBranch.PREF_STRING: + return prefSvc.getComplexValue(name, Ci.nsISupportsString).data; + + case Ci.nsIPrefBranch.PREF_INT: + return prefSvc.getIntPref(name); + + case Ci.nsIPrefBranch.PREF_BOOL: + return prefSvc.getBoolPref(name); + + case Ci.nsIPrefBranch.PREF_INVALID: + return defaultValue; + + default: + // This should never happen. + throw new Error("Error getting pref " + name + + "; its value's type is " + + prefSvc.getPrefType(name) + + ", which I don't know " + + "how to handle."); + } +} +exports.get = get; + +function set(name, value) { + var prefType; + if (typeof value != "undefined" && value != null) + prefType = value.constructor.name; + + switch (prefType) { + case "String": + { + var string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = value; + prefSvc.setComplexValue(name, Ci.nsISupportsString, string); + } + break; + + case "Number": + // We throw if the number is outside the range or not an integer, since + // the result will not be what the consumer wanted to store. + if (value > MAX_INT || value < MIN_INT) + throw new Error("you cannot set the " + name + + " pref to the number " + value + + ", as number pref values must be in the signed " + + "32-bit integer range -(2^31) to 2^31-1. " + + "To store numbers outside that range, store " + + "them as strings."); + if (value % 1 != 0) + throw new Error("cannot store non-integer number: " + value); + prefSvc.setIntPref(name, value); + break; + + case "Boolean": + prefSvc.setBoolPref(name, value); + break; + + default: + throw new Error("can't set pref " + name + " to value '" + value + + "'; it isn't a string, integer, or boolean"); + } +} +exports.set = set; + +function has(name) { + return (prefSvc.getPrefType(name) != Ci.nsIPrefBranch.PREF_INVALID); +} +exports.has = has; + +function keys(root) { + return prefSvc.getChildList(root); +} +exports.keys = keys; + +function isSet(name) { + return (has(name) && prefSvc.prefHasUserValue(name)); +} +exports.isSet = isSet; + +function reset(name) { + try { + prefSvc.clearUserPref(name); + } catch (e if e.result == Cr.NS_ERROR_UNEXPECTED) { + // 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. + } +} +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/tools/addon-sdk-1.12/lib/sdk/private-browsing.js b/tools/addon-sdk-1.12/lib/sdk/private-browsing.js new file mode 100644 index 0000000..6d2a709 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/private-browsing.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 { setMode, getMode, on: onStateChange } = require('./private-browsing/utils'); +const { emit, on, once, off } = require('./event/core'); +const { when: unload } = require('./system/unload'); +const observers = require('./deprecated/observer-service'); + +onStateChange('start', function onStart() { + emit(exports, 'start'); +}); + +onStateChange('stop', function onStop() { + emit(exports, 'stop'); +}); + +// Make sure listeners are cleaned up. +unload(function() off(exports)); + +Object.defineProperty(exports, "isActive", { get: function() getMode() }); +exports.activate = function activate() setMode(true); +exports.deactivate = function deactivate() setMode(false); +exports.on = on.bind(null, exports); +exports.once = once.bind(null, exports); +exports.removeListener = function removeListener(type, listener) { + // Note: We can't just bind `off` as we do it for other methods cause skipping + // a listener argument will remove all listeners for the given event type + // causing misbehavior. This way we make sure all arguments are passed. + off(exports, type, listener); +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/private-browsing/utils.js b/tools/addon-sdk-1.12/lib/sdk/private-browsing/utils.js new file mode 100644 index 0000000..1f57ca0 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/private-browsing/utils.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 { Cc, Ci } = require('chrome'); +const { defer } = require('../lang/functional'); +const observers = require('../deprecated/observer-service'); +const { emit, on, once, off } = require('../event/core'); +const { when: unload } = require('../system/unload'); +const { getWindowLoadingContext } = require('../window/utils'); +const { deprecateFunction } = require('../util/deprecate'); + +let deferredEmit = defer(emit); + +let pbService; + +// Currently, only Firefox implements the private browsing service. +if (require("../system/xul-app").is("Firefox")) { + pbService = Cc["@mozilla.org/privatebrowsing;1"]. + getService(Ci.nsIPrivateBrowsingService); + + // set up an observer for private browsing switches. + observers.add('private-browsing-transition-complete', function onChange() { + // Emit event with in next turn of event loop. + deferredEmit(exports, pbService.privateBrowsingEnabled ? 'start' : 'stop'); + }); +} + +// checks that per-window private browsing implemented +let isWindowPBEnabled = function isWindowPBEnabled(chromeWin) { + return !!(chromeWin && + 'gPrivateBrowsingUI' in chromeWin && + 'privateWindow' in chromeWin.gPrivateBrowsingUI); +} +exports.isWindowPBEnabled = isWindowPBEnabled; + +// We toggle private browsing mode asynchronously in order to work around +// bug 659629. Since private browsing transitions are asynchronous +// anyway, this doesn't significantly change the behavior of the API. +// Note: this method should not be used with `chromeWin` argument until +// the UI has been implemented. Bug 729865 +let setMode = defer(function setMode(value, chromeWin) { + value = !!value; // Cast to boolean. + + if (isWindowPBEnabled(chromeWin)) + return getWindowLoadingContext(chromeWin).usePrivateBrowsing = value; + + // default + return pbService && (pbService.privateBrowsingEnabled = value); +}); +exports.setMode = deprecateFunction( + setMode, + 'require("private-browsing").activate and require("private-browsing").deactivate ' + + 'is deprecated.' +); + +let getMode = function getMode(chromeWin) { + if (isWindowPBEnabled(chromeWin)) + return getWindowLoadingContext(chromeWin).usePrivateBrowsing; + + // default + return pbService ? pbService.privateBrowsingEnabled : false; +}; +exports.getMode = getMode; + +exports.on = on.bind(null, exports); + +// Make sure listeners are cleaned up. +unload(function() off(exports)); diff --git a/tools/addon-sdk-1.12/lib/sdk/querystring.js b/tools/addon-sdk-1.12/lib/sdk/querystring.js new file mode 100644 index 0000000..e0e2273 --- /dev/null +++ b/tools/addon-sdk-1.12/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" +}; + +let 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/tools/addon-sdk-1.12/lib/sdk/request.js b/tools/addon-sdk-1.12/lib/sdk/request.js new file mode 100644 index 0000000..da5be7f --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/request.js @@ -0,0 +1,213 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 } = require("./net/xhr"); +const apiUtils = require("./deprecated/api-utils"); + +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: { + //XXXzpao should probably verify that url is a valid url as well + is: ["string"] + }, + headers: { + map: function (v) v || {}, + is: ["object"], + }, + content: { + map: function (v) v || null, + is: ["string", "object", "null"], + }, + contentType: { + map: function (v) v || "application/x-www-form-urlencoded", + is: ["string"], + }, + overrideMimeType: { + map: function(v) v || null, + is: ["string", "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 GET and +// POST +function runRequest(mode, target) { + let source = request(target) + let { xhr, url, content, contentType, headers, overrideMimeType } = source; + + // 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(); + + // Build the data to be set. For GET 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 (mode == "GET" && data) + url = url + (/\?/.test(url) ? "&" : "?") + data; + + // open the request + xhr.open(mode, url); + + // 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 requests. + xhr.send(mode !== "GET" ? 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 response() { return request(this).response; }, + get: function() { + runRequest('GET', this); + return this; + }, + post: function() { + runRequest('POST', this); + return this; + }, + put: function() { + runRequest('PUT', this); + return this; + } +}); +exports.Request = Request; + +const Response = Class({ + initialize: function initialize(request) { + response(this).request = request; + }, + get text() 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() response(this).request.status, + get statusText() 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; + } +}); + +// 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/tools/addon-sdk-1.12/lib/sdk/selection.js b/tools/addon-sdk-1.12/lib/sdk/selection.js new file mode 100644 index 0000000..da94efe --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/selection.js @@ -0,0 +1,481 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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" +}; + +if (!require("./system/xul-app").is("Firefox")) { + throw new Error([ + "The selection module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +let { Ci, Cc } = require("chrome"), + { setTimeout } = require("./timers"), + { emit, off } = require("./event/core"), + { Unknown } = require("./platform/xpcom"), + { Class, obscure } = require("./core/heritage"), + { EventTarget } = require("./event/target"), + observers = require("./deprecated/observer-service"), + { ns } = require("./core/namespace"); + +// 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. + +let selections = ns(); + +observers.add("document-shown", function (document) { + var window = document.defaultView; + + // We are not interested in documents without valid defaultView + if (!window) + return; + + let selection = selections(window).selection; + + // We want to handle only the windows where we added selection's listeners + if (selection) { + let currentSelection = window.getSelection(); + + // 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) { + + currentSelection.addSelectionListener(SelectionListenerManager); + selections(window).selection = currentSelection; + } + } +}); + +const windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + +// The selection type HTML +const HTML = 0x01; + +// The selection type TEXT +const TEXT = 0x02; + +// The selection type DOM (internal use only) +const DOM = 0x03; + +// 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 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(value, this.rangeNumber); }, + get html() { return getSelection(HTML, this.rangeNumber); }, + set html(value) { setSelection(value, this.rangeNumber); }, + get isContiguous() { + let selection = getSelection(DOM); + + // If there are multiple ranges, the selection is definitely discontiguous. + // It returns `false` also if there are no selection; and `true` if there is + // a single non empty range, or a selection in a text field - contiguous or + // not (text field selection APIs doesn't support multiple selections). + + if (selection.rangeCount > 1) + return false; + + return !!(safeGetRange(selection, 0) || getElementWithSelection()); + } +}); + +/** + * Returns the most recent content window + */ +function context() { + // Overlay names should probably go into the xul-app module instead of here + return windowMediator.getMostRecentWindow("navigator:browser").document. + commandDispatcher.focusedWindow; +} + +/** + * 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 = context(); + 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(window); + + 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."); +} + +/** + * 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. + */ +function safeGetRange(selection, rangeNumber) { + try { + let range = selection.getRangeAt(rangeNumber); + if (!range || range.toString() == "") + return 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) + * + * @param {nsIWindow} [window] + * A reference to a window + */ +function getElementWithSelection(window) { + let element; + + try { + element = (window || context()).document.activeElement; + } + catch (e) { + element = null; + } + + if (!element) + return null; + + let { value, selectionStart, selectionEnd } = element; + + let hasSelection = typeof value === "string" && + !isNaN(selectionStart) && + !isNaN(selectionEnd) && + selectionStart !== selectionEnd; + + return hasSelection ? element : null; +} +/** + * 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(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 = context(); + selection = window.getSelection(); + } + catch (e) { + throw new Error(ERR_CANNOT_CHANGE_SELECTION); + } + + let range = safeGetRange(selection, rangeNumber); + + if (range) { + // Get rid of the current selection and insert our own + range.deleteContents(); + let node = window.document.createElement("span"); + range.surroundContents(node); + + // Some relevant JEP-111 requirements: + + // Setting the text property replaces the selection with the value to + // which the property is set and sets the html property to the same value + // to which the text property is being set. + + // Setting the html property replaces the selection with the value to + // which the property is set and sets the text property to the text version + // of the HTML value. + + // This sets both the HTML and text properties. + node.innerHTML = val; + } else { + let node = getElementWithSelection(window); + + 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); + } +} + +function onLoad(event) { + SelectionListenerManager.onLoad(event); +} + +function onUnload(event) { + SelectionListenerManager.onUnload(event); +} + +function onSelect() { + SelectionListenerManager.onSelect(); +} + +let SelectionListenerManager = Class({ + extends: Unknown, + interfaces: [ 'nsISelectionListener' ], + /** + * This is the nsISelectionListener implementation. This function is called + * by Gecko when a selection is changed interactively. + * + * We only pay attention to the SELECTALL, KEYPRESS, and MOUSEUP selection + * reasons. All reasons are listed here: + * + * http://mxr.mozilla.org/mozilla1.9.2/source/content/base/public/ + * nsISelectionListener.idl + * + * The other reasons (NO_REASON, DRAG_REASON, MOUSEDOWN_REASON) aren't + * applicable to us. + */ + notifySelectionChanged: function notifySelectionChanged(document, selection, + reason) { + if (!["SELECTALL", "KEYPRESS", "MOUSEUP"].some(function(type) reason & + Ci.nsISelectionListener[type + "_REASON"]) || selection.toString() == "") + return; + + this.onSelect(); + }, + + onSelect : function onSelect() { + setTimeout(emit, 0, module.exports, "select"); + }, + + /** + * Part of the Tracker implementation. This function is called by the + * tabs module when a browser is being tracked. Often, that means a new tab + * has been opened, but it can also mean an addon has been installed while + * tabs are already opened. In that case, this function is called for those + * already-opened tabs. + * + * @param browser + * The browser being tracked + */ + onTrack: function onTrack(browser) { + browser.addEventListener("load", onLoad, true); + browser.addEventListener("unload", onUnload, true); + }, + + onLoad: function onLoad(event) { + // Nothing to do without a useful window + let window = event.target.defaultView; + if (!window) + return; + + // Wrap the add selection call with some number of setTimeout 0 because some + // reason it's possible to add a selection listener "too early". 2 sometimes + // works for gmail, and more consistently with 3, so make it 5 to be safe. + let count = 0; + let self = this; + function wrap(count, func) { + if (count-- > 0) + setTimeout(wrap, 0); + else + self.addSelectionListener(window); + } + wrap(); + }, + + addSelectionListener: function addSelectionListener(window) { + // Don't add the selection's listener more than once to the same window. + if ("selection" in selections(window)) + return; + + let selection = window.getSelection(); + + // We ensure that the current selection is an instance of + // `nsISelectionPrivate` before working on it, in case is `null`. + if (selection instanceof Ci.nsISelectionPrivate) { + selection.addSelectionListener(this); + + // 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", onSelect, true); + + selections(window).selection = selection; + } + }, + + onUnload: function onUnload(event) { + // Nothing to do without a useful window + let window = event.target.defaultView; + if (!window) + return; + this.removeSelectionListener(window); + off(exports); + }, + + removeSelectionListener: 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(); + + // We ensure that the current selection is an instance of + // `nsISelectionPrivate` before working on it, in case is `null`. + if (selection instanceof Ci.nsISelectionPrivate) { + selection.removeSelectionListener(this); + + window.removeEventListener("select", onSelect); + + delete selections(window).selection; + } + }, + + /** + * Part of the TabTracker implementation. This function is called by the + * tabs module when a browser is being untracked. Usually, that means a tab + * has been closed. + * + * @param browser + * The browser being untracked + */ + onUntrack: function onUntrack(browser) { + browser.removeEventListener("load", onLoad, true); + browser.removeEventListener("unload", onUnload, true); + } +})(); + +/** + * Install |SelectionListenerManager| as tab tracker in order to watch + * tab opening/closing + */ +require("./deprecated/tab-browser").Tracker(SelectionListenerManager); + +var SelectionIterator = Class(obscure({ + /** + * Exports an iterator so that discontiguous selections can be iterated. + * + * 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. + */ + __iterator__: function() { + let selection = getSelection(DOM); + let count = selection.rangeCount || (getElementWithSelection() ? 1 : 0); + + for (let i = 0; i < count; i++) + yield Selection(i); + } +})); + +var selection = Class({ + extends: EventTarget, + implements: [ Selection, SelectionIterator ] +})(0); + +module.exports = selection; diff --git a/tools/addon-sdk-1.12/lib/sdk/self.js b/tools/addon-sdk-1.12/lib/sdk/self.js new file mode 100644 index 0000000..f9fca1e --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/self.js @@ -0,0 +1,36 @@ +/* vim:st=2:sts=2:sw=2: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 } = require('chrome'); +const { id, name, prefixURI, rootURI, + version, loadReason } = require('@loader/options'); + +const { readURISync } = require('./net/url'); + +const addonDataURI = prefixURI + name + '/data/'; + +function uri(path) { + return addonDataURI + (path || ''); +} + + +// 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.name = name; +exports.loadReason = loadReason; +exports.version = version; +// If `rootURI` is jar:file://...!/ than add-on is packed. +exports.packed = rootURI.indexOf('jar:') === 0 +exports.data = Object.freeze({ + url: uri, + load: function read(path) { + return readURISync(uri(path)); + } +}); diff --git a/tools/addon-sdk-1.12/lib/sdk/simple-prefs.js b/tools/addon-sdk-1.12/lib/sdk/simple-prefs.js new file mode 100644 index 0000000..5cbde49 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/simple-prefs.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": "experimental" +}; + +const { emit, off } = require("./event/core"); +const { when: unload } = require("./system/unload"); +const { PrefsTarget } = require("./preferences/event-target"); +const { id } = require("./self"); +const observers = require("./deprecated/observer-service"); + +const ADDON_BRANCH = "extensions." + id + "."; +const BUTTON_PRESSED = id + "-cmdPressed"; + +const target = PrefsTarget({ branchName: ADDON_BRANCH }); + +// Listen to clicks on buttons +function buttonClick(subject, data) { + emit(target, data); +} +observers.add(BUTTON_PRESSED, buttonClick); + +// Make sure we cleanup listeners on unload. +unload(function() { + observers.remove(BUTTON_PRESSED, buttonClick); +}); + +module.exports = target; diff --git a/tools/addon-sdk-1.12/lib/sdk/simple-storage.js b/tools/addon-sdk-1.12/lib/sdk/simple-storage.js new file mode 100644 index 0000000..2b88cc1 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/simple-storage.js @@ -0,0 +1,237 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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. +let 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/tools/addon-sdk-1.12/lib/sdk/system.js b/tools/addon-sdk-1.12/lib/sdk/system.js new file mode 100644 index 0000000..1e374d3 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/system.js @@ -0,0 +1,155 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 file = require('./io/file'); +const runtime = require("./system/runtime"); + +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. + */ +exports.exit = function exit(code) { + // This is used by 'cfx' to find out exit code. + if ('resultFile' in options && options.resultFile) { + 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(); + } + + appStartup.quit(code ? E_ATTEMPT : E_FORCE); +}; + +exports.stdout = new function() { + let write = dump + if ('logFile' in options && options.logFile) { + let mode = PR_WRONLY | PR_CREATE_FILE | PR_APPEND; + let stream = openFile(options.logFile, mode); + write = function write(data) { + let text = String(data); + stream.write(text, text.length); + stream.flush(); + } + } + return Object.freeze({ write: write }); +}; + +/** + * 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/Code_snippets/File_I%2F%2FO#Getting_special_files + * http://mxr.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(); + +/** + * What processor architecture you're running on: + * `'arm', 'ia32', or 'x64'`. + */ +exports.architecture = runtime.XPCOMABI.split('_')[0]; + +/** + * What compiler used for build: + * `'msvc', 'n32', 'gcc2', 'gcc3', 'sunc', 'ibmc'...` + */ +exports.compiler = runtime.XPCOMABI.split('_')[1]; + +/** + * 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/tools/addon-sdk-1.12/lib/sdk/system/environment.js b/tools/addon-sdk-1.12/lib/sdk/system/environment.js new file mode 100644 index 0000000..045bab1 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/system/environment.js @@ -0,0 +1,58 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 = Proxy.create({ + // XPCOM does not provides a way to enumerate environment variables, so we + // just don't support enumeration. + getPropertyNames: function() [], + getOwnPropertyNames: function() [], + enumerate: function() [], + keys: function() [], + // We do not support freezing, cause it would make it impossible to set new + // environment variables. + fix: function() undefined, + // We present all environment variables as own properties of this object, + // so we just delegate this call to `getOwnPropertyDescriptor`. + getPropertyDescriptor: function(name) this.getOwnPropertyDescriptor(name), + // If environment variable with this name is defined, we generate proprety + // descriptor for it, otherwise fall back to `undefined` so that for consumer + // this property does not exists. + getOwnPropertyDescriptor: function(name) { + return !exists(name) ? undefined : { + value: get(name), + enumerable: false, // Non-enumerable as we don't support enumeration. + configurable: true, // Configurable as it may be deleted. + writable: true // Writable as we do support set. + } + }, + + // New environment variables can be defined just by defining properties + // on this object. + defineProperty: function(name, { value }) set(name, value), + delete: function(name) set(name, null), + + // We present all properties as own, there for we just delegate to `hasOwn`. + has: function(name) this.hasOwn(name), + // We do support checks for existence of an environment variable, via `in` + // operator on this object. + hasOwn: function(name) exists(name), + + // On property get / set we do read / write appropriate environment variables, + // please note though, that variables with names of standard object properties + // intentionally (so that this behaves as normal object) can not be + // read / set. + get: function(proxy, name) Object.prototype[name] || get(name) || undefined, + set: function(proxy, name, value) Object.prototype[name] || set(name, value) +}); diff --git a/tools/addon-sdk-1.12/lib/sdk/system/events.js b/tools/addon-sdk-1.12/lib/sdk/system/events.js new file mode 100644 index 0000000..a75dddc --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/system/events.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 { Cc, Ci } = require('chrome'); +const { Unknown } = require('../platform/xpcom'); +const { Class } = require('../core/heritage'); +const { ns } = require('../core/namespace'); +const { addObserver, removeObserver, notifyObservers } = + Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); + +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 + }; + }, + getHelperForLanguage: function() {}, + getInterfaces: function() {} +}); + +function emit(type, event) { + let subject = 'subject' in event ? Subject(event.subject) : null; + let data = 'data' in event ? event.data : null; + notifyObservers(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) { + // 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; + addObserver(observer, type, weak); + } +} +exports.on = on; + +function once(type, listener) { + // 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); + on(type, function cleanup() { + off(type, listener); + off(type, cleanup); + }, true); +} +exports.once = once; + +function off(type, listener) { + // 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]; + removeObserver(observer, type); + } +} +exports.off = off; diff --git a/tools/addon-sdk-1.12/lib/sdk/system/globals.js b/tools/addon-sdk-1.12/lib/sdk/system/globals.js new file mode 100644 index 0000000..df77295 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/system/globals.js @@ -0,0 +1,54 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +let { Cc, Ci, CC } = require('chrome'); +let { PlainTextConsole } = require('../console/plain-text'); +let { stdout } = require('../system'); +let ScriptError = CC('@mozilla.org/scripterror;1', 'nsIScriptError'); +let consoleService = Cc['@mozilla.org/consoleservice;1'].getService(). + QueryInterface(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; + +// Bug 718230: We need to send console messages to stdout and JS Console +function forsakenConsoleDump(msg, level) { + stdout.write(msg); + + if (level === 'error') { + let error = ScriptError(); + msg = msg.replace(/^error: /, ''); + error.init(msg, null, null, 0, 0, 0, 'Add-on SDK'); + consoleService.logMessage(error); + } + else + consoleService.logStringMessage(msg); +}; +exports.console = new PlainTextConsole(forsakenConsoleDump); + +// 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); + } + } +}); diff --git a/tools/addon-sdk-1.12/lib/sdk/system/runtime.js b/tools/addon-sdk-1.12/lib/sdk/system/runtime.js new file mode 100644 index 0000000..e64b663 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/system/runtime.js @@ -0,0 +1,19 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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.XPCOMABI = runtime.XPCOMABI; diff --git a/tools/addon-sdk-1.12/lib/sdk/system/unload.js b/tools/addon-sdk-1.12/lib/sdk/system/unload.js new file mode 100644 index 0000000..4d2575b --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/system/unload.js @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Parts of this module were taken from narwhal: +// +// http://narwhaljs.org + +module.metadata = { + "stability": "experimental" +}; + +const { on, off } = require('./events'); +const unloadSubject = require('@loader/unload'); + +const observers = []; +const unloaders = []; + +var when = exports.when = function when(observer) { + if (observers.indexOf(observer) != -1) + return; + 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 { + 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/tools/addon-sdk-1.12/lib/sdk/system/xul-app.js b/tools/addon-sdk-1.12/lib/sdk/system/xul-app.js new file mode 100644 index 0000000..6e8b4e8 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/system/xul-app.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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"); + +var appInfo = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULAppInfo); + +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. +// This mapping is duplicated in `app-extensions/bootstrap.js`. They should keep +// in sync, so if you change one, change the other too! + +var ids = exports.ids = { + Firefox: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + Mozilla: "{86c18b42-e466-45a9-ae7a-9b95ba6f5640}", + Sunbird: "{718e30fb-e89b-41dd-9da7-e25a45638b28}", + SeaMonkey: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + Fennec: "{aa3c5121-dab2-40e2-81ca-7ea25febc110}", + Thunderbird: "{3550f703-e582-4d05-9a08-453d09bdfdc6}" +}; + +var is = exports.is = function is(name) { + if (!(name in ids)) + throw new Error("Unkown Mozilla Application: " + name); + return ID == ids[name]; +}; + +var isOneOf = exports.isOneOf = function isOneOf(names) { + for (var i = 0; i < names.length; i++) + if (is(names[i])) + return true; + return false; +}; + +/** + * 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) { + var vc = Cc["@mozilla.org/xpcom/version-comparator;1"] + .getService(Ci.nsIVersionComparator); + return (vc.compare(version, lowInclusive) >= 0) && + (vc.compare(version, highExclusive) < 0); +} + diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs.js b/tools/addon-sdk-1.12/lib/sdk/tabs.js new file mode 100644 index 0000000..f006073 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs.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'; + +module.metadata = { + 'stability': 'stable' +}; + +module.exports = require('./tabs/tabs'); diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/common.js b/tools/addon-sdk-1.12/lib/sdk/tabs/common.js new file mode 100644 index 0000000..dd5bb22 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/common.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +const { validateOptions } = require('../deprecated/api-utils'); + +function Options(options) { + if ('string' === typeof options) + options = { url: options }; + + return validateOptions(options, { + url: { is: ["string"] }, + inBackground: { is: ["undefined", "boolean"] }, + isPinned: { is: ["undefined", "boolean"] }, + onOpen: { is: ["undefined", "function"] }, + onClose: { is: ["undefined", "function"] }, + onReady: { is: ["undefined", "function"] }, + onActivate: { is: ["undefined", "function"] }, + onDeactivate: { is: ["undefined", "function"] } + }); +} +exports.Options = Options; diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/events.js b/tools/addon-sdk-1.12/lib/sdk/tabs/events.js new file mode 100644 index 0000000..eb99f40 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/events.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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", + 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: ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1), + dom: EVENTS[name] + } +}); diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/helpers.js b/tools/addon-sdk-1.12/lib/sdk/tabs/helpers.js new file mode 100644 index 0000000..aaff5cb --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/helpers.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 { getTabForContentWindow } = require('./utils'); +const { Tab } = require('./tab'); + +function getTabForWindow(win) { + let tab = getTabForContentWindow(win); + // We were unable to find the related tab! + if (!tab) + return null; + + return Tab({ tab: tab }); +} +exports.getTabForWindow = getTabForWindow; diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/namespace.js b/tools/addon-sdk-1.12/lib/sdk/tabs/namespace.js new file mode 100644 index 0000000..d359cee --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/namespace.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'; + +let { ns } = require('../core/namespace'); + +exports.tabsNS = ns(); +exports.tabNS = ns(); diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/observer.js b/tools/addon-sdk-1.12/lib/sdk/tabs/observer.js new file mode 100644 index 0000000..19a048c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/observer.js @@ -0,0 +1,99 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 { EventEmitterTrait: EventEmitter } = require("../deprecated/events"); +const { DOMEventAssembler } = require("../deprecated/events/assembler"); +const { Trait } = require("../deprecated/light-traits"); +const { getActiveTab, getTabs, getTabContainer } = require("./utils"); +const { browserWindowIterator } = require("../deprecated/window-utils"); +const { isBrowser } = require('../window/utils'); +const { observer: windowObserver } = require("../windows/observer"); + +const EVENTS = { + "TabOpen": "open", + "TabClose": "close", + "TabSelect": "select", + "TabMove": "move", + "TabPinned": "pinned", + "TabUnpinned": "unpinned" +}; + + +// Event emitter objects used to register listeners and emit events on them +// when they occur. +const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ + /** + * Method is implemented by `EventEmitter` and is used just for emitting + * events on registered listeners. + */ + _emit: Trait.required, + /** + * 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) { + this._emit(EVENTS[event.type], event.target, event); + } +}); + +// 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. +var selectedTab = null; +function onTabSelect(tab) { + if (selectedTab !== tab) { + if (selectedTab) observer._emit('deactivate', selectedTab); + if (tab) observer._emit('activate', selectedTab = tab); + } +}; +observer.on('select', onTabSelect); + +// We also observe opening / closing windows in order to add / remove it's +// containers to the observed list. +function onWindowOpen(chromeWindow) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + observer.observe(getTabContainer(chromeWindow)); +} +windowObserver.on("open", onWindowOpen); + +function onWindowClose(chromeWindow) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + // Bug 751546: Emit `deactivate` event on window close immediatly + // Otherwise we are going to face "dead object" exception on `select` event + if (getActiveTab(chromeWindow) == selectedTab) { + observer._emit("deactivate", selectedTab); + selectedTab = null; + } + observer.ignore(getTabContainer(chromeWindow)); +} +windowObserver.on("close", onWindowClose); + + +// 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", function onWindowActivate(chromeWindow) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + observer._emit("select", getActiveTab(chromeWindow)); +}); + +// We should synchronize state, since probably we already have at least one +// window open. +for each (let window in browserWindowIterator()) onWindowOpen(window); + +exports.observer = observer; diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/tab-fennec.js b/tools/addon-sdk-1.12/lib/sdk/tabs/tab-fennec.js new file mode 100644 index 0000000..42bf91b --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/tab-fennec.js @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 } = require('./namespace'); +const { EventTarget } = require('../event/target'); +const { activateTab, getTabTitle, setTabTitle, closeTab, getTabURL, + setTabURL, getOwnerWindow, getTabContentType } = require('./utils'); +const { emit } = require('../event/core'); +const { when: unload } = require('../system/unload'); + +const { EVENTS } = require('./events'); +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 }; + + EventTarget.prototype.initialize.call(this, options); + let tabInternals = tabNS(this); + + tabInternals.window = options.window || getOwnerWindow(options.tab); + tabInternals.tab = options.tab; + }, + + /** + * The title of the page currently loaded in the tab. + * Changing this property changes an actual title. + * @type {String} + */ + get title() 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() getTabURL(tabNS(this).tab), + set url(url) setTabURL(tabNS(this).tab, url), + + /** + * URI of the favicon for the page currently loaded in this tab. + * @type {String} + */ + get favicon() { + // TODO: provide the real favicon when it is available + console.error(ERR_FENNEC_MSG); + + // return 16x16 blank default + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAEklEQVQ4jWNgGAWjYBSMAggAAAQQAAF/TXiOAAAAAElFTkSuQmCC'; + }, + + getThumbnail: function() { + // TODO: implement! + console.error(ERR_FENNEC_MSG); + + // return 80x45 blank default + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAtCAYAAAA5reyyAAAAJElEQVRoge3BAQEAAACCIP+vbkhAAQAAAAAAAAAAAAAAAADXBjhtAAGQ0AF/AAAAAElFTkSuQmCC'; + }, + + /** + * 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() { + 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() 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, tabNS(this).tab.browser.contentWindow); + }, + + /** + * Make this tab active. + */ + activate: function activate() { + activateTab(tabNS(this).tab, tabNS(this).window); + }, + + /** + * Close the tab + */ + close: function close(callback) { + if (callback) + this.once(EVENTS.close.name, callback); + + closeTab(tabNS(this).tab); + }, + + /** + * Reload the tab + */ + reload: function reload() { + tabNS(this).tab.browser.reload(); + } +}); +exports.Tab = Tab; diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/tab-firefox.js b/tools/addon-sdk-1.12/lib/sdk/tabs/tab-firefox.js new file mode 100644 index 0000000..5a36302 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/tab-firefox.js @@ -0,0 +1,201 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 { Trait } = require("../deprecated/traits"); +const { EventEmitter } = require("../deprecated/events"); +const { defer } = require("../lang/functional"); +const { EVENTS } = require("./events"); +const { getThumbnailURIForWindow } = require("../content/thumbnail"); +const { getFaviconURIForLocation } = require("../io/data"); +const { activateTab, getOwnerWindow, getBrowserForTab, getTabTitle, setTabTitle, + getTabURL, setTabURL, getTabContentType } = require('./utils'); + +// Array of the inner instances of all the wrapped tabs. +const TABS = []; + +/** + * Trait used to create tab wrappers. + */ +const TabTrait = Trait.compose(EventEmitter, { + on: Trait.required, + _emit: Trait.required, + /** + * Tab DOM element that is being wrapped. + */ + _tab: null, + /** + * Window wrapper whose tab this object represents. + */ + window: null, + constructor: function Tab(options) { + this._onReady = this._onReady.bind(this); + this._tab = options.tab; + let window = this.window = options.window || getOwnerWindow(this._tab); + + // Setting event listener if was passed. + for each (let type in EVENTS) { + let listener = options[type.listener]; + if (listener) + this.on(type.name, options[type.listener]); + if ('ready' != type.name) // window spreads this event. + window.tabs.on(type.name, this._onEvent.bind(this, type.name)); + } + + this.on(EVENTS.close.name, this.destroy.bind(this)); + this._browser.addEventListener(EVENTS.ready.dom, this._onReady, true); + + if (options.isPinned) + this.pin(); + + // Since we will have to identify tabs by a DOM elements facade function + // is used as constructor that collects all the instances and makes sure + // that they more then one wrapper is not created per tab. + return this; + }, + destroy: function destroy() { + this._removeAllListeners(); + this._browser.removeEventListener(EVENTS.ready.dom, this._onReady, true); + }, + + /** + * Internal listener that emits public event 'ready' when the page of this + * tab is loaded. + */ + _onReady: function _onReady(event) { + // IFrames events will bubble so we need to ignore those. + if (event.target == this._contentDocument) + this._emit(EVENTS.ready.name, this._public); + }, + /** + * Internal tab event router. Window will emit tab related events for all it's + * tabs, this listener will propagate all the events for this tab to it's + * listeners. + */ + _onEvent: function _onEvent(type, tab) { + if (tab == this._public) + this._emit(type, tab); + }, + /** + * Browser DOM element where page of this tab is currently loaded. + */ + get _browser() getBrowserForTab(this._tab), + /** + * Window DOM element containing this tab. + */ + get _window() getOwnerWindow(this._tab), + /** + * Document object of the page that is currently loaded in this tab. + */ + get _contentDocument() this._browser.contentDocument, + /** + * Window object of the page that is currently loaded in this tab. + */ + get _contentWindow() this._browser.contentWindow, + + /** + * The title of the page currently loaded in the tab. + * Changing this property changes an actual title. + * @type {String} + */ + get title() getTabTitle(this._tab), + set title(title) setTabTitle(this._tab, title), + + /** + * Returns the MIME type that the document loaded in the tab is being + * rendered as. + * @type {String} + */ + get contentType() getTabContentType(this._tab), + + /** + * Location of the page currently loaded in this tab. + * Changing this property will loads page under under the specified location. + * @type {String} + */ + get url() getTabURL(this._tab), + set url(url) setTabURL(this._tab, url), + /** + * URI of the favicon for the page currently loaded in this tab. + * @type {String} + */ + get favicon() getFaviconURIForLocation(this.url), + /** + * The CSS style for the tab + */ + get style() null, // TODO + /** + * 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() + this._window.gBrowser.getBrowserIndexForDocument(this._contentDocument), + set index(value) this._window.gBrowser.moveTabTo(this._tab, value), + /** + * Thumbnail data URI of the page currently loaded in this tab. + * @type {String} + */ + getThumbnail: function getThumbnail() + getThumbnailURIForWindow(this._contentWindow), + /** + * Whether or not tab is pinned (Is an app-tab). + * @type {Boolean} + */ + get isPinned() this._tab.pinned, + pin: function pin() { + this._window.gBrowser.pinTab(this._tab); + }, + unpin: function unpin() { + this._window.gBrowser.unpinTab(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, this._contentWindow); + }, + + /** + * Make this tab active. + * Please note: That this function is called asynchronous since in E10S that + * will be the case. Besides this function is called from a constructor where + * we would like to return instance before firing a 'TabActivated' event. + */ + activate: defer(function activate() { + activateTab(this._tab); + }), + /** + * Close the tab + */ + close: function close(callback) { + if (callback) + this.once(EVENTS.close.name, callback); + this._window.gBrowser.removeTab(this._tab); + }, + /** + * Reload the tab + */ + reload: function reload() { + this._window.gBrowser.reloadTab(this._tab); + } +}); + +function Tab(options) { + let chromeTab = options.tab; + for each (let tab in TABS) { + if (chromeTab == tab._tab) + return tab._public; + } + let tab = TabTrait(options); + TABS.push(tab); + return tab._public; +} +Tab.prototype = TabTrait.prototype; +exports.Tab = Tab; diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/tab.js b/tools/addon-sdk-1.12/lib/sdk/tabs/tab.js new file mode 100644 index 0000000..420baee --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/tab.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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('Firefox')) { + module.exports = require('./tab-firefox'); +} +else if (require('../system/xul-app').is('Fennec')) { + module.exports = require('./tab-fennec'); +} +else { + throw new Error([ + 'The tabs module currently supports only Firefox & Fennec. In the future', + ' we would like it to support other applications, however.' + ].join('')); +} diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/tabs-firefox.js b/tools/addon-sdk-1.12/lib/sdk/tabs/tabs-firefox.js new file mode 100644 index 0000000..92834c2 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/tabs-firefox.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'; + +// TODO: BUG 792670 - remove dependency below +const { browserWindows } = require('../windows'); +const { tabs } = require('../windows/tabs-firefox'); + +Object.defineProperties(tabs, { + open: { value: function open(options) { + if (options.inNewWindow) + // `tabs` option is under review and may be removed. + return browserWindows.open({ tabs: [ options ] }); + // Open in active window if new window was not required. + return browserWindows.activeWindow.tabs.open(options); + }} +}); + +// Workaround for bug 674195. Freezing objects from other compartments fail, +// so we use `Object.freeze` from the same component as objects +// `hasOwnProperty`. Since `hasOwnProperty` here will be from other component +// we override it to support our workaround. +module.exports = Object.create(tabs, { + isPrototypeOf: { value: Object.prototype.isPrototypeOf } +}); diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/tabs.js b/tools/addon-sdk-1.12/lib/sdk/tabs/tabs.js new file mode 100644 index 0000000..fcb3868 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/tabs.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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('Firefox')) { + module.exports = require('./tabs-firefox'); +} +else if (require('../system/xul-app').is('Fennec')) { + module.exports = require('../windows/tabs-fennec').tabs; +} +else { + throw new Error([ + 'The tabs module currently supports only Firefox & Fennec. In the future', + ' we would like it to support other applications, however.' + ].join('')); +} diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/utils.js b/tools/addon-sdk-1.12/lib/sdk/tabs/utils.js new file mode 100644 index 0000000..d923e4b --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/tabs/utils.js @@ -0,0 +1,222 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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("../lang/functional"); +const { windows } = require('../window/utils'); +const { Ci } = require('chrome'); + +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) { + return window.gBrowser; +} +exports.getTabBrowser = getTabBrowser; + +function getTabContainer(window) { + return getTabBrowser(window).tabContainer; +} +exports.getTabContainer = getTabContainer; + +function getTabs(window) { + // fennec + if (window.BrowserApp) + return window.BrowserApp.tabs; + + // firefox - default + return Array.slice(getTabContainer(window).children); +} +exports.getTabs = getTabs; + +function getActiveTab(window) { + return window.gBrowser.selectedTab; +} +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 each (let window in windows()) { + // this function may be called when not using fennec + if (!window.BrowserApp) + continue; + + for each (let tab in 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 + }); + } + return window.gBrowser.addTab(url); +}; +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) + return gBrowser.removeTab(tab); + + let window = getWindowHoldingTab(tab); + // fennec? + if (window && window.BrowserApp) + 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 getTabTitle(tab) { + return getBrowserForTab(tab).contentDocument.title || tab.label || ""; +} +exports.getTabTitle = getTabTitle; + +function setTabTitle(tab, title) { + title = String(title); + if (tab.browser) + tab.browser.contentDocument.title = title; + tab.label = String(title); +} +exports.setTabTitle = setTabTitle; + +function getTabContentWindow(tab) { + return getBrowserForTab(tab).contentWindow; +} +exports.getTabContentWindow = getTabContentWindow; + +function getTabForContentWindow(window) { + // Retrieve the topmost frame container. It can be either <xul:browser>, + // <xul:iframe/> or <html:iframe/>. But in our case, it should be xul:browser. + let browser = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + // Is null for toplevel documents + if (!browser) + return false; + // Retrieve the owner window, should be browser.xul one + let chromeWindow = browser.ownerDocument.defaultView; + + // Ensure that it is top-level browser window. + // We need extra checks because of Mac hidden window that has a broken + // `gBrowser` global attribute. + if ('gBrowser' in chromeWindow && chromeWindow.gBrowser && + 'browsers' in chromeWindow.gBrowser) { + // Looks like we are on Firefox Desktop + // Then search for the position in tabbrowser in order to get the tab object + let browsers = chromeWindow.gBrowser.browsers; + let i = browsers.indexOf(browser); + if (i !== -1) + return chromeWindow.gBrowser.tabs[i]; + return null; + } + else if ('BrowserApp' in chromeWindow) { + // Looks like we are on Firefox Mobile + return chromeWindow.BrowserApp.getTabForWindow(window) + } + + return null; +} +exports.getTabForContentWindow = getTabForContentWindow; + +function getTabURL(tab) { + if (tab.browser) // fennec + return String(tab.browser.currentURI.spec); + return String(getBrowserForTab(tab).currentURI.spec); +} +exports.getTabURL = getTabURL; + +function setTabURL(tab, url) { + url = String(url); + if (tab.browser) + return tab.browser.loadURI(url); + return getBrowserForTab(tab).loadURI(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; diff --git a/tools/addon-sdk-1.12/lib/sdk/tabs/worker.js b/tools/addon-sdk-1.12/lib/sdk/tabs/worker.js new file mode 100644 index 0000000..d2ba336 --- /dev/null +++ b/tools/addon-sdk-1.12/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/tools/addon-sdk-1.12/lib/sdk/test.js b/tools/addon-sdk-1.12/lib/sdk/test.js new file mode 100644 index 0000000..0a322d6 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/test.js @@ -0,0 +1,112 @@ +/* vim:ts=2:sts=2:sw=2: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 BaseAssert = require("./test/assert").Assert; +const { isFunction, isObject } = require("./lang/type"); + +function extend(target) { + let descriptor = {} + Array.slice(arguments, 1).forEach(function(source) { + Object.getOwnPropertyNames(source).forEach(function onEach(name) { + descriptor[name] = Object.getOwnPropertyDescriptor(source, name); + }); + }); + return Object.create(target, descriptor); +} + +/** + * 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); + + // 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. + 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/tools/addon-sdk-1.12/lib/sdk/test/assert.js b/tools/addon-sdk-1.12/lib/sdk/test/assert.js new file mode 100644 index 0000000..5eca7d4 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/test/assert.js @@ -0,0 +1,349 @@ +/* vim:ts=2:sts=2:sw=2: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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) { + return Object.create(Assert.prototype, { _log: { value: logger }}); +} +Assert.prototype = { + fail: function fail(e) { + if (!e || typeof(e) !== 'object') { + this._log.fail(e); + return; + } + let message = e.message; + if ('operator' in e) { + message += [ + " -", + source(e.expected), + e.operator, + source(e.actual) + ].join(" "); + } + this._log.fail(message); + }, + pass: function pass(message) { + this._log.pass(message); + }, + error: function error(e) { + this._log.exception(e); + }, + ok: function ok(value, message) { + if (!!!value) { + this.fail({ + actual: value, + expected: true, + message: message, + operator: "==" + }); + } + else { + this.pass(message); + } + }, + + /** + * 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); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "==" + }); + } + }, + + /** + * 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); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "!=", + }); + } + }, + + /** + * 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); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "deepEqual" + }); + } + }, + + /** + * 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); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "notDeepEqual" + }); + } + }, + + /** + * 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); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "===" + }); + } + }, + + /** + * 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); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "!==" + }) + } + }, + + /** + * 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)) || + // If passed `Error` is a constructor function testing if + // thrown exception is an instance of it. + (isFunction(Error) && instanceOf(exception, Error)))) + { + this.pass(message); + } + + // Otherwise we report assertion failure. + else { + let failure = { + message: message, + operator: "throws" + }; + + if (exception) + failure.actual = exception; + + if (Error) + failure.expected = Error; + + this.fail(failure); + } + } +}; +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/tools/addon-sdk-1.12/lib/sdk/test/harness.js b/tools/addon-sdk-1.12/lib/sdk/test/harness.js new file mode 100644 index 0000000..46584ca --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/test/harness.js @@ -0,0 +1,327 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 { Loader } = require('./loader'); +const { setTimeout } = require('../timers'); +const memory = require('../deprecated/memory'); +const { PlainTextConsole } = require("../console/plain-text"); +const { when: unload } = require("../system/unload"); +const { format } = require("../console/traceback"); +const system = require("../system"); + +// 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"); + +var cService = Cc['@mozilla.org/consoleservice;1'].getService() + .QueryInterface(Ci.nsIConsoleService); + +// 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 = { + passed: 0, + failed: 0, + testRuns: [] +}; + +// 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() { + memory.gc(); + + var mgr = Cc["@mozilla.org/memory-reporter-manager;1"] + .getService(Ci.nsIMemoryReporterManager); + var reporters = mgr.enumerateReporters(); + if (reporters.hasMoreElements()) + print("\n"); + while (reporters.hasMoreElements()) { + var reporter = reporters.getNext(); + reporter.QueryInterface(Ci.nsIMemoryReporter); + print(reporter.description + ": " + reporter.memoryUsed + "\n"); + } + + var weakrefs = [info.weakref.get() + for each (info in memory.getObjects())]; + weakrefs = [weakref for each (weakref in weakrefs) if (weakref)]; + print("Tracked memory objects in testing sandbox: " + + weakrefs.length + "\n"); +} + +var gWeakrefInfo; + +function showResults() { + memory.gc(); + + 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 + "(" + + [p for (p in data)].join(", ") + ")]" + : data; + console.warn("LEAK", warning, info.bin); + } + } + ); + } + + onDone(results); +} + +function cleanup() { + try { + for (let name in loader.modules) + memory.track(loader.modules[name], + "module global scope: " + name); + memory.track(loader, "Cuddlefish Loader"); + + if (profileMemory) { + gWeakrefInfo = [{ weakref: info.weakref, bin: info.bin } + for each (info in memory.getObjects())]; + } + + 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."); + } + + consoleListener.errorsLogged = 0; + loader = null; + + memory.gc(); + } catch (e) { + results.failed++; + console.error("unload.send() threw an exception."); + console.exception(e); + }; + + setTimeout(showResults, 1); +} + +function nextIteration(tests) { + if (tests) { + results.passed += tests.passed; + results.failed += tests.failed; + + if (profileMemory) + reportMemoryUsage(); + + let testRun = []; + for each (let test in tests.testRunSummary) { + let testCopy = {}; + for (let info in test) { + testCopy[info] = test[info]; + } + testRun.push(testCopy); + } + + results.testRuns.push(testRun); + iterationsLeft--; + } + + 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:', + '[JavaScript Warning: "ReferenceErrorL reference to undefined property', + '[Javascript Warning: "Error: Failed to preserve wrapper of wrapped ' + + 'native weak map key', + '[JavaScript Warning: "Duplicate resource declaration for' +]; + +var consoleListener = { + errorsLogged: 0, + observe: function(object) { + if (!(object instanceof Ci.nsIScriptError)) + return; + this.errorsLogged++; + var message = object.QueryInterface(Ci.nsIConsoleMessage).message; + var pointless = [err for each (err in POINTLESS_ERRORS) + if (message.indexOf(err) == 0)]; + if (pointless.length == 0 && message) + print("console: " + message + "\n"); + } +}; + +function TestRunnerConsole(base, options) { + this.__proto__ = { + 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("."); + }, + __proto__: base + }; +} + +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; + + try { + cService.registerListener(consoleListener); + print("Running tests on " + system.name + " " + system.version + + "/Gecko " + system.platformVersion + " (" + + system.id + ") under " + + system.platform + "/" + system.architecture + ".\n"); + + + loader = Loader(module, { + console: new TestRunnerConsole(new PlainTextConsole(print), options) + }); + + nextIteration(); + } catch (e) { + print(format(e) + "\n" + e + "\n"); + onDone({passed: 0, failed: 1}); + } +}; + +unload(function() { + cService.unregisterListener(consoleListener); +}); + diff --git a/tools/addon-sdk-1.12/lib/sdk/test/httpd.js b/tools/addon-sdk-1.12/lib/sdk/test/httpd.js new file mode 100644 index 0000000..48d3516 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/test/httpd.js @@ -0,0 +1,5174 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* +* An implementation of an HTTP server both as a loadable script and as an XPCOM +* component. See the accompanying README file for user documentation on +* httpd.js. +*/ + +module.metadata = { + "stability": "experimental" +}; + +const { components, CC, Cc, Ci, Cr, Cu } = require("chrome"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + + +const PR_UINT32_MAX = Math.pow(2, 32) - 1; + +/** True if debugging output is enabled, false otherwise. */ +var DEBUG = false; // non-const *only* so tweakable in server tests + +/** True if debugging output should be timestamped. */ +var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests + +var gGlobalObject = Cc["@mozilla.org/systemprincipal;1"].createInstance(); + +/** +* Asserts that the given condition holds. If it doesn't, the given message is +* dumped, a stack trace is printed, and an exception is thrown to attempt to +* stop execution (which unfortunately must rely upon the exception not being +* accidentally swallowed by the code that uses it). +*/ +function NS_ASSERT(cond, msg) +{ + if (DEBUG && !cond) + { + dumpn("###!!!"); + dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!")); + dumpn("###!!! Stack follows:"); + + var stack = new Error().stack.split(/\n/); + dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n")); + + throw Cr.NS_ERROR_ABORT; + } +} + +/** Constructs an HTTP error object. */ +function HttpError(code, description) +{ + this.code = code; + this.description = description; +} +HttpError.prototype = +{ + toString: function() + { + return this.code + " " + this.description; + } +}; + +/** +* Errors thrown to trigger specific HTTP server responses. +*/ +const HTTP_400 = new HttpError(400, "Bad Request"); +const HTTP_401 = new HttpError(401, "Unauthorized"); +const HTTP_402 = new HttpError(402, "Payment Required"); +const HTTP_403 = new HttpError(403, "Forbidden"); +const HTTP_404 = new HttpError(404, "Not Found"); +const HTTP_405 = new HttpError(405, "Method Not Allowed"); +const HTTP_406 = new HttpError(406, "Not Acceptable"); +const HTTP_407 = new HttpError(407, "Proxy Authentication Required"); +const HTTP_408 = new HttpError(408, "Request Timeout"); +const HTTP_409 = new HttpError(409, "Conflict"); +const HTTP_410 = new HttpError(410, "Gone"); +const HTTP_411 = new HttpError(411, "Length Required"); +const HTTP_412 = new HttpError(412, "Precondition Failed"); +const HTTP_413 = new HttpError(413, "Request Entity Too Large"); +const HTTP_414 = new HttpError(414, "Request-URI Too Long"); +const HTTP_415 = new HttpError(415, "Unsupported Media Type"); +const HTTP_417 = new HttpError(417, "Expectation Failed"); + +const HTTP_500 = new HttpError(500, "Internal Server Error"); +const HTTP_501 = new HttpError(501, "Not Implemented"); +const HTTP_502 = new HttpError(502, "Bad Gateway"); +const HTTP_503 = new HttpError(503, "Service Unavailable"); +const HTTP_504 = new HttpError(504, "Gateway Timeout"); +const HTTP_505 = new HttpError(505, "HTTP Version Not Supported"); + +/** Creates a hash with fields corresponding to the values in arr. */ +function array2obj(arr) +{ + var obj = {}; + for (var i = 0; i < arr.length; i++) + obj[arr[i]] = arr[i]; + return obj; +} + +/** Returns an array of the integers x through y, inclusive. */ +function range(x, y) +{ + var arr = []; + for (var i = x; i <= y; i++) + arr.push(i); + return arr; +} + +/** An object (hash) whose fields are the numbers of all HTTP error codes. */ +const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505))); + + +/** +* The character used to distinguish hidden files from non-hidden files, a la +* the leading dot in Apache. Since that mechanism also hides files from +* easy display in LXR, ls output, etc. however, we choose instead to use a +* suffix character. If a requested file ends with it, we append another +* when getting the file on the server. If it doesn't, we just look up that +* file. Therefore, any file whose name ends with exactly one of the character +* is "hidden" and available for use by the server. +*/ +const HIDDEN_CHAR = "^"; + +/** +* The file name suffix indicating the file containing overridden headers for +* a requested file. +*/ +const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR; + +/** Type used to denote SJS scripts for CGI-like functionality. */ +const SJS_TYPE = "sjs"; + +/** Base for relative timestamps produced by dumpn(). */ +var firstStamp = 0; + +/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */ +function dumpn(str) +{ + if (DEBUG) + { + var prefix = "HTTPD-INFO | "; + if (DEBUG_TIMESTAMP) + { + if (firstStamp === 0) + firstStamp = Date.now(); + + var elapsed = Date.now() - firstStamp; // milliseconds + var min = Math.floor(elapsed / 60000); + var sec = (elapsed % 60000) / 1000; + + if (sec < 10) + prefix += min + ":0" + sec.toFixed(3) + " | "; + else + prefix += min + ":" + sec.toFixed(3) + " | "; + } + + dump(prefix + str + "\n"); + } +} + +/** Dumps the current JS stack if DEBUG. */ +function dumpStack() +{ + // peel off the frames for dumpStack() and Error() + var stack = new Error().stack.split(/\n/).slice(2); + stack.forEach(dumpn); +} + + +/** The XPCOM thread manager. */ +var gThreadManager = null; + +/** The XPCOM prefs service. */ +var gRootPrefBranch = null; +function getRootPrefBranch() +{ + if (!gRootPrefBranch) + { + gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + } + return gRootPrefBranch; +} + +/** +* JavaScript constructors for commonly-used classes; precreating these is a +* speedup over doing the same from base principles. See the docs at +* http://developer.mozilla.org/en/docs/components.Constructor for details. +*/ +const ServerSocket = CC("@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init"); +const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init"); +const Pipe = CC("@mozilla.org/pipe;1", + "nsIPipe", + "init"); +const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", + "init"); +const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1", + "nsIConverterInputStream", + "init"); +const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1", + "nsIWritablePropertyBag2"); +const SupportsString = CC("@mozilla.org/supports-string;1", + "nsISupportsString"); + +/* These two are non-const only so a test can overwrite them. */ +var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); +var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream"); + +/** +* Returns the RFC 822/1123 representation of a date. +* +* @param date : Number +* the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT +* @returns string +* the representation of the given date +*/ +function toDateString(date) +{ + // + // rfc1123-date = wkday "," SP date1 SP time SP "GMT" + // date1 = 2DIGIT SP month SP 4DIGIT + // ; day month year (e.g., 02 Jun 1982) + // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT + // ; 00:00:00 - 23:59:59 + // wkday = "Mon" | "Tue" | "Wed" + // | "Thu" | "Fri" | "Sat" | "Sun" + // month = "Jan" | "Feb" | "Mar" | "Apr" + // | "May" | "Jun" | "Jul" | "Aug" + // | "Sep" | "Oct" | "Nov" | "Dec" + // + + const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + /** +* Processes a date and returns the encoded UTC time as a string according to +* the format specified in RFC 2616. +* +* @param date : Date +* the date to process +* @returns string +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" +*/ + function toTime(date) + { + var hrs = date.getUTCHours(); + var rv = (hrs < 10) ? "0" + hrs : hrs; + + var mins = date.getUTCMinutes(); + rv += ":"; + rv += (mins < 10) ? "0" + mins : mins; + + var secs = date.getUTCSeconds(); + rv += ":"; + rv += (secs < 10) ? "0" + secs : secs; + + return rv; + } + + /** +* Processes a date and returns the encoded UTC date as a string according to +* the date1 format specified in RFC 2616. +* +* @param date : Date +* the date to process +* @returns string +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" +*/ + function toDate1(date) + { + var day = date.getUTCDate(); + var month = date.getUTCMonth(); + var year = date.getUTCFullYear(); + + var rv = (day < 10) ? "0" + day : day; + rv += " " + monthStrings[month]; + rv += " " + year; + + return rv; + } + + date = new Date(date); + + const fmtString = "%wkday%, %date1% %time% GMT"; + var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]); + rv = rv.replace("%time%", toTime(date)); + return rv.replace("%date1%", toDate1(date)); +} + +/** +* Prints out a human-readable representation of the object o and its fields, +* omitting those whose names begin with "_" if showMembers != true (to ignore +* "private" properties exposed via getters/setters). +*/ +function printObj(o, showMembers) +{ + var s = "******************************\n"; + s += "o = {\n"; + for (var i in o) + { + if (typeof(i) != "string" || + (showMembers || (i.length > 0 && i[0] != "_"))) + s+= " " + i + ": " + o[i] + ",\n"; + } + s += " };\n"; + s += "******************************"; + dumpn(s); +} + +/** +* Instantiates a new HTTP server. +*/ +function nsHttpServer() +{ + if (!gThreadManager) + gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + + /** The port on which this server listens. */ + this._port = undefined; + + /** The socket associated with this. */ + this._socket = null; + + /** The handler used to process requests to this server. */ + this._handler = new ServerHandler(this); + + /** Naming information for this server. */ + this._identity = new ServerIdentity(); + + /** +* Indicates when the server is to be shut down at the end of the request. +*/ + this._doQuit = false; + + /** +* True if the socket in this is closed (and closure notifications have been +* sent and processed if the socket was ever opened), false otherwise. +*/ + this._socketClosed = true; + + /** +* Used for tracking existing connections and ensuring that all connections +* are properly cleaned up before server shutdown; increases by 1 for every +* new incoming connection. +*/ + this._connectionGen = 0; + + /** +* Hash of all open connections, indexed by connection number at time of +* creation. +*/ + this._connections = {}; +} +nsHttpServer.prototype = +{ + classID: components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"), + + // NSISERVERSOCKETLISTENER + + /** +* Processes an incoming request coming in on the given socket and contained +* in the given transport. +* +* @param socket : nsIServerSocket +* the socket through which the request was served +* @param trans : nsISocketTransport +* the transport for the request/response +* @see nsIServerSocketListener.onSocketAccepted +*/ + onSocketAccepted: function(socket, trans) + { + dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")"); + + dumpn(">>> new connection on " + trans.host + ":" + trans.port); + + const SEGMENT_SIZE = 8192; + const SEGMENT_COUNT = 1024; + try + { + var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT) + .QueryInterface(Ci.nsIAsyncInputStream); + var output = trans.openOutputStream(0, 0, 0); + } + catch (e) + { + dumpn("*** error opening transport streams: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + var connectionNumber = ++this._connectionGen; + + try + { + var conn = new Connection(input, output, this, socket.port, trans.port, + connectionNumber); + var reader = new RequestReader(conn); + + // XXX add request timeout functionality here! + + // Note: must use main thread here, or we might get a GC that will cause + // threadsafety assertions. We really need to fix XPConnect so that + // you can actually do things in multi-threaded JS. :-( + input.asyncWait(reader, 0, 0, gThreadManager.mainThread); + } + catch (e) + { + // Assume this connection can't be salvaged and bail on it completely; + // don't attempt to close it so that we can assert that any connection + // being closed is in this._connections. + dumpn("*** error in initial request-processing stages: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + this._connections[connectionNumber] = conn; + dumpn("*** starting connection " + connectionNumber); + }, + + /** +* Called when the socket associated with this is closed. +* +* @param socket : nsIServerSocket +* the socket being closed +* @param status : nsresult +* the reason the socket stopped listening (NS_BINDING_ABORTED if the server +* was stopped using nsIHttpServer.stop) +* @see nsIServerSocketListener.onStopListening +*/ + onStopListening: function(socket, status) + { + dumpn(">>> shutting down server on port " + socket.port); + this._socketClosed = true; + if (!this._hasOpenConnections()) + { + dumpn("*** no open connections, notifying async from onStopListening"); + + // Notify asynchronously so that any pending teardown in stop() has a + // chance to run first. + var self = this; + var stopEvent = + { + run: function() + { + dumpn("*** _notifyStopped async callback"); + self._notifyStopped(); + } + }; + gThreadManager.currentThread + .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + + // NSIHTTPSERVER + + // + // see nsIHttpServer.start + // + start: function(port) + { + this._start(port, "localhost") + }, + + _start: function(port, host) + { + if (this._socket) + throw Cr.NS_ERROR_ALREADY_INITIALIZED; + + this._port = port; + this._doQuit = this._socketClosed = false; + + this._host = host; + + // The listen queue needs to be long enough to handle + // network.http.max-persistent-connections-per-server concurrent connections, + // plus a safety margin in case some other process is talking to + // the server as well. + var prefs = getRootPrefBranch(); + var maxConnections; + try { + // Bug 776860: The original pref was removed in favor of this new one: + maxConnections = prefs.getIntPref("network.http.max-persistent-connections-per-server") + 5; + } + catch(e) { + maxConnections = prefs.getIntPref("network.http.max-connections-per-server") + 5; + } + + try + { + var loopback = true; + if (this._host != "127.0.0.1" && this._host != "localhost") { + var loopback = false; + } + + var socket = new ServerSocket(this._port, + loopback, // true = localhost, false = everybody + maxConnections); + dumpn(">>> listening on port " + socket.port + ", " + maxConnections + + " pending connections"); + socket.asyncListen(this); + this._identity._initialize(port, host, true); + this._socket = socket; + } + catch (e) + { + dumpn("!!! could not start server on port " + port + ": " + e); + throw Cr.NS_ERROR_NOT_AVAILABLE; + } + }, + + // + // see nsIHttpServer.stop + // + stop: function(callback) + { + if (!callback) + throw Cr.NS_ERROR_NULL_POINTER; + if (!this._socket) + throw Cr.NS_ERROR_UNEXPECTED; + + this._stopCallback = typeof callback === "function" + ? callback + : function() { callback.onStopped(); }; + + dumpn(">>> stopping listening on port " + this._socket.port); + this._socket.close(); + this._socket = null; + + // We can't have this identity any more, and the port on which we're running + // this server now could be meaningless the next time around. + this._identity._teardown(); + + this._doQuit = false; + + // socket-close notification and pending request completion happen async + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (file && (!file.exists() || file.isDirectory())) + throw Cr.NS_ERROR_INVALID_ARG; + + this._handler.registerFile(path, file); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // XXX true path validation! + if (path.charAt(0) != "/" || + path.charAt(path.length - 1) != "/" || + (directory && + (!directory.exists() || !directory.isDirectory()))) + throw Cr.NS_ERROR_INVALID_ARG; + + // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping + // exists! + + this._handler.registerDirectory(path, directory); + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + this._handler.registerPathHandler(path, handler); + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(code, handler) + { + this._handler.registerErrorHandler(code, handler); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler: function(handler) + { + this._handler.setIndexHandler(handler); + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType: function(ext, type) + { + this._handler.registerContentType(ext, type); + }, + + // + // see nsIHttpServer.serverIdentity + // + get identity() + { + return this._identity; + }, + + // + // see nsIHttpServer.getState + // + getState: function(path, k) + { + return this._handler._getState(path, k); + }, + + // + // see nsIHttpServer.setState + // + setState: function(path, k, v) + { + return this._handler._setState(path, k, v); + }, + + // + // see nsIHttpServer.getSharedState + // + getSharedState: function(k) + { + return this._handler._getSharedState(k); + }, + + // + // see nsIHttpServer.setSharedState + // + setSharedState: function(k, v) + { + return this._handler._setSharedState(k, v); + }, + + // + // see nsIHttpServer.getObjectState + // + getObjectState: function(k) + { + return this._handler._getObjectState(k); + }, + + // + // see nsIHttpServer.setObjectState + // + setObjectState: function(k, v) + { + return this._handler._setObjectState(k, v); + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIServerSocketListener) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // NON-XPCOM PUBLIC API + + /** +* Returns true iff this server is not running (and is not in the process of +* serving any requests still to be processed when the server was last +* stopped after being run). +*/ + isStopped: function() + { + return this._socketClosed && !this._hasOpenConnections(); + }, + + // PRIVATE IMPLEMENTATION + + /** True if this server has any open connections to it, false otherwise. */ + _hasOpenConnections: function() + { + // + // If we have any open connections, they're tracked as numeric properties on + // |this._connections|. The non-standard __count__ property could be used + // to check whether there are any properties, but standard-wise, even + // looking forward to ES5, there's no less ugly yet still O(1) way to do + // this. + // + for (var n in this._connections) + return true; + return false; + }, + + /** Calls the server-stopped callback provided when stop() was called. */ + _notifyStopped: function() + { + NS_ASSERT(this._stopCallback !== null, "double-notifying?"); + NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now"); + + // + // NB: We have to grab this now, null out the member, *then* call the + // callback here, or otherwise the callback could (indirectly) futz with + // this._stopCallback by starting and immediately stopping this, at + // which point we'd be nulling out a field we no longer have a right to + // modify. + // + var callback = this._stopCallback; + this._stopCallback = null; + try + { + callback(); + } + catch (e) + { + // not throwing because this is specified as being usually (but not + // always) asynchronous + dump("!!! error running onStopped callback: " + e + "\n"); + } + }, + + /** +* Notifies this server that the given connection has been closed. +* +* @param connection : Connection +* the connection that was closed +*/ + _connectionClosed: function(connection) + { + NS_ASSERT(connection.number in this._connections, + "closing a connection " + this + " that we never added to the " + + "set of open connections?"); + NS_ASSERT(this._connections[connection.number] === connection, + "connection number mismatch? " + + this._connections[connection.number]); + delete this._connections[connection.number]; + + // Fire a pending server-stopped notification if it's our responsibility. + if (!this._hasOpenConnections() && this._socketClosed) + this._notifyStopped(); + }, + + /** +* Requests that the server be shut down when possible. +*/ + _requestQuit: function() + { + dumpn(">>> requesting a quit"); + dumpStack(); + this._doQuit = true; + } +}; + + +// +// RFC 2396 section 3.2.2: +// +// host = hostname | IPv4address +// hostname = *( domainlabel "." ) toplabel [ "." ] +// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum +// toplabel = alpha | alpha *( alphanum | "-" ) alphanum +// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit +// + +const HOST_REGEX = + new RegExp("^(?:" + + // *( domainlabel "." ) + "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" + + // toplabel + "[a-z](?:[a-z0-9-]*[a-z0-9])?" + + "|" + + // IPv4 address + "\\d+\\.\\d+\\.\\d+\\.\\d+" + + ")$", + "i"); + + +/** +* Represents the identity of a server. An identity consists of a set of +* (scheme, host, port) tuples denoted as locations (allowing a single server to +* serve multiple sites or to be used behind both HTTP and HTTPS proxies for any +* host/port). Any incoming request must be to one of these locations, or it +* will be rejected with an HTTP 400 error. One location, denoted as the +* primary location, is the location assigned in contexts where a location +* cannot otherwise be endogenously derived, such as for HTTP/1.0 requests. +* +* A single identity may contain at most one location per unique host/port pair; +* other than that, no restrictions are placed upon what locations may +* constitute an identity. +*/ +function ServerIdentity() +{ + /** The scheme of the primary location. */ + this._primaryScheme = "http"; + + /** The hostname of the primary location. */ + this._primaryHost = "127.0.0.1" + + /** The port number of the primary location. */ + this._primaryPort = -1; + + /** +* The current port number for the corresponding server, stored so that a new +* primary location can always be set if the current one is removed. +*/ + this._defaultPort = -1; + + /** +* Maps hosts to maps of ports to schemes, e.g. the following would represent +* https://example.com:789/ and http://example.org/: +* +* { +* "xexample.com": { 789: "https" }, +* "xexample.org": { 80: "http" } +* } +* +* Note the "x" prefix on hostnames, which prevents collisions with special +* JS names like "prototype". +*/ + this._locations = { "xlocalhost": {} }; +} +ServerIdentity.prototype = +{ + // NSIHTTPSERVERIDENTITY + + // + // see nsIHttpServerIdentity.primaryScheme + // + get primaryScheme() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryScheme; + }, + + // + // see nsIHttpServerIdentity.primaryHost + // + get primaryHost() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryHost; + }, + + // + // see nsIHttpServerIdentity.primaryPort + // + get primaryPort() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryPort; + }, + + // + // see nsIHttpServerIdentity.add + // + add: function(scheme, host, port) + { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) + this._locations["x" + host] = entry = {}; + + entry[port] = scheme; + }, + + // + // see nsIHttpServerIdentity.remove + // + remove: function(scheme, host, port) + { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) + return false; + + var present = port in entry; + delete entry[port]; + + if (this._primaryScheme == scheme && + this._primaryHost == host && + this._primaryPort == port && + this._defaultPort !== -1) + { + // Always keep at least one identity in existence at any time, unless + // we're in the process of shutting down (the last condition above). + this._primaryPort = -1; + this._initialize(this._defaultPort, host, false); + } + + return present; + }, + + // + // see nsIHttpServerIdentity.has + // + has: function(scheme, host, port) + { + this._validate(scheme, host, port); + + return "x" + host in this._locations && + scheme === this._locations["x" + host][port]; + }, + + // + // see nsIHttpServerIdentity.has + // + getScheme: function(host, port) + { + this._validate("http", host, port); + + var entry = this._locations["x" + host]; + if (!entry) + return ""; + + return entry[port] || ""; + }, + + // + // see nsIHttpServerIdentity.setPrimary + // + setPrimary: function(scheme, host, port) + { + this._validate(scheme, host, port); + + this.add(scheme, host, port); + + this._primaryScheme = scheme; + this._primaryHost = host; + this._primaryPort = port; + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE IMPLEMENTATION + + /** +* Initializes the primary name for the corresponding server, based on the +* provided port number. +*/ + _initialize: function(port, host, addSecondaryDefault) + { + this._host = host; + if (this._primaryPort !== -1) + this.add("http", host, port); + else + this.setPrimary("http", "localhost", port); + this._defaultPort = port; + + // Only add this if we're being called at server startup + if (addSecondaryDefault && host != "127.0.0.1") + this.add("http", "127.0.0.1", port); + }, + + /** +* Called at server shutdown time, unsets the primary location only if it was +* the default-assigned location and removes the default location from the +* set of locations used. +*/ + _teardown: function() + { + if (this._host != "127.0.0.1") { + // Not the default primary location, nothing special to do here + this.remove("http", "127.0.0.1", this._defaultPort); + } + + // This is a *very* tricky bit of reasoning here; make absolutely sure the + // tests for this code pass before you commit changes to it. + if (this._primaryScheme == "http" && + this._primaryHost == this._host && + this._primaryPort == this._defaultPort) + { + // Make sure we don't trigger the readding logic in .remove(), then remove + // the default location. + var port = this._defaultPort; + this._defaultPort = -1; + this.remove("http", this._host, port); + + // Ensure a server start triggers the setPrimary() path in ._initialize() + this._primaryPort = -1; + } + else + { + // No reason not to remove directly as it's not our primary location + this.remove("http", this._host, this._defaultPort); + } + }, + + /** +* Ensures scheme, host, and port are all valid with respect to RFC 2396. +* +* @throws NS_ERROR_ILLEGAL_VALUE +* if any argument doesn't match the corresponding production +*/ + _validate: function(scheme, host, port) + { + if (scheme !== "http" && scheme !== "https") + { + dumpn("*** server only supports http/https schemes: '" + scheme + "'"); + dumpStack(); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + if (!HOST_REGEX.test(host)) + { + dumpn("*** unexpected host: '" + host + "'"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + if (port < 0 || port > 65535) + { + dumpn("*** unexpected port: '" + port + "'"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + } +}; + + +/** +* Represents a connection to the server (and possibly in the future the thread +* on which the connection is processed). +* +* @param input : nsIInputStream +* stream from which incoming data on the connection is read +* @param output : nsIOutputStream +* stream to write data out the connection +* @param server : nsHttpServer +* the server handling the connection +* @param port : int +* the port on which the server is running +* @param outgoingPort : int +* the outgoing port used by this connection +* @param number : uint +* a serial number used to uniquely identify this connection +*/ +function Connection(input, output, server, port, outgoingPort, number) +{ + dumpn("*** opening new connection " + number + " on port " + outgoingPort); + + /** Stream of incoming data. */ + this.input = input; + + /** Stream for outgoing data. */ + this.output = output; + + /** The server associated with this request. */ + this.server = server; + + /** The port on which the server is running. */ + this.port = port; + + /** The outgoing poort used by this connection. */ + this._outgoingPort = outgoingPort; + + /** The serial number of this connection. */ + this.number = number; + + /** +* The request for which a response is being generated, null if the +* incoming request has not been fully received or if it had errors. +*/ + this.request = null; + + /** State variables for debugging. */ + this._closed = this._processed = false; +} +Connection.prototype = +{ + /** Closes this connection's input/output streams. */ + close: function() + { + dumpn("*** closing connection " + this.number + + " on port " + this._outgoingPort); + + this.input.close(); + this.output.close(); + this._closed = true; + + var server = this.server; + server._connectionClosed(this); + + // If an error triggered a server shutdown, act on it now + if (server._doQuit) + server.stop(function() { /* not like we can do anything better */ }); + }, + + /** +* Initiates processing of this connection, using the data in the given +* request. +* +* @param request : Request +* the request which should be processed +*/ + process: function(request) + { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + + this.request = request; + this.server._handler.handleResponse(this); + }, + + /** +* Initiates processing of this connection, generating a response with the +* given HTTP error code. +* +* @param code : uint +* an HTTP code, so in the range [0, 1000) +* @param request : Request +* incomplete data about the incoming request (since there were errors +* during its processing +*/ + processError: function(code, request) + { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + this.request = request; + this.server._handler.handleError(code, this); + }, + + /** Converts this to a string for debugging purposes. */ + toString: function() + { + return "<Connection(" + this.number + + (this.request ? ", " + this.request.path : "") +"): " + + (this._closed ? "closed" : "open") + ">"; + } +}; + + + +/** Returns an array of count bytes from the given input stream. */ +function readBytes(inputStream, count) +{ + return new BinaryInputStream(inputStream).readByteArray(count); +} + + + +/** Request reader processing states; see RequestReader for details. */ +const READER_IN_REQUEST_LINE = 0; +const READER_IN_HEADERS = 1; +const READER_IN_BODY = 2; +const READER_FINISHED = 3; + + +/** +* Reads incoming request data asynchronously, does any necessary preprocessing, +* and forwards it to the request handler. Processing occurs in three states: +* +* READER_IN_REQUEST_LINE Reading the request's status line +* READER_IN_HEADERS Reading headers in the request +* READER_IN_BODY Reading the body of the request +* READER_FINISHED Entire request has been read and processed +* +* During the first two stages, initial metadata about the request is gathered +* into a Request object. Once the status line and headers have been processed, +* we start processing the body of the request into the Request. Finally, when +* the entire body has been read, we create a Response and hand it off to the +* ServerHandler to be given to the appropriate request handler. +* +* @param connection : Connection +* the connection for the request being read +*/ +function RequestReader(connection) +{ + /** Connection metadata for this request. */ + this._connection = connection; + + /** +* A container providing line-by-line access to the raw bytes that make up the +* data which has been read from the connection but has not yet been acted +* upon (by passing it to the request handler or by extracting request +* metadata from it). +*/ + this._data = new LineData(); + + /** +* The amount of data remaining to be read from the body of this request. +* After all headers in the request have been read this is the value in the +* Content-Length header, but as the body is read its value decreases to zero. +*/ + this._contentLength = 0; + + /** The current state of parsing the incoming request. */ + this._state = READER_IN_REQUEST_LINE; + + /** Metadata constructed from the incoming request for the request handler. */ + this._metadata = new Request(connection.port); + + /** +* Used to preserve state if we run out of line data midway through a +* multi-line header. _lastHeaderName stores the name of the header, while +* _lastHeaderValue stores the value we've seen so far for the header. +* +* These fields are always either both undefined or both strings. +*/ + this._lastHeaderName = this._lastHeaderValue = undefined; +} +RequestReader.prototype = +{ + // NSIINPUTSTREAMCALLBACK + + /** +* Called when more data from the incoming request is available. This method +* then reads the available data from input and deals with that data as +* necessary, depending upon the syntax of already-downloaded data. +* +* @param input : nsIAsyncInputStream +* the stream of incoming data from the connection +*/ + onInputStreamReady: function(input) + { + dumpn("*** onInputStreamReady(input=" + input + ") on thread " + + gThreadManager.currentThread + " (main is " + + gThreadManager.mainThread + ")"); + dumpn("*** this._state == " + this._state); + + // Handle cases where we get more data after a request error has been + // discovered but *before* we can close the connection. + var data = this._data; + if (!data) + return; + + try + { + data.appendBytes(readBytes(input, input.available())); + } + catch (e) + { + if (streamClosed(e)) + { + dumpn("*** WARNING: unexpected error when reading from socket; will " + + "be treated as if the input stream had been closed"); + dumpn("*** WARNING: actual error was: " + e); + } + + // We've lost a race -- input has been closed, but we're still expecting + // to read more data. available() will throw in this case, and since + // we're dead in the water now, destroy the connection. + dumpn("*** onInputStreamReady called on a closed input, destroying " + + "connection"); + this._connection.close(); + return; + } + + switch (this._state) + { + default: + NS_ASSERT(false, "invalid state: " + this._state); + break; + + case READER_IN_REQUEST_LINE: + if (!this._processRequestLine()) + break; + /* fall through */ + + case READER_IN_HEADERS: + if (!this._processHeaders()) + break; + /* fall through */ + + case READER_IN_BODY: + this._processBody(); + } + + if (this._state != READER_FINISHED) + input.asyncWait(this, 0, 0, gThreadManager.currentThread); + }, + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIInputStreamCallback) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE API + + /** +* Processes unprocessed, downloaded data as a request line. +* +* @returns boolean +* true iff the request line has been fully processed +*/ + _processRequestLine: function() + { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + // Servers SHOULD ignore any empty line(s) received where a Request-Line + // is expected (section 4.1). + var data = this._data; + var line = {}; + var readSuccess; + while ((readSuccess = data.readLine(line)) && line.value == "") + dumpn("*** ignoring beginning blank line..."); + + // if we don't have a full line, wait until we do + if (!readSuccess) + return false; + + // we have the first non-blank line + try + { + this._parseRequestLine(line.value); + this._state = READER_IN_HEADERS; + return true; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Processes stored data, assuming it is either at the beginning or in +* the middle of processing request headers. +* +* @returns boolean +* true iff header data in the request has been fully processed +*/ + _processHeaders: function() + { + NS_ASSERT(this._state == READER_IN_HEADERS); + + // XXX things to fix here: + // + // - need to support RFC 2047-encoded non-US-ASCII characters + + try + { + var done = this._parseHeaders(); + if (done) + { + var request = this._metadata; + + // XXX this is wrong for requests with transfer-encodings applied to + // them, particularly chunked (which by its nature can have no + // meaningful Content-Length header)! + this._contentLength = request.hasHeader("Content-Length") + ? parseInt(request.getHeader("Content-Length"), 10) + : 0; + dumpn("_processHeaders, Content-length=" + this._contentLength); + + this._state = READER_IN_BODY; + } + return done; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Processes stored data, assuming it is either at the beginning or in +* the middle of processing the request body. +* +* @returns boolean +* true iff the request body has been fully processed +*/ + _processBody: function() + { + NS_ASSERT(this._state == READER_IN_BODY); + + // XXX handle chunked transfer-coding request bodies! + + try + { + if (this._contentLength > 0) + { + var data = this._data.purge(); + var count = Math.min(data.length, this._contentLength); + dumpn("*** loading data=" + data + " len=" + data.length + + " excess=" + (data.length - count)); + + var bos = new BinaryOutputStream(this._metadata._bodyOutputStream); + bos.writeByteArray(data, count); + this._contentLength -= count; + } + + dumpn("*** remaining body data len=" + this._contentLength); + if (this._contentLength == 0) + { + this._validateRequest(); + this._state = READER_FINISHED; + this._handleResponse(); + return true; + } + + return false; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Does various post-header checks on the data in this request. +* +* @throws : HttpError +* if the request was malformed in some way +*/ + _validateRequest: function() + { + NS_ASSERT(this._state == READER_IN_BODY); + + dumpn("*** _validateRequest"); + + var metadata = this._metadata; + var headers = metadata._headers; + + // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header + var identity = this._connection.server.identity; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) + { + if (!headers.hasHeader("Host")) + { + dumpn("*** malformed HTTP/1.1 or greater request with no Host header!"); + throw HTTP_400; + } + + // If the Request-URI wasn't absolute, then we need to determine our host. + // We have to determine what scheme was used to access us based on the + // server identity data at this point, because the request just doesn't + // contain enough data on its own to do this, sadly. + if (!metadata._host) + { + var host, port; + var hostPort = headers.getHeader("Host"); + var colon = hostPort.indexOf(":"); + if (colon < 0) + { + host = hostPort; + port = ""; + } + else + { + host = hostPort.substring(0, colon); + port = hostPort.substring(colon + 1); + } + + // NB: We allow an empty port here because, oddly, a colon may be + // present even without a port number, e.g. "example.com:"; in this + // case the default port applies. + if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) + { + dumpn("*** malformed hostname (" + hostPort + ") in Host " + + "header, 400 time"); + throw HTTP_400; + } + + // If we're not given a port, we're stuck, because we don't know what + // scheme to use to look up the correct port here, in general. Since + // the HTTPS case requires a tunnel/proxy and thus requires that the + // requested URI be absolute (and thus contain the necessary + // information), let's assume HTTP will prevail and use that. + port = +port || 80; + + var scheme = identity.getScheme(host, port); + if (!scheme) + { + dumpn("*** unrecognized hostname (" + hostPort + ") in Host " + + "header, 400 time"); + throw HTTP_400; + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + } + } + else + { + NS_ASSERT(metadata._host === undefined, + "HTTP/1.0 doesn't allow absolute paths in the request line!"); + + metadata._scheme = identity.primaryScheme; + metadata._host = identity.primaryHost; + metadata._port = identity.primaryPort; + } + + NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port), + "must have a location we recognize by now!"); + }, + + /** +* Handles responses in case of error, either in the server or in the request. +* +* @param e +* the specific error encountered, which is an HttpError in the case where +* the request is in some way invalid or cannot be fulfilled; if this isn't +* an HttpError we're going to be paranoid and shut down, because that +* shouldn't happen, ever +*/ + _handleError: function(e) + { + // Don't fall back into normal processing! + this._state = READER_FINISHED; + + var server = this._connection.server; + if (e instanceof HttpError) + { + var code = e.code; + } + else + { + dumpn("!!! UNEXPECTED ERROR: " + e + + (e.lineNumber ? ", line " + e.lineNumber : "")); + + // no idea what happened -- be paranoid and shut down + code = 500; + server._requestQuit(); + } + + // make attempted reuse of data an error + this._data = null; + + this._connection.processError(code, this._metadata); + }, + + /** +* Now that we've read the request line and headers, we can actually hand off +* the request to be handled. +* +* This method is called once per request, after the request line and all +* headers and the body, if any, have been received. +*/ + _handleResponse: function() + { + NS_ASSERT(this._state == READER_FINISHED); + + // We don't need the line-based data any more, so make attempted reuse an + // error. + this._data = null; + + this._connection.process(this._metadata); + }, + + + // PARSING + + /** +* Parses the request line for the HTTP request associated with this. +* +* @param line : string +* the request line +*/ + _parseRequestLine: function(line) + { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + dumpn("*** _parseRequestLine('" + line + "')"); + + var metadata = this._metadata; + + // clients and servers SHOULD accept any amount of SP or HT characters + // between fields, even though only a single SP is required (section 19.3) + var request = line.split(/[ \t]+/); + if (!request || request.length != 3) + throw HTTP_400; + + metadata._method = request[0]; + + // get the HTTP version + var ver = request[2]; + var match = ver.match(/^HTTP\/(\d+\.\d+)$/); + if (!match) + throw HTTP_400; + + // determine HTTP version + try + { + metadata._httpVersion = new nsHttpVersion(match[1]); + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) + throw "unsupported HTTP version"; + } + catch (e) + { + // we support HTTP/1.0 and HTTP/1.1 only + throw HTTP_501; + } + + + var fullPath = request[1]; + var serverIdentity = this._connection.server.identity; + + var scheme, host, port; + + if (fullPath.charAt(0) != "/") + { + // No absolute paths in the request line in HTTP prior to 1.1 + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) + throw HTTP_400; + + try + { + var uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(fullPath, null, null); + fullPath = uri.path; + scheme = uri.scheme; + host = metadata._host = uri.asciiHost; + port = uri.port; + if (port === -1) + { + if (scheme === "http") + port = 80; + else if (scheme === "https") + port = 443; + else + throw HTTP_400; + } + } + catch (e) + { + // If the host is not a valid host on the server, the response MUST be a + // 400 (Bad Request) error message (section 5.2). Alternately, the URI + // is malformed. + throw HTTP_400; + } + + if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/") + throw HTTP_400; + } + + var splitter = fullPath.indexOf("?"); + if (splitter < 0) + { + // _queryString already set in ctor + metadata._path = fullPath; + } + else + { + metadata._path = fullPath.substring(0, splitter); + metadata._queryString = fullPath.substring(splitter + 1); + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + }, + + /** +* Parses all available HTTP headers in this until the header-ending CRLFCRLF, +* adding them to the store of headers in the request. +* +* @throws +* HTTP_400 if the headers are malformed +* @returns boolean +* true if all headers have now been processed, false otherwise +*/ + _parseHeaders: function() + { + NS_ASSERT(this._state == READER_IN_HEADERS); + + dumpn("*** _parseHeaders"); + + var data = this._data; + + var headers = this._metadata._headers; + var lastName = this._lastHeaderName; + var lastVal = this._lastHeaderValue; + + var line = {}; + while (true) + { + NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)), + lastName === undefined ? + "lastVal without lastName? lastVal: '" + lastVal + "'" : + "lastName without lastVal? lastName: '" + lastName + "'"); + + if (!data.readLine(line)) + { + // save any data we have from the header we might still be processing + this._lastHeaderName = lastName; + this._lastHeaderValue = lastVal; + return false; + } + + var lineText = line.value; + var firstChar = lineText.charAt(0); + + // blank line means end of headers + if (lineText == "") + { + // we're finished with the previous header + if (lastName) + { + try + { + headers.setHeader(lastName, lastVal, true); + } + catch (e) + { + dumpn("*** e == " + e); + throw HTTP_400; + } + } + else + { + // no headers in request -- valid for HTTP/1.0 requests + } + + // either way, we're done processing headers + this._state = READER_IN_BODY; + return true; + } + else if (firstChar == " " || firstChar == "\t") + { + // multi-line header if we've already seen a header line + if (!lastName) + { + // we don't have a header to continue! + throw HTTP_400; + } + + // append this line's text to the value; starts with SP/HT, so no need + // for separating whitespace + lastVal += lineText; + } + else + { + // we have a new header, so set the old one (if one existed) + if (lastName) + { + try + { + headers.setHeader(lastName, lastVal, true); + } + catch (e) + { + dumpn("*** e == " + e); + throw HTTP_400; + } + } + + var colon = lineText.indexOf(":"); // first colon must be splitter + if (colon < 1) + { + // no colon or missing header field-name + throw HTTP_400; + } + + // set header name, value (to be set in the next loop, usually) + lastName = lineText.substring(0, colon); + lastVal = lineText.substring(colon + 1); + } // empty, continuation, start of header + } // while (true) + } +}; + + +/** The character codes for CR and LF. */ +const CR = 0x0D, LF = 0x0A; + +/** +* Calculates the number of characters before the first CRLF pair in array, or +* -1 if the array contains no CRLF pair. +* +* @param array : Array +* an array of numbers in the range [0, 256), each representing a single +* character; the first CRLF is the lowest index i where +* |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|, +* if such an |i| exists, and -1 otherwise +* @returns int +* the index of the first CRLF if any were present, -1 otherwise +*/ +function findCRLF(array) +{ + for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1)) + { + if (array[i + 1] == LF) + return i; + } + return -1; +} + + +/** +* A container which provides line-by-line access to the arrays of bytes with +* which it is seeded. +*/ +function LineData() +{ + /** An array of queued bytes from which to get line-based characters. */ + this._data = []; +} +LineData.prototype = +{ + /** +* Appends the bytes in the given array to the internal data cache maintained +* by this. +*/ + appendBytes: function(bytes) + { + Array.prototype.push.apply(this._data, bytes); + }, + + /** +* Removes and returns a line of data, delimited by CRLF, from this. +* +* @param out +* an object whose "value" property will be set to the first line of text +* present in this, sans CRLF, if this contains a full CRLF-delimited line +* of text; if this doesn't contain enough data, the value of the property +* is undefined +* @returns boolean +* true if a full line of data could be read from the data in this, false +* otherwise +*/ + readLine: function(out) + { + var data = this._data; + var length = findCRLF(data); + if (length < 0) + return false; + + // + // We have the index of the CR, so remove all the characters, including + // CRLF, from the array with splice, and convert the removed array into the + // corresponding string, from which we then strip the trailing CRLF. + // + // Getting the line in this matter acknowledges that substring is an O(1) + // operation in SpiderMonkey because strings are immutable, whereas two + // splices, both from the beginning of the data, are less likely to be as + // cheap as a single splice plus two extra character conversions. + // + var line = String.fromCharCode.apply(null, data.splice(0, length + 2)); + out.value = line.substring(0, length); + + return true; + }, + + /** +* Removes the bytes currently within this and returns them in an array. +* +* @returns Array +* the bytes within this when this method is called +*/ + purge: function() + { + var data = this._data; + this._data = []; + return data; + } +}; + + + +/** +* Creates a request-handling function for an nsIHttpRequestHandler object. +*/ +function createHandlerFunc(handler) +{ + return function(metadata, response) { handler.handle(metadata, response); }; +} + + +/** +* The default handler for directories; writes an HTML response containing a +* slightly-formatted directory listing. +*/ +function defaultIndexHandler(metadata, response) +{ + response.setHeader("Content-Type", "text/html", false); + + var path = htmlEscape(decodeURI(metadata.path)); + + // + // Just do a very basic bit of directory listings -- no need for too much + // fanciness, especially since we don't have a style sheet in which we can + // stick rules (don't want to pollute the default path-space). + // + + var body = '<html>\ +<head>\ +<title>' + path + '</title>\ +</head>\ +<body>\ +<h1>' + path + '</h1>\ +<ol style="list-style-type: none">'; + + var directory = metadata.getProperty("directory").QueryInterface(Ci.nsILocalFile); + NS_ASSERT(directory && directory.isDirectory()); + + var fileList = []; + var files = directory.directoryEntries; + while (files.hasMoreElements()) + { + var f = files.getNext().QueryInterface(Ci.nsIFile); + var name = f.leafName; + if (!f.isHidden() && + (name.charAt(name.length - 1) != HIDDEN_CHAR || + name.charAt(name.length - 2) == HIDDEN_CHAR)) + fileList.push(f); + } + + fileList.sort(fileSort); + + for (var i = 0; i < fileList.length; i++) + { + var file = fileList[i]; + try + { + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) + name = name.substring(0, name.length - 1); + var sep = file.isDirectory() ? "/" : ""; + + // Note: using " to delimit the attribute here because encodeURIComponent + // passes through '. + var item = '<li><a href="' + encodeURIComponent(name) + sep + '">' + + htmlEscape(name) + sep + + '</a></li>'; + + body += item; + } + catch (e) { /* some file system error, ignore the file */ } + } + + body += ' </ol>\ +</body>\ +</html>'; + + response.bodyOutputStream.write(body, body.length); +} + +/** +* Sorts a and b (nsIFile objects) into an aesthetically pleasing order. +*/ +function fileSort(a, b) +{ + var dira = a.isDirectory(), dirb = b.isDirectory(); + + if (dira && !dirb) + return -1; + if (dirb && !dira) + return 1; + + var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase(); + return nameb > namea ? -1 : 1; +} + + +/** +* Converts an externally-provided path into an internal path for use in +* determining file mappings. +* +* @param path +* the path to convert +* @param encoded +* true if the given path should be passed through decodeURI prior to +* conversion +* @throws URIError +* if path is incorrectly encoded +*/ +function toInternalPath(path, encoded) +{ + if (encoded) + path = decodeURI(path); + + var comps = path.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) + { + var comp = comps[i]; + if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) + comps[i] = comp + HIDDEN_CHAR; + } + return comps.join("/"); +} + + +/** +* Adds custom-specified headers for the given file to the given response, if +* any such headers are specified. +* +* @param file +* the file on the disk which is to be written +* @param metadata +* metadata about the incoming request +* @param response +* the Response to which any specified headers/data should be written +* @throws HTTP_500 +* if an error occurred while processing custom-specified headers +*/ +function maybeAddHeaders(file, metadata, response) +{ + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) + name = name.substring(0, name.length - 1); + + var headerFile = file.parent; + headerFile.append(name + HEADERS_SUFFIX); + + if (!headerFile.exists()) + return; + + const PR_RDONLY = 0x01; + var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + try + { + var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + + var line = {value: ""}; + var more = lis.readLine(line); + + if (!more && line.value == "") + return; + + + // request line + + var status = line.value; + if (status.indexOf("HTTP ") == 0) + { + status = status.substring(5); + var space = status.indexOf(" "); + var code, description; + if (space < 0) + { + code = status; + description = ""; + } + else + { + code = status.substring(0, space); + description = status.substring(space + 1, status.length); + } + + response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description); + + line.value = ""; + more = lis.readLine(line); + } + + // headers + while (more || line.value != "") + { + var header = line.value; + var colon = header.indexOf(":"); + + response.setHeader(header.substring(0, colon), + header.substring(colon + 1, header.length), + false); // allow overriding server-set headers + + line.value = ""; + more = lis.readLine(line); + } + } + catch (e) + { + dumpn("WARNING: error in headers for " + metadata.path + ": " + e); + throw HTTP_500; + } + finally + { + fis.close(); + } +} + + +/** +* An object which handles requests for a server, executing default and +* overridden behaviors as instructed by the code which uses and manipulates it. +* Default behavior includes the paths / and /trace (diagnostics), with some +* support for HTTP error pages for various codes and fallback to HTTP 500 if +* those codes fail for any reason. +* +* @param server : nsHttpServer +* the server in which this handler is being used +*/ +function ServerHandler(server) +{ + // FIELDS + + /** +* The nsHttpServer instance associated with this handler. +*/ + this._server = server; + + /** +* A FileMap object containing the set of path->nsILocalFile mappings for +* all directory mappings set in the server (e.g., "/" for /var/www/html/, +* "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2). +* +* Note carefully: the leading and trailing "/" in each path (not file) are +* removed before insertion to simplify the code which uses this. You have +* been warned! +*/ + this._pathDirectoryMap = new FileMap(); + + /** +* Custom request handlers for the server in which this resides. Path-handler +* pairs are stored as property-value pairs in this property. +* +* @see ServerHandler.prototype._defaultPaths +*/ + this._overridePaths = {}; + + /** +* Custom request handlers for the error handlers in the server in which this +* resides. Path-handler pairs are stored as property-value pairs in this +* property. +* +* @see ServerHandler.prototype._defaultErrors +*/ + this._overrideErrors = {}; + + /** +* Maps file extensions to their MIME types in the server, overriding any +* mapping that might or might not exist in the MIME service. +*/ + this._mimeMappings = {}; + + /** +* The default handler for requests for directories, used to serve directories +* when no index file is present. +*/ + this._indexHandler = defaultIndexHandler; + + /** Per-path state storage for the server. */ + this._state = {}; + + /** Entire-server state storage. */ + this._sharedState = {}; + + /** Entire-server state storage for nsISupports values. */ + this._objectState = {}; +} +ServerHandler.prototype = +{ + // PUBLIC API + + /** +* Handles a request to this server, responding to the request appropriately +* and initiating server shutdown if necessary. +* +* This method never throws an exception. +* +* @param connection : Connection +* the connection for this request +*/ + handleResponse: function(connection) + { + var request = connection.request; + var response = new Response(connection); + + var path = request.path; + dumpn("*** path == " + path); + + try + { + try + { + if (path in this._overridePaths) + { + // explicit paths first, then files based on existing directory mappings, + // then (if the file doesn't exist) built-in server default paths + dumpn("calling override for " + path); + this._overridePaths[path](request, response); + } + else + { + this._handleDefault(request, response); + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + if (!(e instanceof HttpError)) + { + dumpn("*** unexpected error: e == " + e); + throw HTTP_500; + } + if (e.code !== 404) + throw e; + + dumpn("*** default: " + (path in this._defaultPaths)); + + response = new Response(connection); + if (path in this._defaultPaths) + this._defaultPaths[path](request, response); + else + throw HTTP_404; + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + var errorCode = "internal"; + + try + { + if (!(e instanceof HttpError)) + throw e; + + errorCode = e.code; + dumpn("*** errorCode == " + errorCode); + + response = new Response(connection); + if (e.customErrorHandling) + e.customErrorHandling(response); + this._handleError(errorCode, request, response); + return; + } + catch (e2) + { + dumpn("*** error handling " + errorCode + " error: " + + "e2 == " + e2 + ", shutting down server"); + + connection.server._requestQuit(); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (!file) + { + dumpn("*** unregistering '" + path + "' mapping"); + delete this._overridePaths[path]; + return; + } + + dumpn("*** registering '" + path + "' as mapping to " + file.path); + file = file.clone(); + + var self = this; + this._overridePaths[path] = + function(request, response) + { + if (!file.exists()) + throw HTTP_404; + + response.setStatusLine(request.httpVersion, 200, "OK"); + self._writeFileResponse(request, file, response, 0, file.fileSize); + }; + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + // XXX true path validation! + if (path.charAt(0) != "/") + throw Cr.NS_ERROR_INVALID_ARG; + + this._handlerToField(handler, this._overridePaths, path); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // strip off leading and trailing '/' so that we can use lastIndexOf when + // determining exactly how a path maps onto a mapped directory -- + // conditional is required here to deal with "/".substring(1, 0) being + // converted to "/".substring(0, 1) per the JS specification + var key = path.length == 1 ? "" : path.substring(1, path.length - 1); + + // the path-to-directory mapping code requires that the first character not + // be "/", or it will go into an infinite loop + if (key.charAt(0) == "/") + throw Cr.NS_ERROR_INVALID_ARG; + + key = toInternalPath(key, false); + + if (directory) + { + dumpn("*** mapping '" + path + "' to the location " + directory.path); + this._pathDirectoryMap.put(key, directory); + } + else + { + dumpn("*** removing mapping for '" + path + "'"); + this._pathDirectoryMap.put(key, null); + } + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(err, handler) + { + if (!(err in HTTP_ERROR_CODES)) + dumpn("*** WARNING: registering non-HTTP/1.1 error code " + + "(" + err + ") handler -- was this intentional?"); + + this._handlerToField(handler, this._overrideErrors, err); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler: function(handler) + { + if (!handler) + handler = defaultIndexHandler; + else if (typeof(handler) != "function") + handler = createHandlerFunc(handler); + + this._indexHandler = handler; + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType: function(ext, type) + { + if (!type) + delete this._mimeMappings[ext]; + else + this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type); + }, + + // PRIVATE API + + /** +* Sets or remove (if handler is null) a handler in an object with a key. +* +* @param handler +* a handler, either function or an nsIHttpRequestHandler +* @param dict +* The object to attach the handler to. +* @param key +* The field name of the handler. +*/ + _handlerToField: function(handler, dict, key) + { + // for convenience, handler can be a function if this is run from xpcshell + if (typeof(handler) == "function") + dict[key] = handler; + else if (handler) + dict[key] = createHandlerFunc(handler); + else + delete dict[key]; + }, + + /** +* Handles a request which maps to a file in the local filesystem (if a base +* path has already been set; otherwise the 404 error is thrown). +* +* @param metadata : Request +* metadata for the incoming request +* @param response : Response +* an uninitialized Response to the given request, to be initialized by a +* request handler +* @throws HTTP_### +* if an HTTP error occurred (usually HTTP_404); note that in this case the +* calling code must handle post-processing of the response +*/ + _handleDefault: function(metadata, response) + { + dumpn("*** _handleDefault()"); + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + + var path = metadata.path; + NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">"); + + // determine the actual on-disk file; this requires finding the deepest + // path-to-directory mapping in the requested URL + var file = this._getFileForPath(path); + + // the "file" might be a directory, in which case we either serve the + // contained index.html or make the index handler write the response + if (file.exists() && file.isDirectory()) + { + file.append("index.html"); // make configurable? + if (!file.exists() || file.isDirectory()) + { + metadata._ensurePropertyBag(); + metadata._bag.setPropertyAsInterface("directory", file.parent); + this._indexHandler(metadata, response); + return; + } + } + + // alternately, the file might not exist + if (!file.exists()) + throw HTTP_404; + + var start, end; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) && + metadata.hasHeader("Range") && + this._getTypeFromFile(file) !== SJS_TYPE) + { + var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/); + if (!rangeMatch) + throw HTTP_400; + + if (rangeMatch[1] !== undefined) + start = parseInt(rangeMatch[1], 10); + + if (rangeMatch[2] !== undefined) + end = parseInt(rangeMatch[2], 10); + + if (start === undefined && end === undefined) + throw HTTP_400; + + // No start given, so the end is really the count of bytes from the + // end of the file. + if (start === undefined) + { + start = Math.max(0, file.fileSize - end); + end = file.fileSize - 1; + } + + // start and end are inclusive + if (end === undefined || end >= file.fileSize) + end = file.fileSize - 1; + + if (start !== undefined && start >= file.fileSize) { + var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable"); + HTTP_416.customErrorHandling = function(errorResponse) + { + maybeAddHeaders(file, metadata, errorResponse); + }; + throw HTTP_416; + } + + if (end < start) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + start = 0; + end = file.fileSize - 1; + } + else + { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize; + response.setHeader("Content-Range", contentRange); + } + } + else + { + start = 0; + end = file.fileSize - 1; + } + + // finally... + dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " + + start + " to " + end + " inclusive"); + this._writeFileResponse(metadata, file, response, start, end - start + 1); + }, + + /** +* Writes an HTTP response for the given file, including setting headers for +* file metadata. +* +* @param metadata : Request +* the Request for which a response is being generated +* @param file : nsILocalFile +* the file which is to be sent in the response +* @param response : Response +* the response to which the file should be written +* @param offset: uint +* the byte offset to skip to when writing +* @param count: uint +* the number of bytes to write +*/ + _writeFileResponse: function(metadata, file, response, offset, count) + { + const PR_RDONLY = 0x01; + + var type = this._getTypeFromFile(file); + if (type === SJS_TYPE) + { + var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + try + { + var sis = new ScriptableInputStream(fis); + var s = Cu.Sandbox(gGlobalObject); + s.importFunction(dump, "dump"); + + // Define a basic key-value state-preservation API across requests, with + // keys initially corresponding to the empty string. + var self = this; + var path = metadata.path; + s.importFunction(function getState(k) + { + return self._getState(path, k); + }); + s.importFunction(function setState(k, v) + { + self._setState(path, k, v); + }); + s.importFunction(function getSharedState(k) + { + return self._getSharedState(k); + }); + s.importFunction(function setSharedState(k, v) + { + self._setSharedState(k, v); + }); + s.importFunction(function getObjectState(k, callback) + { + callback(self._getObjectState(k)); + }); + s.importFunction(function setObjectState(k, v) + { + self._setObjectState(k, v); + }); + s.importFunction(function registerPathHandler(p, h) + { + self.registerPathHandler(p, h); + }); + + // Make it possible for sjs files to access their location + this._setState(path, "__LOCATION__", file.path); + + try + { + // Alas, the line number in errors dumped to console when calling the + // request handler is simply an offset from where we load the SJS file. + // Work around this in a reasonably non-fragile way by dynamically + // getting the line number where we evaluate the SJS file. Don't + // separate these two lines! + var line = new Error().lineNumber; + Cu.evalInSandbox(sis.read(file.fileSize), s); + } + catch (e) + { + dumpn("*** syntax error in SJS at " + file.path + ": " + e); + throw HTTP_500; + } + + try + { + s.handleRequest(metadata, response); + } + catch (e) + { + dump("*** error running SJS at " + file.path + ": " + + e + " on line " + + (e instanceof Error + ? e.lineNumber + " in httpd.js" + : (e.lineNumber - line)) + "\n"); + throw HTTP_500; + } + } + finally + { + fis.close(); + } + } + else + { + try + { + response.setHeader("Last-Modified", + toDateString(file.lastModifiedTime), + false); + } + catch (e) { /* lastModifiedTime threw, ignore */ } + + response.setHeader("Content-Type", type, false); + maybeAddHeaders(file, metadata, response); + response.setHeader("Content-Length", "" + count, false); + + var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + offset = offset || 0; + count = count || file.fileSize; + NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset"); + NS_ASSERT(count >= 0, "bad count"); + NS_ASSERT(offset + count <= file.fileSize, "bad total data size"); + + try + { + if (offset !== 0) + { + // Seek (or read, if seeking isn't supported) to the correct offset so + // the data sent to the client matches the requested range. + if (fis instanceof Ci.nsISeekableStream) + fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset); + else + new ScriptableInputStream(fis).read(offset); + } + } + catch (e) + { + fis.close(); + throw e; + } + + function writeMore() + { + gThreadManager.currentThread + .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL); + } + + var input = new BinaryInputStream(fis); + var output = new BinaryOutputStream(response.bodyOutputStream); + var writeData = + { + run: function() + { + var chunkSize = Math.min(65536, count); + count -= chunkSize; + NS_ASSERT(count >= 0, "underflow"); + + try + { + var data = input.readByteArray(chunkSize); + NS_ASSERT(data.length === chunkSize, + "incorrect data returned? got " + data.length + + ", expected " + chunkSize); + output.writeByteArray(data, data.length); + if (count === 0) + { + fis.close(); + response.finish(); + } + else + { + writeMore(); + } + } + catch (e) + { + try + { + fis.close(); + } + finally + { + response.finish(); + } + throw e; + } + } + }; + + writeMore(); + + // Now that we know copying will start, flag the response as async. + response.processAsync(); + } + }, + + /** +* Get the value corresponding to a given key for the given path for SJS state +* preservation across requests. +* +* @param path : string +* the path from which the given state is to be retrieved +* @param k : string +* the key whose corresponding value is to be returned +* @returns string +* the corresponding value, which is initially the empty string +*/ + _getState: function(path, k) + { + var state = this._state; + if (path in state && k in state[path]) + return state[path][k]; + return ""; + }, + + /** +* Set the value corresponding to a given key for the given path for SJS state +* preservation across requests. +* +* @param path : string +* the path from which the given state is to be retrieved +* @param k : string +* the key whose corresponding value is to be set +* @param v : string +* the value to be set +*/ + _setState: function(path, k, v) + { + if (typeof v !== "string") + throw new Error("non-string value passed"); + var state = this._state; + if (!(path in state)) + state[path] = {}; + state[path][k] = v; + }, + + /** +* Get the value corresponding to a given key for SJS state preservation +* across requests. +* +* @param k : string +* the key whose corresponding value is to be returned +* @returns string +* the corresponding value, which is initially the empty string +*/ + _getSharedState: function(k) + { + var state = this._sharedState; + if (k in state) + return state[k]; + return ""; + }, + + /** +* Set the value corresponding to a given key for SJS state preservation +* across requests. +* +* @param k : string +* the key whose corresponding value is to be set +* @param v : string +* the value to be set +*/ + _setSharedState: function(k, v) + { + if (typeof v !== "string") + throw new Error("non-string value passed"); + this._sharedState[k] = v; + }, + + /** +* Returns the object associated with the given key in the server for SJS +* state preservation across requests. +* +* @param k : string +* the key whose corresponding object is to be returned +* @returns nsISupports +* the corresponding object, or null if none was present +*/ + _getObjectState: function(k) + { + if (typeof k !== "string") + throw new Error("non-string key passed"); + return this._objectState[k] || null; + }, + + /** +* Sets the object associated with the given key in the server for SJS +* state preservation across requests. +* +* @param k : string +* the key whose corresponding object is to be set +* @param v : nsISupports +* the object to be associated with the given key; may be null +*/ + _setObjectState: function(k, v) + { + if (typeof k !== "string") + throw new Error("non-string key passed"); + if (typeof v !== "object") + throw new Error("non-object value passed"); + if (v && !("QueryInterface" in v)) + { + throw new Error("must pass an nsISupports; use wrappedJSObject to ease " + + "pain when using the server from JS"); + } + + this._objectState[k] = v; + }, + + /** +* Gets a content-type for the given file, first by checking for any custom +* MIME-types registered with this handler for the file's extension, second by +* asking the global MIME service for a content-type, and finally by failing +* over to application/octet-stream. +* +* @param file : nsIFile +* the nsIFile for which to get a file type +* @returns string +* the best content-type which can be determined for the file +*/ + _getTypeFromFile: function(file) + { + try + { + var name = file.leafName; + var dot = name.lastIndexOf("."); + if (dot > 0) + { + var ext = name.slice(dot + 1); + if (ext in this._mimeMappings) + return this._mimeMappings[ext]; + } + return Cc["@mozilla.org/uriloader/external-helper-app-service;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(file); + } + catch (e) + { + return "application/octet-stream"; + } + }, + + /** +* Returns the nsILocalFile which corresponds to the path, as determined using +* all registered path->directory mappings and any paths which are explicitly +* overridden. +* +* @param path : string +* the server path for which a file should be retrieved, e.g. "/foo/bar" +* @throws HttpError +* when the correct action is the corresponding HTTP error (i.e., because no +* mapping was found for a directory in path, the referenced file doesn't +* exist, etc.) +* @returns nsILocalFile +* the file to be sent as the response to a request for the path +*/ + _getFileForPath: function(path) + { + // decode and add underscores as necessary + try + { + path = toInternalPath(path, true); + } + catch (e) + { + throw HTTP_400; // malformed path + } + + // next, get the directory which contains this path + var pathMap = this._pathDirectoryMap; + + // An example progression of tmp for a path "/foo/bar/baz/" might be: + // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", "" + var tmp = path.substring(1); + while (true) + { + // do we have a match for current head of the path? + var file = pathMap.get(tmp); + if (file) + { + // XXX hack; basically disable showing mapping for /foo/bar/ when the + // requested path was /foo/bar, because relative links on the page + // will all be incorrect -- we really need the ability to easily + // redirect here instead + if (tmp == path.substring(1) && + tmp.length != 0 && + tmp.charAt(tmp.length - 1) != "/") + file = null; + else + break; + } + + // if we've finished trying all prefixes, exit + if (tmp == "") + break; + + tmp = tmp.substring(0, tmp.lastIndexOf("/")); + } + + // no mapping applies, so 404 + if (!file) + throw HTTP_404; + + + // last, get the file for the path within the determined directory + var parentFolder = file.parent; + var dirIsRoot = (parentFolder == null); + + // Strategy here is to append components individually, making sure we + // never move above the given directory; this allows paths such as + // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling"; + // this component-wise approach also means the code works even on platforms + // which don't use "/" as the directory separator, such as Windows + var leafPath = path.substring(tmp.length + 1); + var comps = leafPath.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) + { + var comp = comps[i]; + + if (comp == "..") + file = file.parent; + else if (comp == "." || comp == "") + continue; + else + file.append(comp); + + if (!dirIsRoot && file.equals(parentFolder)) + throw HTTP_403; + } + + return file; + }, + + /** +* Writes the error page for the given HTTP error code over the given +* connection. +* +* @param errorCode : uint +* the HTTP error code to be used +* @param connection : Connection +* the connection on which the error occurred +*/ + handleError: function(errorCode, connection) + { + var response = new Response(connection); + + dumpn("*** error in request: " + errorCode); + + this._handleError(errorCode, new Request(connection.port), response); + }, + + /** +* Handles a request which generates the given error code, using the +* user-defined error handler if one has been set, gracefully falling back to +* the x00 status code if the code has no handler, and failing to status code +* 500 if all else fails. +* +* @param errorCode : uint +* the HTTP error which is to be returned +* @param metadata : Request +* metadata for the request, which will often be incomplete since this is an +* error +* @param response : Response +* an uninitialized Response should be initialized when this method +* completes with information which represents the desired error code in the +* ideal case or a fallback code in abnormal circumstances (i.e., 500 is a +* fallback for 505, per HTTP specs) +*/ + _handleError: function(errorCode, metadata, response) + { + if (!metadata) + throw Cr.NS_ERROR_NULL_POINTER; + + var errorX00 = errorCode - (errorCode % 100); + + try + { + if (!(errorCode in HTTP_ERROR_CODES)) + dumpn("*** WARNING: requested invalid error: " + errorCode); + + // RFC 2616 says that we should try to handle an error by its class if we + // can't otherwise handle it -- if that fails, we revert to handling it as + // a 500 internal server error, and if that fails we throw and shut down + // the server + + // actually handle the error + try + { + if (errorCode in this._overrideErrors) + this._overrideErrors[errorCode](metadata, response); + else + this._defaultErrors[errorCode](metadata, response); + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + // don't retry the handler that threw + if (errorX00 == errorCode) + throw HTTP_500; + + dumpn("*** error in handling for error code " + errorCode + ", " + + "falling back to " + errorX00 + "..."); + response = new Response(response._connection); + if (errorX00 in this._overrideErrors) + this._overrideErrors[errorX00](metadata, response); + else if (errorX00 in this._defaultErrors) + this._defaultErrors[errorX00](metadata, response); + else + throw HTTP_500; + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(); + return; + } + + // we've tried everything possible for a meaningful error -- now try 500 + dumpn("*** error in handling for error code " + errorX00 + ", falling " + + "back to 500..."); + + try + { + response = new Response(response._connection); + if (500 in this._overrideErrors) + this._overrideErrors[500](metadata, response); + else + this._defaultErrors[500](metadata, response); + } + catch (e2) + { + dumpn("*** multiple errors in default error handlers!"); + dumpn("*** e == " + e + ", e2 == " + e2); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // FIELDS + + /** +* This object contains the default handlers for the various HTTP error codes. +*/ + _defaultErrors: + { + 400: function(metadata, response) + { + // none of the data in metadata is reliable, so hard-code everything here + response.setStatusLine("1.1", 400, "Bad Request"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Bad request\n"; + response.bodyOutputStream.write(body, body.length); + }, + 403: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 403, "Forbidden"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>403 Forbidden</title></head>\ +<body>\ +<h1>403 Forbidden</h1>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 404: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>404 Not Found</title></head>\ +<body>\ +<h1>404 Not Found</h1>\ +<p>\ +<span style='font-family: monospace;'>" + + htmlEscape(metadata.path) + + "</span> was not found.\ +</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 416: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 416, + "Requested Range Not Satisfiable"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head>\ +<title>416 Requested Range Not Satisfiable</title></head>\ +<body>\ +<h1>416 Requested Range Not Satisfiable</h1>\ +<p>The byte range was not valid for the\ +requested resource.\ +</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 500: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 500, + "Internal Server Error"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>500 Internal Server Error</title></head>\ +<body>\ +<h1>500 Internal Server Error</h1>\ +<p>Something's broken in this server and\ +needs to be fixed.</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 501: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 501, "Not Implemented"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>501 Not Implemented</title></head>\ +<body>\ +<h1>501 Not Implemented</h1>\ +<p>This server is not (yet) Apache.</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 505: function(metadata, response) + { + response.setStatusLine("1.1", 505, "HTTP Version Not Supported"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>505 HTTP Version Not Supported</title></head>\ +<body>\ +<h1>505 HTTP Version Not Supported</h1>\ +<p>This server only supports HTTP/1.0 and HTTP/1.1\ +connections.</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + } + }, + + /** +* Contains handlers for the default set of URIs contained in this server. +*/ + _defaultPaths: + { + "/": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>httpd.js</title></head>\ +<body>\ +<h1>httpd.js</h1>\ +<p>If you're seeing this page, httpd.js is up and\ +serving requests! Now set a base path and serve some\ +files!</p>\ +</body>\ +</html>"; + + response.bodyOutputStream.write(body, body.length); + }, + + "/trace": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Request-URI: " + + metadata.scheme + "://" + metadata.host + ":" + metadata.port + + metadata.path + "\n\n"; + body += "Request (semantically equivalent, slightly reformatted):\n\n"; + body += metadata.method + " " + metadata.path; + + if (metadata.queryString) + body += "?" + metadata.queryString; + + body += " HTTP/" + metadata.httpVersion + "\r\n"; + + var headEnum = metadata.headers; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n"; + } + + response.bodyOutputStream.write(body, body.length); + } + } +}; + + +/** +* Maps absolute paths to files on the local file system (as nsILocalFiles). +*/ +function FileMap() +{ + /** Hash which will map paths to nsILocalFiles. */ + this._map = {}; +} +FileMap.prototype = +{ + // PUBLIC API + + /** +* Maps key to a clone of the nsILocalFile value if value is non-null; +* otherwise, removes any extant mapping for key. +* +* @param key : string +* string to which a clone of value is mapped +* @param value : nsILocalFile +* the file to map to key, or null to remove a mapping +*/ + put: function(key, value) + { + if (value) + this._map[key] = value.clone(); + else + delete this._map[key]; + }, + + /** +* Returns a clone of the nsILocalFile mapped to key, or null if no such +* mapping exists. +* +* @param key : string +* key to which the returned file maps +* @returns nsILocalFile +* a clone of the mapped file, or null if no mapping exists +*/ + get: function(key) + { + var val = this._map[key]; + return val ? val.clone() : null; + } +}; + + +// Response CONSTANTS + +// token = *<any CHAR except CTLs or separators> +// CHAR = <any US-ASCII character (0-127)> +// CTL = <any US-ASCII control character (0-31) and DEL (127)> +// separators = "(" | ")" | "<" | ">" | "@" +// | "," | ";" | ":" | "\" | <"> +// | "/" | "[" | "]" | "?" | "=" +// | "{" | "}" | SP | HT +const IS_TOKEN_ARRAY = + [0, 0, 0, 0, 0, 0, 0, 0, // 0 + 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 0, 0, 0, 0, 0, 0, 0, 0, // 16 + 0, 0, 0, 0, 0, 0, 0, 0, // 24 + + 0, 1, 0, 1, 1, 1, 1, 1, // 32 + 0, 0, 1, 1, 0, 1, 1, 0, // 40 + 1, 1, 1, 1, 1, 1, 1, 1, // 48 + 1, 1, 0, 0, 0, 0, 0, 0, // 56 + + 0, 1, 1, 1, 1, 1, 1, 1, // 64 + 1, 1, 1, 1, 1, 1, 1, 1, // 72 + 1, 1, 1, 1, 1, 1, 1, 1, // 80 + 1, 1, 1, 0, 0, 0, 1, 1, // 88 + + 1, 1, 1, 1, 1, 1, 1, 1, // 96 + 1, 1, 1, 1, 1, 1, 1, 1, // 104 + 1, 1, 1, 1, 1, 1, 1, 1, // 112 + 1, 1, 1, 0, 1, 0, 1]; // 120 + + +/** +* Determines whether the given character code is a CTL. +* +* @param code : uint +* the character code +* @returns boolean +* true if code is a CTL, false otherwise +*/ +function isCTL(code) +{ + return (code >= 0 && code <= 31) || (code == 127); +} + +/** +* Represents a response to an HTTP request, encapsulating all details of that +* response. This includes all headers, the HTTP version, status code and +* explanation, and the entity itself. +* +* @param connection : Connection +* the connection over which this response is to be written +*/ +function Response(connection) +{ + /** The connection over which this response will be written. */ + this._connection = connection; + + /** +* The HTTP version of this response; defaults to 1.1 if not set by the +* handler. +*/ + this._httpVersion = nsHttpVersion.HTTP_1_1; + + /** +* The HTTP code of this response; defaults to 200. +*/ + this._httpCode = 200; + + /** +* The description of the HTTP code in this response; defaults to "OK". +*/ + this._httpDescription = "OK"; + + /** +* An nsIHttpHeaders object in which the headers in this response should be +* stored. This property is null after the status line and headers have been +* written to the network, and it may be modified up until it is cleared, +* except if this._finished is set first (in which case headers are written +* asynchronously in response to a finish() call not preceded by +* flushHeaders()). +*/ + this._headers = new nsHttpHeaders(); + + /** +* Set to true when this response is ended (completely constructed if possible +* and the connection closed); further actions on this will then fail. +*/ + this._ended = false; + + /** +* A stream used to hold data written to the body of this response. +*/ + this._bodyOutputStream = null; + + /** +* A stream containing all data that has been written to the body of this +* response so far. (Async handlers make the data contained in this +* unreliable as a way of determining content length in general, but auxiliary +* saved information can sometimes be used to guarantee reliability.) +*/ + this._bodyInputStream = null; + + /** +* A stream copier which copies data to the network. It is initially null +* until replaced with a copier for response headers; when headers have been +* fully sent it is replaced with a copier for the response body, remaining +* so for the duration of response processing. +*/ + this._asyncCopier = null; + + /** +* True if this response has been designated as being processed +* asynchronously rather than for the duration of a single call to +* nsIHttpRequestHandler.handle. +*/ + this._processAsync = false; + + /** +* True iff finish() has been called on this, signaling that no more changes +* to this may be made. +*/ + this._finished = false; + + /** +* True iff powerSeized() has been called on this, signaling that this +* response is to be handled manually by the response handler (which may then +* send arbitrary data in response, even non-HTTP responses). +*/ + this._powerSeized = false; +} +Response.prototype = +{ + // PUBLIC CONSTRUCTION API + + // + // see nsIHttpResponse.bodyOutputStream + // + get bodyOutputStream() + { + if (this._finished) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + if (!this._bodyOutputStream) + { + var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX, + null); + this._bodyOutputStream = pipe.outputStream; + this._bodyInputStream = pipe.inputStream; + if (this._processAsync || this._powerSeized) + this._startAsyncProcessor(); + } + + return this._bodyOutputStream; + }, + + // + // see nsIHttpResponse.write + // + write: function(data) + { + if (this._finished) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + var dataAsString = String(data); + this.bodyOutputStream.write(dataAsString, dataAsString.length); + }, + + // + // see nsIHttpResponse.setStatusLine + // + setStatusLine: function(httpVersion, code, description) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + if (!(code >= 0 && code < 1000)) + throw Cr.NS_ERROR_INVALID_ARG; + + try + { + var httpVer; + // avoid version construction for the most common cases + if (!httpVersion || httpVersion == "1.1") + httpVer = nsHttpVersion.HTTP_1_1; + else if (httpVersion == "1.0") + httpVer = nsHttpVersion.HTTP_1_0; + else + httpVer = new nsHttpVersion(httpVersion); + } + catch (e) + { + throw Cr.NS_ERROR_INVALID_ARG; + } + + // Reason-Phrase = *<TEXT, excluding CR, LF> + // TEXT = <any OCTET except CTLs, but including LWS> + // + // XXX this ends up disallowing octets which aren't Unicode, I think -- not + // much to do if description is IDL'd as string + if (!description) + description = ""; + for (var i = 0; i < description.length; i++) + if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t") + throw Cr.NS_ERROR_INVALID_ARG; + + // set the values only after validation to preserve atomicity + this._httpDescription = description; + this._httpCode = code; + this._httpVersion = httpVer; + }, + + // + // see nsIHttpResponse.setHeader + // + setHeader: function(name, value, merge) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + this._headers.setHeader(name, value, merge); + }, + + // + // see nsIHttpResponse.processAsync + // + processAsync: function() + { + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._processAsync) + return; + this._ensureAlive(); + + dumpn("*** processing connection " + this._connection.number + " async"); + this._processAsync = true; + + /* +* Either the bodyOutputStream getter or this method is responsible for +* starting the asynchronous processor and catching writes of data to the +* response body of async responses as they happen, for the purpose of +* forwarding those writes to the actual connection's output stream. +* If bodyOutputStream is accessed first, calling this method will create +* the processor (when it first is clear that body data is to be written +* immediately, not buffered). If this method is called first, accessing +* bodyOutputStream will create the processor. If only this method is +* called, we'll write nothing, neither headers nor the nonexistent body, +* until finish() is called. Since that delay is easily avoided by simply +* getting bodyOutputStream or calling write(""), we don't worry about it. +*/ + if (this._bodyOutputStream && !this._asyncCopier) + this._startAsyncProcessor(); + }, + + // + // see nsIHttpResponse.seizePower + // + seizePower: function() + { + if (this._processAsync) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + return; + this._ensureAlive(); + + dumpn("*** forcefully seizing power over connection " + + this._connection.number + "..."); + + // Purge any already-written data without sending it. We could as easily + // swap out the streams entirely, but that makes it possible to acquire and + // unknowingly use a stale reference, so we require there only be one of + // each stream ever for any response to avoid this complication. + if (this._asyncCopier) + this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED); + this._asyncCopier = null; + if (this._bodyOutputStream) + { + var input = new BinaryInputStream(this._bodyInputStream); + var avail; + while ((avail = input.available()) > 0) + input.readByteArray(avail); + } + + this._powerSeized = true; + if (this._bodyOutputStream) + this._startAsyncProcessor(); + }, + + // + // see nsIHttpResponse.finish + // + finish: function() + { + if (!this._processAsync && !this._powerSeized) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._finished) + return; + + dumpn("*** finishing connection " + this._connection.number); + this._startAsyncProcessor(); // in case bodyOutputStream was never accessed + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + this._finished = true; + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // POST-CONSTRUCTION API (not exposed externally) + + /** +* The HTTP version number of this, as a string (e.g. "1.1"). +*/ + get httpVersion() + { + this._ensureAlive(); + return this._httpVersion.toString(); + }, + + /** +* The HTTP status code of this response, as a string of three characters per +* RFC 2616. +*/ + get httpCode() + { + this._ensureAlive(); + + var codeString = (this._httpCode < 10 ? "0" : "") + + (this._httpCode < 100 ? "0" : "") + + this._httpCode; + return codeString; + }, + + /** +* The description of the HTTP status code of this response, or "" if none is +* set. +*/ + get httpDescription() + { + this._ensureAlive(); + + return this._httpDescription; + }, + + /** +* The headers in this response, as an nsHttpHeaders object. +*/ + get headers() + { + this._ensureAlive(); + + return this._headers; + }, + + // + // see nsHttpHeaders.getHeader + // + getHeader: function(name) + { + this._ensureAlive(); + + return this._headers.getHeader(name); + }, + + /** +* Determines whether this response may be abandoned in favor of a newly +* constructed response. A response may be abandoned only if it is not being +* sent asynchronously and if raw control over it has not been taken from the +* server. +* +* @returns boolean +* true iff no data has been written to the network +*/ + partiallySent: function() + { + dumpn("*** partiallySent()"); + return this._processAsync || this._powerSeized; + }, + + /** +* If necessary, kicks off the remaining request processing needed to be done +* after a request handler performs its initial work upon this response. +*/ + complete: function() + { + dumpn("*** complete()"); + if (this._processAsync || this._powerSeized) + { + NS_ASSERT(this._processAsync ^ this._powerSeized, + "can't both send async and relinquish power"); + return; + } + + NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?"); + + this._startAsyncProcessor(); + + // Now make sure we finish processing this request! + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + }, + + /** +* Abruptly ends processing of this response, usually due to an error in an +* incoming request but potentially due to a bad error handler. Since we +* cannot handle the error in the usual way (giving an HTTP error page in +* response) because data may already have been sent (or because the response +* might be expected to have been generated asynchronously or completely from +* scratch by the handler), we stop processing this response and abruptly +* close the connection. +* +* @param e : Error +* the exception which precipitated this abort, or null if no such exception +* was generated +*/ + abort: function(e) + { + dumpn("*** abort(<" + e + ">)"); + + // This response will be ended by the processor if one was created. + var copier = this._asyncCopier; + if (copier) + { + // We dispatch asynchronously here so that any pending writes of data to + // the connection will be deterministically written. This makes it easier + // to specify exact behavior, and it makes observable behavior more + // predictable for clients. Note that the correctness of this depends on + // callbacks in response to _waitToReadData in WriteThroughCopier + // happening asynchronously with respect to the actual writing of data to + // bodyOutputStream, as they currently do; if they happened synchronously, + // an event which ran before this one could write more data to the + // response body before we get around to canceling the copier. We have + // tests for this in test_seizepower.js, however, and I can't think of a + // way to handle both cases without removing bodyOutputStream access and + // moving its effective write(data, length) method onto Response, which + // would be slower and require more code than this anyway. + gThreadManager.currentThread.dispatch({ + run: function() + { + dumpn("*** canceling copy asynchronously..."); + copier.cancel(Cr.NS_ERROR_UNEXPECTED); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + } + else + { + this.end(); + } + }, + + /** +* Closes this response's network connection, marks the response as finished, +* and notifies the server handler that the request is done being processed. +*/ + end: function() + { + NS_ASSERT(!this._ended, "ending this response twice?!?!"); + + this._connection.close(); + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + + this._finished = true; + this._ended = true; + }, + + // PRIVATE IMPLEMENTATION + + /** +* Sends the status line and headers of this response if they haven't been +* sent and initiates the process of copying data written to this response's +* body to the network. +*/ + _startAsyncProcessor: function() + { + dumpn("*** _startAsyncProcessor()"); + + // Handle cases where we're being called a second time. The former case + // happens when this is triggered both by complete() and by processAsync(), + // while the latter happens when processAsync() in conjunction with sent + // data causes abort() to be called. + if (this._asyncCopier || this._ended) + { + dumpn("*** ignoring second call to _startAsyncProcessor"); + return; + } + + // Send headers if they haven't been sent already and should be sent, then + // asynchronously continue to send the body. + if (this._headers && !this._powerSeized) + { + this._sendHeaders(); + return; + } + + this._headers = null; + this._sendBody(); + }, + + /** +* Signals that all modifications to the response status line and headers are +* complete and then sends that data over the network to the client. Once +* this method completes, a different response to the request that resulted +* in this response cannot be sent -- the only possible action in case of +* error is to abort the response and close the connection. +*/ + _sendHeaders: function() + { + dumpn("*** _sendHeaders()"); + + NS_ASSERT(this._headers); + NS_ASSERT(!this._powerSeized); + + // request-line + var statusLine = "HTTP/" + this.httpVersion + " " + + this.httpCode + " " + + this.httpDescription + "\r\n"; + + // header post-processing + + var headers = this._headers; + headers.setHeader("Connection", "close", false); + headers.setHeader("Server", "httpd.js", false); + if (!headers.hasHeader("Date")) + headers.setHeader("Date", toDateString(Date.now()), false); + + // Any response not being processed asynchronously must have an associated + // Content-Length header for reasons of backwards compatibility with the + // initial server, which fully buffered every response before sending it. + // Beyond that, however, it's good to do this anyway because otherwise it's + // impossible to test behaviors that depend on the presence or absence of a + // Content-Length header. + if (!this._processAsync) + { + dumpn("*** non-async response, set Content-Length"); + + var bodyStream = this._bodyInputStream; + var avail = bodyStream ? bodyStream.available() : 0; + + // XXX assumes stream will always report the full amount of data available + headers.setHeader("Content-Length", "" + avail, false); + } + + + // construct and send response + dumpn("*** header post-processing completed, sending response head..."); + + // request-line + var preambleData = [statusLine]; + + // headers + var headEnum = headers.enumerator; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + var values = headers.getHeaderValues(fieldName); + for (var i = 0, sz = values.length; i < sz; i++) + preambleData.push(fieldName + ": " + values[i] + "\r\n"); + } + + // end request-line/headers + preambleData.push("\r\n"); + + var preamble = preambleData.join(""); + + var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null); + responseHeadPipe.outputStream.write(preamble, preamble.length); + + var response = this; + var copyObserver = + { + onStartRequest: function(request, cx) + { + dumpn("*** preamble copying started"); + }, + + onStopRequest: function(request, cx, statusCode) + { + dumpn("*** preamble copying complete " + + "[status=0x" + statusCode.toString(16) + "]"); + + if (!components.isSuccessCode(statusCode)) + { + dumpn("!!! header copying problems: non-success statusCode, " + + "ending response"); + + response.end(); + } + else + { + response._sendBody(); + } + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + var headerCopier = this._asyncCopier = + new WriteThroughCopier(responseHeadPipe.inputStream, + this._connection.output, + copyObserver, null); + + responseHeadPipe.outputStream.close(); + + // Forbid setting any more headers or modifying the request line. + this._headers = null; + }, + + /** +* Asynchronously writes the body of the response (or the entire response, if +* seizePower() has been called) to the network. +*/ + _sendBody: function() + { + dumpn("*** _sendBody"); + + NS_ASSERT(!this._headers, "still have headers around but sending body?"); + + // If no body data was written, we're done + if (!this._bodyInputStream) + { + dumpn("*** empty body, response finished"); + this.end(); + return; + } + + var response = this; + var copyObserver = + { + onStartRequest: function(request, context) + { + dumpn("*** onStartRequest"); + }, + + onStopRequest: function(request, cx, statusCode) + { + dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]"); + + if (statusCode === Cr.NS_BINDING_ABORTED) + { + dumpn("*** terminating copy observer without ending the response"); + } + else + { + if (!components.isSuccessCode(statusCode)) + dumpn("*** WARNING: non-success statusCode in onStopRequest"); + + response.end(); + } + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + dumpn("*** starting async copier of body data..."); + this._asyncCopier = + new WriteThroughCopier(this._bodyInputStream, this._connection.output, + copyObserver, null); + }, + + /** Ensures that this hasn't been ended. */ + _ensureAlive: function() + { + NS_ASSERT(!this._ended, "not handling response lifetime correctly"); + } +}; + +/** +* Size of the segments in the buffer used in storing response data and writing +* it to the socket. +*/ +Response.SEGMENT_SIZE = 8192; + +/** Serves double duty in WriteThroughCopier implementation. */ +function notImplemented() +{ + throw Cr.NS_ERROR_NOT_IMPLEMENTED; +} + +/** Returns true iff the given exception represents stream closure. */ +function streamClosed(e) +{ + return e === Cr.NS_BASE_STREAM_CLOSED || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED); +} + +/** Returns true iff the given exception represents a blocked stream. */ +function wouldBlock(e) +{ + return e === Cr.NS_BASE_STREAM_WOULD_BLOCK || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK); +} + +/** +* Copies data from source to sink as it becomes available, when that data can +* be written to sink without blocking. +* +* @param source : nsIAsyncInputStream +* the stream from which data is to be read +* @param sink : nsIAsyncOutputStream +* the stream to which data is to be copied +* @param observer : nsIRequestObserver +* an observer which will be notified when the copy starts and finishes +* @param context : nsISupports +* context passed to observer when notified of start/stop +* @throws NS_ERROR_NULL_POINTER +* if source, sink, or observer are null +*/ +function WriteThroughCopier(source, sink, observer, context) +{ + if (!source || !sink || !observer) + throw Cr.NS_ERROR_NULL_POINTER; + + /** Stream from which data is being read. */ + this._source = source; + + /** Stream to which data is being written. */ + this._sink = sink; + + /** Observer watching this copy. */ + this._observer = observer; + + /** Context for the observer watching this. */ + this._context = context; + + /** +* True iff this is currently being canceled (cancel has been called, the +* callback may not yet have been made). +*/ + this._canceled = false; + + /** +* False until all data has been read from input and written to output, at +* which point this copy is completed and cancel() is asynchronously called. +*/ + this._completed = false; + + /** Required by nsIRequest, meaningless. */ + this.loadFlags = 0; + /** Required by nsIRequest, meaningless. */ + this.loadGroup = null; + /** Required by nsIRequest, meaningless. */ + this.name = "response-body-copy"; + + /** Status of this request. */ + this.status = Cr.NS_OK; + + /** Arrays of byte strings waiting to be written to output. */ + this._pendingData = []; + + // start copying + try + { + observer.onStartRequest(this, context); + this._waitToReadData(); + this._waitForSinkClosure(); + } + catch (e) + { + dumpn("!!! error starting copy: " + e + + ("lineNumber" in e ? ", line " + e.lineNumber : "")); + dumpn(e.stack); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + } +} +WriteThroughCopier.prototype = +{ + /* nsISupports implementation */ + + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIInputStreamCallback) || + iid.equals(Ci.nsIOutputStreamCallback) || + iid.equals(Ci.nsIRequest) || + iid.equals(Ci.nsISupports)) + { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // NSIINPUTSTREAMCALLBACK + + /** +* Receives a more-data-in-input notification and writes the corresponding +* data to the output. +* +* @param input : nsIAsyncInputStream +* the input stream on whose data we have been waiting +*/ + onInputStreamReady: function(input) + { + if (this._source === null) + return; + + dumpn("*** onInputStreamReady"); + + // + // Ordinarily we'll read a non-zero amount of data from input, queue it up + // to be written and then wait for further callbacks. The complications in + // this method are the cases where we deviate from that behavior when errors + // occur or when copying is drawing to a finish. + // + // The edge cases when reading data are: + // + // Zero data is read + // If zero data was read, we're at the end of available data, so we can + // should stop reading and move on to writing out what we have (or, if + // we've already done that, onto notifying of completion). + // A stream-closed exception is thrown + // This is effectively a less kind version of zero data being read; the + // only difference is that we notify of completion with that result + // rather than with NS_OK. + // Some other exception is thrown + // This is the least kind result. We don't know what happened, so we + // act as though the stream closed except that we notify of completion + // with the result NS_ERROR_UNEXPECTED. + // + + var bytesWanted = 0, bytesConsumed = -1; + try + { + input = new BinaryInputStream(input); + + bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE); + dumpn("*** input wanted: " + bytesWanted); + + if (bytesWanted > 0) + { + var data = input.readByteArray(bytesWanted); + bytesConsumed = data.length; + this._pendingData.push(String.fromCharCode.apply(String, data)); + } + + dumpn("*** " + bytesConsumed + " bytes read"); + + // Handle the zero-data edge case in the same place as all other edge + // cases are handled. + if (bytesWanted === 0) + throw Cr.NS_BASE_STREAM_CLOSED; + } + catch (e) + { + if (streamClosed(e)) + { + dumpn("*** input stream closed"); + e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED; + } + else + { + dumpn("!!! unexpected error reading from input, canceling: " + e); + e = Cr.NS_ERROR_UNEXPECTED; + } + + this._doneReadingSource(e); + return; + } + + var pendingData = this._pendingData; + + NS_ASSERT(bytesConsumed > 0); + NS_ASSERT(pendingData.length > 0, "no pending data somehow?"); + NS_ASSERT(pendingData[pendingData.length - 1].length > 0, + "buffered zero bytes of data?"); + + NS_ASSERT(this._source !== null); + + // Reading has gone great, and we've gotten data to write now. What if we + // don't have a place to write that data, because output went away just + // before this read? Drop everything on the floor, including new data, and + // cancel at this point. + if (this._sink === null) + { + pendingData.length = 0; + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we've read the data, and we know we have a place to write it. We + // need to queue up the data to be written, but *only* if none is queued + // already -- if data's already queued, the code that actually writes the + // data will make sure to wait on unconsumed pending data. + try + { + if (pendingData.length === 1) + this._waitToWriteData(); + } + catch (e) + { + dumpn("!!! error waiting to write data just read, swallowing and " + + "writing only what we already have: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Whee! We successfully read some data, and it's successfully queued up to + // be written. All that remains now is to wait for more data to read. + try + { + this._waitToReadData(); + } + catch (e) + { + dumpn("!!! error waiting to read more data: " + e); + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + } + }, + + + // NSIOUTPUTSTREAMCALLBACK + + /** +* Callback when data may be written to the output stream without blocking, or +* when the output stream has been closed. +* +* @param output : nsIAsyncOutputStream +* the output stream on whose writability we've been waiting, also known as +* this._sink +*/ + onOutputStreamReady: function(output) + { + if (this._sink === null) + return; + + dumpn("*** onOutputStreamReady"); + + var pendingData = this._pendingData; + if (pendingData.length === 0) + { + // There's no pending data to write. The only way this can happen is if + // we're waiting on the output stream's closure, so we can respond to a + // copying failure as quickly as possible (rather than waiting for data to + // be available to read and then fail to be copied). Therefore, we must + // be done now -- don't bother to attempt to write anything and wrap + // things up. + dumpn("!!! output stream closed prematurely, ending copy"); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + + NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?"); + + // + // Write out the first pending quantum of data. The possible errors here + // are: + // + // The write might fail because we can't write that much data + // Okay, we've written what we can now, so re-queue what's left and + // finish writing it out later. + // The write failed because the stream was closed + // Discard pending data that we can no longer write, stop reading, and + // signal that copying finished. + // Some other error occurred. + // Same as if the stream were closed, but notify with the status + // NS_ERROR_UNEXPECTED so the observer knows something was wonky. + // + + try + { + var quantum = pendingData[0]; + + // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on + // undefined behavior! We're only using this because writeByteArray + // is unusably broken for asynchronous output streams; see bug 532834 + // for details. + var bytesWritten = output.write(quantum, quantum.length); + if (bytesWritten === quantum.length) + pendingData.shift(); + else + pendingData[0] = quantum.substring(bytesWritten); + + dumpn("*** wrote " + bytesWritten + " bytes of data"); + } + catch (e) + { + if (wouldBlock(e)) + { + NS_ASSERT(pendingData.length > 0, + "stream-blocking exception with no data to write?"); + NS_ASSERT(pendingData[0].length > 0, + "stream-blocking exception with empty quantum?"); + this._waitToWriteData(); + return; + } + + if (streamClosed(e)) + dumpn("!!! output stream prematurely closed, signaling error..."); + else + dumpn("!!! unknown error: " + e + ", quantum=" + quantum); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // The day is ours! Quantum written, now let's see if we have more data + // still to write. + try + { + if (pendingData.length > 0) + { + this._waitToWriteData(); + return; + } + } + catch (e) + { + dumpn("!!! unexpected error waiting to write pending data: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we have no more pending data to write -- but might we get more in + // the future? + if (this._source !== null) + { + /* +* If we might, then wait for the output stream to be closed. (We wait +* only for closure because we have no data to write -- and if we waited +* for a specific amount of data, we would get repeatedly notified for no +* reason if over time the output stream permitted more and more data to +* be written to it without blocking.) +*/ + this._waitForSinkClosure(); + } + else + { + /* +* On the other hand, if we can't have more data because the input +* stream's gone away, then it's time to notify of copy completion. +* Victory! +*/ + this._sink = null; + this._cancelOrDispatchCancelCallback(Cr.NS_OK); + } + }, + + + // NSIREQUEST + + /** Returns true if the cancel observer hasn't been notified yet. */ + isPending: function() + { + return !this._completed; + }, + + /** Not implemented, don't use! */ + suspend: notImplemented, + /** Not implemented, don't use! */ + resume: notImplemented, + + /** +* Cancels data reading from input, asynchronously writes out any pending +* data, and causes the observer to be notified with the given error code when +* all writing has finished. +* +* @param status : nsresult +* the status to pass to the observer when data copying has been canceled +*/ + cancel: function(status) + { + dumpn("*** cancel(" + status.toString(16) + ")"); + + if (this._canceled) + { + dumpn("*** suppressing a late cancel"); + return; + } + + this._canceled = true; + this.status = status; + + // We could be in the middle of absolutely anything at this point. Both + // input and output might still be around, we might have pending data to + // write, and in general we know nothing about the state of the world. We + // therefore must assume everything's in progress and take everything to its + // final steady state (or so far as it can go before we need to finish + // writing out remaining data). + + this._doneReadingSource(status); + }, + + + // PRIVATE IMPLEMENTATION + + /** +* Stop reading input if we haven't already done so, passing e as the status +* when closing the stream, and kick off a copy-completion notice if no more +* data remains to be written. +* +* @param e : nsresult +* the status to be used when closing the input stream +*/ + _doneReadingSource: function(e) + { + dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")"); + + this._finishSource(e); + if (this._pendingData.length === 0) + this._sink = null; + else + NS_ASSERT(this._sink !== null, "null output?"); + + // If we've written out all data read up to this point, then it's time to + // signal completion. + if (this._sink === null) + { + NS_ASSERT(this._pendingData.length === 0, "pending data still?"); + this._cancelOrDispatchCancelCallback(e); + } + }, + + /** +* Stop writing output if we haven't already done so, discard any data that +* remained to be sent, close off input if it wasn't already closed, and kick +* off a copy-completion notice. +* +* @param e : nsresult +* the status to be used when closing input if it wasn't already closed +*/ + _doneWritingToSink: function(e) + { + dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")"); + + this._pendingData.length = 0; + this._sink = null; + this._doneReadingSource(e); + }, + + /** +* Completes processing of this copy: either by canceling the copy if it +* hasn't already been canceled using the provided status, or by dispatching +* the cancel callback event (with the originally provided status, of course) +* if it already has been canceled. +* +* @param status : nsresult +* the status code to use to cancel this, if this hasn't already been +* canceled +*/ + _cancelOrDispatchCancelCallback: function(status) + { + dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")"); + + NS_ASSERT(this._source === null, "should have finished input"); + NS_ASSERT(this._sink === null, "should have finished output"); + NS_ASSERT(this._pendingData.length === 0, "should have no pending data"); + + if (!this._canceled) + { + this.cancel(status); + return; + } + + var self = this; + var event = + { + run: function() + { + dumpn("*** onStopRequest async callback"); + + self._completed = true; + try + { + self._observer.onStopRequest(self, self._context, self.status); + } + catch (e) + { + NS_ASSERT(false, + "how are we throwing an exception here? we control " + + "all the callers! " + e); + } + } + }; + + gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** +* Kicks off another wait for more data to be available from the input stream. +*/ + _waitToReadData: function() + { + dumpn("*** _waitToReadData"); + this._source.asyncWait(this, 0, Response.SEGMENT_SIZE, + gThreadManager.mainThread); + }, + + /** +* Kicks off another wait until data can be written to the output stream. +*/ + _waitToWriteData: function() + { + dumpn("*** _waitToWriteData"); + + var pendingData = this._pendingData; + NS_ASSERT(pendingData.length > 0, "no pending data to write?"); + NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?"); + + this._sink.asyncWait(this, 0, pendingData[0].length, + gThreadManager.mainThread); + }, + + /** +* Kicks off a wait for the sink to which data is being copied to be closed. +* We wait for stream closure when we don't have any data to be copied, rather +* than waiting to write a specific amount of data. We can't wait to write +* data because the sink might be infinitely writable, and if no data appears +* in the source for a long time we might have to spin quite a bit waiting to +* write, waiting to write again, &c. Waiting on stream closure instead means +* we'll get just one notification if the sink dies. Note that when data +* starts arriving from the sink we'll resume waiting for data to be written, +* dropping this closure-only callback entirely. +*/ + _waitForSinkClosure: function() + { + dumpn("*** _waitForSinkClosure"); + + this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0, + gThreadManager.mainThread); + }, + + /** +* Closes input with the given status, if it hasn't already been closed; +* otherwise a no-op. +* +* @param status : nsresult +* status code use to close the source stream if necessary +*/ + _finishSource: function(status) + { + dumpn("*** _finishSource(" + status.toString(16) + ")"); + + if (this._source !== null) + { + this._source.closeWithStatus(status); + this._source = null; + } + } +}; + + +/** +* A container for utility functions used with HTTP headers. +*/ +const headerUtils = +{ + /** +* Normalizes fieldName (by converting it to lowercase) and ensures it is a +* valid header field name (although not necessarily one specified in RFC +* 2616). +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not match the field-name production in RFC 2616 +* @returns string +* fieldName converted to lowercase if it is a valid header, for characters +* where case conversion is possible +*/ + normalizeFieldName: function(fieldName) + { + if (fieldName == "") + throw Cr.NS_ERROR_INVALID_ARG; + + for (var i = 0, sz = fieldName.length; i < sz; i++) + { + if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)]) + { + dumpn(fieldName + " is not a valid header field name!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + } + + return fieldName.toLowerCase(); + }, + + /** +* Ensures that fieldValue is a valid header field value (although not +* necessarily as specified in RFC 2616 if the corresponding field name is +* part of the HTTP protocol), normalizes the value if it is, and +* returns the normalized value. +* +* @param fieldValue : string +* a value to be normalized as an HTTP header field value +* @throws NS_ERROR_INVALID_ARG +* if fieldValue does not match the field-value production in RFC 2616 +* @returns string +* fieldValue as a normalized HTTP header field value +*/ + normalizeFieldValue: function(fieldValue) + { + // field-value = *( field-content | LWS ) + // field-content = <the OCTETs making up the field-value + // and consisting of either *TEXT or combinations + // of token, separators, and quoted-string> + // TEXT = <any OCTET except CTLs, + // but including LWS> + // LWS = [CRLF] 1*( SP | HT ) + // + // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + // qdtext = <any TEXT except <">> + // quoted-pair = "\" CHAR + // CHAR = <any US-ASCII character (octets 0 - 127)> + + // Any LWS that occurs between field-content MAY be replaced with a single + // SP before interpreting the field value or forwarding the message + // downstream (section 4.2); we replace 1*LWS with a single SP + var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " "); + + // remove leading/trailing LWS (which has been converted to SP) + val = val.replace(/^ +/, "").replace(/ +$/, ""); + + // that should have taken care of all CTLs, so val should contain no CTLs + for (var i = 0, len = val.length; i < len; i++) + if (isCTL(val.charCodeAt(i))) + throw Cr.NS_ERROR_INVALID_ARG; + + // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly + // normalize, however, so this can be construed as a tightening of the + // spec and not entirely as a bug + return val; + } +}; + + + +/** +* Converts the given string into a string which is safe for use in an HTML +* context. +* +* @param str : string +* the string to make HTML-safe +* @returns string +* an HTML-safe version of str +*/ +function htmlEscape(str) +{ + // this is naive, but it'll work + var s = ""; + for (var i = 0; i < str.length; i++) + s += "&#" + str.charCodeAt(i) + ";"; + return s; +} + + +/** +* Constructs an object representing an HTTP version (see section 3.1). +* +* @param versionString +* a string of the form "#.#", where # is an non-negative decimal integer with +* or without leading zeros +* @throws +* if versionString does not specify a valid HTTP version number +*/ +function nsHttpVersion(versionString) +{ + var matches = /^(\d+)\.(\d+)$/.exec(versionString); + if (!matches) + throw "Not a valid HTTP version!"; + + /** The major version number of this, as a number. */ + this.major = parseInt(matches[1], 10); + + /** The minor version number of this, as a number. */ + this.minor = parseInt(matches[2], 10); + + if (isNaN(this.major) || isNaN(this.minor) || + this.major < 0 || this.minor < 0) + throw "Not a valid HTTP version!"; +} +nsHttpVersion.prototype = +{ + /** +* Returns the standard string representation of the HTTP version represented +* by this (e.g., "1.1"). +*/ + toString: function () + { + return this.major + "." + this.minor; + }, + + /** +* Returns true if this represents the same HTTP version as otherVersion, +* false otherwise. +* +* @param otherVersion : nsHttpVersion +* the version to compare against this +*/ + equals: function (otherVersion) + { + return this.major == otherVersion.major && + this.minor == otherVersion.minor; + }, + + /** True if this >= otherVersion, false otherwise. */ + atLeast: function(otherVersion) + { + return this.major > otherVersion.major || + (this.major == otherVersion.major && + this.minor >= otherVersion.minor); + } +}; + +nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0"); +nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1"); + + +/** +* An object which stores HTTP headers for a request or response. +* +* Note that since headers are case-insensitive, this object converts headers to +* lowercase before storing them. This allows the getHeader and hasHeader +* methods to work correctly for any case of a header, but it means that the +* values returned by .enumerator may not be equal case-sensitively to the +* values passed to setHeader when adding headers to this. +*/ +function nsHttpHeaders() +{ + /** +* A hash of headers, with header field names as the keys and header field +* values as the values. Header field names are case-insensitive, but upon +* insertion here they are converted to lowercase. Header field values are +* normalized upon insertion to contain no leading or trailing whitespace. +* +* Note also that per RFC 2616, section 4.2, two headers with the same name in +* a message may be treated as one header with the same field name and a field +* value consisting of the separate field values joined together with a "," in +* their original order. This hash stores multiple headers with the same name +* in this manner. +*/ + this._headers = {}; +} +nsHttpHeaders.prototype = +{ + /** +* Sets the header represented by name and value in this. +* +* @param name : string +* the header name +* @param value : string +* the header value +* @throws NS_ERROR_INVALID_ARG +* if name or value is not a valid header component +*/ + setHeader: function(fieldName, fieldValue, merge) + { + var name = headerUtils.normalizeFieldName(fieldName); + var value = headerUtils.normalizeFieldValue(fieldValue); + + // The following three headers are stored as arrays because their real-world + // syntax prevents joining individual headers into a single header using + // ",". See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77> + if (merge && name in this._headers) + { + if (name === "www-authenticate" || + name === "proxy-authenticate" || + name === "set-cookie") + { + this._headers[name].push(value); + } + else + { + this._headers[name][0] += "," + value; + NS_ASSERT(this._headers[name].length === 1, + "how'd a non-special header have multiple values?") + } + } + else + { + this._headers[name] = [value]; + } + }, + + /** +* Returns the value for the header specified by this. +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @throws NS_ERROR_NOT_AVAILABLE +* if the given header does not exist in this +* @returns string +* the field value for the given header, possibly with non-semantic changes +* (i.e., leading/trailing whitespace stripped, whitespace runs replaced +* with spaces, etc.) at the option of the implementation; multiple +* instances of the header will be combined with a comma, except for +* the three headers noted in the description of getHeaderValues +*/ + getHeader: function(fieldName) + { + return this.getHeaderValues(fieldName).join("\n"); + }, + + /** +* Returns the value for the header specified by fieldName as an array. +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @throws NS_ERROR_NOT_AVAILABLE +* if the given header does not exist in this +* @returns [string] +* an array of all the header values in this for the given +* header name. Header values will generally be collapsed +* into a single header by joining all header values together +* with commas, but certain headers (Proxy-Authenticate, +* WWW-Authenticate, and Set-Cookie) violate the HTTP spec +* and cannot be collapsed in this manner. For these headers +* only, the returned array may contain multiple elements if +* that header has been added more than once. +*/ + getHeaderValues: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + + if (name in this._headers) + return this._headers[name]; + else + throw Cr.NS_ERROR_NOT_AVAILABLE; + }, + + /** +* Returns true if a header with the given field name exists in this, false +* otherwise. +* +* @param fieldName : string +* the field name whose existence is to be determined in this +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @returns boolean +* true if the header's present, false otherwise +*/ + hasHeader: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + return (name in this._headers); + }, + + /** +* Returns a new enumerator over the field names of the headers in this, as +* nsISupportsStrings. The names returned will be in lowercase, regardless of +* how they were input using setHeader (header names are case-insensitive per +* RFC 2616). +*/ + get enumerator() + { + var headers = []; + for (var i in this._headers) + { + var supports = new SupportsString(); + supports.data = i; + headers.push(supports); + } + + return new nsSimpleEnumerator(headers); + } +}; + + +/** +* Constructs an nsISimpleEnumerator for the given array of items. +* +* @param items : Array +* the items, which must all implement nsISupports +*/ +function nsSimpleEnumerator(items) +{ + this._items = items; + this._nextIndex = 0; +} +nsSimpleEnumerator.prototype = +{ + hasMoreElements: function() + { + return this._nextIndex < this._items.length; + }, + getNext: function() + { + if (!this.hasMoreElements()) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + return this._items[this._nextIndex++]; + }, + QueryInterface: function(aIID) + { + if (Ci.nsISimpleEnumerator.equals(aIID) || + Ci.nsISupports.equals(aIID)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + + +/** +* A representation of the data in an HTTP request. +* +* @param port : uint +* the port on which the server receiving this request runs +*/ +function Request(port) +{ + /** Method of this request, e.g. GET or POST. */ + this._method = ""; + + /** Path of the requested resource; empty paths are converted to '/'. */ + this._path = ""; + + /** Query string, if any, associated with this request (not including '?'). */ + this._queryString = ""; + + /** Scheme of requested resource, usually http, always lowercase. */ + this._scheme = "http"; + + /** Hostname on which the requested resource resides. */ + this._host = undefined; + + /** Port number over which the request was received. */ + this._port = port; + + var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null); + + /** Stream from which data in this request's body may be read. */ + this._bodyInputStream = bodyPipe.inputStream; + + /** Stream to which data in this request's body is written. */ + this._bodyOutputStream = bodyPipe.outputStream; + + /** +* The headers in this request. +*/ + this._headers = new nsHttpHeaders(); + + /** +* For the addition of ad-hoc properties and new functionality without having +* to change nsIHttpRequest every time; currently lazily created, as its only +* use is in directory listings. +*/ + this._bag = null; +} +Request.prototype = +{ + // SERVER METADATA + + // + // see nsIHttpRequest.scheme + // + get scheme() + { + return this._scheme; + }, + + // + // see nsIHttpRequest.host + // + get host() + { + return this._host; + }, + + // + // see nsIHttpRequest.port + // + get port() + { + return this._port; + }, + + // REQUEST LINE + + // + // see nsIHttpRequest.method + // + get method() + { + return this._method; + }, + + // + // see nsIHttpRequest.httpVersion + // + get httpVersion() + { + return this._httpVersion.toString(); + }, + + // + // see nsIHttpRequest.path + // + get path() + { + return this._path; + }, + + // + // see nsIHttpRequest.queryString + // + get queryString() + { + return this._queryString; + }, + + // HEADERS + + // + // see nsIHttpRequest.getHeader + // + getHeader: function(name) + { + return this._headers.getHeader(name); + }, + + // + // see nsIHttpRequest.hasHeader + // + hasHeader: function(name) + { + return this._headers.hasHeader(name); + }, + + // + // see nsIHttpRequest.headers + // + get headers() + { + return this._headers.enumerator; + }, + + // + // see nsIPropertyBag.enumerator + // + get enumerator() + { + this._ensurePropertyBag(); + return this._bag.enumerator; + }, + + // + // see nsIHttpRequest.headers + // + get bodyInputStream() + { + return this._bodyInputStream; + }, + + // + // see nsIPropertyBag.getProperty + // + getProperty: function(name) + { + this._ensurePropertyBag(); + return this._bag.getProperty(name); + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE IMPLEMENTATION + + /** Ensures a property bag has been created for ad-hoc behaviors. */ + _ensurePropertyBag: function() + { + if (!this._bag) + this._bag = new WritablePropertyBag(); + } +}; + + +// XPCOM trappings +if ("XPCOMUtils" in this && // Firefox 3.6 doesn't load XPCOMUtils in this scope for some reason... + "generateNSGetFactory" in XPCOMUtils) { + var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]); +} + + + +/** +* Creates a new HTTP server listening for loopback traffic on the given port, +* starts it, and runs the server until the server processes a shutdown request, +* spinning an event loop so that events posted by the server's socket are +* processed. +* +* This method is primarily intended for use in running this script from within +* xpcshell and running a functional HTTP server without having to deal with +* non-essential details. +* +* Note that running multiple servers using variants of this method probably +* doesn't work, simply due to how the internal event loop is spun and stopped. +* +* @note +* This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code); +* you should use this server as a component in Mozilla 1.8. +* @param port +* the port on which the server will run, or -1 if there exists no preference +* for a specific port; note that attempting to use some values for this +* parameter (particularly those below 1024) may cause this method to throw or +* may result in the server being prematurely shut down +* @param basePath +* a local directory from which requests will be served (i.e., if this is +* "/home/jwalden/" then a request to /index.html will load +* /home/jwalden/index.html); if this is omitted, only the default URLs in +* this server implementation will be functional +*/ +function server(port, basePath) +{ + if (basePath) + { + var lp = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + lp.initWithPath(basePath); + } + + // if you're running this, you probably want to see debugging info + DEBUG = true; + + var srv = new nsHttpServer(); + if (lp) + srv.registerDirectory("/", lp); + srv.registerContentType("sjs", SJS_TYPE); + srv.identity.setPrimary("http", "localhost", port); + srv.start(port); + + var thread = gThreadManager.currentThread; + while (!srv.isStopped()) + thread.processNextEvent(true); + + // get rid of any pending requests + while (thread.hasPendingEvents()) + thread.processNextEvent(true); + + DEBUG = false; +} + +function startServerAsync(port, basePath) +{ + if (basePath) + { + var lp = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + lp.initWithPath(basePath); + } + + var srv = new nsHttpServer(); + if (lp) + srv.registerDirectory("/", lp); + srv.registerContentType("sjs", "sjs"); + srv.identity.setPrimary("http", "localhost", port); + srv.start(port); + return srv; +} + +exports.nsHttpServer = nsHttpServer; +exports.ScriptableInputStream = ScriptableInputStream; +exports.server = server; +exports.startServerAsync = startServerAsync; diff --git a/tools/addon-sdk-1.12/lib/sdk/test/loader.js b/tools/addon-sdk-1.12/lib/sdk/test/loader.js new file mode 100644 index 0000000..282cf5e --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/test/loader.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 { Loader, resolveURI, Require, + unload, override, descriptor } = require('../loader/cuddlefish'); + +exports.Loader = function(module, globals, packaging) { + let options = packaging || require("@loader/options"); + options = override(options, { + globals: override(require('../system/globals'), globals || {}) + }); + + let loader = Loader(options); + return Object.create(loader, descriptor({ + require: Require(loader, module), + sandbox: function(id) { + let requirement = loader.resolve(id, module.id); + let uri = resolveURI(requirement, loader.mapping); + return loader.sandboxes[uri]; + }, + unload: function(reason) { + unload(loader, reason); + } + })); +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/test/runner.js b/tools/addon-sdk-1.12/lib/sdk/test/runner.js new file mode 100644 index 0000000..4db9aca --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/test/runner.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"; + +var obsvc = require("../deprecated/observer-service"); +var { exit, stdout } = require("../system"); +var cfxArgs = require("@test/options"); +var { Cc, Ci} = require("chrome"); + +function runTests(findAndRunTests) { + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Ci.nsIWindowWatcher); + + let ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + let msg = 'Running tests...'; + let markup = '<?xml version="1.0"?><window xmlns="' + ns + + '" windowtype="test:runner"><label>' + msg + '</label></window>'; + let url = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + escape(markup); + + + var window = ww.openWindow(null, url, "harness", "centerscreen", null); + + 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"); + + window.close(); + if (tests.failed == 0) { + if (tests.passed === 0) + stdout.write("No tests were run\n"); + exit(0); + } else { + printFailedTests(tests, cfxArgs.verbose, stdout.write); + exit(1); + } + }; + + // We have to wait for this window to be fully loaded *and* focused + // in order to avoid it to mess with our various window/focus tests. + // We are first waiting for our window to be fully loaded before ensuring + // that it will take the focus, and then we wait for it to be focused. + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + window.addEventListener("focus", function onfocus() { + window.removeEventListener("focus", onfocus, true); + // Finally, we 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(function () { + harness.runTests({ + findAndRunTests: findAndRunTests, + iterations: cfxArgs.iterations || 1, + filter: cfxArgs.filter, + profileMemory: cfxArgs.profileMemory, + stopOnError: cfxArgs.stopOnError, + verbose: cfxArgs.verbose, + print: stdout.write, + onDone: onDone + }); + }, 0); + }, true); + window.focus(); + }, true); +} + +function printFailedTests(tests, verbose, print) { + if (!verbose) + return; + + let iterationNumber = 0; + let singleIteration = tests.testRuns.length == 1; + let padding = singleIteration ? "" : " "; + + print("\nThe following tests failed:\n"); + + for each (let testRun in tests.testRuns) { + iterationNumber++; + + if (!singleIteration) + print(" Iteration " + iterationNumber + ":\n"); + + for each (let test in 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(function(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 unit-test-finder.findTests() + let tests = []; + for each (let name in 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: tests, + stopOnError: cfxArgs.stopOnError, + onDone: nextIteration + }); + }); +} diff --git a/tools/addon-sdk-1.12/lib/sdk/test/tmp-file.js b/tools/addon-sdk-1.12/lib/sdk/test/tmp-file.js new file mode 100644 index 0000000..634d2f1 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/test/tmp-file.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 { Cc, Ci } = require("chrome"); + +const system = require("sdk/system"); +const file = require("sdk/io/file"); +const unload = require("sdk/system/unload"); + +// Retrieve the path to the OS temporary directory: +const tmpDir = require("sdk/system").pathFor("TmpD"); + +// List of all tmp file created +let files = []; + +// Remove all tmp files on addon disabling +unload.when(function () { + files.forEach(function (path){ + // Catch exception in order to avoid leaking following files + try { + if (file.exists(path)) + file.remove(path); + } + catch(e) { + console.exception(e); + } + }); +}); + +// Utility function that synchronously reads local resource from the given +// `uri` and returns content string. Read in binary mode. +function readBinaryURI(uri) { + let ioservice = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + let channel = ioservice.newChannel(uri, "UTF-8", null); + let stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(channel.open()); + + let data = ""; + while (true) { + let available = stream.available(); + if (available <= 0) + break; + data += stream.readBytes(available); + } + stream.close(); + + return data; +} + +// Create a temporary file from a given string and returns its path +exports.createFromString = function createFromString(data, tmpName) { + let filename = (tmpName ? tmpName : "tmp-file") + "-" + (new Date().getTime()); + let path = file.join(tmpDir, filename); + + let tmpFile = file.open(path, "wb"); + tmpFile.write(data); + tmpFile.close(); + + // Register tmp file path + files.push(path); + + return path; +} + +// Create a temporary file from a given URL and returns its path +exports.createFromURL = function createFromURL(url, tmpName) { + let data = readBinaryURI(url); + return exports.createFromString(data, tmpName); +} + diff --git a/tools/addon-sdk-1.12/lib/sdk/timers.js b/tools/addon-sdk-1.12/lib/sdk/timers.js new file mode 100644 index 0000000..bc72717 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/timers.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": "deprecated" +}; + +const { 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); + +// Last timer id. +let lastID = 0; + +// Sets typer either by timeout or by interval +// depending on a given type. +function setTimer(type, callback, delay) { + let id = ++ lastID; + let timer = timers[id] = Timer(); + let args = Array.slice(arguments, 3); + timer.initWithCallback({ + notify: function notify() { + try { + if (type === TYPE_ONE_SHOT) + delete timers[id]; + callback.apply(null, args); + } + catch(error) { + console.exception(error); + } + } + }, delay || 0, type); + return id; +} + +function unsetTimer(id) { + let timer = timers[id]; + delete timers[id]; + if (timer) + timer.cancel(); +} + +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); + +unload(function() { Object.keys(timers).forEach(unsetTimer) }); diff --git a/tools/addon-sdk-1.12/lib/sdk/url.js b/tools/addon-sdk-1.12/lib/sdk/url.js new file mode 100644 index 0000000..3581398 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/url.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": "experimental" +}; + +const { Cc, Ci, Cr } = require("chrome"); + +const { Class } = require("./core/heritage"); +const base64 = require("./base64"); + +var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + +var resProt = ios.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + +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); + } + catch (e 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; +} + +let 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; +}; + +let 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.newChannelFromURI(uri); + 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) {} + + var host = null; + try { + host = uri.host; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + var port = null; + try { + port = uri.port == -1 ? null : uri.port; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + this.__defineGetter__("scheme", function() uri.scheme); + this.__defineGetter__("userPass", function() userPass); + this.__defineGetter__("host", function() host); + this.__defineGetter__("port", function() port); + this.__defineGetter__("path", function() uri.path); + + Object.defineProperties(this, { + toString: { + value: function URL_toString() new String(uri.spec).toString(), + enumerable: false + }, + valueOf: { + value: function() new String(uri.spec).valueOf(), + enumerable: false + }, + toSource: { + value: function() new String(uri.spec).toSource(), + 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; diff --git a/tools/addon-sdk-1.12/lib/sdk/util/array.js b/tools/addon-sdk-1.12/lib/sdk/util/array.js new file mode 100644 index 0000000..764eeca --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/util/array.js @@ -0,0 +1,89 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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} + */ +exports.unique = function unique(array) { + var value = []; + return array.forEach(function(element) { + add(value, element); + }); + return value; +}; + +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; +}; diff --git a/tools/addon-sdk-1.12/lib/sdk/util/collection.js b/tools/addon-sdk-1.12/lib/sdk/util/collection.js new file mode 100644 index 0000000..54e7313 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/util/collection.js @@ -0,0 +1,112 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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); + + obj.__defineSetter__(propName, function (itemOrItems) { + array.splice(0, array.length); + publicIface.add(itemOrItems); + }); + + obj.__defineGetter__(propName, function () { + 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/tools/addon-sdk-1.12/lib/sdk/util/deprecate.js b/tools/addon-sdk-1.12/lib/sdk/util/deprecate.js new file mode 100644 index 0000000..b38606a --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/util/deprecate.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 traceback = require("../console/traceback"); + +function deprecateUsage(msg) { + // Print caller stacktrace in order to help figuring out which code + // does use deprecated thing + let stack = traceback.get().slice(0, -2); + console.error("DEPRECATED: " + msg + "\n" + traceback.format(stack)); +} +exports.deprecateUsage = deprecateUsage; + +function deprecateFunction(fun, msg) { + return function deprecated() { + deprecateUsage(msg); + return fun.apply(this, arguments); + }; +} +exports.deprecateFunction = deprecateFunction; diff --git a/tools/addon-sdk-1.12/lib/sdk/util/list.js b/tools/addon-sdk-1.12/lib/sdk/util/list.js new file mode 100644 index 0000000..0a143d9 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/util/list.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'; + +const { Class } = require('../core/heritage'); +const listNS = require('../core/namespace').ns(); + +const List = Class({ + /** + * 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() listNS(this).keyValueMap.length, + /** + * Returns a string representing this list. + * @returns {String} + */ + toString: function toString() '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 each(let element in array) + yield onKeyValue ? [++i, element] : onKeys ? ++i : element; + } +}); +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/tools/addon-sdk-1.12/lib/sdk/util/object.js b/tools/addon-sdk-1.12/lib/sdk/util/object.js new file mode 100644 index 0000000..4b1a2d4 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/util/object.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": "unstable" +}; + +/** + * 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) { + Object.getOwnPropertyNames(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; + + diff --git a/tools/addon-sdk-1.12/lib/sdk/util/registry.js b/tools/addon-sdk-1.12/lib/sdk/util/registry.js new file mode 100644 index 0000000..7d385f0 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/util/registry.js @@ -0,0 +1,61 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 { EventEmitter } = require('../deprecated/events'); +const unload = require('../system/unload'); + +const Registry = EventEmitter.compose({ + _registry: null, + _constructor: null, + constructor: function Registry(constructor) { + this._registry = []; + this._constructor = constructor; + this.on('error', this._onError = this._onError.bind(this)); + unload.ensure(this, "_destructor"); + }, + _destructor: function _destructor() { + let _registry = this._registry.slice(0); + for each (let instance in _registry) + this._emit('remove', instance); + this._registry.splice(0); + }, + _onError: function _onError(e) { + if (!this._listeners('error').length) + console.error(e); + }, + has: function has(instance) { + let _registry = this._registry; + return ( + (0 <= _registry.indexOf(instance)) || + (instance && instance._public && 0 <= _registry.indexOf(instance._public)) + ); + }, + add: function add(instance) { + let { _constructor, _registry } = this; + if (!(instance instanceof _constructor)) + instance = new _constructor(instance); + if (0 > _registry.indexOf(instance)) { + _registry.push(instance); + this._emit('add', instance); + } + return instance; + }, + remove: function remove(instance) { + let _registry = this._registry; + let index = _registry.indexOf(instance) + if (0 <= index) { + this._emit('remove', instance); + _registry.splice(index, 1); + } + } +}); +exports.Registry = Registry; + diff --git a/tools/addon-sdk-1.12/lib/sdk/util/uuid.js b/tools/addon-sdk-1.12/lib/sdk/util/uuid.js new file mode 100644 index 0000000..0ebf981 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/util/uuid.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" +}; + +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) id ? parseUUID(id) : generateUUID() diff --git a/tools/addon-sdk-1.12/lib/sdk/widget.js b/tools/addon-sdk-1.12/lib/sdk/widget.js new file mode 100644 index 0000000..f1236aa --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/widget.js @@ -0,0 +1,938 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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" +}; + +// Widget content types +const CONTENT_TYPE_URI = 1; +const CONTENT_TYPE_HTML = 2; +const CONTENT_TYPE_IMAGE = 3; + +const ERR_CONTENT = "No content or contentURL property found. Widgets must " + + "have one or the other.", + ERR_LABEL = "The widget must have a non-empty label property.", + ERR_ID = "You have to specify a unique value for the id property of " + + "your widget in order for the application to remember its " + + "position.", + ERR_DESTROYED = "The widget has been destroyed and can no longer be used."; + +// Supported events, mapping from DOM event names to our event names +const EVENTS = { + "click": "click", + "mouseover": "mouseover", + "mouseout": "mouseout", +}; + +if (!require("./system/xul-app").is("Firefox")) { + throw new Error([ + "The widget module currently supports only Firefox. In the future ", + "it will support other applications. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +const { validateOptions } = require("./deprecated/api-utils"); +const panels = require("./panel"); +const { EventEmitter, EventEmitterTrait } = require("./deprecated/events"); +const { Trait } = require("./deprecated/traits"); +const LightTrait = require('./deprecated/light-traits').Trait; +const { Loader, Symbiont } = require("./content/content"); +const { Cortex } = require('./deprecated/cortex'); +const windowsAPI = require("./windows"); +const { WindowTracker } = require("./deprecated/window-utils"); +const { isBrowser } = require("./window/utils"); +const { setTimeout } = require("./timers"); +const unload = require("./system/unload"); +const { uuid } = require("./util/uuid"); + +// Data types definition +const valid = { + number: { is: ["null", "undefined", "number"] }, + string: { is: ["null", "undefined", "string"] }, + id: { + is: ["string"], + ok: function (v) v.length > 0, + msg: ERR_ID, + readonly: true + }, + label: { + is: ["string"], + ok: function (v) v.length > 0, + msg: ERR_LABEL + }, + panel: { + is: ["null", "undefined", "object"], + ok: function(v) !v || v instanceof panels.Panel + }, + width: { + is: ["null", "undefined", "number"], + map: function (v) { + if (null === v || undefined === v) v = 16; + return v; + }, + defaultValue: 16 + }, + allow: { + is: ["null", "undefined", "object"], + map: function (v) { + if (!v) v = { script: true }; + return v; + }, + get defaultValue() ({ script: true }) + }, +}; + +// Widgets attributes definition +let widgetAttributes = { + label: valid.label, + id: valid.id, + tooltip: valid.string, + width: valid.width, + content: valid.string, + panel: valid.panel, + allow: valid.allow +}; + +// Import data definitions from loader, but don't compose with it as Model +// functions allow us to recreate easily all Loader code. +let loaderAttributes = require("./content/loader").validationAttributes; +for (let i in loaderAttributes) + widgetAttributes[i] = loaderAttributes[i]; + +widgetAttributes.contentURL.optional = true; + +// Widgets public events list, that are automatically binded in options object +const WIDGET_EVENTS = [ + "click", + "mouseover", + "mouseout", + "error", + "message", + "attach" +]; + +// `Model` utility functions that help creating these various Widgets objects +let model = { + + // Validate one attribute using api-utils.js:validateOptions function + _validate: function _validate(name, suspect, validation) { + let $1 = {}; + $1[name] = suspect; + let $2 = {}; + $2[name] = validation; + return validateOptions($1, $2)[name]; + }, + + /** + * This method has two purposes: + * 1/ Validate and define, on a given object, a set of attribute + * 2/ Emit a "change" event on this object when an attribute is changed + * + * @params {Object} object + * Object on which we can bind attributes on and watch for their changes. + * This object must have an EventEmitter interface, or, at least `_emit` + * method + * @params {Object} attrs + * Dictionary of attributes definition following api-utils:validateOptions + * scheme + * @params {Object} values + * Dictionary of attributes default values + */ + setAttributes: function setAttributes(object, attrs, values) { + let properties = {}; + for (let name in attrs) { + let value = values[name]; + let req = attrs[name]; + + // Retrieve default value from typedef if the value is not defined + if ((typeof value == "undefined" || value == null) && req.defaultValue) + value = req.defaultValue; + + // Check for valid value if value is defined or mandatory + if (!req.optional || typeof value != "undefined") + value = model._validate(name, value, req); + + // In any case, define this property on `object` + let property = null; + if (req.readonly) { + property = { + value: value, + writable: false, + enumerable: true, + configurable: false + }; + } + else { + property = model._createWritableProperty(name, value); + } + + properties[name] = property; + } + Object.defineProperties(object, properties); + }, + + // Generate ES5 property definition for a given attribute + _createWritableProperty: function _createWritableProperty(name, value) { + return { + get: function () { + return value; + }, + set: function (newValue) { + value = newValue; + // The main goal of all this Model stuff is here: + // We want to forward all changes to some listeners + this._emit("change", name, value); + }, + enumerable: true, + configurable: false + }; + }, + + /** + * Automagically register listeners in options dictionary + * by detecting listener attributes with name starting with `on` + * + * @params {Object} object + * Target object that need to follow EventEmitter interface, or, at least, + * having `on` method. + * @params {Array} events + * List of events name to automatically bind. + * @params {Object} listeners + * Dictionary of event listener functions to register. + */ + setEvents: function setEvents(object, events, listeners) { + for (let i = 0, l = events.length; i < l; i++) { + let name = events[i]; + let onName = "on" + name[0].toUpperCase() + name.substr(1); + if (!listeners[onName]) + continue; + object.on(name, listeners[onName].bind(object)); + } + } + +}; + + +/** + * Main Widget class: entry point of the widget API + * + * Allow to control all widget across all existing windows with a single object. + * Widget.getView allow to retrieve a WidgetView instance to control a widget + * specific to one window. + */ +const WidgetTrait = LightTrait.compose(EventEmitterTrait, LightTrait({ + + _initWidget: function _initWidget(options) { + model.setAttributes(this, widgetAttributes, options); + + browserManager.validate(this); + + // We must have at least content or contentURL defined + if (!(this.content || this.contentURL)) + throw new Error(ERR_CONTENT); + + this._views = []; + + // Set tooltip to label value if we don't have tooltip defined + if (!this.tooltip) + this.tooltip = this.label; + + model.setEvents(this, WIDGET_EVENTS, options); + + this.on('change', this._onChange.bind(this)); + + let self = this; + this._port = EventEmitterTrait.create({ + emit: function () { + let args = arguments; + self._views.forEach(function(v) v.port.emit.apply(v.port, args)); + } + }); + // expose wrapped port, that exposes only public properties. + this._port._public = Cortex(this._port); + + // Register this widget to browser manager in order to create new widget on + // all new windows + browserManager.addItem(this); + }, + + _onChange: function _onChange(name, value) { + // Set tooltip to label value if we don't have tooltip defined + if (name == 'tooltip' && !value) { + // we need to change tooltip again in order to change the value of the + // attribute itself + this.tooltip = this.label; + return; + } + + // Forward attributes changes to WidgetViews + if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) { + this._views.forEach(function(v) v[name] = value); + } + }, + + _onEvent: function _onEvent(type, eventData) { + this._emit(type, eventData); + }, + + _createView: function _createView() { + // Create a new WidgetView instance + let view = WidgetView(this); + + // Keep a reference to it + this._views.push(view); + + return view; + }, + + // a WidgetView instance is destroyed + _onViewDestroyed: function _onViewDestroyed(view) { + let idx = this._views.indexOf(view); + this._views.splice(idx, 1); + }, + + /** + * Called on browser window closed, to destroy related WidgetViews + * @params {ChromeWindow} window + * Window that has been closed + */ + _onWindowClosed: function _onWindowClosed(window) { + for each (let view in this._views) { + if (view._isInChromeWindow(window)) { + view.destroy(); + break; + } + } + }, + + /** + * Get the WidgetView instance related to a BrowserWindow instance + * @params {BrowserWindow} window + * BrowserWindow reference from "windows" module + */ + getView: function getView(window) { + for each (let view in this._views) { + if (view._isInWindow(window)) { + return view._public; + } + } + return null; + }, + + get port() this._port._public, + set port(v) {}, // Work around Cortex failure with getter without setter + // See bug 653464 + _port: null, + + postMessage: function postMessage(message) { + this._views.forEach(function(v) v.postMessage(message)); + }, + + destroy: function destroy() { + if (this.panel) + this.panel.destroy(); + + // Dispatch destroy calls to views + // we need to go backward as we remove items from this array in + // _onViewDestroyed + for (let i = this._views.length - 1; i >= 0; i--) + this._views[i].destroy(); + + // Unregister widget to stop creating it over new windows + // and allow creation of new widget with same id + browserManager.removeItem(this); + } + +})); + +// Widget constructor +const Widget = function Widget(options) { + let w = WidgetTrait.create(Widget.prototype); + w._initWidget(options); + + // Return a Cortex of widget in order to hide private attributes like _onEvent + let _public = Cortex(w); + unload.ensure(_public, "destroy"); + return _public; +} +exports.Widget = Widget; + + + +/** + * WidgetView is an instance of a widget for a specific window. + * + * This is an external API that can be retrieved by calling Widget.getView or + * by watching `attach` event on Widget. + */ +const WidgetViewTrait = LightTrait.compose(EventEmitterTrait, LightTrait({ + + // Reference to the matching WidgetChrome + // set right after constructor call + _chrome: null, + + // Public interface of the WidgetView, passed in `attach` event or in + // Widget.getView + _public: null, + + _initWidgetView: function WidgetView__initWidgetView(baseWidget) { + this._baseWidget = baseWidget; + + model.setAttributes(this, widgetAttributes, baseWidget); + + this.on('change', this._onChange.bind(this)); + + let self = this; + this._port = EventEmitterTrait.create({ + emit: function () { + if (!self._chrome) + throw new Error(ERR_DESTROYED); + self._chrome.update(self._baseWidget, "emit", arguments); + } + }); + // expose wrapped port, that exposes only public properties. + this._port._public = Cortex(this._port); + + this._public = Cortex(this); + }, + + // Called by WidgetChrome, when the related Worker is applied to the document, + // so that we can start sending events to it + _onWorkerReady: function () { + // Emit an `attach` event with a WidgetView instance without private attrs + this._baseWidget._emit("attach", this._public); + }, + + _onChange: function WidgetView__onChange(name, value) { + if (name == 'tooltip' && !value) { + this.tooltip = this.label; + return; + } + + // Forward attributes changes to WidgetChrome instance + if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) { + this._chrome.update(this._baseWidget, name, value); + } + }, + + _onEvent: function WidgetView__onEvent(type, eventData, domNode) { + // Dispatch event in view + this._emit(type, eventData); + + // And forward it to the main Widget object + if ("click" == type || type.indexOf("mouse") == 0) + this._baseWidget._onEvent(type, this._public); + else + this._baseWidget._onEvent(type, eventData); + + // Special case for click events: if the widget doesn't have a click + // handler, but it does have a panel, display the panel. + if ("click" == type && !this._listeners("click").length && this.panel) + this.panel.show(domNode); + }, + + _isInWindow: function WidgetView__isInWindow(window) { + return windowsAPI.BrowserWindow({ + window: this._chrome.window + }) == window; + }, + + _isInChromeWindow: function WidgetView__isInChromeWindow(window) { + return this._chrome.window == window; + }, + + _onPortEvent: function WidgetView__onPortEvent(args) { + let port = this._port; + port._emit.apply(port, args); + let basePort = this._baseWidget._port; + basePort._emit.apply(basePort, args); + }, + + get port() this._port._public, + set port(v) {}, // Work around Cortex failure with getter without setter + // See bug 653464 + _port: null, + + postMessage: function WidgetView_postMessage(message) { + if (!this._chrome) + throw new Error(ERR_DESTROYED); + this._chrome.update(this._baseWidget, "postMessage", message); + }, + + destroy: function WidgetView_destroy() { + this._chrome.destroy(); + delete this._chrome; + this._baseWidget._onViewDestroyed(this); + this._emit("detach"); + } + +})); + +const WidgetView = function WidgetView(baseWidget) { + let w = WidgetViewTrait.create(WidgetView.prototype); + w._initWidgetView(baseWidget); + return w; +} + + + +/** + * Keeps track of all browser windows. + * Exposes methods for adding/removing widgets + * across all open windows (and future ones). + * Create a new instance of BrowserWindow per window. + */ +let browserManager = { + items: [], + windows: [], + + // Registers the manager to listen for window openings and closings. Note + // that calling this method can cause onTrack to be called immediately if + // there are open windows. + init: function () { + let windowTracker = new WindowTracker(this); + unload.ensure(windowTracker); + }, + + // Registers a window with the manager. This is a WindowTracker callback. + onTrack: function browserManager_onTrack(window) { + if (isBrowser(window)) { + let win = new BrowserWindow(window); + win.addItems(this.items); + this.windows.push(win); + } + }, + + // Unregisters a window from the manager. It's told to undo all + // modifications. This is a WindowTracker callback. Note that when + // WindowTracker is unloaded, it calls onUntrack for every currently opened + // window. The browserManager therefore doesn't need to specially handle + // unload itself, since unloading the browserManager means untracking all + // currently opened windows. + onUntrack: function browserManager_onUntrack(window) { + if (isBrowser(window)) { + this.items.forEach(function(i) i._onWindowClosed(window)); + for (let i = 0; i < this.windows.length; i++) { + if (this.windows[i].window == window) { + this.windows.splice(i, 1)[0]; + return; + } + } + + } + }, + + // Used to validate widget by browserManager before adding it, + // in order to check input very early in widget constructor + validate : function (item) { + let idx = this.items.indexOf(item); + if (idx > -1) + throw new Error("The widget " + item + " has already been added."); + if (item.id) { + let sameId = this.items.filter(function(i) i.id == item.id); + if (sameId.length > 0) + throw new Error("This widget ID is already used: " + item.id); + } else { + item.id = this.items.length; + } + }, + + // Registers an item with the manager. It's added to all currently registered + // windows, and when new windows are registered it will be added to them, too. + addItem: function browserManager_addItem(item) { + this.items.push(item); + this.windows.forEach(function (w) w.addItems([item])); + }, + + // Unregisters an item from the manager. It's removed from all windows that + // are currently registered. + removeItem: function browserManager_removeItem(item) { + let idx = this.items.indexOf(item); + if (idx > -1) + this.items.splice(idx, 1); + } +}; + + + +/** + * Keeps track of a single browser window. + * + * This is where the core of how a widget's content is added to a window lives. + */ +function BrowserWindow(window) { + this.window = window; + this.doc = window.document; +} + +BrowserWindow.prototype = { + + // Adds an array of items to the window. + addItems: function BW_addItems(items) { + items.forEach(this._addItemToWindow, this); + }, + + _addItemToWindow: function BW__addItemToWindow(baseWidget) { + // Create a WidgetView instance + let widget = baseWidget._createView(); + + // Create a WidgetChrome instance + let item = new WidgetChrome({ + widget: widget, + doc: this.doc, + window: this.window + }); + + widget._chrome = item; + + this._insertNodeInToolbar(item.node); + + // We need to insert Widget DOM Node before finishing widget view creation + // (because fill creates an iframe and tries to access its docShell) + item.fill(); + }, + + _insertNodeInToolbar: function BW__insertNodeInToolbar(node) { + // Add to the customization palette + let toolbox = this.doc.getElementById("navigator-toolbox"); + let palette = toolbox.palette; + palette.appendChild(node); + + // Search for widget toolbar by reading toolbar's currentset attribute + let container = null; + let toolbars = this.doc.getElementsByTagName("toolbar"); + let id = node.getAttribute("id"); + for (let i = 0, l = toolbars.length; i < l; i++) { + let toolbar = toolbars[i]; + if (toolbar.getAttribute("currentset").indexOf(id) == -1) + continue; + container = toolbar; + } + + // if widget isn't in any toolbar, add it to the addon-bar + // TODO: we may want some "first-launch" module to do this only on very + // first execution + if (!container) { + container = this.doc.getElementById("addon-bar"); + // TODO: find a way to make the following code work when we use "cfx run": + // http://mxr.mozilla.org/mozilla-central/source/browser/base/content/browser.js#8586 + // until then, force display of addon bar directly from sdk code + // https://bugzilla.mozilla.org/show_bug.cgi?id=627484 + if (container.collapsed) + this.window.toggleAddonBar(); + } + + // Now retrieve a reference to the next toolbar item + // by reading currentset attribute on the toolbar + let nextNode = null; + let currentSet = container.getAttribute("currentset"); + let ids = (currentSet == "__empty") ? [] : currentSet.split(","); + let idx = ids.indexOf(id); + if (idx != -1) { + for (let i = idx; i < ids.length; i++) { + nextNode = this.doc.getElementById(ids[i]); + if (nextNode) + break; + } + } + + // Finally insert our widget in the right toolbar and in the right position + container.insertItem(id, nextNode, null, false); + + // Update DOM in order to save position: which toolbar, and which position + // in this toolbar. But only do this the first time we add it to the toolbar + // Otherwise, this code will collide with other instance of Widget module + // during Firefox startup. See bug 685929. + if (ids.indexOf(id) == -1) { + container.setAttribute("currentset", container.currentSet); + // Save DOM attribute in order to save position on new window opened + this.window.document.persist(container.id, "currentset"); + } + } +} + + +/** + * Final Widget class that handles chrome DOM Node: + * - create initial DOM nodes + * - receive instruction from WidgetView through update method and update DOM + * - watch for DOM events and forward them to WidgetView + */ +function WidgetChrome(options) { + this.window = options.window; + this._doc = options.doc; + this._widget = options.widget; + this._symbiont = null; // set later + this.node = null; // set later + + this._createNode(); +} + +// Update a property of a widget. +WidgetChrome.prototype.update = function WC_update(updatedItem, property, value) { + switch(property) { + case "contentURL": + case "content": + this.setContent(); + break; + case "width": + this.node.style.minWidth = value + "px"; + this.node.querySelector("iframe").style.width = value + "px"; + break; + case "tooltip": + this.node.setAttribute("tooltiptext", value); + break; + case "postMessage": + this._symbiont.postMessage(value); + break; + case "emit": + let port = this._symbiont.port; + port.emit.apply(port, value); + break; + } +} + +// Add a widget to this window. +WidgetChrome.prototype._createNode = function WC__createNode() { + // XUL element container for widget + let node = this._doc.createElement("toolbaritem"); + let guid = String(uuid()); + + // Temporary work around require("self") failing on unit-test execution ... + let jetpackID = "testID"; + try { + jetpackID = require("./self").id; + } catch(e) {} + + // Compute an unique and stable widget id with jetpack id and widget.id + let id = "widget:" + jetpackID + "-" + this._widget.id; + node.setAttribute("id", id); + node.setAttribute("label", this._widget.label); + node.setAttribute("tooltiptext", this._widget.tooltip); + node.setAttribute("align", "center"); + // Bug 626326: Prevent customize toolbar context menu to appear + node.setAttribute("context", ""); + + // TODO move into a stylesheet, configurable by consumers. + // Either widget.style, exposing the style object, or a URL + // (eg, can load local stylesheet file). + node.setAttribute("style", [ + "overflow: hidden; margin: 1px 2px 1px 2px; padding: 0px;", + "min-height: 16px;", + ].join("")); + + node.style.minWidth = this._widget.width + "px"; + + this.node = node; +} + +// Initial population of a widget's content. +WidgetChrome.prototype.fill = function WC_fill() { + // Create element + var iframe = this._doc.createElement("iframe"); + iframe.setAttribute("type", "content"); + iframe.setAttribute("transparent", "transparent"); + iframe.style.overflow = "hidden"; + iframe.style.height = "16px"; + iframe.style.maxHeight = "16px"; + iframe.style.width = this._widget.width + "px"; + iframe.setAttribute("flex", "1"); + iframe.style.border = "none"; + iframe.style.padding = "0px"; + + // Do this early, because things like contentWindow are null + // until the node is attached to a document. + this.node.appendChild(iframe); + + // add event handlers + this.addEventHandlers(); + + // set content + this.setContent(); +} + +// Get widget content type. +WidgetChrome.prototype.getContentType = function WC_getContentType() { + if (this._widget.content) + return CONTENT_TYPE_HTML; + return (this._widget.contentURL && /\.(jpg|gif|png|ico)$/.test(this._widget.contentURL)) + ? CONTENT_TYPE_IMAGE : CONTENT_TYPE_URI; +} + +// Set widget content. +WidgetChrome.prototype.setContent = function WC_setContent() { + let type = this.getContentType(); + let contentURL = null; + + switch (type) { + case CONTENT_TYPE_HTML: + contentURL = "data:text/html;charset=utf-8," + encodeURIComponent(this._widget.content); + break; + case CONTENT_TYPE_URI: + contentURL = this._widget.contentURL; + break; + case CONTENT_TYPE_IMAGE: + let imageURL = this._widget.contentURL; + contentURL = "data:text/html;charset=utf-8,<html><body><img src='" + + encodeURI(imageURL) + "'></body></html>"; + break; + default: + throw new Error("The widget's type cannot be determined."); + } + + let iframe = this.node.firstElementChild; + + let self = this; + // Cleanup previously created symbiont (in case we are update content) + if (this._symbiont) + this._symbiont.destroy(); + + this._symbiont = Trait.compose(Symbiont.resolve({ + _onContentScriptEvent: "_onContentScriptEvent-not-used", + _onInit: "_initSymbiont" + }), { + // Overload `Symbiont._onInit` in order to know when the related worker + // is ready. + _onInit: function () { + this._initSymbiont(); + self._widget._onWorkerReady(); + }, + _onContentScriptEvent: function () { + // Redirect events to WidgetView + self._widget._onPortEvent(arguments); + } + })({ + frame: iframe, + contentURL: contentURL, + contentScriptFile: this._widget.contentScriptFile, + contentScript: this._widget.contentScript, + contentScriptWhen: this._widget.contentScriptWhen, + contentScriptOptions: this._widget.contentScriptOptions, + allow: this._widget.allow, + onMessage: function(message) { + setTimeout(function() { + self._widget._onEvent("message", message); + }, 0); + } + }); +} + +// Detect if document consists of a single image. +WidgetChrome._isImageDoc = function WC__isImageDoc(doc) { + return doc.body.childNodes.length == 1 && + doc.body.firstElementChild && + doc.body.firstElementChild.tagName == "IMG"; +} + +// Set up all supported events for a widget. +WidgetChrome.prototype.addEventHandlers = function WC_addEventHandlers() { + let contentType = this.getContentType(); + + let self = this; + let listener = function(e) { + // Ignore event firings that target the iframe. + if (e.target == self.node.firstElementChild) + return; + + // The widget only supports left-click for now, + // so ignore right-clicks. + if (e.type == "click" && e.button == 2) + return; + + // Proxy event to the widget + setTimeout(function() { + self._widget._onEvent(EVENTS[e.type], null, self.node); + }, 0); + }; + + this.eventListeners = {}; + let iframe = this.node.firstElementChild; + for (let type in EVENTS) { + iframe.addEventListener(type, listener, true, true); + + // Store listeners for later removal + this.eventListeners[type] = listener; + } + + // On document load, make modifications required for nice default + // presentation. + function loadListener(e) { + let containerStyle = self.window.getComputedStyle(self.node.parentNode); + // Ignore event firings that target the iframe + if (e.target == iframe) + return; + // Ignore about:blank loads + if (e.type == "load" && e.target.location == "about:blank") + return; + + // We may have had an unload event before that cleaned up the symbiont + if (!self._symbiont) + self.setContent(); + + let doc = e.target; + if (contentType == CONTENT_TYPE_IMAGE || WidgetChrome._isImageDoc(doc)) { + // Force image content to size. + // Add-on authors must size their images correctly. + doc.body.firstElementChild.style.width = self._widget.width + "px"; + doc.body.firstElementChild.style.height = "16px"; + } + + // Extend the add-on bar's default text styles to the widget. + doc.body.style.color = containerStyle.color; + doc.body.style.fontFamily = containerStyle.fontFamily; + doc.body.style.fontSize = containerStyle.fontSize; + doc.body.style.fontWeight = containerStyle.fontWeight; + doc.body.style.textShadow = containerStyle.textShadow; + // Allow all content to fill the box by default. + doc.body.style.margin = "0"; + } + iframe.addEventListener("load", loadListener, true); + this.eventListeners["load"] = loadListener; + + // Register a listener to unload symbiont if the toolbaritem is moved + // on user toolbars customization + function unloadListener(e) { + if (e.target.location == "about:blank") + return; + self._symbiont.destroy(); + self._symbiont = null; + // This may fail but not always, it depends on how the node is + // moved or removed + try { + self.setContent(); + } catch(e) {} + + } + + iframe.addEventListener("unload", unloadListener, true); + this.eventListeners["unload"] = unloadListener; +} + +// Remove and unregister the widget from everything +WidgetChrome.prototype.destroy = function WC_destroy(removedItems) { + // remove event listeners + for (let type in this.eventListeners) { + let listener = this.eventListeners[type]; + this.node.firstElementChild.removeEventListener(type, listener, true); + } + // remove dom node + this.node.parentNode.removeChild(this.node); + // cleanup symbiont + this._symbiont.destroy(); + // cleanup itself + this.eventListeners = null; + this._widget = null; + this._symbiont = null; +} + +// Init the browserManager only after setting prototypes and such above, because +// it will cause browserManager.onTrack to be called immediately if there are +// open windows. +browserManager.init(); diff --git a/tools/addon-sdk-1.12/lib/sdk/window/browser.js b/tools/addon-sdk-1.12/lib/sdk/window/browser.js new file mode 100644 index 0000000..2b63a83 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/window/browser.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 { getMode } = require('../private-browsing/utils'); +const { EventTarget } = require('../event/target'); + +const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec, consider using require("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() getWindowTitle(windowNS(this).window), + // NOTE: Fennec only has one window, which is assumed below + // TODO: remove assumption below + // NOTE: tabs requires windows + get tabs() require('../tabs'), + get activeTab() require('../tabs').activeTab, + on: method(on), + removeListener: method(off), + once: method(once), + get isPrivateBrowsing() getMode(windowNS(this).window), +}); +exports.BrowserWindow = BrowserWindow; diff --git a/tools/addon-sdk-1.12/lib/sdk/window/namespace.js b/tools/addon-sdk-1.12/lib/sdk/window/namespace.js new file mode 100644 index 0000000..b486f88 --- /dev/null +++ b/tools/addon-sdk-1.12/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/tools/addon-sdk-1.12/lib/sdk/window/utils.js b/tools/addon-sdk-1.12/lib/sdk/window/utils.js new file mode 100644 index 0000000..0bca8b1 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/window/utils.js @@ -0,0 +1,205 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1']. + getService(Ci.nsIWindowWatcher); +const appShellService = Cc['@mozilla.org/appshell/appShellService;1']. + getService(Ci.nsIAppShellService); +const observers = require('../deprecated/observer-service'); +const WM = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator); + +const BROWSER = 'navigator:browser', + URI_BROWSER = 'chrome://browser/content/browser.xul', + NAME = '_blank', + FEATURES = 'chrome,all,dialog=no'; + +function getMostRecentBrowserWindow() { + return WM.getMostRecentWindow(BROWSER); +} +exports.getMostRecentBrowserWindow = getMostRecentBrowserWindow; + +/** + * 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 `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; + +/** + * 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; + +function getWindowDocShell(window) window.gBrowser.docShell; +exports.getWindowDocShell = getWindowDocShell; + +function getWindowLoadingContext(window) { + return getWindowDocShell(window). + QueryInterface(Ci.nsILoadContext); +} +exports.getWindowLoadingContext = getWindowLoadingContext; + +/** + * Removes given window from the application's window registry. Unless + * `options.close` is `false` window is automatically closed on application + * quit. + * @params {nsIDOMWindow} window + * @params {Boolean} options.close + */ +function backgroundify(window, options) { + let base = getBaseWindow(window); + base.visibility = false; + base.enabled = false; + appShellService.unregisterTopLevelWindow(getXULWindow(window)); + if (!options || options.close !== false) + observers.add('quit-application-granted', window.close.bind(window)); + + return window; +} +exports.backgroundify = backgroundify; + +/** + * Takes hash of options and serializes it to a features string that + * can be used passed to `window.open`. For more details on features string see: + * https://developer.mozilla.org/en/DOM/window.open#Position_and_size_features + */ +function serializeFeatures(options) { + return Object.keys(options).reduce(function(result, name) { + let value = options[name]; + 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 }`. + */ +function open(uri, options) { + options = options || {}; + return windowWatcher. + openWindow(options.parent || null, + uri, + options.name || null, + serializeFeatures(options.features || {}), + options.args || null); +} +exports.open = open; + +/** + * 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 browser = WM.getMostRecentWindow(BROWSER); + return browser.openDialog.apply( + browser, + array.flatten([ + options.url || URI_BROWSER, + options.name || '_blank', + options.features || 'chrome,all,dialog=no', + options.args || null + ]) + ); +} +exports.openDialog = openDialog; + +/** + * Returns an array of all currently opened windows. + * Note that these windows may still be loading. + */ +function windows() { + let list = []; + let winEnum = windowWatcher.getWindowEnumerator(); + while (winEnum.hasMoreElements()) { + let window = winEnum.getNext().QueryInterface(Ci.nsIDOMWindow); + list.push(window); + } + return list; +} +exports.windows = windows; + +/** + * 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) { + return window.document.documentElement.getAttribute("windowtype") === BROWSER; +}; +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; + +function getFrames(window) { + return Array.slice(window.frames).reduce(function(frames, frame) { + return frames.concat(frame, getFrames(frame)) + }, []) +} +exports.getFrames = getFrames; diff --git a/tools/addon-sdk-1.12/lib/sdk/windows.js b/tools/addon-sdk-1.12/lib/sdk/windows.js new file mode 100644 index 0000000..15399c9 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/windows.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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' +}; + +if (require('./system/xul-app').is('Firefox')) { + module.exports = require('./windows/firefox'); +} +else if (require('./system/xul-app').is('Fennec')) { + module.exports = require('./windows/fennec'); +} +else { + throw new Error([ + 'The windows module currently supports only Firefox & Fennec. In the future', + ' we would like it to support other applications, however.' + ].join('')); +} diff --git a/tools/addon-sdk-1.12/lib/sdk/windows/dom.js b/tools/addon-sdk-1.12/lib/sdk/windows/dom.js new file mode 100644 index 0000000..ac2b86b --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/windows/dom.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'; + +const { Trait } = require('../deprecated/traits'); +const { getWindowTitle } = require('../window/utils'); +const { getMode } = require('../private-browsing/utils'); + +module.metadata = { + "stability": "unstable" +}; + +const WindowDom = Trait.compose({ + _window: Trait.required, + get title() { + return getWindowTitle(this._window); + }, + close: function close() { + let window = this._window; + if (window) window.close(); + return this._public; + }, + activate: function activate() { + let window = this._window; + if (window) window.focus(); + return this._public; + }, + get isPrivateBrowsing() { + return getMode(this._window); + } +}); +exports.WindowDom = WindowDom; diff --git a/tools/addon-sdk-1.12/lib/sdk/windows/fennec.js b/tools/addon-sdk-1.12/lib/sdk/windows/fennec.js new file mode 100644 index 0000000..a497ea9 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/windows/fennec.js @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 windowUtils = require('../deprecated/window-utils'); +const { WindowTracker } = windowUtils; +const { isBrowser } = 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("tabs") instead'; + +// NOTE: On Fennec there is only one window. + +let BrowserWindows = Class({ + implements: [ List ], + extends: EventTarget, + initialize: function() { + List.prototype.initialize.apply(this); + }, + get activeWindow() { + let window = windowUtils.activeBrowserWindow; + 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 each (let window in 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 `BrowserWindowTrait` + * @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 + var 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/tools/addon-sdk-1.12/lib/sdk/windows/firefox.js b/tools/addon-sdk-1.12/lib/sdk/windows/firefox.js new file mode 100644 index 0000000..9a55a08 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/windows/firefox.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'; + +const { Cc, Ci, Cr } = require('chrome'), + { Trait } = require('../deprecated/traits'), + { List } = require('../deprecated/list'), + { EventEmitter } = require('../deprecated/events'), + { WindowTabs, WindowTabTracker } = require('./tabs-firefox'), + { WindowDom } = require('./dom'), + { WindowLoader } = require('./loader'), + { isBrowser, getWindowDocShell } = require('../window/utils'), + { Options } = require('../tabs/common'), + apiUtils = require('../deprecated/api-utils'), + unload = require('../system/unload'), + windowUtils = require('../deprecated/window-utils'), + { WindowTrackerTrait } = windowUtils, + { ns } = require('../core/namespace'), + { observer: windowObserver } = require('./observer'), + { isWindowPBEnabled } = require('../private-browsing/utils'); + +/** + * Window trait composes safe wrappers for browser window that are E10S + * compatible. + */ +const BrowserWindowTrait = Trait.compose( + EventEmitter, + WindowDom.resolve({ close: '_close' }), + WindowTabs, + WindowTabTracker, + WindowLoader, + /* WindowSidebars, */ + Trait.compose({ + _emit: Trait.required, + _close: Trait.required, + _load: Trait.required, + /** + * Constructor returns wrapper of the specified chrome window. + * @param {nsIWindow} window + */ + constructor: function BrowserWindow(options) { + // Register this window ASAP, in order to avoid loop that would try + // to create this window instance over and over (see bug 648244) + windows.push(this); + + // make sure we don't have unhandled errors + this.on('error', console.exception.bind(console)); + + if ('onOpen' in options) + this.on('open', options.onOpen); + if ('onClose' in options) + this.on('close', options.onClose); + if ('onActivate' in options) + this.on('activate', options.onActivate); + if ('onDeactivate' in options) + this.on('deactivate', options.onDeactivate); + if ('window' in options) + this._window = options.window; + + if ('tabs' in options) { + this._tabOptions = Array.isArray(options.tabs) ? + options.tabs.map(Options) : + [ Options(options.tabs) ]; + } + else if ('url' in options) { + this._tabOptions = [ Options(options.url) ]; + } + + this._load(); + return this; + }, + destroy: function () this._onUnload(), + _tabOptions: [], + _onLoad: function() { + try { + this._initWindowTabTracker(); + } + catch(e) { + this._emit('error', e); + } + + this._emitOnObject(browserWindows, 'open', this._public); + }, + _onUnload: function() { + if (!this._window) + return; + this._destroyWindowTabTracker(); + this._emitOnObject(browserWindows, 'close', this._public); + this._window = null; + // Removing reference from the windows array. + windows.splice(windows.indexOf(this), 1); + this._removeAllListeners(); + }, + close: function close(callback) { + // maybe we should deprecate this with message ? + if (callback) this.on('close', callback); + return this._close(); + } + }) +); + +/** + * Gets a `BrowserWindowTrait` for the given `chromeWindow` if previously + * registered, `null` otherwise. + */ +function getRegisteredWindow(chromeWindow) { + for each (let window in windows) { + if (chromeWindow === window._window) + return window; + } + + return null; +} + +/** + * Wrapper for `BrowserWindowTrait`. Creates new instance if wrapper for + * window doesn't exists yet. If wrapper already exists then returns it + * instead. + * @params {Object} options + * Options that are passed to the the `BrowserWindowTrait` + * @returns {BrowserWindow} + * @see BrowserWindowTrait + */ +function BrowserWindow(options) { + let window = null; + + if ("window" in options) + window = getRegisteredWindow(options.window); + + return (window || BrowserWindowTrait(options))._public; +} +// to have proper `instanceof` behavior will go away when #596248 is fixed. +BrowserWindow.prototype = BrowserWindowTrait.prototype; +exports.BrowserWindow = BrowserWindow; + +const windows = []; + +const browser = ns(); + +function onWindowActivation (chromeWindow, event) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + + let window = getRegisteredWindow(chromeWindow); + + if (window) + window._emit(event.type, window._public); + else + window = BrowserWindowTrait({ window: chromeWindow }); + + browser(browserWindows).internals._emit(event.type, window._public); +} + +windowObserver.on("activate", onWindowActivation); +windowObserver.on("deactivate", onWindowActivation); + +/** + * `BrowserWindows` trait is composed out of `List` trait and it represents + * "live" list of currently open browser windows. Instance mutates itself + * whenever new browser window gets opened / closed. + */ +// Very stupid to resolve all `toStrings` but this will be fixed by #596248 +const browserWindows = Trait.resolve({ toString: null }).compose( + List.resolve({ constructor: '_initList' }), + EventEmitter.resolve({ toString: null }), + WindowTrackerTrait.resolve({ constructor: '_initTracker', toString: null }), + Trait.compose({ + _emit: Trait.required, + _add: Trait.required, + _remove: Trait.required, + + // public API + + /** + * Constructor creates instance of `Windows` that represents live list of open + * windows. + */ + constructor: function BrowserWindows() { + browser(this._public).internals = this; + + this._trackedWindows = []; + this._initList(); + this._initTracker(); + unload.ensure(this, "_destructor"); + }, + _destructor: function _destructor() { + this._removeAllListeners('open'); + this._removeAllListeners('close'); + this._removeAllListeners('activate'); + this._removeAllListeners('deactivate'); + this._clear(); + + delete browser(this._public).internals; + }, + /** + * This property represents currently active window. + * Property is non-enumerable, in order to preserve array like enumeration. + * @type {Window|null} + */ + get activeWindow() { + let window = windowUtils.activeBrowserWindow; + return window ? BrowserWindow({window: window}) : null; + }, + open: function open(options) { + if (typeof options === "string") + // `tabs` option is under review and may be removed. + options = { tabs: [Options(options)] }; + return BrowserWindow(options); + }, + + /** + * Internal listener which is called whenever new window gets open. + * Creates wrapper and adds to this list. + * @param {nsIWindow} chromeWindow + */ + _onTrack: function _onTrack(chromeWindow) { + if (!isBrowser(chromeWindow)) return; + let window = BrowserWindow({ window: chromeWindow }); + this._add(window); + this._emit('open', window); + }, + /** + * Internal listener which is called whenever window gets closed. + * Cleans up references and removes wrapper from this list. + * @param {nsIWindow} window + */ + _onUntrack: function _onUntrack(chromeWindow) { + if (!isBrowser(chromeWindow)) return; + let window = BrowserWindow({ window: chromeWindow }); + this._remove(window); + this._emit('close', window); + + // Bug 724404: do not leak this module and linked windows: + // We have to do it on untrack and not only when `_onUnload` is called + // when windows are closed, otherwise, we will leak on addon disabling. + window.destroy(); + } + }).resolve({ toString: null }) +)(); + +exports.browserWindows = browserWindows; diff --git a/tools/addon-sdk-1.12/lib/sdk/windows/loader.js b/tools/addon-sdk-1.12/lib/sdk/windows/loader.js new file mode 100644 index 0000000..be3d84c --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/windows/loader.js @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.metadata = { + "stability": "unstable" +}; + +const { Cc, Ci } = require('chrome'), + { setTimeout } = require('../timers'), + { Trait } = require('../deprecated/traits'), + { openDialog } = require('../window/utils'), + + ON_LOAD = 'load', + ON_UNLOAD = 'unload', + STATE_LOADED = 'complete'; + +/** + * Trait provides private `_window` property and requires `_onLoad` property + * that will be called when `_window` is loaded. If `_window` property value + * is changed with already loaded window `_onLoad` still will be called. + */ +const WindowLoader = Trait.compose({ + /** + * Internal listener that is called when window is loaded. + * Please keep in mind that this trait will not handle exceptions that may + * be thrown by this method so method itself should take care of + * handling them. + * @param {nsIWindow} window + */ + _onLoad: Trait.required, + _tabOptions: Trait.required, + /** + * Internal listener that is called when `_window`'s DOM 'unload' event + * is dispatched. Please note that this trait will not handle exceptions that + * may be thrown by this method so method itself should take care of + * handling them. + */ + _onUnload: Trait.required, + _load: function _load() { + if (this.__window) return; + this._window = openDialog({ + args: this._tabOptions.map(function(options) options.url).join("|") + }); + }, + /** + * Private window who's load event is being tracked. Once window is loaded + * `_onLoad` is called. + * @type {nsIWindow} + */ + get _window() this.__window, + set _window(window) { + let _window = this.__window; + if (!window) window = null; + + if (window !== _window) { + if (_window) { + _window.removeEventListener(ON_UNLOAD, this.__unloadListener, false); + _window.removeEventListener(ON_LOAD, this.__loadListener, false); + } + + if (window) { + window.addEventListener( + ON_UNLOAD, + this.__unloadListener || + (this.__unloadListener = this._unloadListener.bind(this)) + , + false + ); + + this.__window = window; + + // If window is not loaded yet setting up a listener. + if (STATE_LOADED != window.document.readyState) { + window.addEventListener( + ON_LOAD, + this.__loadListener || + (this.__loadListener = this._loadListener.bind(this)) + , + false + ); + } + else { // If window is loaded calling listener next turn of event loop. + this._onLoad(window) + } + } + } + }, + __window: null, + /** + * Internal method used for listening 'load' event on the `_window`. + * Method takes care of removing itself from 'load' event listeners once + * event is being handled. + */ + _loadListener: function _loadListener(event) { + let window = this._window; + if (!event.target || event.target.defaultView != window) return; + window.removeEventListener(ON_LOAD, this.__loadListener, false); + this._onLoad(window); + }, + __loadListener: null, + /** + * Internal method used for listening 'unload' event on the `_window`. + * Method takes care of removing itself from 'unload' event listeners once + * event is being handled. + */ + _unloadListener: function _unloadListener(event) { + let window = this._window; + if (!event.target + || event.target.defaultView != window + || STATE_LOADED != window.document.readyState + ) return; + window.removeEventListener(ON_UNLOAD, this.__unloadListener, false); + this._onUnload(window); + }, + __unloadListener: null +}); +exports.WindowLoader = WindowLoader; + diff --git a/tools/addon-sdk-1.12/lib/sdk/windows/observer.js b/tools/addon-sdk-1.12/lib/sdk/windows/observer.js new file mode 100644 index 0000000..1097bf3 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/windows/observer.js @@ -0,0 +1,52 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of 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 { EventEmitterTrait: EventEmitter } = require("../deprecated/events"); +const { WindowTracker, windowIterator } = require("../deprecated/window-utils"); +const { DOMEventAssembler } = require("../deprecated/events/assembler"); +const { Trait } = require("../deprecated/light-traits"); + +// Event emitter objects used to register listeners and emit events on them +// when they occur. +const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ + /** + * Method is implemented by `EventEmitter` and is used just for emitting + * events on registered listeners. + */ + _emit: Trait.required, + /** + * 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: function handleEvent(event) { + this._emit(event.type, event.target, event); + } +}); + +// Using `WindowTracker` to track window events. +WindowTracker({ + onTrack: function onTrack(chromeWindow) { + observer._emit("open", chromeWindow); + observer.observe(chromeWindow); + }, + onUntrack: function onUntrack(chromeWindow) { + observer._emit("close", chromeWindow); + observer.ignore(chromeWindow); + } +}); + +exports.observer = observer; diff --git a/tools/addon-sdk-1.12/lib/sdk/windows/tabs-fennec.js b/tools/addon-sdk-1.12/lib/sdk/windows/tabs-fennec.js new file mode 100644 index 0000000..ddf4aeb --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/windows/tabs-fennec.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'; + +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 } = require('../tabs/utils'); +const { Options } = require('../tabs/common'); +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 mainWindow = windowNS(browserWindows.activeWindow).window; + +const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec'; + +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); + + // TabClose + window.BrowserApp.deck.addEventListener(EVENTS.close.dom, onTabClose, 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 rawTab = openTab(windowNS(activeWin).window, options.url, { + inBackground: options.inBackground + }); + + // 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.onActivate) + tab.on('activate', options.onActivate); + + return tab; + } +}); +let 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); + window.BrowserApp.deck.removeEventListener(EVENTS.close.dom, onTabClose, 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; +} + +function getTabForBrowser(browser) { + return getTabForRawTab(getRawTabForBrowser(browser)); +} + +function getRawTabForBrowser(browser) { + let tabs = mainWindow.BrowserApp.tabs; + for (let i = 0; i < tabs.length; i++) { + let tab = tabs[i]; + if (tab.browser === browser) + return tab + } + return null; +} + +// TabOpen +function onTabOpen(event) { + let browser = event.target; + + 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; + + // TabReady + let onReady = tabNS(tab).onReady = onTabReady.bind(tab); + browser.addEventListener(EVENTS.ready.dom, onReady, false); + + emit(tab, 'open', tab); + emit(gTabs, 'open', tab); +}; + +function onTabReady() { + emit(this, 'ready', this); + emit(gTabs, 'ready', this); +} + +// TabSelect +function onTabSelect(event) { + // Set value whenever new tab becomes active. + let tab = getTabForBrowser(event.target); + emit(tab, 'activate', tab); + emit(gTabs, 'activate', tab); + + for each (let t in gTabs) { + if (t === tab) continue; + emit(t, 'deactivate', t); + emit(gTabs, 'deactivate', t); + } +}; + +// TabClose +function onTabClose(event) { + let tab = getTabForBrowser(event.target); + removeTab(tab); + + emit(gTabs, 'close', tab); + emit(tab, 'close', tab); +}; + +function getTabForRawTab(rawTab) { + for each (let tab in gTabs) { + if (tabNS(tab).tab === rawTab) + return tab; + } + return null; +} + +unload(function() { + for each (let tab in gTabs) { + let tabInternals = tabNS(tab); + tabInternals.tab.browser.removeEventListener(EVENTS.ready.dom, tabInternals.onReady, false); + tabInternals.onReady = null; + tabInternals.tab = null; + tabInternals.window = null; + } +}); diff --git a/tools/addon-sdk-1.12/lib/sdk/windows/tabs-firefox.js b/tools/addon-sdk-1.12/lib/sdk/windows/tabs-firefox.js new file mode 100644 index 0000000..f41e752 --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/windows/tabs-firefox.js @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { Trait } = require("../deprecated/traits"); +const { List } = require("../deprecated/list"); +const { Tab } = require("../tabs/tab"); +const { EventEmitter } = require("../deprecated/events"); +const { EVENTS } = require("../tabs/events"); +const { getOwnerWindow, getActiveTab, getTabs, + openTab, activateTab } = require("../tabs/utils"); +const { Options } = require("../tabs/common"); +const { observer: tabsObserver } = require("../tabs/observer"); + +const TAB_BROWSER = "tabbrowser"; + +/** + * This is a trait that is used in composition of window wrapper. Trait tracks + * tab related events of the wrapped window in order to keep track of open + * tabs and maintain their wrappers. Every new tab gets wrapped and jetpack + * type event is emitted. + */ +const WindowTabTracker = Trait.compose({ + /** + * Chrome window whose tabs are tracked. + */ + _window: Trait.required, + /** + * Function used to emit events. + */ + _emit: EventEmitter.required, + _tabOptions: Trait.required, + /** + * Function to add event listeners. + */ + on: EventEmitter.required, + removeListener: EventEmitter.required, + /** + * Initializes tab tracker for a browser window. + */ + _initWindowTabTracker: function _initWindowTabTracker() { + // Ugly hack that we have to remove at some point (see Bug 658059). At this + // point it is necessary to invoke lazy `tabs` getter on the windows object + // which creates a `TabList` instance. + this.tabs; + + // Binding all methods used as event listeners to the instance. + this._onTabReady = this._emitEvent.bind(this, "ready"); + this._onTabOpen = this._onTabEvent.bind(this, "open"); + this._onTabClose = this._onTabEvent.bind(this, "close"); + this._onTabActivate = this._onTabEvent.bind(this, "activate"); + this._onTabDeactivate = this._onTabEvent.bind(this, "deactivate"); + this._onTabPinned = this._onTabEvent.bind(this, "pinned"); + this._onTabUnpinned = this._onTabEvent.bind(this, "unpinned"); + + for each (let tab in getTabs(this._window)) { + // We emulate "open" events for all open tabs since gecko does not emits + // them on the tabs that new windows are open with. Also this is + // necessary to synchronize tabs lists with an actual state. + this._onTabOpen(tab); + } + + // We also emulate "activate" event so that it's picked up by a tab list. + this._onTabActivate(getActiveTab(this._window)); + + // Setting up event listeners + tabsObserver.on("open", this._onTabOpen); + tabsObserver.on("close", this._onTabClose); + tabsObserver.on("activate", this._onTabActivate); + tabsObserver.on("deactivate", this._onTabDeactivate); + tabsObserver.on("pinned", this._onTabPinned); + tabsObserver.on("unpinned", this._onTabUnpinned); + }, + _destroyWindowTabTracker: function _destroyWindowTabTracker() { + // We emulate close events on all tabs, since gecko does not emits such + // events by itself. + for each (let tab in this.tabs) + this._emitEvent("close", tab); + + this._tabs._clear(); + + tabsObserver.removeListener("open", this._onTabOpen); + tabsObserver.removeListener("close", this._onTabClose); + tabsObserver.removeListener("activate", this._onTabActivate); + tabsObserver.removeListener("deactivate", this._onTabDeactivate); + }, + _onTabEvent: function _onTabEvent(type, tab) { + if (this._window === getOwnerWindow(tab)) { + let options = this._tabOptions.shift() || {}; + options.tab = tab; + options.window = this._public; + + // creating tab wrapper and adding listener to "ready" events. + let wrappedTab = Tab(options); + + // Setting up an event listener for ready events. + if (type === "open") + wrappedTab.on("ready", this._onTabReady); + + this._emitEvent(type, wrappedTab); + } + }, + _emitEvent: function _emitEvent(type, tab) { + // Notifies combined tab list that tab was added / removed. + tabs._emit(type, tab); + // Notifies contained tab list that window was added / removed. + this._tabs._emit(type, tab); + } +}); +exports.WindowTabTracker = WindowTabTracker; + +/** + * This trait is used to create live representation of open tab lists. Each + * window wrapper's tab list is represented by an object created from this + * trait. It is also used to represent list of all the open windows. Trait is + * composed out of `EventEmitter` in order to emit 'TabOpen', 'TabClose' events. + * **Please note** that objects created by this trait can't be exposed outside + * instead you should expose it's `_public` property, see comments in + * constructor for details. + */ +const TabList = List.resolve({ constructor: "_init" }).compose( + // This is ugly, but necessary. Will be removed by #596248 + EventEmitter.resolve({ toString: null }), + Trait.compose({ + on: Trait.required, + _emit: Trait.required, + constructor: function TabList(options) { + this._window = options.window; + // Add new items to the list + this.on(EVENTS.open.name, this._add.bind(this)); + + // Remove closed items from the list + this.on(EVENTS.close.name, this._remove.bind(this)); + + // Set value whenever new tab becomes active. + this.on("activate", function onTabActivate(tab) { + this._activeTab = tab; + }.bind(this)); + + // Initialize list. + this._init(); + // This list is not going to emit any events, object holding this list + // will do it instead, to make that possible we return a private API. + return this; + }, + get activeTab() this._activeTab, + _activeTab: null, + + open: function open(options) { + options = Options(options); + this._window._tabOptions.push(options); + let tab = openTab(this._window._window, options.url); + if (!options.inBackground) + activateTab(tab); + } + // This is ugly, but necessary. Will be removed by #596248 + }).resolve({ toString: null }) +); + +/** + * Combined list of all open tabs on all the windows. + * type {TabList} + */ +var tabs = TabList({ window: null }); +exports.tabs = tabs._public; + +/** + * Trait is a part of composition that represents window wrapper. This trait is + * composed out of `WindowTabTracker` that allows it to keep track of open tabs + * on the window being wrapped. + */ +const WindowTabs = Trait.compose( + WindowTabTracker, + Trait.compose({ + _window: Trait.required, + /** + * List of tabs + */ + get tabs() (this._tabs || (this._tabs = TabList({ window: this })))._public, + _tabs: null, + }) +); +exports.WindowTabs = WindowTabs; |