diff options
author | Rogan Creswick <creswick@galois.com> | 2012-03-30 17:07:02 -0700 |
---|---|---|
committer | Rogan Creswick <creswick@galois.com> | 2012-03-30 17:07:02 -0700 |
commit | f6ab6622aab00fe7c2f4c3dc41f786ebbe0f0d73 (patch) | |
tree | 870111038542cd27153e1396ebdc063573249689 /tools/addon-sdk-1.5/packages/api-utils/lib |
initial revision
Diffstat (limited to 'tools/addon-sdk-1.5/packages/api-utils/lib')
73 files changed, 15375 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/api-utils.js b/tools/addon-sdk-1.5/packages/api-utils/lib/api-utils.js new file mode 100644 index 0000000..7f9ee1f --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/api-utils.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"; + +// 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, req] in Iterator(requirements)) { + 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.5/packages/api-utils/lib/app-strings.js b/tools/addon-sdk-1.5/packages/api-utils/lib/app-strings.js new file mode 100644 index 0000000..c03ae7c --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/app-strings.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"; + +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.5/packages/api-utils/lib/array.js b/tools/addon-sdk-1.5/packages/api-utils/lib/array.js new file mode 100644 index 0000000..dadcfa6 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/array.js @@ -0,0 +1,69 @@ +/* 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"; + +/** + * 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); +}; + +/** + * 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; +}; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/base.js b/tools/addon-sdk-1.5/packages/api-utils/lib/base.js new file mode 100644 index 0000000..63acabc --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/base.js @@ -0,0 +1,177 @@ +/* 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"; + +// Instead of inheriting from `Object.prototype` we copy all interesting +// properties from it and then freeze. This way we can guarantee integrity +// of components build on top. +exports.Base = Object.freeze(Object.create(null, { + toString: { value: Object.prototype.toString }, + toLocaleString: { value: Object.prototype.toLocaleString }, + toSource: { value: Object.prototype.toSource }, + valueOf: { value: Object.prototype.valueOf }, + isPrototypeOf: { value: Object.prototype.isPrototypeOf }, + /** + * Creates an object that inherits from `this` object (Analog of + * `new Object()`). + * @examples + * + * var Dog = Base.extend({ + * bark: function bark() { + * return 'Ruff! Ruff!' + * } + * }); + * var dog = Dog.new(); + */ + new: { value: function create() { + var object = Object.create(this); + object.initialize.apply(object, arguments); + return object; + }}, + /** + * When new instance of the this prototype is created it's `initialize` + * method is called with all the arguments passed to the `new`. You can + * override `initialize` to set up an instance. + */ + initialize: { value: function initialize() { + }}, + /** + * Merges all the properties of the passed objects into `this` instance (This + * method can be used on instances only as prototype objects are frozen). + * + * 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. + * + * @examples + * + * var Pet = Dog.extend({ + * initialize: function initialize(options) { + * // this.name = options.name -> would have thrown (frozen prototype) + * this.merge(options) // will override all properties. + * }, + * call: function(name) { + * return this.name === name ? this.bark() : '' + * }, + * name: null + * }) + * var pet = Pet.new({ name: 'Benzy', breed: 'Labrador' }) + * pet.call('Benzy') // 'Ruff! Ruff!' + */ + merge: { value: function merge() { + var descriptor = {}; + Array.prototype.forEach.call(arguments, function (properties) { + Object.getOwnPropertyNames(properties).forEach(function(name) { + descriptor[name] = Object.getOwnPropertyDescriptor(properties, name); + }); + }); + Object.defineProperties(this, descriptor); + return this; + }}, + /** + * Takes any number of argument objects and returns frozen, composite object + * that inherits from `this` object and combines all of the own properties of + * the argument objects. (Objects returned by this function are frozen as + * they are intended to be used as types). + * + * 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. + * @examples + * + * // ## Object composition ## + * + * var HEX = Base.extend({ + * hex: function hex() { + * return '#' + this.color; + * } + * }) + * + * var RGB = Base.extend({ + * red: function red() { + * return parseInt(this.color.substr(0, 2), 16); + * }, + * green: function green() { + * return parseInt(this.color.substr(2, 2), 16); + * }, + * blue: function blue() { + * return parseInt(this.color.substr(4, 2), 16); + * } + * }) + * + * var CMYK = Base.extend(RGB, { + * black: function black() { + * var color = Math.max(Math.max(this.red(), this.green()), this.blue()); + * return (1 - color / 255).toFixed(4); + * }, + * cyan: function cyan() { + * var K = this.black(); + * return (((1 - this.red() / 255).toFixed(4) - K) / (1 - K)).toFixed(4); + * }, + * magenta: function magenta() { + * var K = this.black(); + * return (((1 - this.green() / 255).toFixed(4) - K) / (1 - K)).toFixed(4); + * }, + * yellow: function yellow() { + * var K = this.black(); + * return (((1 - this.blue() / 255).toFixed(4) - K) / (1 - K)).toFixed(4); + * } + * }) + * + * var Color = Base.extend(HEX, RGB, CMYK, { + * initialize: function Color(color) { + * this.color = color; + * } + * }); + * + * // ## Prototypal inheritance ## + * + * var Pixel = Color.extend({ + * initialize: function Pixel(x, y, hex) { + * Color.initialize.call(this, hex); + * this.x = x; + * this.y = y; + * }, + * toString: function toString() { + * return this.x + ':' + this.y + '@' + this.hex(); + * } + * }); + * + * var pixel = Pixel.new(11, 23, 'CC3399') + * pixel.toString(); // 11:23@#CC3399 + * + * pixel.red(); // 204 + * pixel.green(); // 51 + * pixel.blue(); // 153 + * + * pixel.cyan(); // 0.0000 + * pixel.magenta(); // 0.7500 + * pixel.yellow(); // 0.2500 + * + */ + extend: { value: function extend() { + return Object.freeze(this.merge.apply(Object.create(this), arguments)); + }} +})); + +/** + * Function takes prototype object that implements `initialize` method, and + * returns `constructor` function (with correct prototype property), that can + * be used for simulating classes for given prototypes. + */ +exports.Class = Object.freeze(function Class(prototype) { + function constructor() { + var instance = Object.create(prototype); + prototype.initialize.apply(instance, arguments); + return instance; + } + return Object.freeze(Object.defineProperties(constructor, { + prototype: { value: prototype }, + new: { value: constructor } + })); +}); diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/byte-streams.js b/tools/addon-sdk-1.5/packages/api-utils/lib/byte-streams.js new file mode 100644 index 0000000..edc8407 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/byte-streams.js @@ -0,0 +1,102 @@ +/* -*- 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"; + +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("./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.5/packages/api-utils/lib/channel.js b/tools/addon-sdk-1.5/packages/api-utils/lib/channel.js new file mode 100644 index 0000000..a888af8 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/channel.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/. */ + +const { jetpackID } = require('@packaging'); +const { when } = require('./unload'); + +// TODO: Create a bug report and remove this workaround once it's fixed. +// Only function needs defined in the context of the message manager window +// can be registered via `addMessageListener`. +function listener(callee) { + return function listener() { return callee.apply(this, arguments); }; +} +function messageListener(scope, callee) { + return scope ? scope.eval('(' + listener + ')')(callee) : callee +} + +exports.channel = function channel(scope, messageManager, address, raw) { + address = jetpackID + ':' + address + return { + input: function input(next, stop) { + let listener = messageListener(scope, function onMessage(message) { + if (false === next(raw ? message : message.json) && listener) { + messageManager.removeMessageListener(address, listener); + listener = null; + } + }); + messageManager.addMessageListener(address, listener); + + // Bug 724433: do not leak `listener` on addon disabling + when(function () { + if (listener) { + messageManager.removeMessageListener(address, listener); + listener = null; + } + }); + }, + output: function output(data) { + messageManager.sendAsyncMessage(address, data); + }, + sync: !messageManager.sendSyncMessage ? null : function sync(data) { + messageManager.sendSyncMessage(address, data); + } + }; +}; + diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/collection.js b/tools/addon-sdk-1.5/packages/api-utils/lib/collection.js new file mode 100644 index 0000000..b9dd352 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/collection.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"; + +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.5/packages/api-utils/lib/content.js b/tools/addon-sdk-1.5/packages/api-utils/lib/content.js new file mode 100644 index 0000000..a4acb94 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/content.js @@ -0,0 +1,11 @@ +/* -*- 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"; + +exports.Loader = require('./content/loader').Loader; +exports.Symbiont = require('./content/symbiont').Symbiont; +exports.Worker = require('./content/worker').Worker; + diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/content/loader.js b/tools/addon-sdk-1.5/packages/api-utils/lib/content/loader.js new file mode 100644 index 0000000..ebbc340 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/content/loader.js @@ -0,0 +1,169 @@ +/* -*- 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"; + +const { EventEmitter } = require('../events'); +const { validateOptions, getTypeOf } = require('../api-utils'); +const { URL } = require('../url'); +const file = require('../file'); + +// 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: function(value) 'undefined' === getTypeOf(value) ? null : value, + ok: function(value) { + if (getTypeOf(value) === 'array') { + // Make sure every item is a local file URL. + return value.every(function (item) { + try { + return URL(item).scheme === 'resource' + } + catch(e) { + return false; + } + }); + } + return true; + }, + msg: 'The `contentScriptFile` option must be a local URL or an array of' + + 'URLs.' + }, + contentScript: { + is: ['undefined', 'null', 'string', 'array'], + map: function(value) 'undefined' === getTypeOf(value) ? null : value, + ok: function(value) 'array' !== getTypeOf(value) ? true : + value.every(function(item) 'string' === getTypeOf(item)) + , + msg: 'The `contentScript` option must be a string or an array of strings.' + }, + contentScriptWhen: { + is: ['string'], + ok: function(value) ['start', 'ready', 'end'].indexOf(value) >= 0, + map: function(value) { + return value || 'end'; + }, + msg: 'The `contentScriptWhen` option must be either "start", "ready" or "end".' + } +}; +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', + /** + * 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.5/packages/api-utils/lib/content/symbiont.js b/tools/addon-sdk-1.5/packages/api-utils/lib/content/symbiont.js new file mode 100644 index 0000000..e7c959c --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/content/symbiont.js @@ -0,0 +1,183 @@ +/* -*- 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"; + +const { Worker } = require('./worker'); +const { Loader } = require('./loader'); +const hiddenFrames = require('../hidden-frame'); +const observers = require('../observer-service'); +const unload = require('../unload'); + +/** + * 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 ('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); + } + }); + 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(require("@packaging").uriPrefix) == 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.5/packages/api-utils/lib/content/worker.js b/tools/addon-sdk-1.5/packages/api-utils/lib/content/worker.js new file mode 100644 index 0000000..b5231cb --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/content/worker.js @@ -0,0 +1,614 @@ +/* -*- 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"; + +const { Trait } = require('../traits'); +const { EventEmitter, EventEmitterTrait } = require('../events'); +const { Ci, Cu, Cc } = require('chrome'); +const timer = require('../timer'); +const { URL } = require('../url'); +const unload = require('../unload'); +const observers = require('../observer-service'); +const { Cortex } = require('../cortex'); +const { Enqueued } = require('../utils/function'); +const self = require("self"); +const { sandbox, evaluate, load } = require("../sandbox"); +const { merge } = require('../utils/object'); + +const CONTENT_PROXY_URL = self.data.url("content-proxy.js"); + +const JS_VERSION = '1.8'; + +const ERR_DESTROYED = + "The page has been destroyed and can no longer be used."; + +/** + * 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 = {}; + +function ensureArgumentsAreJSON(array, window) { + // 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; + } + // If a window is given, we use its `JSON.parse` object in order to + // create JS objects for its compartments (See bug 714891) + let parse = JSON.parse; + if (window) { + // As we can't directly rely on `window.wrappedJSObject.JSON`, we create + // a temporary sandbox in order to get access to a safe `JSON` object: + parse = Cu.Sandbox(window).JSON.parse; + } + return parse(JSON.stringify(array, replacer)); +} + +/** + * Extended `EventEmitter` allowing us to emit events asynchronously. + */ +const AsyncEventEmitter = EventEmitter.compose({ + /** + * Emits event in the next turn of event loop. + */ + _asyncEmit: function _asyncEmit() { + timer.setTimeout(function emitter(emit, scope, params) { + emit.apply(scope, params); + }, 0, this._emit, this, arguments) + } +}); + +/** + * Local trait providing implementation of the workers global scope. + * Used to configure global object in the sandbox. + * @see http://www.w3.org/TR/workers/#workerglobalscope + */ +const WorkerGlobalScope = AsyncEventEmitter.compose({ + on: Trait.required, + _removeAllListeners: Trait.required, + + // 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 + _timers: null, + + setTimeout: function setTimeout(callback, delay) { + let params = Array.slice(arguments, 2); + let id = timer.setTimeout(function(self) { + try { + delete self._timers[id]; + callback.apply(null, params); + } catch(e) { + self._addonWorker._asyncEmit('error', e); + } + }, delay, this); + this._timers[id] = true; + return id; + }, + clearTimeout: function clearTimeout(id){ + delete this._timers[id]; + return timer.clearTimeout(id); + }, + + setInterval: function setInterval(callback, delay) { + let params = Array.slice(arguments, 2); + let id = timer.setInterval(function(self) { + try { + callback.apply(null, params); + } catch(e) { + self._addonWorker._asyncEmit('error', e); + } + }, delay, this); + this._timers[id] = true; + return id; + }, + clearInterval: function clearInterval(id) { + delete this._timers[id]; + return timer.clearInterval(id); + }, + + /** + * `onMessage` function defined in the global scope of the worker context. + */ + get _onMessage() this.__onMessage, + set _onMessage(value) { + let listener = this.__onMessage; + if (listener && value !== listener) { + this.removeListener('message', listener); + this.__onMessage = undefined; + } + if (value) + this.on('message', this.__onMessage = value); + }, + __onMessage: undefined, + + /** + * Function for sending data to the addon side. + * Validates that data is a `JSON` or primitive value and emits + * 'message' event on the worker in the next turn of the event loop. + * _Later this will be sending data across process boundaries._ + * @param {JSON|String|Number|Boolean} data + */ + postMessage: function postMessage(data) { + if (!this._addonWorker) + throw new Error(ERR_DESTROYED); + this._addonWorker._asyncEmit('message', ensureArgumentsAreJSON(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 self.on / self.emit + */ + get port() this._port._public, + + /** + * Same object than this.port but private API. + * Allow access to _asyncEmit, in order to send event to port. + */ + _port: null, + + /** + * Alias to the global scope in the context of worker. Similar to + * `window` concept. + */ + get self() this._public, + + /** + * Configures sandbox and loads content scripts into it. + * @param {Worker} worker + * content worker + */ + constructor: function WorkerGlobalScope(worker) { + this._addonWorker = worker; + + // Hack in order to allow addon worker to access _asyncEmit + // as this is the private object of WorkerGlobalScope + worker._contentWorker = this; + + // create an event emitter that receive and send events from/to the addon + let contentWorker = this; + this._port = EventEmitterTrait.create({ + emit: function () { + let addonWorker = contentWorker._addonWorker; + if (!addonWorker) + throw new Error(ERR_DESTROYED); + addonWorker._onContentScriptEvent.apply(addonWorker, arguments); + } + }); + // create emit that executes in next turn of event loop. + this._port._asyncEmit = Enqueued(this._port._emit); + // expose wrapped port, that exposes only public properties. + this._port._public = Cortex(this._port); + + // We receive an unwrapped window, with raw js access + let window = worker._window; + + let proto = window; + let proxySandbox = null; + // Build content proxies only if the document has a non-system principal + if (window.wrappedJSObject) { + // Instantiate the proxy code in another Sandbox in order to prevent + // content script from polluting globals used by proxy code + proxySandbox = sandbox(window, { wantXrays: true }); + proxySandbox.console = console; + // Execute the proxy code + load(proxySandbox, CONTENT_PROXY_URL); + // Get a reference of the window's proxy + proto = proxySandbox.create(window); + } + + // Create the sandbox and bind it to window in order for content scripts to + // have access to all standard globals (window, document, ...) + let content = this._sandbox = sandbox(window, { + sandboxPrototype: proto, + wantXrays: true + }); + merge(content, { + // We need "this === window === top" to be true in toplevel scope: + get window() content, + get top() content, + // 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 + }); + + // Internal feature that is only used by SDK tests: + // Expose unlock key to content script context. + // See `PRIVATE_KEY` definition for more information. + if (proxySandbox && worker._expose_key) + content.UNWRAP_ACCESS_KEY = proxySandbox.UNWRAP_ACCESS_KEY; + + // Initialize timer lists + this._timers = {}; + + let self = this; + let publicAPI = this._public; + + merge(content, { + console: console, + self: publicAPI.self, + setTimeout: publicAPI.setTimeout, + clearTimeout: publicAPI.clearTimeout, + setInterval: publicAPI.setInterval, + clearInterval: publicAPI.clearInterval, + get onMessage() self._onMessage, + set onMessage(value) { + console.warn("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>."); + self._onMessage = value; + }, + // Deprecated use of on/postMessage from globals + postMessage: function postMessage() { + console.warn("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>."); + publicAPI.postMessage.apply(publicAPI, arguments); + }, + on: function on() { + console.warn("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>."); + publicAPI.on.apply(publicAPI, arguments); + } + }); + + // Temporary fix for test-widget, that pass self.postMessage to proxy code + // that first try to access to `___proxy` and then call it through `apply`. + // We need to move function given to content script to a sandbox + // with same principal than the content script. + // In the meantime, we need to allow such access explicitly + // by using `__exposedProps__` property, documented here: + // https://developer.mozilla.org/en/XPConnect_wrappers + content.self.postMessage.__exposedProps__ = { + ___proxy: 'rw', + apply: 'rw' + } + + // 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", { + get: function () publicAPI + } + ); + } + + // 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 + ); + } + }, + _destructor: function _destructor() { + this._removeAllListeners(); + // Unregister all setTimeout/setInterval + // We can use `clearTimeout` for both setTimeout/setInterval + // as internal implementation of timer module use same method for both. + for (let id in this._timers) + timer.clearTimeout(id); + this._sandbox = null; + this._addonWorker = null; + this.__onMessage = undefined; + }, + + /** + * 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._asyncEmit('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._asyncEmit('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 = AsyncEventEmitter.compose({ + on: Trait.required, + _asyncEmit: 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); + this._contentWorker._asyncEmit('message', + ensureArgumentsAreJSON(data, this._window)); + }, + + /** + * 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(arguments) + }); + // create emit that executes in next turn of event loop. + this._port._asyncEmit = Enqueued(this._port._emit); + // 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 _asyncEmit, 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; + } + + // We throw exception when the worker has been destroyed + if (!this._contentWorker) { + throw new Error(ERR_DESTROYED); + } + + let scope = this._contentWorker._port; + // Ensure that we pass only JSON values + let array = Array.prototype.slice.call(args); + scope._asyncEmit.apply(scope, ensureArgumentsAreJSON(array, this._window)); + }, + + // Is worker connected to the content worker (i.e. WorkerGlobalScope) ? + _inited: false, + + // 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 ('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)); + + unload.ensure(this._public, "destroy"); + + // Ensure that worker._port is initialized for contentWorker to be able + // to send use event during WorkerGlobalScope(this) + this.port; + + // will set this._contentWorker pointing to the private API: + WorkerGlobalScope(this); + + // Mainly enable worker.port.emit to send event to the content worker + this._inited = true; + + // 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; + }, + + get url() { + // this._window will be null after detach + return this._window ? this._window.document.location.href : null; + }, + + get tab() { + if (this._window) { + let tab = require("../tabs/tab"); + // this._window will be null after detach + return tab.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._destructor(); + this._contentWorker = null; + 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() { + // Ensure that we pass only JSON values + let array = Array.prototype.slice.call(arguments); + this._port._asyncEmit.apply(this._port, ensureArgumentsAreJSON(array)); + }, + + /** + * 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.5/packages/api-utils/lib/cortex.js b/tools/addon-sdk-1.5/packages/api-utils/lib/cortex.js new file mode 100644 index 0000000..1c3a1b3 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/cortex.js @@ -0,0 +1,109 @@ +/* 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"; + +// `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.5/packages/api-utils/lib/cuddlefish.js b/tools/addon-sdk-1.5/packages/api-utils/lib/cuddlefish.js new file mode 100644 index 0000000..b501786 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/cuddlefish.js @@ -0,0 +1,307 @@ +/* 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/. */ +var EXPORTED_SYMBOLS = [ 'Loader' ]; + +!function(exports) { + +"use strict"; + +const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu, + results: Cr, manager: Cm } = Components; +const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); +const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1']. + getService(Ci.mozIJSSubScriptLoader); + +const Sandbox = { + new: function ({ principal, prototype, name, existingSandbox }) { + let options = { sandboxPrototype: prototype || Sandbox.prototype, + wantXrays: Sandbox.wantXrays }; + if (name) + options.sandboxName = name; + if (existingSandbox) + options.sameGroupAs = existingSandbox.sandbox; + let sandbox = Object.create(Sandbox, { + sandbox: { value: Cu.Sandbox(principal || Sandbox.principal, options) } + }); + // There are few properties (dump, Iterator) that by default appear in + // sandboxes shadowing properties provided by a prototype. To workaround + // this we override all such properties by copying them directly to the + // sandbox. + Object.keys(prototype).forEach(function onEach(key) { + if (sandbox.sandbox[key] !== prototype[key]) + sandbox.sandbox[key] = prototype[key]; + }); + return sandbox; + }, + evaluate: function evaluate(source, uri, lineNumber) { + return Cu.evalInSandbox( + source, + this.sandbox, + this.version, + uri, + lineNumber || this.lineNumber + ); + }, + load: function load(uri) { + scriptLoader.loadSubScript(uri, this.sandbox, 'UTF-8'); + }, + merge: function merge(properties) { + Object.getOwnPropertyNames(properties).forEach(function(name) { + Object.defineProperty(this.sandbox, name, + Object.getOwnPropertyDescriptor(properties, name)); + }, this); + }, + principal: systemPrincipal, + version: '1.8', + lineNumber: 1, + wantXrays: false, + prototype: {} +}; + +// the Module object made available to CommonJS modules when they are +// evaluated, along with 'exports' and 'uri' +const Module = { + new: function(id, path, uri) { + let module = Object.create(this); + + module.id = id; + module.path = path; + module.uri = uri; + module.exports = {}; + + return module; + }, + // TODO: I'd like to remove this, it's not used adds complexity and does + // not has much adoption in commonjs either. + setExports: function setExports(exports) { + this.exports = exports; + } +}; + +const Loader = { + new: function (options) { + let loader = Object.create(Loader, { + // Manifest generated by a linker, containing map of module url's mapped + // to it's requirements, comes from harness-options.json + manifest: { value: options.manifest || {} }, + + // Following property may be passed in (usually for mocking purposes) in + // order to override default modules cache. + modules: { value: options.modules || Object.create(Loader.modules) }, + globals: { value: options.globals || {} }, + + uriPrefix: { value: options.uriPrefix }, + + sandboxes: { value: {} }, + + // Workaround loader instances Object.freeze, + // we need to set attributes lazily, like `loader.data.channel`. + data: { value: {} } + }); + loader.require = this.require.bind(loader, options.loader); + + // some 'magic' modules, that have no corresponding .js file + loader.modules['@packaging'] = Object.freeze({ + id: '@packaging', + exports: JSON.parse(JSON.stringify(options)) + }); + loader.modules['@loader'] = Object.freeze({ + exports: Object.freeze({ Loader: Loader }), + id: '@loader' + }); + + // This special module defines globals which will be added to every + // module this loader creates + let globals = loader.require('api-utils/globals!'); + Object.getOwnPropertyNames(globals).forEach(function(name) { + Object.defineProperty(loader.globals, name, + Object.getOwnPropertyDescriptor(globals, name)); + }); + // Freeze globals so that modules won't have a chance to mutate scope of + // other modules. + Object.freeze(globals); + + // Override global `dump` so that it behaves same as in any other module ( + // currently we override dump to write to a file instead of `stdout` so that + // python can read it on windows). + dump = globals.dump; + + return Object.freeze(loader); + }, + modules: { + 'chrome': Object.freeze({ + exports: Object.freeze({ + Cc: Cc, + CC: CC, + Ci: Ci, + Cu: Cu, + Cr: Cr, + Cm: Cm, + components: Components, + messageManager: 'addMessageListener' in exports ? exports : null + }), + id: 'chrome' + }), + 'self': function self(loader, requirer) { + return loader.require('api-utils/self!').create(requirer.path); + }, + }, + + // populate a Module by evaluating the CommonJS module code in the sandbox + load: function load(module) { + let require = Loader.require.bind(this, module.path); + require.main = this.main; + + // Get an existing module sandbox, if any, so we can reuse its compartment + // when creating the new one to reduce memory consumption. + let existingSandbox = [this.sandboxes[p] for (p in this.sandboxes)][0]; + + // XXX Always set "principal" to work around bug 705795, which generates + // 'reference to undefined property "principal"' warnings when the argument + // is deconstructed in the "new" function's parameter list. + // FIXME: stop setting "principal" once bug 705795 is fixed. + let sandbox = this.sandboxes[module.path] = + Sandbox.new({ principal: null, + prototype: this.globals, + name: module.uri, + existingSandbox: existingSandbox }); + sandbox.merge({ + require: require, + module: module, + exports: module.exports + }); + + sandbox.load(module.uri); + + // Workaround for bug 674195. Freezing objects from other sandboxes fail, + // so we create descendant and freeze it instead. + if (typeof(module.exports) === 'object') { + module.exports = Object.prototype.isPrototypeOf(module.exports) ? + Object.freeze(module.exports) : + Object.freeze(Object.create(module.exports)); + } + }, + + // this require() is the main entry point for regular CommonJS modules. The + // bind() in load (above) causes those modules to get a very restricted + // form of this require(): one which can only ever reference this one + // loader, and which always uses their URI as a "base" (so they're limited + // to their own manifest entries, and can't import anything off the + // manifest). + require: function require(base, id) { + let module, manifest = this.manifest[base], requirer = this.modules[base]; + + if (!id) + throw Error("you must provide a module name when calling require() from " + + (requirer && requirer.id), base, id); + + // If we have a manifest for requirer, then all it's requirements have been + // registered by linker and we should have a `path` to the required module. + // Even pseudo-modules like 'chrome', 'self', '@packaging', and '@loader' + // have pseudo-paths: exactly those same names. + // details see: Bug-697422. + let requirement = manifest && manifest.requirements[id]; + if (!requirement) + throw Error("Module: " + (requirer && requirer.id) + ' located at ' + + base + " has no authority to load: " + id); + let path = requirement.path; + + if (path in this.modules) { + module = this.modules[path]; + } + else { + let uri = this.uriPrefix + path; + module = this.modules[path] = Module.new(id, path, uri); + this.load(module); + Object.freeze(module); + } + + // "magic" modules which have contents that depend upon who imports them + // (like "self") are expressed in the Loader's pre-populated 'modules' + // table as callable functions, which are given the reference to this + // Loader and a copy of the importer's URI + // + // TODO: Find a better way to implement `self`. + // Maybe something like require('self!path/to/data') + if (typeof(module) === 'function') + module = module(this, requirer); + + return module.exports; + }, + + // process.process() will eventually cause a call to main() to be evaluated + // in the addon's context. This function loads and executes the addon's + // entry point module. + main: function main(id, path) { + try { + let uri = this.uriPrefix + path; + let module = this.modules[path] = Module.new(id, path, uri); + this.load(module); // this is where the addon's main.js finally runs + let program = Object.freeze(module).exports; + + if (typeof(program.onUnload) === 'function') + this.require('api-utils/unload').when(program.onUnload); + + if (program.main) { + let { exit, staticArgs } = this.require('api-utils/system'); + let { loadReason } = this.require('@packaging'); + program.main({ loadReason: loadReason, staticArgs: staticArgs }, + { print: function($) dump($ + '\n'), quit: exit }); + } + } catch (error) { + Cu.reportError(error); + if (this.globals.console) this.globals.console.exception(error); + throw error; + } + }, + + // This is the main entry-point: bootstrap.js calls this when the add-on is + // installed. The order of calls is a bit confusing, but here's what + // happens (in temporal order): + // * process.spawn creates a new XUL 'browser' element which will house the + // main addon code. When e10s is active, this uses a real separate OS + // process. When e10s is disabled, this element lives in the one original + // process. Either way, its API is the same. + // * Grab the channel named "require!" and attach a handler which will load + // modules (in the chrome process) when requested to by the addon + // process. This handler uses Loader.require to import the module, then + // calls the module's .initialize() function to connect a new channel. + // The remote caller winds up with a channel reference, which they can + // use to send messages to the newly loaded module. This is for e10s. + // * After the channel handler is attached, process.process() (invoked by + // process.spawn()) will use loadScript() to evaluate code in the + // 'browser' element (which is where the main addon code starts running), + // to do the following: + // * create a Loader, initialized with the same manifest and + // harness-options.json that we've got + // * invoke it's main() method, with the name and path of the addon's + // entry module (which comes from linker via harness-options.js, and is + // usually main.js). That executes main(), above. + // * main() loads the addon's main.js, which executes all top-level + // forms. If the module defines an "exports.main=" function, we invoke + // that too. This is where the addon finally gets to run. + spawn: function spawn(id, path) { + let loader = this; + let process = this.require('api-utils/process'); + process.spawn(id, path)(function(addon) { + // Listen to `require!` channel's input messages from the add-on process + // and load modules being required. + addon.channel('require!').input( + function({ requirer: { path }, id }) { + try { + Loader.require.call(loader, path, id).initialize(addon.channel(id)); + } catch (error) { + this.globals.console.exception(error); + } + }); + }); + }, + unload: function unload(reason, callback) { + this.require('api-utils/unload').send(reason, callback); + } +}; +exports.Loader = Loader; + +}(this); diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/dom/events.js b/tools/addon-sdk-1.5/packages/api-utils/lib/dom/events.js new file mode 100644 index 0000000..2168f42 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/dom/events.js @@ -0,0 +1,136 @@ +/* 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"; + +// 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.5/packages/api-utils/lib/dom/events/keys.js b/tools/addon-sdk-1.5/packages/api-utils/lib/dom/events/keys.js new file mode 100644 index 0000000..53107ae --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/dom/events/keys.js @@ -0,0 +1,60 @@ +/* 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 { emit } = require("../events"); +const { getCodeForKey, toJSON } = require("../../keyboard/utils"); +const { has } = require("../../array"); +const { isString } = require("../../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.5/packages/api-utils/lib/env!.js b/tools/addon-sdk-1.5/packages/api-utils/lib/env!.js new file mode 100644 index 0000000..f7144ad --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/env!.js @@ -0,0 +1,20 @@ +/* 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/. */ + +const { messageManager } = require("chrome"); +const { channel } = require("./channel"); + +module.exports = function load(module) { + return { + require: function require(id) { + // Load required module on the chrome process. + channel(messageManager, messageManager, 'require!').sync({ + requirer: module, + id: id + }); + return channel(messageManager, messageManager, id); + } + }; +}; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/environment.js b/tools/addon-sdk-1.5/packages/api-utils/lib/environment.js new file mode 100644 index 0000000..fd95c67 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/environment.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'; + +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.5/packages/api-utils/lib/errors.js b/tools/addon-sdk-1.5/packages/api-utils/lib/errors.js new file mode 100644 index 0000000..7029158 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/errors.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"; + +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.5/packages/api-utils/lib/events.js b/tools/addon-sdk-1.5/packages/api-utils/lib/events.js new file mode 100644 index 0000000..526befa --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/events.js @@ -0,0 +1,173 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 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) { + this._emit('error', 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.5/packages/api-utils/lib/events/assembler.js b/tools/addon-sdk-1.5/packages/api-utils/lib/events/assembler.js new file mode 100644 index 0000000..5d11be2 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/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.5/packages/api-utils/lib/file.js b/tools/addon-sdk-1.5/packages/api-utils/lib/file.js new file mode 100644 index 0000000..0b4eac9 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/file.js @@ -0,0 +1,192 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +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.5/packages/api-utils/lib/find-tests.js b/tools/addon-sdk-1.5/packages/api-utils/lib/find-tests.js new file mode 100644 index 0000000..60550bf --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/find-tests.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 left intentionally blank diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/globals!.js b/tools/addon-sdk-1.5/packages/api-utils/lib/globals!.js new file mode 100644 index 0000000..6d6888f --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/globals!.js @@ -0,0 +1,98 @@ +/* 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"; + +let { Cc, Ci } = require('chrome'); +let { PlainTextConsole } = require('./plain-text-console'); +let options = require('@packaging'); +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 = (function define(global) { + const PR_WRONLY = 0x02; + const PR_CREATE_FILE = 0x08; + const PR_APPEND = 0x10; + let print = Object.getPrototypeOf(global).dump + if (print) return print; + if ('logFile' in options) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(options.logFile); + let stream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(file, PR_WRONLY|PR_CREATE_FILE|PR_APPEND, -1, 0); + + return function print(message) { + message = String(message); + stream.write(message, message.length); + stream.flush(); + }; + } + return dump; +})(this); + +// Override the default Iterator function with one that passes +// a second argument to custom iterator methods that identifies +// the call as originating from an Iterator function so the custom +// iterator method can return [key, value] pairs just like default +// iterators called via the default Iterator function. +exports.Iterator = (function(DefaultIterator) { + return function Iterator(obj, keysOnly) { + if ("__iterator__" in obj && !keysOnly) + return obj.__iterator__.call(obj, false, true); + return DefaultIterator(obj, keysOnly); + }; +})(Iterator); + +// TODO: Remove memory from the globals, as it raises security concerns and +// there is no real reason to favor global memory over +// `require('api-utils/memory')`. For details see: Bug-620559 +exports.memory = require('./memory'); + +// Bug 718230: We need to send console messages to stdout and JS Console +function forsakenConsoleDump(msg, level) { + exports.dump(msg); + + if (level === "error") { + let err = Cc["@mozilla.org/scripterror;1"]. + createInstance(Ci.nsIScriptError); + msg = msg.replace(/^error: /, ""); + err.init(msg, null, null, 0, 0, 0, "Add-on SDK"); + consoleService.logMessage(err); + } + 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() { + function define(factory) { + factory = Array.slice(arguments).pop(); + factory.call(this, this.require, this.exports, this.module); + } + + return function getter() { + // Redefine `define` as a static property to make sure that module + // gets access to the same function so that `define === define` is + // `true`. + Object.defineProperty(this, 'define', { + configurable: false, + value: define.bind(this) + }); + return this.define; + } + })() +}); diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/hidden-frame.js b/tools/addon-sdk-1.5/packages/api-utils/lib/hidden-frame.js new file mode 100644 index 0000000..6f0016b --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/hidden-frame.js @@ -0,0 +1,166 @@ +/* -*- 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"; + +const {Cc, Ci} = require("chrome"); +const errors = require("./errors"); +const apiUtils = require("./api-utils"); +const timer = require("./timer"); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +let hostFrame, hostDocument, hiddenWindow, isHostFrameReady = false; + +if (!require("./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; + + for each (let [key, val] in Iterator(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; + } + } + }))) { + if (typeof(val) != "undefined") + options[key] = val; + } + + require("./collection").addCollectionProperty(this, "onReady"); + if (options.onReady) + this.onReady.add(options.onReady); + + 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() { + 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; + + entry.unload(); + cache.splice(cache.indexOf(entry), 1); +} + +require("./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.5/packages/api-utils/lib/httpd.js b/tools/addon-sdk-1.5/packages/api-utils/lib/httpd.js new file mode 100644 index 0000000..7f6a363 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/httpd.js @@ -0,0 +1,5166 @@ +/* -*- 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. +*/ + + +var {components,Cc,Ci,Cr,Cu} = require("chrome"); +components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +const CC = components.Constructor; + +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-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 = + 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.5/packages/api-utils/lib/keyboard/hotkeys.js b/tools/addon-sdk-1.5/packages/api-utils/lib/keyboard/hotkeys.js new file mode 100644 index 0000000..bb296ed --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/keyboard/hotkeys.js @@ -0,0 +1,107 @@ +/* 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 { 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.5/packages/api-utils/lib/keyboard/observer.js b/tools/addon-sdk-1.5/packages/api-utils/lib/keyboard/observer.js new file mode 100644 index 0000000..dd24865 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/keyboard/observer.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 { EventEmitterTrait: EventEmitter } = require("../events"); +const { DOMEventAssembler } = require("../events/assembler"); +const { browserWindowIterator, 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.5/packages/api-utils/lib/keyboard/utils.js b/tools/addon-sdk-1.5/packages/api-utils/lib/keyboard/utils.js new file mode 100644 index 0000000..4693a12 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/keyboard/utils.js @@ -0,0 +1,186 @@ +/* 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 { Cc, Ci } = require("chrome"); +const runtime = require("../runtime"); +const { isString } = require("../type"); +const array = require("../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.5/packages/api-utils/lib/light-traits.js b/tools/addon-sdk-1.5/packages/api-utils/lib/light-traits.js new file mode 100644 index 0000000..954d093 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/light-traits.js @@ -0,0 +1,596 @@ +/* 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"; + +// `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.5/packages/api-utils/lib/list.js b/tools/addon-sdk-1.5/packages/api-utils/lib/list.js new file mode 100644 index 0000000..4c6c126 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/list.js @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Trait } = require('./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.5/packages/api-utils/lib/match-pattern.js b/tools/addon-sdk-1.5/packages/api-utils/lib/match-pattern.js new file mode 100644 index 0000000..60753d2 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/match-pattern.js @@ -0,0 +1,103 @@ +/* -*- 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"; +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.5/packages/api-utils/lib/memory.js b/tools/addon-sdk-1.5/packages/api-utils/lib/memory.js new file mode 100644 index 0000000..d7c7f47 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/memory.js @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +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) + 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("./unload").when( + function() { + trackedObjects = {}; + if (timer) { + timer.cancel(); + timer = null; + } + }); diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/message-manager.js b/tools/addon-sdk-1.5/packages/api-utils/lib/message-manager.js new file mode 100644 index 0000000..25ed6f7 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/message-manager.js @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 BAD_LISTENER = "The event listener must be a function."; + +const { Cc, Ci, Cu, CC } = require("chrome"); +const { setTimeout } = require("./timer"); + +const { ns } = require("./namespace"); + +const { curry, invoke } = require("./utils/function"); + +const Sandbox = require("./sandbox"); + +// JSON.stringify is buggy with cross-sandbox values, +// it may return "{}" on functions. Use a replacer to match them correctly. +const jsonFixer = function (k, v) typeof v === "function" ? undefined : v; + +/** + * Defers invoking the function until the current call stack has cleared. + * + * @param {Function} fn + * The function to defer. + * + * @returns {Function} + * The deferred function + */ +const defer = function(fn) function() { + setTimeout(invoke, 0, fn, arguments, this) +}; + +/** + * Adds a message listener. + * This listener will receive messages sent from the remote frame. + * + * @param {String} name + * The name of the message for which to add a listener. + * @param {Function} listener + * The listener function called when the message is received. + */ +function addMessageListener(name, listener) { + if (typeof listener !== "function") + throw new Error(BAD_LISTENER); + + let listeners = frame(this).listeners; + + if (name in listeners) { + if (~listeners[name].indexOf(listener)) + return; + } else { + listeners[name] = []; + } + + listeners[name].push(listener); +} + +/** + * Removes a message listener previously added by calling addMessageListener. + * + * @param {String} name + * The name of the message for which to remove a listener. + * @param {Function} listener + * The listener function has to be removed. + */ +function removeMessageListener(name, listener) { + if (typeof listener !== "function") + throw new Error(BAD_LISTENER); + + let listeners = frame(this).listeners; + + if (!(name in listeners)) + return; + + let index = listeners[name].indexOf(listener); + + if (~index) { + listeners[name].splice(index, 1); + } +} + +/** + * Sends a message to the listeners. + * + * @param {Boolean} sync + * Indicates if the call is synchronous or asynchronous + * @param {String} name + * The name of the message to send to the listeners. + * @param {Object} [data=null] + * A JSON object containing data to be delivered to the listeners. + * + * @returns {Array|undefined} + * An array with the return values of the listeners if `sync` is `true`, + * otherwise `undefined`. + */ +function sendMessage(sync, name, data) { + typeof data === "undefined" && (data = null); + + let listeners = frame(frame(this).receiver).listeners; + + let responses = []; + + let returnValue = sync ? responses : undefined; + + if (!(name in listeners)) + return returnValue; + + let json = JSON.parse(JSON.stringify(data, jsonFixer)); + + for each(let listener in listeners[name]) { + try { + let response = listener.call(null, { + sync : sync, + name : name, + json : json, + target : null + }); + + if (sync) { + if (typeof response === "undefined") + responses.push(response); + else + responses.push(JSON.parse(JSON.stringify(response, jsonFixer))); + } + + } catch (e) { + console.exception(e); + } + } + return returnValue; +}; + +let sendSyncMessage = curry(sendMessage, true); +let sendAsyncMessage = curry(defer(sendMessage), false); + +let frame = ns({receiver: null, listeners: null}); + +/** + * The MessageManager object emulates the Message Manager API, without creating + * new processes. It useful in mono process context, like Fennec. + * + * @see + * https://developer.mozilla.org/en/The_message_manager + */ +function MessageManager() { + + let sandbox = Sandbox.sandbox(null, { wantXrays : false }); + + Object.defineProperties(sandbox, { + addMessageListener: {value: addMessageListener.bind(sandbox)}, + + removeMessageListener: { value: removeMessageListener.bind(sandbox)}, + + sendAsyncMessage: {value: sendAsyncMessage.bind(sandbox)}, + + sendSyncMessage: { value: sendSyncMessage.bind(sandbox) } + }); + + frame(this).receiver = sandbox; + frame(sandbox).receiver = this; + + frame(this).listeners = {}; + frame(sandbox).listeners = {}; +} + +MessageManager.prototype = { + constructor: MessageManager, + + addMessageListener : addMessageListener, + + removeMessageListener : removeMessageListener, + + sendAsyncMessage : sendAsyncMessage, + + /** + * Loads a script into the remote frame. + * + * @param {String} uri + * The URL of the script to load into the frame; this must be an absolute + * local URL, but data: URLs are supported. + * @param {Boolean} allowDelayedLoad + * Not used. + */ + loadFrameScript: function loadFrameScript(uri, async) { + if (arguments.length < loadFrameScript.length) + throw new Error("Not enough arguments"); + + let sandbox = frame(this).receiver; + + try { + Sandbox.load(sandbox, uri); + } catch (e) { + console.exception(e) + } + } +} + +Object.freeze(MessageManager); +Object.freeze(MessageManager.prototype); + +exports.MessageManager = MessageManager; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/namespace.js b/tools/addon-sdk-1.5/packages/api-utils/lib/namespace.js new file mode 100644 index 0000000..b25284e --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/namespace.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"; + +/** + * Function creates a new namespace. Optionally `prototype` object may be + * passed, in which case namespace objects will inherit from it. Returned value + * is a function that can be used to get access to the namespaced properties + * for the passed object. + * @examples + * const ns = Namespace(); + * ns(myObject).secret = secret; + */ +exports.Namespace = function Namespace(prototype) { + prototype = prototype || Object.prototype; + const map = new WeakMap(); + return function namespace(target) { + return map.get(target) || + map.set(target, Object.create(prototype)), 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 = exports.Namespace; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/observer-service.js b/tools/addon-sdk-1.5/packages/api-utils/lib/observer-service.js new file mode 100644 index 0000000..2651da7 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/observer-service.js @@ -0,0 +1,178 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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"); +var xpcom = require("./xpcom"); + +/** + * A service for adding, removing and notifying observers of notifications. + * Wraps the nsIObserverService interface. + * + * @version 0.2 + */ + +var service = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + +/** + * A cache of observers that have been added. + * + * We use this to remove observers when a caller calls |Observers.remove|. + */ +var 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. + */ + get APPLICATION_READY() packaging.jetpackID + "_APPLICATION_READY" +}; + +/** + * 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 thisObject {Object} [optional] + * the object to use as |this| when calling a Function callback + * + * @returns the observer + */ +var add = exports.add = function add(topic, callback, thisObject) { + var observer = new Observer(topic, callback, thisObject); + service.addObserver(observer, topic, true); + cache.push(observer); + + return observer; +}; + +/** + * 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 thisObject {Object} [optional] + * the object being used as |this| when calling a Function callback + */ +var remove = exports.remove = function remove(topic, callback, thisObject) { + // This seems fairly inefficient, but I'm not sure how much better + // we can make it. We could index by topic, but we can't index by callback + // or thisObject, as far as I know, since the keys to JavaScript hashes + // (a.k.a. objects) can apparently only be primitive values. + let observers = cache.filter(function(v) { + return (v.topic == topic && + v.callback == callback && + v.thisObject == thisObject); + }); + + if (observers.length) { + service.removeObserver(observers[0], topic); + cache.splice(cache.indexOf(observers[0]), 1); + } +}; + +/** + * 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 }) + */ +var notify = exports.notify = function notify(topic, subject, data) { + subject = (typeof subject == "undefined") ? null : new Subject(subject); + data = (typeof data == "undefined") ? null : data; + service.notifyObservers(subject, topic, data); +}; + +function Observer(topic, callback, thisObject) { + memory.track(this); + this.topic = topic; + this.callback = callback; + this.thisObject = thisObject; +} + +Observer.prototype = { + QueryInterface: xpcom.utils.generateQI([Ci.nsIObserver, + Ci.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 { + if (typeof this.callback == "function") { + if (this.thisObject) + this.callback.call(this.thisObject, subject, data); + else + this.callback(subject, data); + } else // typeof this.callback == "object" (nsIObserver) + this.callback.observe(subject, topic, data); + } catch (e) { + console.exception(e); + } + } +}; + +function Subject(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 + }; +} + +Subject.prototype = { + QueryInterface: xpcom.utils.generateQI([]), + getHelperForLanguage: function() {}, + getInterfaces: function() {} +}; + +require("./unload").when( + function removeAllObservers() { + // Make a copy of cache first, since cache will be changing as we + // iterate through it. + cache.slice().forEach( + function(observer) { + remove(observer.topic, observer.callback, observer.thisObject); + }); + }); diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/passwords/utils.js b/tools/addon-sdk-1.5/packages/api-utils/lib/passwords/utils.js new file mode 100644 index 0000000..5dc6098 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/passwords/utils.js @@ -0,0 +1,101 @@ +/* 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 { Cc, Ci, components: { Constructor: CConstructor } } = 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 = CConstructor("@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.5/packages/api-utils/lib/plain-text-console.js b/tools/addon-sdk-1.5/packages/api-utils/lib/plain-text-console.js new file mode 100644 index 0000000..d76fddb --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/plain-text-console.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/. */ + +"use strict"; + +const {Cc,Ci} = require("chrome"); + +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) { + print(level + ": " + 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); + }, + + info: function info() { + message(this.print, "info", arguments); + }, + + warn: function warn() { + message(this.print, "warning", arguments); + }, + + error: function error() { + message(this.print, "error", arguments); + }, + + debug: function debug() { + message(this.print, "debug", arguments); + }, + + 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)]); + } +}; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/preferences-service.js b/tools/addon-sdk-1.5/packages/api-utils/lib/preferences-service.js new file mode 100644 index 0000000..ad29308 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/preferences-service.js @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// The 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"); + +var prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService).getBranch(null); + +var get = exports.get = 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."); + } +}; + +var set = exports.set = 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"); + } +}; + +var has = exports.has = function has(name) { + return (prefSvc.getPrefType(name) != Ci.nsIPrefBranch.PREF_INVALID); +}; + +var isSet = exports.isSet = function isSet(name) { + return (has(name) && prefSvc.prefHasUserValue(name)); +}; + +var reset = exports.reset = 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. + } +}; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/process.js b/tools/addon-sdk-1.5/packages/api-utils/lib/process.js new file mode 100644 index 0000000..96854a9 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/process.js @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Cc, Ci } = require("chrome"); +const { createRemoteBrowser } = require("api-utils/window-utils"); +const { channel } = require("./channel"); +const packaging = require('@packaging'); +const { when } = require('./unload'); +const { MessageManager } = require('./message-manager'); + +const addonService = '@mozilla.org/addon/service;1' in Cc ? + Cc['@mozilla.org/addon/service;1'].getService(Ci.nsIAddonService) : null + +const ENABLE_E10S = packaging.enable_e10s; + +const isFennec = require("./xul-app").is("Fennec"); + +function loadScript(target, uri, sync) { + return 'loadScript' in target ? target.loadScript(uri, sync) + : target.loadFrameScript(uri, sync) +} + +function process(target, id, path, scope) { + // Please note that even though `loadScript`, is executed before channel is + // returned, users still are able to subscribe for messages before any message + // will be sent. That's because `loadScript` queues script execution on the + // other process, which means they will execute async (on the next turn of + // event loop), while the channel for messages is returned immediately (in + // the same turn of event loop). + + loadScript(target, packaging.uriPrefix + packaging.loader, false); + loadScript(target, 'data:,let loader = Loader.new(' + + JSON.stringify(packaging) + ');\n' + + 'loader.main("' + id + '", "' + path + '");', false); + + when(function (reason) { + // Please note that it's important to unload remote loader + // synchronously (using synchronous frame script), to make sure that we + // don't stop during unload. + // Bug 724433: Take care to nullify all globals set by `cuddlefish.js` + // otherwise, we will leak any still defined global. + // `dump` is set in Loader.new method, `dump = globals.dump;` + loadScript(target, 'data:,loader.unload("' + reason + '");' + + 'loader = null; Loader = null; dump = null;', true); + }); + + return { channel: channel.bind(null, scope, target) } +} + +exports.spawn = function spawn(id, path) { + return function promise(deliver) { + // If `nsIAddonService` is available we use it to create an add-on process, + // otherwise we fallback to the remote browser's message manager. + if (ENABLE_E10S && addonService) { + console.log('!!!!!!!!!!!!!!!!!!!! Using addon process !!!!!!!!!!!!!!!!!!'); + deliver(process(addonService.createAddon(), id, path)); + } else if (isFennec) { + deliver(process(new MessageManager(), id, path)); + } else { + createRemoteBrowser(ENABLE_E10S)(function(browser) { + let messageManager = browser.QueryInterface(Ci.nsIFrameLoaderOwner). + frameLoader.messageManager + let window = browser.ownerDocument.defaultView; + deliver(process(messageManager, id, path, window)); + }); + } + }; +}; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/runtime.js b/tools/addon-sdk-1.5/packages/api-utils/lib/runtime.js new file mode 100644 index 0000000..845d146 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/runtime.js @@ -0,0 +1,15 @@ +/* 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 { 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.5/packages/api-utils/lib/sandbox.js b/tools/addon-sdk-1.5/packages/api-utils/lib/sandbox.js new file mode 100644 index 0000000..6130637 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/sandbox.js @@ -0,0 +1,46 @@ +/* -*- 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"; + +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.5/packages/api-utils/lib/self!.js b/tools/addon-sdk-1.5/packages/api-utils/lib/self!.js new file mode 100644 index 0000000..fd69f9a --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/self!.js @@ -0,0 +1,48 @@ +/* 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 { jetpackID, name, manifest, metadata, uriPrefix } = require('@packaging'); + +const XMLHttpRequest = CC('@mozilla.org/xmlextras/xmlhttprequest;1', + 'nsIXMLHttpRequest'); + +// Utility function that synchronously reads local resource from the given +// `uri` and returns content string. +function readURI(uri) { + let request = XMLHttpRequest(); + request.open('GET', uri, false); + request.overrideMimeType('text/plain'); + request.send(); + return request.responseText; +} + +// 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. +const uri = 'addon:' + jetpackID + +function url(root, path) root + (path || "") +function read(root, path) readURI(url(root, path)) + +exports.create = function create(base) { + let moduleData = manifest[base] && manifest[base].requirements['self']; + let root = uriPrefix + moduleData.dataURIPrefix; + return Object.freeze({ + id: 'self', + exports: Object.freeze({ + id: jetpackID, + uri: uri, + name: name, + version: metadata[name].version, + data: { + url: url.bind(null, root), + load: read.bind(null, root) + } + }) + }); +}; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/system.js b/tools/addon-sdk-1.5/packages/api-utils/lib/system.js new file mode 100644 index 0000000..3986645 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/system.js @@ -0,0 +1,120 @@ +/* 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'; + +const { Cc, Ci, CC } = require('chrome'); +const options = require('@packaging'); +const file = require('./file'); +const runtime = require("./runtime.js"); + +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 { 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('./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) { + let stream = file.open(options.resultFile, 'w'); + stream.write(code ? 'FAIL' : 'OK'); + stream.close(); + } + + appStartup.quit(code ? E_ATTEMPT : E_FORCE); +}; + +/** + * 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.5/packages/api-utils/lib/tab-browser.js b/tools/addon-sdk-1.5/packages/api-utils/lib/tab-browser.js new file mode 100644 index 0000000..13d365a --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/tab-browser.js @@ -0,0 +1,727 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +const {Cc,Ci,Cu} = require("chrome"); +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("./collection"); + +// 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("./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("timer").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 wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + var win = wm.getMostRecentWindow("navigator:browser"); + 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(mainWindow.gBrowser.selectedTab); + } + 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("./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("./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; +}; + +exports.__defineGetter__("activeTab", function() { + const wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + let mainWindow = wm.getMostRecentWindow("navigator:browser"); + return mainWindow.gBrowser.selectedTab; +}); + +/******************* 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(window.gBrowser.selectedTab) + : 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("./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("./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("./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("./timer").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("./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.5/packages/api-utils/lib/tabs/events.js b/tools/addon-sdk-1.5/packages/api-utils/lib/tabs/events.js new file mode 100644 index 0000000..5c7dd0c --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/tabs/events.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const ON_PREFIX = "on"; +const TAB_PREFIX = "Tab"; + +const EVENTS = { + ready: "DOMContentLoaded", + open: "TabOpen", + close: "TabClose", + activate: "TabSelect", + deactivate: null +} +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.5/packages/api-utils/lib/tabs/observer.js b/tools/addon-sdk-1.5/packages/api-utils/lib/tabs/observer.js new file mode 100644 index 0000000..a1b4c14 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/tabs/observer.js @@ -0,0 +1,93 @@ +/* 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 { EventEmitterTrait: EventEmitter } = require("../events"); +const { DOMEventAssembler } = require("../events/assembler"); +const { Trait } = require("../light-traits"); +const { getActiveTab, getTabs, getTabContainers } = require("./utils"); +const { browserWindowIterator, isBrowser } = require("../window-utils"); +const { observer: windowObserver } = require("../windows/observer"); + +const EVENTS = { + "TabOpen": "open", + "TabClose": "close", + "TabSelect": "select", + "TabMove": "move", + "TabPinned": "pin", + "TabUnpinned": "unpin" +}; + + +// 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 dispatches 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. + getTabContainers(chromeWindow).forEach(function (container) { + observer.observe(container); + }); +} +windowObserver.on("open", onWindowOpen); + +function onWindowClose(chromeWindow) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + getTabContainers(chromeWindow).forEach(function (container) { + observer.ignore(container); + }); +} +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.5/packages/api-utils/lib/tabs/tab.js b/tools/addon-sdk-1.5/packages/api-utils/lib/tabs/tab.js new file mode 100644 index 0000000..af6992e --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/tabs/tab.js @@ -0,0 +1,264 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Ci } = require('chrome'); +const { Trait } = require("../traits"); +const { EventEmitter } = require("../events"); +const { validateOptions } = require("../api-utils"); +const { Enqueued } = require("../utils/function"); +const { EVENTS } = require("./events"); +const { getThumbnailURIForWindow } = require("../utils/thumbnail"); +const { getFaviconURIForLocation } = require("../utils/data"); + + + +// 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; + // 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() this._window.gBrowser.getBrowserForTab(this._tab), + /** + * Window DOM element containing this tab. + */ + get _window() this._tab.ownerDocument.defaultView, + /** + * 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() this._contentDocument.title, + set title(value) this._contentDocument.title = String(value), + /** + * Location of the page currently loaded in this tab. + * Changing this property will loads page under under the specified location. + * @type {String} + */ + get url() String(this._browser.currentURI.spec), + set url(value) this._changeLocation(String(value)), + // "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. + _changeLocation: Enqueued(function(url) this._browser.loadURI(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) { + let { Worker } = require("../content/worker"); + options.window = this._contentWindow; + let worker = Worker(options); + worker.once("detach", function detach() { + worker.destroy(); + }); + return worker; + }, + + /** + * Make this tab active. + * Please note: That this function is called synchronous 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: Enqueued(function activate() { + if (this._window) // Ignore if window is closed by the time this is invoked. + this._window.gBrowser.selectedTab = 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; + +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; + + +exports.getTabForWindow = function (win) { + // Get browser window + let topWindow = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + if (!topWindow.gBrowser) return null; + + // Get top window object, in case we are in a content iframe + let topContentWindow; + try { + topContentWindow = win.top; + } catch(e) { + // It may throw if win is not a valid content window + return null; + } + + function getWindowID(obj) { + return obj.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID; + } + + // Search for related Tab + let topWindowId = getWindowID(topContentWindow); + for (let i = 0; i < topWindow.gBrowser.browsers.length; i++) { + let w = topWindow.gBrowser.browsers[i].contentWindow; + if (getWindowID(w) == topWindowId) { + return Tab({ + // TODO: api-utils should not depend on addon-kit! + window: require("addon-kit/windows").BrowserWindow({ window: topWindow }), + tab: topWindow.gBrowser.tabs[i] + }); + } + } + + // We were unable to find the related tab! + return null; +} diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/tabs/utils.js b/tools/addon-sdk-1.5/packages/api-utils/lib/tabs/utils.js new file mode 100644 index 0000000..1b773c4 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/tabs/utils.js @@ -0,0 +1,54 @@ +/* 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"; + +function getTabContainer(tabBrowser) { + return tabBrowser.tabContainer; +} +exports.getTabContainer = getTabContainer; + +function getTabBrowsers(window) { + return Array.slice(window.document.getElementsByTagName("tabbrowser")); +} +exports.getTabBrowsers = getTabBrowsers; + +function getTabContainers(window) { + return getTabBrowsers(window).map(getTabContainer); +} +exports.getTabContainers = getTabContainers; + +function getTabs(window) { + return getTabContainers(window).reduce(function (tabs, container) { + tabs.push.apply(tabs, container.children); + return tabs; + }, []); +} +exports.getTabs = getTabs; + +function getActiveTab(window) { + return window.gBrowser.selectedTab; +} +exports.getActiveTab = getActiveTab; + +function getOwnerWindow(tab) { + return tab.ownerDocument.defaultView; +} +exports.getOwnerWindow = getOwnerWindow; + +function openTab(window, url) { + return window.gBrowser.addTab(url); +} +exports.openTab = openTab; + +function closeTab(tab) { + return getOwnerWindow(tab).gBrowser.removeTab(tab); +} +exports.closeTab = closeTab; + +function activateTab(tab) { + getOwnerWindow(tab).gBrowser.selectedTab = tab; +} +exports.activateTab = activateTab; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/test.js b/tools/addon-sdk-1.5/packages/api-utils/lib/test.js new file mode 100644 index 0000000..e69a2f4 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/test.js @@ -0,0 +1,108 @@ +/* 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"; + +const BaseAssert = require("./test/assert").Assert; +const { isFunction, isObject } = require("./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.5/packages/api-utils/lib/test/assert.js b/tools/addon-sdk-1.5/packages/api-utils/lib/test/assert.js new file mode 100644 index 0000000..fa40807 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/test/assert.js @@ -0,0 +1,331 @@ +/* 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"; + +const { isFunction, isNull, isObject, isString, isRegExp, isArray, isDate, + isPrimitive, isUndefined, instanceOf, source } = require("../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) { + this._log.fail(e.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.5/packages/api-utils/lib/text-streams.js b/tools/addon-sdk-1.5/packages/api-utils/lib/text-streams.js new file mode 100644 index 0000000..96217eb --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/text-streams.js @@ -0,0 +1,240 @@ +/* -*- 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"; + +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("./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.5/packages/api-utils/lib/timer.js b/tools/addon-sdk-1.5/packages/api-utils/lib/timer.js new file mode 100644 index 0000000..1e0cff2 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/timer.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/. */ + +"use strict"; + +const {Cc,Ci} = require("chrome"); +var xpcom = require("./xpcom"); + +var timerClass = Cc["@mozilla.org/timer;1"]; +var nextID = 1; +var timers = {}; + +function TimerCallback(timerID, callback, params) { + this._callback = callback; + this._params = params; +}; +TimerCallback.prototype = { + QueryInterface : xpcom.utils.generateQI([Ci.nsITimerCallback]) +}; + +function TimeoutCallback(timerID, callback, params) { + memory.track(this); + TimerCallback.apply(this, arguments) + this._timerID = timerID; +}; +TimeoutCallback.prototype = new TimerCallback(); +TimeoutCallback.prototype.notify = function notifyOnTimeout(timer) { + try { + delete timers[this._timerID]; + this._callback.apply(null, this._params); + } catch (e) { + console.exception(e); + } +}; + +function IntervalCallback(timerID, callback, params) { + memory.track(this); + TimerCallback.apply(this, arguments) +}; +IntervalCallback.prototype = new TimerCallback(); +IntervalCallback.prototype.notify = function notifyOnInterval() { + try { + this._callback.apply(null, this._params); + } catch (e) { + console.exception(e); + } +}; + + +var setTimeout = exports.setTimeout = function setTimeout(callback, delay) { + return makeTimer( + Ci.nsITimer.TYPE_ONE_SHOT, + callback, + TimeoutCallback, + delay, + Array.slice(arguments, 2)); +}; + +var clearTimeout = exports.clearTimeout = function clearTimeout(timerID) { + cancelTimer(timerID); +}; + +var setInterval = exports.setInterval = function setInterval(callback, delay) { + return makeTimer( + Ci.nsITimer.TYPE_REPEATING_SLACK, + callback, + IntervalCallback, + delay, + Array.slice(arguments, 2)); +}; + +var clearInterval = exports.clearInterval = function clearInterval(timerID) { + cancelTimer(timerID); +}; + +function makeTimer(type, callback, callbackType, delay, params) { + var timer = timerClass.createInstance(Ci.nsITimer); + + memory.track(timer, "nsITimer"); + + var timerID = nextID++; + timers[timerID] = timer; + + timer.initWithCallback( + new callbackType(timerID, callback, params), + delay || 0, + type + ); + return timerID; +} + +function cancelTimer(timerID) { + var timer = timers[timerID]; + if (timer) { + timer.cancel(); + delete timers[timerID]; + } +} + +require("./unload").when( + function cancelAllPendingTimers() { + var timerIDs = [timerID for (timerID in timers)]; + timerIDs.forEach(function(timerID) { cancelTimer(timerID); }); + }); + diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/traceback.js b/tools/addon-sdk-1.5/packages/api-utils/lib/traceback.js new file mode 100644 index 0000000..c55fd9d --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/traceback.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Cc,Ci,components} = require("chrome"); + +// Undo the auto-parentification of URLs done in bug 418356. +function deParentifyURL(url) { + return url ? url.split(" -> ").slice(-1)[0] : url; +} + +// TODO: We might want to move this function to url or some similar +// module. +function getLocalFile(path) { + var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + var channel = ios.newChannel(path, null, null); + var iStream = channel.open(); + var siStream = Cc['@mozilla.org/scriptableinputstream;1'] + .createInstance(Ci.nsIScriptableInputStream); + siStream.init(iStream); + var data = new String(); + data += siStream.read(-1); + siStream.close(); + iStream.close(); + return data; +} + +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 getLocalFile(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 funcName = funcSig.slice(0, funcSig.indexOf("(")); + 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.5/packages/api-utils/lib/traits.js b/tools/addon-sdk-1.5/packages/api-utils/lib/traits.js new file mode 100644 index 0000000..7c1c4ec --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/traits.js @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 { + 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.5/packages/api-utils/lib/traits/core.js b/tools/addon-sdk-1.5/packages/api-utils/lib/traits/core.js new file mode 100644 index 0000000..6e802c7 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/traits/core.js @@ -0,0 +1,317 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +// 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.5/packages/api-utils/lib/type.js b/tools/addon-sdk-1.5/packages/api-utils/lib/type.js new file mode 100644 index 0000000..56df14e --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/type.js @@ -0,0 +1,340 @@ +/* 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"; + +/** + * 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.5/packages/api-utils/lib/unit-test-finder.js b/tools/addon-sdk-1.5/packages/api-utils/lib/unit-test-finder.js new file mode 100644 index 0000000..958034d --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/unit-test-finder.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"; + +const file = require("./file"); +const packaging = require('@packaging'); +const suites = packaging.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 = { + _makeTest: function _makeTest(suite, name, test) { + function runTest(runner) { + console.info("executing '" + suite + "." + name + "'"); + test(runner); + } + return runTest; + }, + + 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: self._makeTest(suite, name, module[name]), + name: suite + "." + name + }); + } + } + }); + + cb(tests); + } +}; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/unit-test.js b/tools/addon-sdk-1.5/packages/api-utils/lib/unit-test.js new file mode 100644 index 0000000..4108d25 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/unit-test.js @@ -0,0 +1,438 @@ +/* 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"; +var timer = require("./timer"); + +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) { + 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; + } + 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("./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); + 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 { + 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.5/packages/api-utils/lib/unload.js b/tools/addon-sdk-1.5/packages/api-utils/lib/unload.js new file mode 100644 index 0000000..aba5923 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/unload.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/. */ + +// Parts of this module were taken from narwhal: +// +// http://narwhaljs.org + +var observers = []; +var unloaders = []; + +var when = exports.when = function when(observer) { + if (observers.indexOf(observer) != -1) + return; + observers.unshift(observer); +}; + +var send = exports.send = function send(reason, onError) { + onError = onError || console.exception; + observers.forEach(function (observer) { + try { + observer(reason); + } catch (e) { + onError(e); + } + }); +}; + +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; + } + }; + + unloaders.push(unloadWrapper); + + obj[destructorName] = unloadWrapper; +}; + +when( + function(reason) { + unloaders.slice().forEach( + function(unloadWrapper) { + unloadWrapper(reason); + }); + }); diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/url.js b/tools/addon-sdk-1.5/packages/api-utils/lib/url.js new file mode 100644 index 0000000..29fc12c --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/url.js @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Cc,Ci,Cr} = require("chrome"); + +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; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/utils/data.js b/tools/addon-sdk-1.5/packages/api-utils/lib/utils/data.js new file mode 100644 index 0000000..33356cd --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/utils/data.js @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Cc, Ci, Cu } = require("chrome"); +const IOService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); +const AppShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); + +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm"); +const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + +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 + + base64Encode(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 = getChromeURIContent; + +/** + * Creates a base-64 encoded ASCII string from a string of binary data. + */ +function base64Encode(data) AppShellService.hiddenDOMWindow.btoa(String(data)); +exports.base64Encode = base64Encode; + +/** + * Decodes a string of data which has been encoded using base-64 encoding. + */ +function base64Decode(data) AppShellService.hiddenDOMWindow.atob(String(data)); +exports.base64Decode = base64Decode; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/utils/function.js b/tools/addon-sdk-1.5/packages/api-utils/lib/utils/function.js new file mode 100644 index 0000000..d943c65 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/utils/function.js @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 { setTimeout } = require("../timer"); + +/** + * 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 Enqueued(callee) { + return function enqueued() + setTimeout(invoke, 0, callee, arguments, this); +} +exports.Enqueued = Enqueued; + +/** + * 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; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/utils/object.js b/tools/addon-sdk-1.5/packages/api-utils/lib/utils/object.js new file mode 100644 index 0000000..165aa0b --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/utils/object.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"; + +/** + * 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. + * @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 = {}; + Array.slice(arguments, 1).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.5/packages/api-utils/lib/utils/registry.js b/tools/addon-sdk-1.5/packages/api-utils/lib/utils/registry.js new file mode 100644 index 0000000..2ce6a6e --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/utils/registry.js @@ -0,0 +1,57 @@ +/* -*- 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"; + +const { EventEmitter } = require('../events'); +const unload = require('../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.5/packages/api-utils/lib/utils/thumbnail.js b/tools/addon-sdk-1.5/packages/api-utils/lib/utils/thumbnail.js new file mode 100644 index 0000000..dfd03dd --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/utils/thumbnail.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"; + +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.5/packages/api-utils/lib/window-utils.js b/tools/addon-sdk-1.5/packages/api-utils/lib/window-utils.js new file mode 100644 index 0000000..66af98b --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/window-utils.js @@ -0,0 +1,249 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Cc, Ci } = require("chrome"); +const { EventEmitter } = require('./events'), + { Trait } = require('./traits'); +const { when } = require('./unload'); +const errors = require("./errors"); + +const gWindowWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); +const appShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); + +const XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + +/** + * An iterator for XUL windows currently in the application. + * + * @return A generator that yields XUL windows exposing the + * nsIDOMWindow interface. + */ +var windowIterator = exports.windowIterator = function windowIterator() { + let winEnum = gWindowWatcher.getWindowEnumerator(); + while (winEnum.hasMoreElements()) + yield winEnum.getNext().QueryInterface(Ci.nsIDOMWindow); +}; + +/** + * 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; + +var WindowTracker = exports.WindowTracker = function WindowTracker(delegate) { + if (!(this instanceof WindowTracker)) { + return new WindowTracker(delegate); + } + + this._delegate = delegate; + this._loadingWindows = []; + + for (let window in windowIterator()) + this._regWindow(window); + gWindowWatcher.registerNotification(this); + + require("./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() { + gWindowWatcher.unregisterNotification(this); + for (let window in windowIterator()) + 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); + }) +}; + +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); +}; + +exports.__defineGetter__("activeWindow", function() { + return Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow(null); +}); +exports.__defineSetter__("activeWindow", function(window) { + try { + window.focus(); + } + catch (e) { } +}); + +exports.__defineGetter__("activeBrowserWindow", function() { + return Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow("navigator:browser"); +}); + +/** + * Returns the ID of the window's current inner window. + */ +exports.getInnerId = function getInnerId(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; +}; + +/** + * Returns the ID of the window's outer window. + */ +exports.getOuterId = function getOuterId(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).outerWindowID; +}; + +function isBrowser(window) { + return window.document.documentElement.getAttribute("windowtype") === + "navigator:browser"; +}; +exports.isBrowser = isBrowser; + +exports.hiddenWindow = appShellService.hiddenDOMWindow; + +function createHiddenXULFrame() { + return function promise(deliver) { + let window = appShellService.hiddenDOMWindow; + let document = window.document; + let isXMLDoc = (document.contentType == "application/xhtml+xml" || + document.contentType == "application/vnd.mozilla.xul+xml") + + if (isXMLDoc) { + deliver(window) + } + else { + let frame = document.createElement('iframe'); + // This is ugly but we need window for XUL document in order to create + // browser elements. + frame.setAttribute('src', 'chrome://browser/content/hiddenWindow.xul'); + frame.addEventListener('DOMContentLoaded', function onLoad(event) { + frame.removeEventListener('DOMContentLoaded', onLoad, false); + deliver(frame.contentWindow); + }, false); + document.documentElement.appendChild(frame); + } + } +}; +exports.createHiddenXULFrame = createHiddenXULFrame; + +exports.createRemoteBrowser = function createRemoteBrowser(remote) { + return function promise(deliver) { + createHiddenXULFrame()(function(hiddenWindow) { + let document = hiddenWindow.document; + let browser = document.createElementNS(XUL, "browser"); + // Remote="true" enable everything here: + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1347 + if (remote !== false) + browser.setAttribute("remote","true"); + // Type="content" is mandatory to enable stuff here: + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1776 + browser.setAttribute("type","content"); + // 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) + browser.setAttribute("style","-moz-binding: none;"); + // Flex it in order to be visible (optional, for debug purpose) + browser.setAttribute("flex", "1"); + document.documentElement.appendChild(browser); + + // Bug 724433: do not leak this <browser> DOM node + when(function () { + document.documentElement.removeChild(browser); + }); + + // Return browser + deliver(browser); + }); + }; +}; + +require("./unload").when( + function() { + gDocsToClose.slice().forEach( + function(doc) { doc.defaultView.close(); }); + }); diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/windows/dom.js b/tools/addon-sdk-1.5/packages/api-utils/lib/windows/dom.js new file mode 100644 index 0000000..1ff966a --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/windows/dom.js @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict'; + +const { Trait } = require('../traits'); + +const WindowDom = Trait.compose({ + _window: Trait.required, + get title() { + let window = this._window; + return window && window.document ? window.document.title : null + }, + 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; + } +}); +exports.WindowDom = WindowDom; + diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/windows/loader.js b/tools/addon-sdk-1.5/packages/api-utils/lib/windows/loader.js new file mode 100644 index 0000000..b596e73 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/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"; + +const { Cc, Ci } = require('chrome'), + { setTimeout } = require("../timer"), + { Trait } = require('../traits'), + + WM = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator), + + URI_BROWSER = 'chrome://browser/content/browser.xul', + NAME = '_blank', + FEATURES = 'chrome,all,dialog=no', + PARAMS = [ URI_BROWSER, NAME, FEATURES ], + ON_LOAD = 'load', + ON_UNLOAD = 'unload', + STATE_LOADED = 'complete', + BROWSER = 'navigator:browser'; + +/** + * 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; + let params = PARAMS.slice() + params.push(this._tabOptions.map(function(options) options.url).join("|")) + let browser = WM.getMostRecentWindow(BROWSER); + this._window = browser.openDialog.apply(browser, params); + }, + /** + * 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.5/packages/api-utils/lib/windows/observer.js b/tools/addon-sdk-1.5/packages/api-utils/lib/windows/observer.js new file mode 100644 index 0000000..45191b7 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/windows/observer.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 { EventEmitterTrait: EventEmitter } = require("../events"); +const { WindowTracker, windowIterator } = require("../window-utils"); +const { DOMEventAssembler } = require("../events/assembler"); +const { Trait } = require("../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); + } +}); + +// Making observer aware of already opened windows. +for each (let window in windowIterator()) + observer.observe(window); + +exports.observer = observer; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/windows/tabs.js b/tools/addon-sdk-1.5/packages/api-utils/lib/windows/tabs.js new file mode 100644 index 0000000..33c5ed5 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/windows/tabs.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"; + +const { Trait } = require("../traits"); +const { List } = require("../list"); +const { Tab, Options } = require("../tabs/tab"); +const { EventEmitter } = require("../events"); +const { EVENTS } = require("../tabs/events"); +const { getOwnerWindow, getActiveTab, getTabs, + openTab, activateTab } = require("../tabs/utils"); +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"); + + 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); + }, + _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; diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/xhr.js b/tools/addon-sdk-1.5/packages/api-utils/lib/xhr.js new file mode 100644 index 0000000..7c4fc29 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/xhr.js @@ -0,0 +1,149 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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"); + +// ## 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); + }; + }); + +require("./unload").when( + function() { + requests.slice().forEach(function(request) { request._unload(); }); + }); diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/xpcom.js b/tools/addon-sdk-1.5/packages/api-utils/lib/xpcom.js new file mode 100644 index 0000000..7f8caa6 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/xpcom.js @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Cc,Ci,Cm,Cr,Cu} = require("chrome"); + +var jsm = {}; +Cu.import("resource://gre/modules/XPCOMUtils.jsm", jsm); +var utils = exports.utils = jsm.XPCOMUtils; + +Cm.QueryInterface(Ci.nsIComponentRegistrar); + +var factories = []; + +function Factory(options) { + memory.track(this); + + this.wrappedJSObject = this; + this.create = options.create; + this.uuid = options.uuid; + this.name = options.name; + this.contractID = options.contractID; + + Cm.registerFactory(this.uuid, + this.name, + this.contractID, + this); + + var self = this; + + factories.push(this); +} + +Factory.prototype = { + createInstance: function(outer, iid) { + try { + if (outer) + throw Cr.NS_ERROR_NO_AGGREGATION; + return (new this.create()).QueryInterface(iid); + } catch (e) { + console.exception(e); + if (e instanceof Ci.nsIException) + throw e; + else + throw Cr.NS_ERROR_FAILURE; + } + }, + unregister: function() { + var index = factories.indexOf(this); + if (index == -1) + throw new Error("factory already unregistered"); + + var self = this; + + factories.splice(index, 1); + Cm.unregisterFactory(this.uuid, this); + }, + QueryInterface: utils.generateQI([Ci.nsIFactory]) +}; + +var makeUuid = exports.makeUuid = function makeUuid() { + var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + var uuid = uuidGenerator.generateUUID(); + return uuid; +}; + +var autoRegister = exports.autoRegister = 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("./runtime"); + var osDirName = runtime.OS + "_" + runtime.XPCOMABI; + var platformVersion = require("./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); +}; + +var register = exports.register = function register(options) { + options = {__proto__: options}; + if (!options.uuid) + options.uuid = makeUuid(); + return new Factory(options); +}; + +var getClass = exports.getClass = function getClass(contractID, iid) { + if (!iid) + iid = Ci.nsISupports; + return Cm.getClassObjectByContractID(contractID, iid); +}; + +require("./unload").when( + function() { + var copy = factories.slice(); + copy.reverse(); + copy.forEach(function(factory) { factory.unregister(); }); + }); diff --git a/tools/addon-sdk-1.5/packages/api-utils/lib/xul-app.js b/tools/addon-sdk-1.5/packages/api-utils/lib/xul-app.js new file mode 100644 index 0000000..9272d88 --- /dev/null +++ b/tools/addon-sdk-1.5/packages/api-utils/lib/xul-app.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"; + +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); +} + |