From cb0e50182e239689cd561c8aacb623a4bc4b48ee Mon Sep 17 00:00:00 2001 From: Wes Kocher Date: Sat, 13 Apr 2013 17:25:38 -0700 Subject: [PATCH] Bug 861277 - Uplift addon-sdk's integration branch --- .../source/doc/dev-guide-source/credits.md | 44 +- .../doc/module-source/sdk/content/mod.md | 105 ++++ .../source/doc/module-source/sdk/panel.md | 2 +- .../doc/module-source/sdk/platform/xpcom.md | 2 +- .../source/doc/module-source/sdk/request.md | 4 +- .../doc/module-source/sdk/stylesheet/style.md | 53 ++ .../doc/module-source/sdk/stylesheet/utils.md | 41 ++ addon-sdk/source/doc/module-source/sdk/url.md | 11 + .../doc/module-source/sdk/util/array.md | 156 +++++ .../doc/module-source/sdk/util/object.md | 75 +++ .../library-detector/data/icons/mootools.png | Bin addon-sdk/source/lib/sdk/addon/window.js | 5 +- addon-sdk/source/lib/sdk/content/loader.js | 23 +- addon-sdk/source/lib/sdk/content/mod.js | 60 ++ addon-sdk/source/lib/sdk/content/worker.js | 51 +- addon-sdk/source/lib/sdk/core/disposable.js | 55 +- addon-sdk/source/lib/sdk/event/core.js | 25 + addon-sdk/source/lib/sdk/event/target.js | 21 +- addon-sdk/source/lib/sdk/event/utils.js | 3 + addon-sdk/source/lib/sdk/frame/utils.js | 18 +- addon-sdk/source/lib/sdk/lang/weak-set.js | 56 ++ addon-sdk/source/lib/sdk/page-mod.js | 99 +--- .../source/lib/sdk/page-mod/match-pattern.js | 9 +- addon-sdk/source/lib/sdk/panel.js | 559 ++++++------------ addon-sdk/source/lib/sdk/panel/events.js | 27 + addon-sdk/source/lib/sdk/panel/utils.js | 322 ++++++++++ addon-sdk/source/lib/sdk/request.js | 2 + addon-sdk/source/lib/sdk/stylesheet/style.js | 83 +++ addon-sdk/source/lib/sdk/stylesheet/utils.js | 79 +++ .../source/lib/sdk/system/environment.js | 5 +- addon-sdk/source/lib/sdk/util/array.js | 12 + addon-sdk/source/lib/sdk/util/contract.js | 51 ++ addon-sdk/source/lib/sdk/view/core.js | 20 - addon-sdk/source/lib/sdk/window/utils.js | 18 + addon-sdk/source/lib/sdk/worker/utils.js | 76 +++ .../cuddlefish/mobile-utils/bootstrap.js | 2 +- .../addons/require/packages/panel/main.js | 5 - .../require/packages/panel/package.json | 3 - .../addons/require/packages/panel/page-mod.js | 5 - addon-sdk/source/test/addons/require/panel.js | 5 - .../source/test/tabs/test-firefox-tabs.js | 46 +- addon-sdk/source/test/test-content-loader.js | 10 - addon-sdk/source/test/test-disposable.js | 25 + addon-sdk/source/test/test-environment.js | 4 +- addon-sdk/source/test/test-match-pattern.js | 10 + addon-sdk/source/test/test-page-mod.js | 10 +- addon-sdk/source/test/test-panel.js | 17 +- addon-sdk/source/test/test-request.js | 47 +- addon-sdk/source/test/test-selection.js | 24 +- addon-sdk/source/test/test-system-events.js | 2 +- addon-sdk/source/test/test-url.js | 6 + 51 files changed, 1698 insertions(+), 695 deletions(-) create mode 100644 addon-sdk/source/doc/module-source/sdk/content/mod.md create mode 100644 addon-sdk/source/doc/module-source/sdk/stylesheet/style.md create mode 100644 addon-sdk/source/doc/module-source/sdk/stylesheet/utils.md create mode 100644 addon-sdk/source/doc/module-source/sdk/util/array.md create mode 100644 addon-sdk/source/doc/module-source/sdk/util/object.md mode change 100644 => 100755 addon-sdk/source/examples/library-detector/data/icons/mootools.png create mode 100644 addon-sdk/source/lib/sdk/content/mod.js create mode 100644 addon-sdk/source/lib/sdk/lang/weak-set.js create mode 100644 addon-sdk/source/lib/sdk/panel/events.js create mode 100644 addon-sdk/source/lib/sdk/panel/utils.js create mode 100644 addon-sdk/source/lib/sdk/stylesheet/style.js create mode 100644 addon-sdk/source/lib/sdk/stylesheet/utils.js create mode 100644 addon-sdk/source/lib/sdk/util/contract.js create mode 100644 addon-sdk/source/lib/sdk/worker/utils.js delete mode 100644 addon-sdk/source/test/addons/require/packages/panel/main.js delete mode 100644 addon-sdk/source/test/addons/require/packages/panel/package.json delete mode 100644 addon-sdk/source/test/addons/require/packages/panel/page-mod.js delete mode 100644 addon-sdk/source/test/addons/require/panel.js diff --git a/addon-sdk/source/doc/dev-guide-source/credits.md b/addon-sdk/source/doc/dev-guide-source/credits.md index 0b588ab9c5b4..a02758b90158 100644 --- a/addon-sdk/source/doc/dev-guide-source/credits.md +++ b/addon-sdk/source/doc/dev-guide-source/credits.md @@ -14,8 +14,6 @@ We'd like to thank our many Jetpack project contributors! They include: * [Heather Arthur](https://github.com/harthur) * Dietrich Ayala - - ### B ### * [Romain B](https://github.com/Niamor) @@ -27,18 +25,15 @@ We'd like to thank our many Jetpack project contributors! They include: * Daniel Buchner * James Burke - - ### C ### * [Shane Caraveo](https://github.com/mixedpuppy) * [Matěj Cepl](https://github.com/mcepl) * Marc Chevrier +* [Timothy Guan-tin Chien](https://github.com/timdream) * Hernán Rodriguez Colmeiro * [David Creswick](https://github.com/dcrewi) - - ### D ### * dexter @@ -46,15 +41,11 @@ We'd like to thank our many Jetpack project contributors! They include: * Connor Dunn * dynamis - - ### F ### -* [Matteo Ferretti (ZER0)](https://github.com/ZER0) +* [Matteo Ferretti](https://github.com/ZER0) * fuzzykiller - - ### G ### * [Marcio Galli](https://github.com/taboca) @@ -65,8 +56,6 @@ We'd like to thank our many Jetpack project contributors! They include: * Jeff Griffiths * [David Guo](https://github.com/dglol) - - ### H ### * Mark Hammond @@ -74,20 +63,14 @@ We'd like to thank our many Jetpack project contributors! They include: * Lloyd Hilaiel * Bobby Holley - - ### I ### * Shun Ikejima - - ### J ### * Eric H. Jung - - ### K ### * Hrishikesh Kale @@ -95,15 +78,11 @@ We'd like to thank our many Jetpack project contributors! They include: * Lajos Koszti * [Vladimir Kukushkin](https://github.com/kukushechkin) - - ### L ### * Edward Lee * Gregg Lind - - ### M ### * [Nils Maier](https://github.com/nmaier) @@ -113,8 +92,6 @@ We'd like to thank our many Jetpack project contributors! They include: * Zandr Milewski * Noelle Murata - - ### N ### * Siavash Askari Nasr @@ -122,16 +99,12 @@ We'd like to thank our many Jetpack project contributors! They include: * Dương H. Nguyễn * Nick Nguyen - - ### O ### * [ongaeshi](https://github.com/ongaeshi) * Paul O’Shannessy * Les Orchard - - ### P ### * Robert Pankowecki @@ -139,16 +112,13 @@ We'd like to thank our many Jetpack project contributors! They include: * [Alexandre Poirot](https://github.com/ochameau) * Nickolay Ponomarev - - ### R ### * Aza Raskin - - ### S ### +* [Jordan Santell](https://github.com/jsantell) * Till Schneidereit * Justin Scott * Ayan Shah @@ -160,8 +130,6 @@ We'd like to thank our many Jetpack project contributors! They include: * [J. Ryan Stinnett](https://github.com/jryans) * [Mihai Sucan](https://github.com/mihaisucan) - - ### T ### * taku0 @@ -171,8 +139,6 @@ We'd like to thank our many Jetpack project contributors! They include: * Dave Townsend * [Matthias Tylkowski](https://github.com/tylkomat) - - ### V ### * Peter Van der Beken @@ -181,8 +147,6 @@ We'd like to thank our many Jetpack project contributors! They include: * [Erik Vold](https://github.com/erikvold) * Vladimir Vukicevic - - ### W ### * Brian Warner @@ -191,8 +155,6 @@ We'd like to thank our many Jetpack project contributors! They include: * Blake Winton * Michal Wojciechowski - - ### Z ### * Piotr Zalewa diff --git a/addon-sdk/source/doc/module-source/sdk/content/mod.md b/addon-sdk/source/doc/module-source/sdk/content/mod.md new file mode 100644 index 000000000000..c1454db9fe24 --- /dev/null +++ b/addon-sdk/source/doc/module-source/sdk/content/mod.md @@ -0,0 +1,105 @@ + + +The `mod` module provides functions to modify a page content. + + +@function + Function applies given `modification` to a given `window`. + + For example, the following code applies a style to a content window, adding + a border to all divs in page: + + var attachTo = require("sdk/content/mod").attachTo; + var Style = require("sdk/stylesheet/style").Style; + + var style = Style({ + source: "div { border: 4px solid gray }" + }); + + // assuming window points to the content page we want to modify + attachTo(style, window); + +@param modification {object} + The modification we want to apply to the target. + +@param window {nsIDOMWindow} + The window to be modified. + + + +@function + Function removes attached `modification` from a given `window`. + If `window` is not specified, `modification` is removed from all the windows + it's being attached to. + + For example, the following code applies and removes a style to a content + window, adding a border to all divs in page: + + var { attachTo, detachFrom } = require("sdk/content/mod"); + var Style = require("sdk/stylesheet/style").Style; + + var style = Style({ + source: "div { border: 4px solid gray }" + }); + + // assuming window points to the content page we want to modify + attachTo(style, window); + // ... + detachFrom(style, window); + +@param modification {object} + The modification we want to remove from the target + +@param window {nsIDOMWindow} + The window to be modified. + If `window` is not provided `modification` is removed from all targets it's + being attached to. + + + +@function + Function takes `target`, value representing content (page) and returns + `nsIDOMWindow` for that content. + If `target` does not represents valid content `null` is returned. + For example target can be a content window itself in which case it's will be + returned back. + +@param target {object} + The object for which we want to obtain the window represented or contained. + If a `nsIDOMWindow` is given, it works as an identify function, returns + `target` itself. +@returns {nsIDOMWindow|null} + The window represented or contained by the `target`, if any. Returns `null` + otherwise. + + + +@function + Function applies given `modification` to a given `target` representing a + content to be modified. + +@param modification {object} + The modification we want to apply to the target + +@param target {object} + Target is a value that representing content to be modified. It is valid only + when `getTargetWindow(target)` returns nsIDOMWindow of content it represents. + + + +@function + Function removes attached `modification`. If `target` is specified + `modification` is removed from that `target` only, otherwise `modification` is + removed from all the targets it's being attached to. + +@param modification {object} + The modification we want to remove from the target + +@param target {object} + Target is a value that representing content to be modified. It is valid only + when `getTargetWindow(target)` returns `nsIDOMWindow` of content it represents. + If `target` is not provided `modification` is removed from all targets it's + being attached to. + diff --git a/addon-sdk/source/doc/module-source/sdk/panel.md b/addon-sdk/source/doc/module-source/sdk/panel.md index 0c4cdf98a8d9..c4fecb2f82f7 100644 --- a/addon-sdk/source/doc/module-source/sdk/panel.md +++ b/addon-sdk/source/doc/module-source/sdk/panel.md @@ -425,7 +425,7 @@ Creates a panel. Set to `false` to prevent taking the focus away when the panel is shown. Only turn this off if necessary, to prevent accessibility issue. Optional, default to `true`. - @prop [contentURL] {string} + @prop [contentURL] {string,URL} The URL of the content to load in the panel. @prop [allow] {object} An optional object describing permissions for the content. It should diff --git a/addon-sdk/source/doc/module-source/sdk/platform/xpcom.md b/addon-sdk/source/doc/module-source/sdk/platform/xpcom.md index 196a8c680c84..96797cb11287 100644 --- a/addon-sdk/source/doc/module-source/sdk/platform/xpcom.md +++ b/addon-sdk/source/doc/module-source/sdk/platform/xpcom.md @@ -48,7 +48,7 @@ interface to listen for and log all topic notifications: observerService.addObserver(this, this.topic, false); }, unregister: function() { - addObserver.removeObserver(this, this.topic, false); + addObserver.removeObserver(this, this.topic); }, observe: function observe(subject, topic, data) { console.log('star observer:', subject, topic, data); diff --git a/addon-sdk/source/doc/module-source/sdk/request.md b/addon-sdk/source/doc/module-source/sdk/request.md index 3b551951b8d1..cc439caec69d 100644 --- a/addon-sdk/source/doc/module-source/sdk/request.md +++ b/addon-sdk/source/doc/module-source/sdk/request.md @@ -62,8 +62,8 @@ This constructor creates a request object that can be used to make network requests. The constructor takes a single parameter `options` which is used to set several properties on the resulting `Request`. @param options {object} - @prop url {string} - This is the url to which the request will be made. + @prop [url] {string,url} + This is the url to which the request will be made. Can either be a [String](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String) or an instance of the SDK's [URL](https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/url.html#URL). @prop [onComplete] {function} This function will be called when the request has received a response (or in diff --git a/addon-sdk/source/doc/module-source/sdk/stylesheet/style.md b/addon-sdk/source/doc/module-source/sdk/stylesheet/style.md new file mode 100644 index 000000000000..3a860a3430eb --- /dev/null +++ b/addon-sdk/source/doc/module-source/sdk/stylesheet/style.md @@ -0,0 +1,53 @@ + + +Module provides `Style` function that can be used to construct content style +modification via stylesheet files or CSS rules. + + +@class + +@constructor + The Style constructor creates an object that represents style modifications + via stylesheet file(s) or/and CSS rules. Stylesheet file URL(s) are verified + to be local to an add-on, while CSS rules are virified to be a string or + array of strings. + + The style created can be applied to a content by calling `attach`, + and removed using `detach`. Those functions are part of [content/mod](modules/sdk/content/mod.html) module. +@param options {object} + Options for the style. All these options are optional. Although if you + don't supply any stylesheet or CSS rules, your style won't be very useful. + + @prop uri {string,array} + A string, or an array of strings, that represents local URI to stylesheet. + @prop source {string,array} + A string, or an array of strings, that contains CSS rules. Those rules + are applied after the rules in the stylesheet specified with `uri` options, + if provided. + @prop [type="author"] {string} + The type of the sheet. It accepts the following values: `"agent"`, `"user"` + and `"author"`. + If not provided, the default value is `"author"`. + + + +@property {string} + An array of strings that contains the CSS rule(s) specified in the constructor's + option; `null` if no `source` option was given to the constructor. + This property is read-only. + + +@property {string} + An array of strings that contains the stylesheet local URI(s) specified in the + constructor's option; `null` if no `uri` option was given to the + constructor. + This property is read-only. + + + @property {string} + The type of the sheet. If no type is provided in constructor's option, + it returns the default value, `"author"`. This property is read-only. + + diff --git a/addon-sdk/source/doc/module-source/sdk/stylesheet/utils.md b/addon-sdk/source/doc/module-source/sdk/stylesheet/utils.md new file mode 100644 index 000000000000..d9740c36a1b3 --- /dev/null +++ b/addon-sdk/source/doc/module-source/sdk/stylesheet/utils.md @@ -0,0 +1,41 @@ + + +Module provides helper functions for working with stylesheets. + + + @function + Synchronously loads a style sheet from `uri` and adds it to the list of + additional style sheets of the document. + The sheets added takes effect immediately, and only on the document of the + `window` given. + @param window {nsIDOMWindow} + @param uri {string, nsIURI} + @param [type="author"] {string} + The type of the sheet. It accepts the following values: `"agent"`, `"user"` + and `"author"`. + If not provided, the default value is `"author"`. + + + + @function + Remove the document style sheet at `sheetURI` from the list of additional + style sheets of the document. The removal takes effect immediately. + @param window {nsIDOMWindow} + @param uri {string, nsIURI} + @param [type="author"] {string} + The type of the sheet. It accepts the following values: `"agent"`, `"user"` + and `"author"`. + If not provided, the default value is `"author"`. + + + + @function + Verifies that the `type` given is a valid stylesheet's type. + The values considered valid are: `"agent"`, `"user"` and `"author"`. + @param type {string} + The type of the sheet. + @returns {boolean} + `true` if the `type` given is valid, otherwise `false`. + diff --git a/addon-sdk/source/doc/module-source/sdk/url.md b/addon-sdk/source/doc/module-source/sdk/url.md index f124acd0926c..f475a36258a2 100644 --- a/addon-sdk/source/doc/module-source/sdk/url.md +++ b/addon-sdk/source/doc/module-source/sdk/url.md @@ -83,6 +83,17 @@ The `url` module provides functionality for the parsing and retrieving of URLs. The converted URL as a string. + +@function + Checks the validity of a URI. `isValidURI("http://mozilla.org")` would return `true`, whereas `isValidURI("mozilla.org")` would return `false`. + +@param uri {string} + The URI, as a string, to be tested. + +@returns {boolean} + A boolean indicating whether or not the URI is valid. + + @class diff --git a/addon-sdk/source/doc/module-source/sdk/util/array.md b/addon-sdk/source/doc/module-source/sdk/util/array.md new file mode 100644 index 000000000000..bb6384fbaa75 --- /dev/null +++ b/addon-sdk/source/doc/module-source/sdk/util/array.md @@ -0,0 +1,156 @@ + + +The `util/array` module provides simple helper functions for working with +arrays. + + +@function +Returns `true` if the given [`Array`](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array) contains the element or `false` otherwise. +A simplified version of `array.indexOf(element) >= 0`. + + let { has } = require('sdk/util/array'); + let a = ['rock', 'roll', 100]; + + has(a, 'rock'); // true + has(a, 'rush'); // false + has(a, 100); // true + +@param array {array} + The array to search. + +@param element {*} + The element to search for in the array. + +@returns {boolean} + A boolean indicating whether or not the element was found in the array. + + + +@function +Returns `true` if the given [`Array`](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array) contains any of the elements in the +`elements` array, or `false` otherwise. + + let { hasAny } = require('sdk/util/array'); + let a = ['rock', 'roll', 100]; + + hasAny(a, ['rock', 'bright', 'light']); // true + hasAny(a, ['rush', 'coil', 'jet']); // false + +@param array {array} + The array to search for elements. + +@param elements {array} + An array of elements to search for in `array`. + +@returns {boolean} + A boolean indicating whether or not any of the elements were found + in the array. + + + +@function +If the given [array](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array) does not already contain the given element, this function +adds the element to the array and returns `true`. Otherwise, this function +does not add the element and returns `false`. + + let { add } = require('sdk/util/array'); + let a = ['alice', 'bob', 'carol']; + + add(a, 'dave'); // true + add(a, 'dave'); // false + add(a, 'alice'); // false + + console.log(a); // ['alice', 'bob', 'carol', 'dave'] + +@param array {array} + The array to add the element to. + +@param element {*} + The element to add + +@returns {boolean} + A boolean indicating whether or not the element was added to the array. + + + +@function +If the given [array](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array) contains the given element, this function +removes the element from the array and returns `true`. Otherwise, this function +does not alter the array and returns `false`. + + let { remove } = require('sdk/util/array'); + let a = ['alice', 'bob', 'carol']; + + remove(a, 'dave'); // false + remove(a, 'bob'); // true + remove(a, 'bob'); // false + + console.log(a); // ['alice', 'carol'] + +@param array {array} + The array to remove the element from. + +@param element {*} + The element to remove from the array if it contains it. + +@returns {boolean} + A boolean indicating whether or not the element was removed from the array. + + + +@function +Produces a duplicate-free version of the given [array](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array). + + let { unique } = require('sdk/util/array'); + let a = [1, 1, 1, 1, 3, 4, 7, 7, 10, 10, 10, 10]; + let b = unique(a); + + console.log(a); // [1, 1, 1, 1, 3, 4, 7, 7, 10, 10, 10, 10]; + console.log(b); // [1, 3, 4, 7, 10]; + +@param array {array} + Source array. + +@returns {array} + The new, duplicate-free array. + + + +@function +Flattens a nested [array](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array) of any depth. + + let { flatten } = require('sdk/util/array'); + let a = ['cut', ['ice', ['fire']], 'elec']; + let b = flatten(a); + + console.log(a); // ['cut', ['ice', ['fire']], 'elec'] + console.log(b); // ['cut', 'ice', 'fire', 'elec']; + +@param array {array} + The array to flatten. + +@returns {array} + The new, flattened array. + + + +@function +Iterates over an [iterator](https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Iterators_and_Generators#Iterators) and returns the results as an [array](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array). + + let { fromIterator } = require('sdk/util/array'); + let i = new Set(); + i.add('otoro'); + i.add('unagi'); + i.add('keon'); + + fromIterator(i) // ['otoro', 'unagi', 'keon'] + +@param iterator {iterator} + The [`Iterator`](https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Iterators_and_Generators#Iterators) object over which to iterate and place results into an array. + +@returns {array} + The iterator's results in an array. + + diff --git a/addon-sdk/source/doc/module-source/sdk/util/object.md b/addon-sdk/source/doc/module-source/sdk/util/object.md new file mode 100644 index 000000000000..19380a2b95d8 --- /dev/null +++ b/addon-sdk/source/doc/module-source/sdk/util/object.md @@ -0,0 +1,75 @@ + + +The `util/object` module provides simple helper functions for working with +objects. + + +@function +Merges all of the properties of all arguments into the first argument. If +two or more argument objects have properties with the same name, the +property is overwritten with precedence from right to left, implying that +properties of the object on the left are overwritten by a same named property +of an object on the right. Properties are merged with [descriptors](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/defineProperty#Description) onto the source object. + +Any argument given with "falsy" values (null, undefined) in case of objects +are skipped. + + let { merge } = require("sdk/util/object"); + var a = { jetpacks: "are yes", foo: 10 } + var b = merge(a, { foo: 5, bar: 6 }, { foo: 50, location: "SF" }); + + b === a // true + b.jetpacks // "are yes" + b.foo // 50 + b.bar // 6 + b.location // "SF" + + // Merge also translates property descriptors + var c = { "type": "addon" }; + var d = {}; + Object.defineProperty(d, "name", { + value: "jetpacks", + configurable: false + }); + merge(c, d); + var props = Object.getOwnPropertyDescriptor(c, "name"); + console.log(props.configurable); // true + +@param source {object} + The object that other properties are merged into. + +@param arguments {object} + `n` amount of additional objects that are merged into `source` object. + +@returns {object} + The `source` object. + + + +@function +Returns an object that inherits from the first argument and contains +all of the properties from all following arguments, with precedence from +right to left. + +`extend(source1, source2, source3)` is the equivalent of +`merge(Object.create(source1), source2, source3)`. + + let { extend } = require("sdk/util/object"); + var a = { alpha: "a" }; + var b = { beta: "b" }; + var g = { gamma: "g", alpha: null }; + var x = extend(a, b, g); + + console.log(a); // { alpha: "a" } + console.log(b); // { beta: "b" } + console.log(g); // { gamma: "g", alpha: null } + console.log(x); // { alpha: null, beta: "b", gamma: "g" } + +@param arguments {object} + `n` arguments that get merged into a new object. + +@returns {object} + The new, merged object. + diff --git a/addon-sdk/source/examples/library-detector/data/icons/mootools.png b/addon-sdk/source/examples/library-detector/data/icons/mootools.png old mode 100644 new mode 100755 diff --git a/addon-sdk/source/lib/sdk/addon/window.js b/addon-sdk/source/lib/sdk/addon/window.js index 9bfd61658538..6a3b16163b31 100644 --- a/addon-sdk/source/lib/sdk/addon/window.js +++ b/addon-sdk/source/lib/sdk/addon/window.js @@ -54,4 +54,7 @@ exports.ready = promise; exports.window = window; // Still close window on unload to claim memory back early. -unload(function() { window.close() }); +unload(function() { + window.close() + frame.parentNode.removeChild(frame); +}); diff --git a/addon-sdk/source/lib/sdk/content/loader.js b/addon-sdk/source/lib/sdk/content/loader.js index b01675c3e8fc..c5a97dd79cdb 100644 --- a/addon-sdk/source/lib/sdk/content/loader.js +++ b/addon-sdk/source/lib/sdk/content/loader.js @@ -12,27 +12,24 @@ module.metadata = { const { EventEmitter } = require('../deprecated/events'); const { validateOptions } = require('../deprecated/api-utils'); -const { URL } = require('../url'); +const { isValidURI, URL } = require('../url'); const file = require('../io/file'); +const { contract } = require('../util/contract'); const LOCAL_URI_SCHEMES = ['resource', 'data']; // Returns `null` if `value` is `null` or `undefined`, otherwise `value`. -function ensureNull(value) { - return value == null ? null : value; -} +function ensureNull(value) value == null ? null : value // map of property validations const valid = { contentURL: { - ok: function (value) { - try { - URL(value); - } - catch(e) { - return false; - } - return true; + map: function(url) !url ? ensureNull(url) : url.toString(), + is: ['undefined', 'null', 'string'], + ok: function (url) { + if (url === null) + return true; + return isValidURI(url); }, msg: 'The `contentURL` option must be a valid URL.' }, @@ -202,3 +199,5 @@ const Loader = EventEmitter.compose({ _contentScript: null }); exports.Loader = Loader; + +exports.contract = contract(valid); diff --git a/addon-sdk/source/lib/sdk/content/mod.js b/addon-sdk/source/lib/sdk/content/mod.js new file mode 100644 index 000000000000..c2a204ffa1e4 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/mod.js @@ -0,0 +1,60 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +const { Ci } = require("chrome"); +const method = require("method/core"); +const { add, remove, iterator } = require("../lang/weak-set"); + +let getTargetWindow = method("getTargetWindow"); + +getTargetWindow.define(function (target) { + if (target instanceof Ci.nsIDOMWindow) + return target; + if (target instanceof Ci.nsIDOMDocument) + return target.defaultView || null; + + return null; +}); + +exports.getTargetWindow = getTargetWindow; + +let attachTo = method("attachTo"); +exports.attachTo = attachTo; + +let detachFrom = method("detatchFrom"); +exports.detachFrom = detachFrom; + +function attach(modification, target) { + let window = getTargetWindow(target); + + attachTo(modification, window); + + // modification are stored per content; `window` reference can still be the + // same even if the content is changed, therefore `document` is used instead. + add(modification, window.document); +} +exports.attach = attach; + +function detach(modification, target) { + if (target) { + let window = getTargetWindow(target); + detachFrom(modification, window); + remove(modification, window.document); + } + else { + let documents = iterator(modification); + for (let document of documents) { + detachFrom(modification, document.defaultView); + remove(modification, document); + } + } +} +exports.detach = detach; diff --git a/addon-sdk/source/lib/sdk/content/worker.js b/addon-sdk/source/lib/sdk/content/worker.js index bf8ccb25a615..ef1cd4fe955d 100644 --- a/addon-sdk/source/lib/sdk/content/worker.js +++ b/addon-sdk/source/lib/sdk/content/worker.js @@ -463,48 +463,55 @@ const Worker = EventEmitter.compose({ constructor: function Worker(options) { options = options || {}; - if ('window' in options) - this._window = options.window; if ('contentScriptFile' in options) this.contentScriptFile = options.contentScriptFile; if ('contentScriptOptions' in options) this.contentScriptOptions = options.contentScriptOptions; if ('contentScript' in options) this.contentScript = options.contentScript; - if ('onError' in options) - this.on('error', options.onError); - if ('onMessage' in options) - this.on('message', options.onMessage); - if ('onDetach' in options) - this.on('detach', options.onDetach); + + this._setListeners(options); // 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; + unload.ensure(this._public, "destroy"); + + // Ensure that worker._port is initialized for contentWorker to be able + // to send events during worker initialization. + this.port; + + this._documentUnload = this._documentUnload.bind(this); + this._pageShow = this._pageShow.bind(this); + this._pageHide = this._pageHide.bind(this); + + if ("window" in options) this._attach(options.window); + }, + + _setListeners: function(options) { + 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); + }, + + _attach: function(window) { + this._window = window; // Track document unload to destroy this worker. // We can't watch for unload event on page's window object as it // prevents bfcache from working: // https://developer.mozilla.org/En/Working_with_BFCache this._windowID = getInnerId(this._window); - observers.add("inner-window-destroyed", - this._documentUnload = this._documentUnload.bind(this)); + observers.add("inner-window-destroyed", this._documentUnload); // Listen to pagehide event in order to freeze the content script // while the document is frozen in bfcache: - this._window.addEventListener("pageshow", - this._pageShow = this._pageShow.bind(this), - true); - this._window.addEventListener("pagehide", - this._pageHide = this._pageHide.bind(this), - true); - - unload.ensure(this._public, "destroy"); - - // Ensure that worker._port is initialized for contentWorker to be able - // to send use event during WorkerSandbox(this) - this.port; + this._window.addEventListener("pageshow", this._pageShow, true); + this._window.addEventListener("pagehide", this._pageHide, true); // will set this._contentWorker pointing to the private API: this._contentWorker = WorkerSandbox(this); diff --git a/addon-sdk/source/lib/sdk/core/disposable.js b/addon-sdk/source/lib/sdk/core/disposable.js index fc6b1f6f3b21..a15e2d72926a 100644 --- a/addon-sdk/source/lib/sdk/core/disposable.js +++ b/addon-sdk/source/lib/sdk/core/disposable.js @@ -13,33 +13,49 @@ let { Class } = require("./heritage"); let { on, off } = require('../system/events'); let unloadSubject = require('@loader/unload'); -function DisposeHandler(disposable) { - return function onDisposal({subject}) { - if (subject.wrappedJSObject === unloadSubject) { - off("sdk:loader:destroy", onDisposal); - disposable.destroy(); +let disposables = WeakMap(); + +function initialize(instance) { + // Create an event handler that will dispose instance on unload. + function handler(event) { + if (event.subject.wrappedJSObject === unloadSubject) { + dispose(instance); + instance.dispose(); } } + + // Form weak reference between disposable instance and an unload event + // handler associated with it. This will make sure that event handler can't + // be garbage collected as long as instance is referenced. Also note that + // system events intentionally hold weak reference to an event handler, this + // will let GC claim both instance and an unload handler before actual add-on + // unload if instance contains no other references. + disposables.set(instance, handler); + on("sdk:loader:destroy", handler); } +exports.initialize = initialize; + +function dispose(instance) { + // Disposes given instance by removing it from weak map so that handler can + // be GC-ed even if references to instance are kept. Also unregister unload + // handler. + + let handler = disposables.get(instance); + if (handler) off("sdk:loader:destroy", handler); + disposables.delete(instance); +} +exports.dispose = dispose; // Base type that takes care of disposing it's instances on add-on unload. // Also makes sure to remove unload listener if it's already being disposed. let Disposable = Class({ - initialize: function dispose() { - this.setupDisposal(); + initialize: function setupDisposable() { + // First setup instance before initializing it's disposal. If instance + // fails to initialize then there is no instance to be disposed at the + // unload. this.setup.apply(this, arguments); + initialize(this); }, - setupDisposal: function setupDisposal() { - // Create `onDisposal` handler that will be invoked on unload of - // the add-on, unless this is destroyed earlier. - Object.defineProperty(this, "onDisposal", { value: DisposeHandler(this) }); - on("sdk:loader:destroy", this.onDisposal); - }, - teardownDisposable: function tearDisposal() { - // Removes `onDisposal` handler so that it won't be invoked on unload. - off("sdk:loader:destroy", this.onDisposal); - }, - setup: function setup() { // Implement your initialize logic here. }, @@ -50,9 +66,8 @@ let Disposable = Class({ destroy: function destroy() { // Destroying disposable removes unload handler so that attempt to dispose // won't be made at unload & delegates to dispose. - this.teardownDisposable(); + dispose(this); this.dispose(); } }); - exports.Disposable = Disposable; diff --git a/addon-sdk/source/lib/sdk/event/core.js b/addon-sdk/source/lib/sdk/event/core.js index 2ff037f8e38e..7908bf43fee1 100644 --- a/addon-sdk/source/lib/sdk/event/core.js +++ b/addon-sdk/source/lib/sdk/event/core.js @@ -17,6 +17,8 @@ const { ns } = require('../core/namespace'); const event = ns(); +const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/; + // Utility function to access given event `target` object's event listeners for // the specific event `type`. If listeners for this type does not exists they // will be created. @@ -158,3 +160,26 @@ function count(target, type) { return observers(target, type).length; } exports.count = count; + +/** + * Registers listeners on the given event `target` from the given `listeners` + * dictionary. Iterates over the listeners and if property name matches name + * pattern `onEventType` and property is a function, then registers it as + * an `eventType` listener on `target`. + * + * @param {Object} target + * The type of event. + * @param {Object} listeners + * Dictionary of listeners. + */ +function setListeners(target, listeners) { + Object.keys(listeners || {}).forEach(function onEach(key) { + let match = EVENT_TYPE_PATTERN.exec(key); + let type = match && match[1].toLowerCase(); + let listener = listeners[key]; + + if (type && typeof(listener) === 'function') + on(target, type, listener); + }); +} +exports.setListeners = setListeners; diff --git a/addon-sdk/source/lib/sdk/event/target.js b/addon-sdk/source/lib/sdk/event/target.js index d970927600f2..9d2ca9ec5e4c 100644 --- a/addon-sdk/source/lib/sdk/event/target.js +++ b/addon-sdk/source/lib/sdk/event/target.js @@ -10,35 +10,28 @@ module.metadata = { "stability": "stable" }; -const { on, once, off } = require('./core'); +const { on, once, off, setListeners } = require('./core'); const { method } = require('../lang/functional'); const { Class } = require('../core/heritage'); -const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/; - /** * `EventTarget` is an exemplar for creating an objects that can be used to * add / remove event listeners on them. Events on these objects may be emitted * via `emit` function exported by 'event/core' module. */ const EventTarget = Class({ + /** + * Method initializes `this` event source. It goes through properties of a + * given `options` and registers listeners for the ones that look like an + * event listeners. + */ /** * Method initializes `this` event source. It goes through properties of a * given `options` and registers listeners for the ones that look like an * event listeners. */ initialize: function initialize(options) { - options = options || {}; - // Go through each property and registers event listeners for those - // that have a name matching following pattern (`onEventType`). - Object.keys(options).forEach(function onEach(key) { - let match = EVENT_TYPE_PATTERN.exec(key); - let type = match && match[1].toLowerCase(); - let listener = options[key]; - - if (type && typeof(listener) === 'function') - this.on(type, listener); - }, this); + setListeners(this, options); }, /** * Registers an event `listener` that is called every time events of diff --git a/addon-sdk/source/lib/sdk/event/utils.js b/addon-sdk/source/lib/sdk/event/utils.js index 2ad11db401ae..1603c0eabd6f 100644 --- a/addon-sdk/source/lib/sdk/event/utils.js +++ b/addon-sdk/source/lib/sdk/event/utils.js @@ -99,3 +99,6 @@ exports.merge = merge; function expand(f, inputs) merge(map(f, inputs)) exports.expand = expand; + +function pipe(from, to) on(from, "*", emit.bind(emit, to)) +exports.pipe = pipe; diff --git a/addon-sdk/source/lib/sdk/frame/utils.js b/addon-sdk/source/lib/sdk/frame/utils.js index 1ae3d888b012..d5c2a3c4ae41 100644 --- a/addon-sdk/source/lib/sdk/frame/utils.js +++ b/addon-sdk/source/lib/sdk/frame/utils.js @@ -40,12 +40,16 @@ exports.getDocShell = getDocShell; * @params {Boolean} options.allowPlugins * Whether to allow plugin execution. Defaults to `false`. */ -function create(document, options) { +function create(target, options) { + target = target instanceof Ci.nsIDOMDocument ? target.documentElement : + target instanceof Ci.nsIDOMWindow ? target.document.documentElement : + target; options = options || {}; let remote = options.remote || false; let namespaceURI = options.namespaceURI || XUL; let isXUL = namespaceURI === XUL; let nodeName = isXUL && options.browser ? 'browser' : 'iframe'; + let document = target.ownerDocument; let frame = document.createElementNS(namespaceURI, nodeName); // Type="content" is mandatory to enable stuff here: @@ -53,7 +57,7 @@ function create(document, options) { frame.setAttribute('type', options.type || 'content'); frame.setAttribute('src', options.uri || 'about:blank'); - document.documentElement.appendChild(frame); + target.appendChild(frame); // Load in separate process if `options.remote` is `true`. // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1347 @@ -79,8 +83,18 @@ function create(document, options) { docShell.allowAuth = options.allowAuth || false; docShell.allowJavascript = options.allowJavascript || false; docShell.allowPlugins = options.allowPlugins || false; + + // Control whether the document can move/resize the window. Requires + // recently added platform capability, so we test to avoid exceptions + // in cases where capability is not present yet. + if ("allowWindowControl" in docShell && "allowWindowControl" in options) + docShell.allowWindowControl = !!options.allowWindowControl; } return frame; } exports.create = create; + +function swapFrameLoaders(from, to) + from.QueryInterface(Ci.nsIFrameLoaderOwner).swapFrameLoaders(to) +exports.swapFrameLoaders = swapFrameLoaders; diff --git a/addon-sdk/source/lib/sdk/lang/weak-set.js b/addon-sdk/source/lib/sdk/lang/weak-set.js new file mode 100644 index 000000000000..95aee9e9620e --- /dev/null +++ b/addon-sdk/source/lib/sdk/lang/weak-set.js @@ -0,0 +1,56 @@ +"use strict"; + +const { Cu } = require("chrome"); + +function makeGetterFor(Type) { + let cache = new WeakMap(); + + return function getFor(target) { + if (!cache.has(target)) + cache.set(target, new Type()); + + return cache.get(target); + } +} + +let getLookupFor = makeGetterFor(WeakMap); +let getRefsFor = makeGetterFor(Set); + +function add(target, value) { + if (has(target, value)) + return; + + getLookupFor(target).set(value, true); + getRefsFor(target).add(Cu.getWeakReference(value)); +} +exports.add = add; + +function remove(target, value) { + getLookupFor(target).delete(value); +} +exports.remove = remove; + +function has(target, value) { + return getLookupFor(target).has(value); +} +exports.has = has; + +function clear(target) { + getLookupFor(target).clear(); + getRefsFor(target).clear(); +} +exports.clear = clear; + +function iterator(target) { + let refs = getRefsFor(target); + + for (let ref of refs) { + let value = ref.get(); + + if (has(target, value)) + yield value; + else + refs.delete(ref); + } +} +exports.iterator = iterator; diff --git a/addon-sdk/source/lib/sdk/page-mod.js b/addon-sdk/source/lib/sdk/page-mod.js index 816f7eaca99e..e0b1db37542c 100644 --- a/addon-sdk/source/lib/sdk/page-mod.js +++ b/addon-sdk/source/lib/sdk/page-mod.js @@ -26,14 +26,8 @@ const { getTabs, getTabContentWindow, getTabForContentWindow, getURI: getTabURI } = require('./tabs/utils'); const { has, hasAny } = require('./util/array'); const { ignoreWindow } = require('sdk/private-browsing/utils'); - -const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]. - getService(Ci.nsIStyleSheetService); - -const USER_SHEET = styleSheetService.USER_SHEET; - -const io = Cc['@mozilla.org/network/io-service;1']. - getService(Ci.nsIIOService); +const { Style } = require("./stylesheet/style"); +const { attach, detach } = require("./content/mod"); // Valid values for `attachTo` option const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame']; @@ -135,29 +129,16 @@ const PageMod = Loader.compose(EventEmitter, { else rules.add(include); - let styleRules = ""; - - if (contentStyleFile) - styleRules = [].concat(contentStyleFile).map(readURISync).join(""); - - if (contentStyle) - styleRules += [].concat(contentStyle).join(""); - - if (styleRules) { - this._onRuleUpdate = this._onRuleUpdate.bind(this); - - this._styleRules = styleRules; - - this._registerStyleSheet(); - rules.on('add', this._onRuleUpdate); - rules.on('remove', this._onRuleUpdate); + if (contentStyle || contentStyleFile) { + this._style = Style({ + uri: contentStyleFile, + source: contentStyle + }); } this.on('error', this._onUncaughtError = this._onUncaughtError.bind(this)); pageModManager.add(this._public); - this._loadingWindows = []; - // `_applyOnExistingDocuments` has to be called after `pageModManager.add()` // otherwise its calls to `_onContent` method won't do anything. if ('attachTo' in options && has(options.attachTo, 'existing')) @@ -166,20 +147,14 @@ const PageMod = Loader.compose(EventEmitter, { destroy: function destroy() { - this._unregisterStyleSheet(); - - this.include.removeListener('add', this._onRuleUpdate); - this.include.removeListener('remove', this._onRuleUpdate); + if (this._style) + detach(this._style); for each (let rule in this.include) this.include.remove(rule); pageModManager.remove(this._public); - this._loadingWindows = []; - }, - _loadingWindows: [], - _applyOnExistingDocuments: function _applyOnExistingDocuments() { let mod = this; // Returns true if the tab match one rule @@ -216,6 +191,9 @@ const PageMod = Loader.compose(EventEmitter, { if (!isTopDocument && !has(this.attachTo, "frame")) return; + if (this._style) + attach(this._style, window); + // Immediatly evaluate content script if the document state is already // matching contentScriptWhen expectations let state = window.document.readyState; @@ -261,59 +239,6 @@ const PageMod = Loader.compose(EventEmitter, { _onUncaughtError: function _onUncaughtError(e) { if (this._listeners('error').length == 1) console.exception(e); - }, - _onRuleUpdate: function _onRuleUpdate(){ - this._registerStyleSheet(); - }, - - _registerStyleSheet : function _registerStyleSheet() { - let rules = this.include; - let styleRules = this._styleRules; - - let documentRules = []; - - this._unregisterStyleSheet(); - - for each (let rule in rules) { - let pattern = RULES[rule]; - - if (!pattern) - continue; - - if (pattern.regexp) - documentRules.push("regexp(\"" + pattern.regexp.source + "\")"); - else if (pattern.exactURL) - documentRules.push("url(" + pattern.exactURL + ")"); - else if (pattern.domain) - documentRules.push("domain(" + pattern.domain + ")"); - else if (pattern.urlPrefix) - documentRules.push("url-prefix(" + pattern.urlPrefix + ")"); - else if (pattern.anyWebPage) - documentRules.push("regexp(\"^(https?|ftp)://.*?\")"); - } - - let uri = "data:text/css;charset=utf-8,"; - if (documentRules.length > 0) - uri += encodeURIComponent("@-moz-document " + - documentRules.join(",") + " {" + styleRules + "}"); - else - uri += encodeURIComponent(styleRules); - - this._registeredStyleURI = io.newURI(uri, null, null); - - styleSheetService.loadAndRegisterSheet( - this._registeredStyleURI, - USER_SHEET - ); - }, - - _unregisterStyleSheet : function () { - let uri = this._registeredStyleURI; - - if (uri && styleSheetService.sheetRegistered(uri, USER_SHEET)) - styleSheetService.unregisterSheet(uri, USER_SHEET); - - this._registeredStyleURI = null; } }); exports.PageMod = function(options) PageMod(options) diff --git a/addon-sdk/source/lib/sdk/page-mod/match-pattern.js b/addon-sdk/source/lib/sdk/page-mod/match-pattern.js index 5c42e66fdbf4..1b92b1456790 100644 --- a/addon-sdk/source/lib/sdk/page-mod/match-pattern.js +++ b/addon-sdk/source/lib/sdk/page-mod/match-pattern.js @@ -96,8 +96,15 @@ MatchPattern.prototype = { return true; if (this.exactURL && this.exactURL == urlStr) return true; + + // Tests the urlStr against domain and check if + // wildcard submitted (*.domain.com), it only allows + // subdomains (sub.domain.com) or from the root (http://domain.com) + // and reject non-matching domains (otherdomain.com) + // bug 856913 if (this.domain && url.host && - url.host.slice(-this.domain.length) == this.domain) + (url.host === this.domain || + url.host.slice(-this.domain.length - 1) === "." + this.domain)) return true; if (this.urlPrefix && 0 == urlStr.indexOf(this.urlPrefix)) return true; diff --git a/addon-sdk/source/lib/sdk/panel.js b/addon-sdk/source/lib/sdk/panel.js index c6081166786b..1eb3ebb71ba7 100644 --- a/addon-sdk/source/lib/sdk/panel.js +++ b/addon-sdk/source/lib/sdk/panel.js @@ -14,405 +14,238 @@ module.metadata = { const { Ci } = require("chrome"); const { validateOptions: valid } = require('./deprecated/api-utils'); -const { Symbiont } = require('./content/content'); -const { EventEmitter } = require('./deprecated/events'); const { setTimeout } = require('./timers'); -const { on, off, emit } = require('./system/events'); -const runtime = require('./system/runtime'); -const { getDocShell } = require("./frame/utils"); -const { getWindow } = require('./panel/window'); const { isPrivateBrowsingSupported } = require('./self'); const { isWindowPBSupported } = require('./private-browsing/utils'); -const { getNodeView } = require('./view/core'); +const { Class } = require("./core/heritage"); +const { merge } = require("./util/object"); +const { WorkerHost, Worker, detach, attach } = require("./worker/utils"); +const { Disposable } = require("./core/disposable"); +const { contract: loaderContract } = require("./content/loader"); +const { contract } = require("./util/contract"); +const { on, off, emit, setListeners } = require("./event/core"); +const { EventTarget } = require("./event/target"); +const domPanel = require("./panel/utils"); +const { events } = require("./panel/events"); +const systemEvents = require("./system/events"); +const { filter, pipe } = require("./event/utils"); +const { getNodeView } = require("./view/core"); -const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", - ON_SHOW = 'popupshown', - ON_HIDE = 'popuphidden', - validNumber = { is: ['number', 'undefined', 'null'] }, - validBoolean = { is: ['boolean', 'undefined', 'null'] }, - ADDON_ID = require('./self').id; - -if (isPrivateBrowsingSupported && isWindowPBSupported) { +if (isPrivateBrowsingSupported && isWindowPBSupported) throw Error('The panel module cannot be used with per-window private browsing at the moment, see Bug 816257'); + +let isArray = Array.isArray; +let assetsURI = require("./self").data.url(); + +function isAddonContent({ contentURL }) { + return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0; } -/** - * Emits show and hide events. - */ -const Panel = Symbiont.resolve({ - constructor: '_init', - _onInit: '_onSymbiontInit', - destroy: '_symbiontDestructor', - _documentUnload: '_workerDocumentUnload' -}).compose({ - _frame: Symbiont.required, - _init: Symbiont.required, - _onSymbiontInit: Symbiont.required, - _symbiontDestructor: Symbiont.required, - _emit: Symbiont.required, - on: Symbiont.required, - removeListener: Symbiont.required, +function hasContentScript({ contentScript, contentScriptFile }) { + return (isArray(contentScript) ? contentScript.length > 0 : + !!contentScript) || + (isArray(contentScriptFile) ? contentScriptFile.length > 0 : + !!contentScriptFile); +} - _inited: false, +function requiresAddonGlobal(model) { + return isAddonContent(model) && !hasContentScript(model); +} - /** - * If set to `true` frame loaders between xul panel frame and - * hidden frame are swapped. If set to `false` frame loaders are - * set back to normal. Setting the value that was already set will - * have no effect. - */ - set _frameLoadersSwapped(value) { - if (this.__frameLoadersSwapped == value) return; - this._frame.QueryInterface(Ci.nsIFrameLoaderOwner) - .swapFrameLoaders(this._viewFrame); - this.__frameLoadersSwapped = value; - }, - __frameLoadersSwapped: false, +function getAttachEventType(model) { + let when = model.contentScriptWhen; + return requiresAddonGlobal(model) ? "sdk-panel-content-changed" : + when === "start" ? "sdk-panel-content-changed" : + when === "end" ? "sdk-panel-document-loaded" : + when === "ready" ? "sdk-panel-content-loaded" : + null; +} - constructor: function Panel(options) { - this._onShow = this._onShow.bind(this); - this._onHide = this._onHide.bind(this); - this._onAnyPanelShow = this._onAnyPanelShow.bind(this); - on('sdk-panel-show', this._onAnyPanelShow); - this.on('inited', this._onSymbiontInit.bind(this)); - this.on('propertyChange', this._onChange.bind(this)); +let number = { is: ['number', 'undefined', 'null'] }; +let boolean = { is: ['boolean', 'undefined', 'null'] }; - options = options || {}; - if ('onShow' in options) - this.on('show', options.onShow); - if ('onHide' in options) - this.on('hide', options.onHide); - if ('width' in options) - this.width = options.width; - if ('height' in options) - this.height = options.height; - if ('contentURL' in options) - this.contentURL = options.contentURL; - if ('focus' in options) { - var value = options.focus; - var validatedValue = valid({ $: value }, { $: validBoolean }).$; - this._focus = - (typeof validatedValue == 'boolean') ? validatedValue : this._focus; +let panelContract = contract(merge({ + width: number, + height: number, + focus: boolean, +}, loaderContract.rules)); + + +function isDisposed(panel) !views.has(panel); + +let panels = new WeakMap(); +let models = new WeakMap(); +let views = new WeakMap(); +let workers = new WeakMap(); + +function viewFor(panel) views.get(panel) +exports.viewFor = viewFor; + +function modelFor(panel) models.get(panel) +function panelFor(view) panels.get(view) +function workerFor(panel) workers.get(panel) + +// Utility function takes `panel` instance and makes sure it will be +// automatically hidden as soon as other panel is shown. +let setupAutoHide = new function() { + let refs = new WeakMap(); + + return function setupAutoHide(panel) { + // Create system event listener that reacts to any panel showing and + // hides given `panel` if it's not the one being shown. + function listener({subject}) { + // It could be that listener is not GC-ed in the same cycle as + // panel in such case we remove listener manually. + let view = viewFor(panel); + if (!view) systemEvents.off("sdk-panel-show", listener); + else if (subject !== view) panel.hide(); } - this._init(options); + // system event listener is intentionally weak this way we'll allow GC + // to claim panel if it's no longer referenced by an add-on code. This also + // helps minimizing cleanup required on unload. + systemEvents.on("sdk-panel-show", listener); + // To make sure listener is not claimed by GC earlier than necessary we + // associate it with `panel` it's associated with. This way it won't be + // GC-ed earlier than `panel` itself. + refs.set(panel, listener); + } +} + +const Panel = Class({ + implements: [ + // Generate accessors for the validated properties that update model on + // set and return values from model on get. + panelContract.properties(modelFor), + EventTarget, + Disposable + ], + extends: WorkerHost(workerFor), + setup: function setup(options) { + let model = merge({ + width: 320, + height: 240, + focus: true, + }, panelContract(options)); + models.set(this, model); + + // Setup listeners. + setListeners(this, options); + + // Setup view + let view = domPanel.make(); + panels.set(view, this); + views.set(this, view); + + // Load panel content. + domPanel.setURL(view, model.contentURL); + + setupAutoHide(this); + + let worker = new Worker(options); + workers.set(this, worker); + + // pipe events from worker to a panel. + pipe(worker, this); }, - _destructor: function _destructor() { + dispose: function dispose() { this.hide(); - this._removeAllListeners('show'); - this._removeAllListeners('hide'); - this._removeAllListeners('propertyChange'); - this._removeAllListeners('inited'); - off('sdk-panel-show', this._onAnyPanelShow); - // defer cleanup to be performed after panel gets hidden - this._xulPanel = null; - this._symbiontDestructor(this); - this._removeAllListeners(); - }, - destroy: function destroy() { - this._destructor(); + off(this); + + detach(workerFor(this)); + + domPanel.dispose(viewFor(this)); + + // Release circular reference between view and panel instance. This + // way view will be GC-ed. And panel as well once all the other refs + // will be removed from it. + views.delete(this); }, /* Public API: Panel.width */ - get width() this._width, - set width(value) - this._width = valid({ $: value }, { $: validNumber }).$ || this._width, - _width: 320, + get width() modelFor(this).width, + set width(value) this.resize(value, this.height), /* Public API: Panel.height */ - get height() this._height, - set height(value) - this._height = valid({ $: value }, { $: validNumber }).$ || this._height, - _height: 240, + get height() modelFor(this).height, + set height(value) this.resize(this.width, value), + /* Public API: Panel.focus */ - get focus() this._focus, - _focus: true, + get focus() modelFor(this).focus, + + get contentURL() modelFor(this).contentURL, + set contentURL(value) { + let model = modelFor(this); + model.contentURL = panelContract({ contentURL: value }).contentURL; + domPanel.setURL(viewFor(this), model.contentURL); + }, /* Public API: Panel.isShowing */ - get isShowing() !!this._xulPanel && this._xulPanel.state == "open", + get isShowing() !isDisposed(this) && domPanel.isOpen(viewFor(this)), /* Public API: Panel.show */ show: function show(anchor) { - anchor = anchor ? getNodeView(anchor) : null; - let anchorWindow = getWindow(anchor); + let model = modelFor(this); + let view = viewFor(this); + let anchorView = getNodeView(anchor); - // If there is no open window, or the anchor is in a private window - // then we will not be able to display the panel - if (!anchorWindow) { - return; - } + if (!isDisposed(this)) + domPanel.show(view, model.width, model.height, model.focus, anchorView); - let document = anchorWindow.document; - let xulPanel = this._xulPanel; - let panel = this; - if (!xulPanel) { - xulPanel = this._xulPanel = document.createElementNS(XUL_NS, 'panel'); - xulPanel.setAttribute("type", "arrow"); - - // One anonymous node has a big padding that doesn't work well with - // Jetpack, as we would like to display an iframe that completely fills - // the panel. - // -> Use a XBL wrapper with inner stylesheet to remove this padding. - let css = ".panel-inner-arrowcontent, .panel-arrowcontent {padding: 0;}"; - let originalXBL = "chrome://global/content/bindings/popup.xml#arrowpanel"; - let binding = - '' + - '' + - '' + - '' + - '' + - '' + - ''; - xulPanel.style.MozBinding = 'url("data:text/xml;charset=utf-8,' + - document.defaultView.encodeURIComponent(binding) + '")'; - - let frame = document.createElementNS(XUL_NS, 'iframe'); - frame.setAttribute('type', 'content'); - frame.setAttribute('flex', '1'); - frame.setAttribute('transparent', 'transparent'); - - if (runtime.OS === "Darwin") { - frame.style.borderRadius = "6px"; - frame.style.padding = "1px"; - } - - // Load an empty document in order to have an immediatly loaded iframe, - // so swapFrameLoaders is going to work without having to wait for load. - frame.setAttribute("src","data:;charset=utf-8,"); - - xulPanel.appendChild(frame); - document.getElementById("mainPopupSet").appendChild(xulPanel); - } - let { width, height, focus } = this, x, y, position; - - if (!anchor) { - // Open the popup in the middle of the window. - x = document.documentElement.clientWidth / 2 - width / 2; - y = document.documentElement.clientHeight / 2 - height / 2; - position = null; - } - else { - // Open the popup by the anchor. - let rect = anchor.getBoundingClientRect(); - - let window = anchor.ownerDocument.defaultView; - - let zoom = window.mozScreenPixelsPerCSSPixel; - let screenX = rect.left + window.mozInnerScreenX * zoom; - let screenY = rect.top + window.mozInnerScreenY * zoom; - - // Set up the vertical position of the popup relative to the anchor - // (always display the arrow on anchor center) - let horizontal, vertical; - if (screenY > window.screen.availHeight / 2 + height) - vertical = "top"; - else - vertical = "bottom"; - - if (screenY > window.screen.availWidth / 2 + width) - horizontal = "left"; - else - horizontal = "right"; - - let verticalInverse = vertical == "top" ? "bottom" : "top"; - position = vertical + "center " + verticalInverse + horizontal; - - // Allow panel to flip itself if the panel can't be displayed at the - // specified position (useful if we compute a bad position or if the - // user moves the window and panel remains visible) - xulPanel.setAttribute("flip","both"); - } - - // Resize the iframe instead of using panel.sizeTo - // because sizeTo doesn't work with arrow panels - xulPanel.firstChild.style.width = width + "px"; - xulPanel.firstChild.style.height = height + "px"; - - // Only display xulPanel if Panel.hide() was not called - // after Panel.show(), but before xulPanel.openPopup - // was loaded - emit('sdk-panel-show', { data: ADDON_ID, subject: xulPanel }); - - // Prevent the panel from getting focus when showing up - // if focus is set to false - xulPanel.setAttribute("noautofocus",!focus); - - // Wait for the XBL binding to be constructed - function waitForBinding() { - if (!xulPanel.openPopup) { - setTimeout(waitForBinding, 50); - return; - } - - if (xulPanel.state !== 'hiding') { - xulPanel.openPopup(anchor, position, x, y); - } - } - waitForBinding(); - - return this._public; + return this; }, + /* Public API: Panel.hide */ hide: function hide() { - // The popuphiding handler takes care of swapping back the frame loaders - // and removing the XUL panel from the application window, we just have to - // trigger it by hiding the popup. - // XXX Sometimes I get "TypeError: xulPanel.hidePopup is not a function" - // when quitting the host application while a panel is visible. To suppress - // them, this now checks for "hidePopup" in xulPanel before calling it. - // It's not clear if there's an actual issue or the error is just normal. - let xulPanel = this._xulPanel; - if (xulPanel && "hidePopup" in xulPanel) - xulPanel.hidePopup(); - return this._public; + // Quit immediately if panel is disposed or there is no state change. + domPanel.close(viewFor(this)); + + return this; }, /* Public API: Panel.resize */ resize: function resize(width, height) { - this.width = width; - this.height = height; - // Resize the iframe instead of using panel.sizeTo - // because sizeTo doesn't work with arrow panels - let xulPanel = this._xulPanel; - if (xulPanel) { - xulPanel.firstChild.style.width = width + "px"; - xulPanel.firstChild.style.height = height + "px"; - } - }, + let model = modelFor(this); + let view = viewFor(this); + let change = panelContract({ + width: width || model.width, + height: height || model.height + }); - // While the panel is visible, this is the XUL we use to display it. - // Otherwise, it's null. - get _xulPanel() this.__xulPanel, - set _xulPanel(value) { - let xulPanel = this.__xulPanel; - if (value === xulPanel) return; - if (xulPanel) { - xulPanel.removeEventListener(ON_HIDE, this._onHide, false); - xulPanel.removeEventListener(ON_SHOW, this._onShow, false); - xulPanel.parentNode.removeChild(xulPanel); - } - if (value) { - value.addEventListener(ON_HIDE, this._onHide, false); - value.addEventListener(ON_SHOW, this._onShow, false); - } - this.__xulPanel = value; - }, - __xulPanel: null, - get _viewFrame() this.__xulPanel.children[0], - /** - * When the XUL panel becomes hidden, we swap frame loaders back to move - * the content of the panel to the hidden frame & remove panel element. - */ - _onHide: function _onHide() { - try { - this._frameLoadersSwapped = false; - this._xulPanel = null; - this._emit('hide'); - } catch(e) { - this._emit('error', e); - } - }, + model.width = change.width + model.height = change.height - /** - * Retrieve computed text color style in order to apply to the iframe - * document. As MacOS background is dark gray, we need to use skin's - * text color. - */ - _applyStyleToDocument: function _applyStyleToDocument() { - if (this._defaultStyleApplied) - return; - try { - let win = this._xulPanel.ownerDocument.defaultView; - let node = win.document.getAnonymousElementByAttribute( - this._xulPanel, "class", "panel-arrowcontent"); - if (!node) { - // Before bug 764755, anonymous content was different: - // TODO: Remove this when targeting FF16+ - node = win.document.getAnonymousElementByAttribute( - this._xulPanel, "class", "panel-inner-arrowcontent"); - } - let textColor = win.getComputedStyle(node).getPropertyValue("color"); - let doc = this._xulPanel.firstChild.contentDocument; - let style = doc.createElement("style"); - style.textContent = "body { color: " + textColor + "; }"; - let container = doc.head ? doc.head : doc.documentElement; + domPanel.resize(view, model.width, model.height); - if (container.firstChild) - container.insertBefore(style, container.firstChild); - else - container.appendChild(style); - this._defaultStyleApplied = true; - } - catch(e) { - console.error("Unable to apply panel style"); - console.exception(e); - } - }, - - /** - * When the XUL panel becomes shown, we swap frame loaders between panel - * frame and hidden frame to preserve state of the content dom. - */ - _onShow: function _onShow() { - try { - if (!this._inited) { // defer if not initialized yet - this.on('inited', this._onShow.bind(this)); - } else { - this._frameLoadersSwapped = true; - this._applyStyleToDocument(); - this._emit('show'); - } - } catch(e) { - this._emit('error', e); - } - }, - - /** - * When any panel is displayed, system-wide, close `this` - * panel unless it's the most recently displayed panel - */ - _onAnyPanelShow: function _onAnyPanelShow(e) { - if (e.subject !== this._xulPanel) - this.hide(); - }, - - /** - * Notification that panel was fully initialized. - */ - _onInit: function _onInit() { - this._inited = true; - - // Avoid panel document from resizing the browser window - // New platform capability added through bug 635673 - let docShell = getDocShell(this._frame); - if (docShell && "allowWindowControl" in docShell) - docShell.allowWindowControl = false; - - // perform all deferred tasks like initSymbiont, show, hide ... - // TODO: We're publicly exposing a private event here; this - // 'inited' event should really be made private, somehow. - this._emit('inited'); - }, - - // Catch document unload event in order to rebind load event listener with - // Symbiont._initFrame if Worker._documentUnload destroyed the worker - _documentUnload: function(subject, topic, data) { - if (this._workerDocumentUnload(subject, topic, data)) { - this._initFrame(this._frame); - return true; - } - return false; - }, - - _onChange: function _onChange(e) { - this._frameLoadersSwapped = false; - if ('contentURL' in e && this._frame) { - // Cleanup the worker before injecting the content script in the new - // document - this._workerCleanup(); - this._initFrame(this._frame); - } + return this; } }); -exports.Panel = function(options) Panel(options) -exports.Panel.prototype = Panel.prototype; +exports.Panel = Panel; + +// Filter panel events to only panels that are create by this module. +let panelEvents = filter(function({target}) panelFor(target), events); + +// Panel events emitted after panel has being shown. +let shows = filter(function({type}) type === "sdk-panel-shown", panelEvents); + +// Panel events emitted after panel became hidden. +let hides = filter(function({type}) type === "sdk-panel-hidden", panelEvents); + +// Panel events emitted after content inside panel is ready. For different +// panels ready may mean different state based on `contentScriptWhen` attribute. +// Weather given event represents readyness is detected by `getAttachEventType` +// helper function. +let ready = filter(function({type, target}) + getAttachEventType(modelFor(panelFor(target))) === type, panelEvents); + +// Panel events emitted after content document in the panel has changed. +let change = filter(function({type}) type === "sdk-panel-content-changed", + panelEvents); + +// Forward panel show / hide events to panel's own event listeners. +on(shows, "data", function({target}) emit(panelFor(target), "show")); +on(hides, "data", function({target}) emit(panelFor(target), "hide")); + +on(ready, "data", function({target}) { + let worker = workerFor(panelFor(target)); + attach(worker, domPanel.getContentDocument(target).defaultView); +}); diff --git a/addon-sdk/source/lib/sdk/panel/events.js b/addon-sdk/source/lib/sdk/panel/events.js new file mode 100644 index 000000000000..e2adcb2e723c --- /dev/null +++ b/addon-sdk/source/lib/sdk/panel/events.js @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This module basically translates system/events to a SDK standard events +// so that `map`, `filter` and other utilities could be used with them. + +module.metadata = { + "stability": "experimental" +}; + +const events = require("../system/events"); +const { emit } = require("../event/core"); + +let channel = {}; + +function forward({ subject, type, data }) + emit(channel, "data", { target: subject, type: type, data: data }); + +["sdk-panel-show", "sdk-panel-hide", "sdk-panel-shown", + "sdk-panel-hidden", "sdk-panel-content-changed", "sdk-panel-content-loaded", + "sdk-panel-document-loaded" +].forEach(function(type) events.on(type, forward)); + +exports.events = channel; diff --git a/addon-sdk/source/lib/sdk/panel/utils.js b/addon-sdk/source/lib/sdk/panel/utils.js new file mode 100644 index 000000000000..63029cce38d4 --- /dev/null +++ b/addon-sdk/source/lib/sdk/panel/utils.js @@ -0,0 +1,322 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { Cc, Ci } = require("chrome"); +const { setTimeout } = require("../timers"); +const { platform } = require("../system"); +const { getMostRecentBrowserWindow, getOwnerBrowserWindow, + getHiddenWindow } = require("../window/utils"); +const { create: createFrame, swapFrameLoaders } = require("../frame/utils"); +const { window: addonWindow } = require("../addon/window"); +const events = require("../system/events"); + + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +function open(panel, width, height, anchor) { + // Wait for the XBL binding to be constructed + if (!panel.openPopup) setTimeout(open, 50, panel, width, height, anchor); + else display(panel, width, height, anchor); +} +exports.open = open; + +function isOpen(panel) { + return panel.state === "open" +} +exports.isOpen = isOpen; + + +function close(panel) { + // Sometimes "TypeError: panel.hidePopup is not a function" is thrown + // when quitting the host application while a panel is visible. To suppress + // these errors, check for "hidePopup" in panel before calling it. + // It's not clear if there's an issue or it's expected behavior. + + return panel.hidePopup && panel.hidePopup(); +} +exports.close = close + + +function resize(panel, width, height) { + // Resize the iframe instead of using panel.sizeTo + // because sizeTo doesn't work with arrow panels + panel.firstChild.style.width = width + "px"; + panel.firstChild.style.height = height + "px"; +} +exports.resize = resize + +function display(panel, width, height, anchor) { + let document = panel.ownerDocument; + let x = null; + let y = null; + let position = null; + + // Panel XBL has some SDK incompatible styling decisions. We shim panel + // instances until proper fix for Bug 859504 is shipped. + shimDefaultStyle(panel); + + if (!anchor) { + // Open the popup in the middle of the window. + x = document.documentElement.clientWidth / 2 - width / 2; + y = document.documentElement.clientHeight / 2 - height / 2; + position = null; + } + else { + // Open the popup by the anchor. + let rect = anchor.getBoundingClientRect(); + + let window = anchor.ownerDocument.defaultView; + + let zoom = window.mozScreenPixelsPerCSSPixel; + let screenX = rect.left + window.mozInnerScreenX * zoom; + let screenY = rect.top + window.mozInnerScreenY * zoom; + + // Set up the vertical position of the popup relative to the anchor + // (always display the arrow on anchor center) + let horizontal, vertical; + if (screenY > window.screen.availHeight / 2 + height) + vertical = "top"; + else + vertical = "bottom"; + + if (screenY > window.screen.availWidth / 2 + width) + horizontal = "left"; + else + horizontal = "right"; + + let verticalInverse = vertical == "top" ? "bottom" : "top"; + position = vertical + "center " + verticalInverse + horizontal; + + // Allow panel to flip itself if the panel can't be displayed at the + // specified position (useful if we compute a bad position or if the + // user moves the window and panel remains visible) + panel.setAttribute("flip", "both"); + } + + // Resize the iframe instead of using panel.sizeTo + // because sizeTo doesn't work with arrow panels + panel.firstChild.style.width = width + "px"; + panel.firstChild.style.height = height + "px"; + + panel.openPopup(anchor, position, x, y); +} +exports.display = display; + +// This utility function is just a workaround until Bug 859504 has shipped. +function shimDefaultStyle(panel) { + let document = panel.ownerDocument; + // Please note that `panel` needs to be part of document in order to reach + // it's anonymous nodes. One of the anonymous node has a big padding which + // doesn't work well since panel frame needs to fill all of the panel. + // XBL binding is a not the best option as it's applied asynchronously, and + // makes injected frames behave in strange way. Also this feels a lot + // cheaper to do. + ["panel-inner-arrowcontent", "panel-arrowcontent"].forEach(function(value) { + let node = document.getAnonymousElementByAttribute(panel, "class", value); + if (node) node.style.padding = 0; + }); +} + +function show(panel, width, height, focus, anchor) { + // Prevent the panel from getting focus when showing up + // if focus is set to false + panel.setAttribute("noautofocus", !focus); + + + let window = anchor && getOwnerBrowserWindow(anchor); + let { document } = window ? window : getMostRecentBrowserWindow(); + attach(panel, document); + open(panel, width, height, anchor); +} +exports.show = show + +function setupPanelFrame(frame) { + frame.setAttribute("flex", 1); + frame.setAttribute("transparent", "transparent"); + frame.setAttribute("showcaret", true); + frame.setAttribute("autocompleteenabled", true); + if (platform === "darwin") { + frame.style.borderRadius = "6px"; + frame.style.padding = "1px"; + } +} + +let EVENT_NAMES = { + "popupshowing": "sdk-panel-show", + "popuphiding": "sdk-panel-hide", + "popupshown": "sdk-panel-shown", + "popuphidden": "sdk-panel-hidden", + "document-element-inserted": "sdk-panel-content-changed", + "DOMContentLoaded": "sdk-panel-content-loaded", + "load": "sdk-panel-document-loaded" +}; + +function make(document) { + document = document || getMostRecentBrowserWindow().document; + let panel = document.createElementNS(XUL_NS, "panel"); + panel.setAttribute("type", "arrow"); + + // Note that panel is a parent of `viewFrame` who's `docShell` will be + // configured at creation time. If `panel` and there for `viewFrame` won't + // have an owner document attempt to access `docShell` will throw. There + // for we attach panel to a document. + attach(panel, document); + + let frameOptions = { + allowJavascript: true, + allowPlugins: true, + allowAuth: true, + allowWindowControl: false, + // Need to override `nodeName` to use `iframe` as `browsers` save session + // history and in consequence do not dispatch "inner-window-destroyed" + // notifications. + browser: false, + // Note that use of this URL let's use swap frame loaders earlier + // than if we used default "about:blank". + uri: "data:text/plain;charset=utf-8," + }; + + let backgroundFrame = createFrame(addonWindow, frameOptions); + setupPanelFrame(backgroundFrame); + + let viewFrame = createFrame(panel, frameOptions); + setupPanelFrame(viewFrame); + + function onDisplayChange({type}) { + try { swapFrameLoaders(backgroundFrame, viewFrame); } + catch(error) { console.exception(error); } + events.emit(EVENT_NAMES[type], { subject: panel }); + } + + function onContentReady({target, type}) { + if (target === getContentDocument(panel)) { + style(panel); + events.emit(EVENT_NAMES[type], { subject: panel }); + } + } + + function onContentLoad({target, type}) { + if (target === getContentDocument(panel)) + events.emit(EVENT_NAMES[type], { subject: panel }); + } + + function onContentChange({subject, type}) { + let document = subject; + if (document === getContentDocument(panel) && document.defaultView) + events.emit(EVENT_NAMES[type], { subject: panel }); + } + + function onPanelStateChange({type}) { + events.emit(EVENT_NAMES[type], { subject: panel }) + } + + panel.addEventListener("popupshowing", onDisplayChange, false); + panel.addEventListener("popuphiding", onDisplayChange, false); + panel.addEventListener("popupshown", onPanelStateChange, false); + panel.addEventListener("popuphidden", onPanelStateChange, false); + + // Panel content document can be either in panel `viewFrame` or in + // a `backgroundFrame` depending on panel state. Listeners are set + // on both to avoid setting and removing listeners on panel state changes. + + panel.addEventListener("DOMContentLoaded", onContentReady, true); + backgroundFrame.addEventListener("DOMContentLoaded", onContentReady, true); + + panel.addEventListener("load", onContentLoad, true); + backgroundFrame.addEventListener("load", onContentLoad, true); + + events.on("document-element-inserted", onContentChange); + + + panel.backgroundFrame = backgroundFrame; + + // Store event listener on the panel instance so that it won't be GC-ed + // while panel is alive. + panel.onContentChange = onContentChange; + + return panel; +} +exports.make = make; + +function attach(panel, document) { + document = document || getMostRecentBrowserWindow().document; + let container = document.getElementById("mainPopupSet"); + if (container !== panel.parentNode) { + detach(panel); + document.getElementById("mainPopupSet").appendChild(panel); + } +} +exports.attach = attach; + +function detach(panel) { + if (panel.parentNode) panel.parentNode.removeChild(panel); +} +exports.detach = detach; + +function dispose(panel) { + panel.backgroundFrame.parentNode.removeChild(panel.backgroundFrame); + panel.backgroundFrame = null; + events.off("document-element-inserted", panel.onContentChange); + panel.onContentChange = null; + detach(panel); +} +exports.dispose = dispose; + +function style(panel) { + /** + Injects default OS specific panel styles into content document that is loaded + into given panel. Optionally `document` of the browser window can be + given to inherit styles from it, by default it will use either panel owner + document or an active browser's document. It should not matter though unless + Firefox decides to style windows differently base on profile or mode like + chrome for example. + **/ + + try { + let document = panel.ownerDocument; + let contentDocument = getContentDocument(panel); + let window = document.defaultView; + let node = document.getAnonymousElementByAttribute(panel, "class", + "panel-arrowcontent") || + // Before bug 764755, anonymous content was different: + // TODO: Remove this when targeting FF16+ + document.getAnonymousElementByAttribute(panel, "class", + "panel-inner-arrowcontent"); + + let color = window.getComputedStyle(node).getPropertyValue("color"); + + let style = contentDocument.createElement("style"); + style.id = "sdk-panel-style"; + style.textContent = "body { color: " + color + "; }"; + + let container = contentDocument.head ? contentDocument.head : + contentDocument.documentElement; + + if (container.firstChild) + container.insertBefore(style, container.firstChild); + else + container.appendChild(style); + } + catch (error) { + console.error("Unable to apply panel style"); + console.exception(error); + } +} +exports.style = style; + +function getContentFrame(panel) isOpen(panel) ? panel.firstChild : + panel.backgroundFrame +exports.getContentFrame = getContentFrame; + +function getContentDocument(panel) getContentFrame(panel).contentDocument +exports.getContentDocument = getContentDocument; + +function setURL(panel, url) getContentFrame(panel).setAttribute("src", url) +exports.setURL = setURL; diff --git a/addon-sdk/source/lib/sdk/request.js b/addon-sdk/source/lib/sdk/request.js index 331701867ba3..514631f0384f 100644 --- a/addon-sdk/source/lib/sdk/request.js +++ b/addon-sdk/source/lib/sdk/request.js @@ -25,6 +25,8 @@ const request = ns(); // reuse it. const { validateOptions, validateSingleOption } = new OptionsValidator({ url: { + // Also converts a URL instance to string, bug 857902 + map: function (url) url.toString(), ok: isValidURI }, headers: { diff --git a/addon-sdk/source/lib/sdk/stylesheet/style.js b/addon-sdk/source/lib/sdk/stylesheet/style.js new file mode 100644 index 000000000000..8d770d5bf128 --- /dev/null +++ b/addon-sdk/source/lib/sdk/stylesheet/style.js @@ -0,0 +1,83 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +const { Cc, Ci } = require("chrome"); +const { Class } = require("../core/heritage"); +const { ns } = require("../core/namespace"); +const { URL } = require('../url'); +const events = require("../system/events"); +const { loadSheet, removeSheet, isTypeValid } = require("./utils"); +const { isString } = require("../lang/type"); +const { attachTo, detachFrom, getTargetWindow } = require("../content/mod"); + +const { freeze, create } = Object; +const LOCAL_URI_SCHEMES = ['resource', 'data']; + +function isLocalURL(item) { + try { + return LOCAL_URI_SCHEMES.indexOf(URL(item).scheme) > -1; + } + catch(e) {} + + return false; +} + +function Style({ source, uri, type }) { + source = source == null ? null : freeze([].concat(source)); + uri = uri == null ? null : freeze([].concat(uri)); + type = type == null ? "author" : type; + + if (source && !source.every(isString)) + throw new Error('Style.source must be a string or an array of strings.'); + + if (uri && !uri.every(isLocalURL)) + throw new Error('Style.uri must be a local URL or an array of local URLs'); + + if (type && !isTypeValid(type)) + throw new Error('Style.type must be "agent", "user" or "author"'); + + return freeze(create(Style.prototype, { + "source": { value: source, enumerable: true }, + "uri": { value: uri, enumerable: true }, + "type": { value: type, enumerable: true } + })); +}; + +exports.Style = Style; + +attachTo.define(Style, function (style, window) { + if (style.uri) { + for (let uri of style.uri) + loadSheet(window, uri, style.type); + } + + if (style.source) { + let uri = "data:text/css;charset=utf-8,"; + + uri += encodeURIComponent(style.source.join("")); + + loadSheet(window, uri, style.type); + } +}); + +detachFrom.define(Style, function (style, window) { + if (style.uri) + for (let uri of style.uri) + removeSheet(window, uri); + + if (style.source) { + let uri = "data:text/css;charset=utf-8,"; + + uri += encodeURIComponent(style.source.join("")); + + removeSheet(window, uri, style.type); + } +}); diff --git a/addon-sdk/source/lib/sdk/stylesheet/utils.js b/addon-sdk/source/lib/sdk/stylesheet/utils.js new file mode 100644 index 000000000000..9220954d5749 --- /dev/null +++ b/addon-sdk/source/lib/sdk/stylesheet/utils.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +const { Cc, Ci } = require("chrome"); + +const io = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); + +const SHEET_TYPE = { + "agent": "AGENT_SHEET", + "user": "USER_SHEET", + "author": "AUTHOR_SHEET" +}; + +function getDOMWindowUtils(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); +}; + +/** + * Synchronously loads a style sheet from `uri` and adds it to the list of + * additional style sheets of the document. + * The sheets added takes effect immediately, and only on the document of the + * `window` given. + */ +function loadSheet(window, url, type) { + if (!(type && type in SHEET_TYPE)) + type = "author"; + + type = SHEET_TYPE[type]; + + if (!(url instanceof Ci.nsIURI)) + url = io.newURI(url, null, null); + + let winUtils = getDOMWindowUtils(window); + try { + winUtils.loadSheet(url, winUtils[type]); + } + catch (e) {}; +}; +exports.loadSheet = loadSheet; + +/** + * Remove the document style sheet at `sheetURI` from the list of additional + * style sheets of the document. The removal takes effect immediately. + */ +function removeSheet(window, url, type) { + if (!(type && type in SHEET_TYPE)) + type = "author"; + + type = SHEET_TYPE[type]; + + if (!(url instanceof Ci.nsIURI)) + url = io.newURI(url, null, null); + + let winUtils = getDOMWindowUtils(window); + + try { + winUtils.removeSheet(url, winUtils[type]); + } + catch (e) {}; +}; +exports.removeSheet = removeSheet; + +/** + * Returns `true` if the `type` given is valid, otherwise `false`. + * The values currently accepted are: "agent", "user" and "author". + */ +function isTypeValid(type) { + return type in SHEET_TYPE; +} +exports.isTypeValid = isTypeValid; diff --git a/addon-sdk/source/lib/sdk/system/environment.js b/addon-sdk/source/lib/sdk/system/environment.js index 045bab1cf53f..4a51476aa373 100644 --- a/addon-sdk/source/lib/sdk/system/environment.js +++ b/addon-sdk/source/lib/sdk/system/environment.js @@ -41,7 +41,10 @@ exports.env = Proxy.create({ // 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), + delete: function(name) { + set(name, null); + return true; + }, // We present all properties as own, there for we just delegate to `hasOwn`. has: function(name) this.hasOwn(name), diff --git a/addon-sdk/source/lib/sdk/util/array.js b/addon-sdk/source/lib/sdk/util/array.js index 539c2166ba43..a4c563e63928 100644 --- a/addon-sdk/source/lib/sdk/util/array.js +++ b/addon-sdk/source/lib/sdk/util/array.js @@ -101,3 +101,15 @@ function fromIterator(iterator) { return array; } exports.fromIterator = fromIterator; + + +function find(array, predicate) { + var index = 0; + var count = array.length; + while (index < count) { + var value = array[index]; + if (predicate(value)) return value; + else index = index + 1; + } +} +exports.find = find; diff --git a/addon-sdk/source/lib/sdk/util/contract.js b/addon-sdk/source/lib/sdk/util/contract.js new file mode 100644 index 000000000000..85a6028d93da --- /dev/null +++ b/addon-sdk/source/lib/sdk/util/contract.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { validateOptions: valid } = require("../deprecated/api-utils"); + +// Function takes property validation rules and returns function that given +// an `options` object will return validated / normalized options back. If +// option(s) are invalid validator will throw exception described by rules. +// Returned will also have contain `rules` property with a given validation +// rules and `properties` function that can be used to generate validated +// property getter and setters can be mixed into prototype. For more details +// see `properties` function below. +function contract(rules) { + function validator(options) { + return valid(options || {}, rules); + } + validator.rules = rules + validator.properties = function(modelFor) { + return properties(modelFor, rules); + } + return validator; +} +exports.contract = contract + +// Function takes `modelFor` instance state model accessor functions and +// a property validation rules and generates object with getters and setters +// that can be mixed into prototype. Property accessors update model for the +// given instance. If you wish to react to property updates you can always +// override setters to put specific logic. +function properties(modelFor, rules) { + let descriptor = Object.keys(rules).reduce(function(descriptor, name) { + descriptor[name] = { + get: function() { return modelFor(this)[name] }, + set: function(value) { + let change = {}; + change[name] = value; + modelFor(this)[name] = valid(change, rules)[name]; + } + } + return descriptor + }, {}); + return Object.create(Object.prototype, descriptor); +} +exports.properties = properties diff --git a/addon-sdk/source/lib/sdk/view/core.js b/addon-sdk/source/lib/sdk/view/core.js index f53c70293fb5..f5cb109134aa 100644 --- a/addon-sdk/source/lib/sdk/view/core.js +++ b/addon-sdk/source/lib/sdk/view/core.js @@ -10,11 +10,6 @@ module.metadata = { }; var { Ci } = require("chrome"); - -/** -Temporarily emulate method so we don't have to uplift whole method -implementation. - var method = require("method/core"); // Returns DOM node associated with a view for @@ -27,20 +22,5 @@ getNodeView.define(function(value) { return value; return null; }); -**/ - -let implementations = new WeakMap(); - -function getNodeView(value) { - if (value instanceof Ci.nsIDOMNode) - return value; - if (implementations.has(value)) - return implementations.get(value)(value); - - return null; -} -getNodeView.implement = function(value, implementation) { - implementations.set(value, implementation) -} exports.getNodeView = getNodeView; diff --git a/addon-sdk/source/lib/sdk/window/utils.js b/addon-sdk/source/lib/sdk/window/utils.js index 13532a6fbc21..8eb383f5fcd4 100644 --- a/addon-sdk/source/lib/sdk/window/utils.js +++ b/addon-sdk/source/lib/sdk/window/utils.js @@ -340,3 +340,21 @@ function getFrames(window) { }, []) } exports.getFrames = getFrames; + +function getOwnerBrowserWindow(node) { + /** + Takes DOM node and returns browser window that contains it. + **/ + + let window = node.ownerDocument.defaultView.top; + // If anchored window is browser then it's target browser window. + if (isBrowser(window)) return window; + // Otherwise iterate over each browser window and find a one that + // contains browser for the anchored window document. + let document = window.document; + let browsers = windows("navigator:browser", { includePrivate: true }); + return array.find(browsers, function isTargetBrowser(window) { + return !!window.gBrowser.getBrowserForDocument(document); + }); +} +exports.getOwnerBrowserWindow = getOwnerBrowserWindow; diff --git a/addon-sdk/source/lib/sdk/worker/utils.js b/addon-sdk/source/lib/sdk/worker/utils.js new file mode 100644 index 000000000000..a7d46767ef22 --- /dev/null +++ b/addon-sdk/source/lib/sdk/worker/utils.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +// This module attempts to hide trait based nature of the worker so that +// code depending on workers could be de-trait-ified without changing worker +// implementation. + +const { Worker: WorkerTrait } = require("../content/worker"); +const { Loader } = require("../content/loader"); +const { merge } = require("../util/object"); +const { emit } = require("../event/core"); + +const LegacyWorker = WorkerTrait.resolve({ + _setListeners: "__setListeners", +}).compose(Loader, { + _setListeners: function() {}, + attach: function(window) this._attach(window), + detach: function() this._workerCleanup() +}); + +// Weak map that stores mapping between regular worker instances and +// legacy trait based worker instances. +let traits = new WeakMap(); + +function traitFor(worker) traits.get(worker, null); + +function WorkerHost(workerFor) { + // Define worker properties that just proxy to a wrapped trait. + return ["postMessage", "port", "url", "tab"].reduce(function(proto, name) { + Object.defineProperty(proto, name, { + enumerable: true, + configurable: false, + get: function() traitFor(workerFor(this))[name], + set: function(value) traitFor(workerFor(this))[name] = value + }); + return proto; + }, {}); +} +exports.WorkerHost = WorkerHost; + +// Type representing worker instance. +function Worker(options) { + let worker = Object.create(Worker.prototype); + let trait = new LegacyWorker(options); + ["pageshow", "pagehide", "detach", "message", "error"].forEach(function(key) { + trait.on(key, function() { + emit.apply(emit, [worker, key].concat(Array.slice(arguments))); + // Workaround lack of ability to listen on all events by emulating + // such ability. This will become obsolete once Bug 821065 is fixed. + emit.apply(emit, [worker, "*", key].concat(Array.slice(arguments))); + }); + }); + traits.set(worker, trait); + return worker; +} +exports.Worker = Worker; + +function detach(worker) { + let trait = traitFor(worker); + if (trait) trait.detach(); +} +exports.detach = detach; + +function attach(worker, window) { + let trait = traitFor(worker); + // Cleanup the worker before injecting the content script into a new document. + trait.attach(window); +} +exports.attach = attach; diff --git a/addon-sdk/source/python-lib/cuddlefish/mobile-utils/bootstrap.js b/addon-sdk/source/python-lib/cuddlefish/mobile-utils/bootstrap.js index 752c9629b8cc..3fb3624e459b 100644 --- a/addon-sdk/source/python-lib/cuddlefish/mobile-utils/bootstrap.js +++ b/addon-sdk/source/python-lib/cuddlefish/mobile-utils/bootstrap.js @@ -40,7 +40,7 @@ function startup(data, reason) { try { let QuitObserver = { observe: function (aSubject, aTopic, aData) { - Services.obs.removeObserver(QuitObserver, "quit-application", false); + Services.obs.removeObserver(QuitObserver, "quit-application"); dump("MU: APPLICATION-QUIT\n"); } }; diff --git a/addon-sdk/source/test/addons/require/packages/panel/main.js b/addon-sdk/source/test/addons/require/packages/panel/main.js deleted file mode 100644 index a5e6bddfedca..000000000000 --- a/addon-sdk/source/test/addons/require/packages/panel/main.js +++ /dev/null @@ -1,5 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -exports.id = "panel-main"; diff --git a/addon-sdk/source/test/addons/require/packages/panel/package.json b/addon-sdk/source/test/addons/require/packages/panel/package.json deleted file mode 100644 index 2446c2e53df1..000000000000 --- a/addon-sdk/source/test/addons/require/packages/panel/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "id": "test-panel" -} \ No newline at end of file diff --git a/addon-sdk/source/test/addons/require/packages/panel/page-mod.js b/addon-sdk/source/test/addons/require/packages/panel/page-mod.js deleted file mode 100644 index 6c90f46c1a6f..000000000000 --- a/addon-sdk/source/test/addons/require/packages/panel/page-mod.js +++ /dev/null @@ -1,5 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -exports.id = "page-mod"; diff --git a/addon-sdk/source/test/addons/require/panel.js b/addon-sdk/source/test/addons/require/panel.js deleted file mode 100644 index 11b47d4c8169..000000000000 --- a/addon-sdk/source/test/addons/require/panel.js +++ /dev/null @@ -1,5 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -exports.id = "local-panel"; diff --git a/addon-sdk/source/test/tabs/test-firefox-tabs.js b/addon-sdk/source/test/tabs/test-firefox-tabs.js index 35c832f2b462..191a68184f73 100644 --- a/addon-sdk/source/test/tabs/test-firefox-tabs.js +++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js @@ -1040,47 +1040,31 @@ exports.testOnLoadEventWithImage = function(test) { exports.testOnPageShowEvent = function (test) { test.waitUntilDone(); - let firstUrl = 'about:home'; - let secondUrl = 'about:newtab'; + let firstUrl = 'data:text/html;charset=utf-8,First'; + let secondUrl = 'data:text/html;charset=utf-8,Second'; openBrowserWindow(function(window, browser) { let tabs = require('sdk/tabs'); - let wait = 500; - let counter = 1; - tabs.on('pageshow', function setup(tab, persisted) { - if (counter === 1) + let counter = 0; + tabs.on('pageshow', function onPageShow(tab, persisted) { + counter++; + if (counter === 1) { test.assert(!persisted, 'page should not be cached on initial load'); - - if (wait > 5000) { - test.fail('Page was not cached after 5s') - closeBrowserWindow(window, function() test.done()); + tab.url = secondUrl; } - - if (tab.url === firstUrl) { - // If first page has persisted, pass - if (persisted) { - tabs.removeListener('pageshow', setup); - test.pass('pageshow event called on history.back()'); - closeBrowserWindow(window, function() test.done()); - } - // On the first run, or if the page wasn't cached - // the first time due to not waiting long enough, - // try again with a longer delay (this is terrible - // and ugly) - else { - counter++; - timer.setTimeout(function () { - tab.url = secondUrl; - wait *= 2; - }, wait); - } - } - else { + else if (counter === 2) { + test.assert(!persisted, 'second test page should not be cached either'); tab.attach({ contentScript: 'setTimeout(function () { window.history.back(); }, 0)' }); } + else { + test.assert(persisted, 'when we get back to the fist page, it has to' + + 'come from cache'); + tabs.removeListener('pageshow', onPageShow); + closeBrowserWindow(window, function() test.done()); + } }); tabs.open({ diff --git a/addon-sdk/source/test/test-content-loader.js b/addon-sdk/source/test/test-content-loader.js index 1804ac9220db..86dc671181f6 100644 --- a/addon-sdk/source/test/test-content-loader.js +++ b/addon-sdk/source/test/test-content-loader.js @@ -10,16 +10,6 @@ exports['test:contentURL'] = function(test) { let loader = Loader(), value, emitted = 0, changes = 0; - test.assertRaises( - function() loader.contentURL = undefined, - 'The `contentURL` option must be a valid URL.', - 'Must throw an exception if `contentURL` is not URL.' - ); - test.assertRaises( - function() loader.contentURL = null, - 'The `contentURL` option must be a valid URL.', - 'Must throw an exception if `contentURL` is not URL.' - ); test.assertRaises( function() loader.contentURL = 4, 'The `contentURL` option must be a valid URL.', diff --git a/addon-sdk/source/test/test-disposable.js b/addon-sdk/source/test/test-disposable.js index bb57a7764ba2..34c4bd109951 100644 --- a/addon-sdk/source/test/test-disposable.js +++ b/addon-sdk/source/test/test-disposable.js @@ -163,4 +163,29 @@ exports["test loader unloads do not affect other loaders"] = function(assert) { assert.equal(disposals, 2, "2 destroy calls"); } +exports["test disposables that throw"] = function(assert) { + let loader = Loader(module); + let { Disposable } = loader.require("sdk/core/disposable"); + + let disposals = 0 + + let Foo = Class({ + extends: Disposable, + setup: function setup(a, b) { + throw Error("Boom!") + }, + dispose: function dispose() { + disposals = disposals + 1 + } + }) + + assert.throws(function() { + let foo1 = Foo() + }, /Boom/, "disposable constructors may throw"); + + loader.unload(); + + assert.equal(disposals, 0, "no disposal if constructor threw"); +} + require('test').run(exports); diff --git a/addon-sdk/source/test/test-environment.js b/addon-sdk/source/test/test-environment.js index 413997f36631..8475d244d282 100644 --- a/addon-sdk/source/test/test-environment.js +++ b/addon-sdk/source/test/test-environment.js @@ -41,8 +41,8 @@ exports['test set'] = function(assert) { exports['test unset'] = function(assert) { env.BLA4 = 'bla'; - assert.equal(env.BLA4, 'bla', 'BLA4 env varibale is set'); - delete env.BLA4; + assert.equal(env.BLA4, 'bla', 'BLA4 env variable is set'); + assert.equal(delete env.BLA4, true, 'BLA4 env variable is removed'); assert.equal(env.BLA4, undefined, 'BLA4 env variable is unset'); assert.equal('BLA4' in env, false, 'BLA4 env variable no longer exists' ); }; diff --git a/addon-sdk/source/test/test-match-pattern.js b/addon-sdk/source/test/test-match-pattern.js index 60cdac2ebe73..01691088cb0a 100644 --- a/addon-sdk/source/test/test-match-pattern.js +++ b/addon-sdk/source/test/test-match-pattern.js @@ -33,6 +33,8 @@ exports.testMatchPatternTestTrue = function(test) { ok(/.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); ok(/https:.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok('*.sample.com', 'http://ex.sample.com/foo.html'); + ok('*.amp.le.com', 'http://ex.amp.le.com'); }; exports.testMatchPatternTestFalse = function(test) { @@ -70,6 +72,14 @@ exports.testMatchPatternTestFalse = function(test) { ok(/.*zilla/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); ok(/.*Zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=655464"); // bug 655464 ok(/https:.*zilla/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + + // bug 856913 + ok('*.ign.com', 'http://www.design.com'); + ok('*.ign.com', 'http://design.com'); + ok('*.zilla.com', 'http://bugzilla.mozilla.com'); + ok('*.zilla.com', 'http://mo-zilla.com'); + ok('*.amp.le.com', 'http://amp-le.com'); + ok('*.amp.le.com', 'http://examp.le.com'); }; exports.testMatchPatternErrors = function(test) { diff --git a/addon-sdk/source/test/test-page-mod.js b/addon-sdk/source/test/test-page-mod.js index 3eb07526e643..55fcea33eccc 100644 --- a/addon-sdk/source/test/test-page-mod.js +++ b/addon-sdk/source/test/test-page-mod.js @@ -556,7 +556,7 @@ exports.testAttachToTabsOnly = function(test) { openToplevelWindow(); } else { - openBrowserIframe(); + openBrowserIframe(); } }, false); element.setAttribute('src', 'data:text/html;charset=utf-8,foo'); @@ -751,7 +751,7 @@ exports.testPageModCssList = function(test) { "data:text/css;charset=utf-8,div { border: 1px solid black; }", "data:text/css;charset=utf-8,div { border: 10px solid black; }", // Highlight evaluation order between contentStylesheet & contentStylesheetFile - "data:text/cs;charset=utf-8s,div { height: 1000px; }", + "data:text/css;charset=utf-8s,div { height: 1000px; }", // Highlight precedence between the author and user style sheet "data:text/css;charset=utf-8,div { width: 200px; max-width: 640px!important}", ], @@ -779,13 +779,13 @@ exports.testPageModCssList = function(test) { test.assertEqual( style.width, "320px", - "PageMod author/user style sheet precedence works" + "PageMod add-on author/page author style sheet precedence works" ); test.assertEqual( style.maxWidth, - "640px", - "PageMod author/user style sheet precedence with !important works" + "480px", + "PageMod add-on author/page author style sheet precedence with !important works" ); done(); diff --git a/addon-sdk/source/test/test-panel.js b/addon-sdk/source/test/test-panel.js index 86f9a5c40dae..80e0d26d4937 100644 --- a/addon-sdk/source/test/test-panel.js +++ b/addon-sdk/source/test/test-panel.js @@ -15,6 +15,7 @@ const { defer } = require('sdk/core/promise'); const { getMostRecentBrowserWindow } = require('sdk/window/utils'); const { getWindow } = require('sdk/panel/window'); const { pb } = require('./private-browsing/helper'); +const { URL } = require('sdk/url'); const SVG_URL = self.data.url('mofo_logo.SVG'); @@ -125,7 +126,8 @@ exports["test Document Reload"] = function(assert, done) { ""; let messageCount = 0; let panel = Panel({ - contentURL: "data:text/html;charset=utf-8," + encodeURIComponent(content), + // using URL here is intentional, see bug 859009 + contentURL: URL("data:text/html;charset=utf-8," + encodeURIComponent(content)), contentScript: "self.postMessage(window.location.href)", onMessage: function (message) { messageCount++; @@ -516,19 +518,10 @@ exports["test Automatic Destroy"] = function(assert) { assert.pass("check automatic destroy"); }; -exports["test Wait For Init Then Show Then Destroy"] = makeEventOrderTest({ - test: function(assert, done, expect, panel) { - expect('inited', function() { panel.show(); }). - then('show', function() { panel.destroy(); }). - then('hide', function() { done(); }); - } -}); - -exports["test Show Then Wait For Init Then Destroy"] = makeEventOrderTest({ +exports["test Show Then Destroy"] = makeEventOrderTest({ test: function(assert, done, expect, panel) { panel.show(); - expect('inited'). - then('show', function() { panel.destroy(); }). + expect('show', function() { panel.destroy(); }). then('hide', function() { done(); }); } }); diff --git a/addon-sdk/source/test/test-request.js b/addon-sdk/source/test/test-request.js index 471240e766bf..9b5ed3b32793 100644 --- a/addon-sdk/source/test/test-request.js +++ b/addon-sdk/source/test/test-request.js @@ -5,7 +5,8 @@ const { Request } = require("sdk/request"); const { pathFor } = require("sdk/system"); const file = require("sdk/io/file"); - +const { URL } = require("sdk/url"); +const { extend } = require("sdk/util/object"); const { Loader } = require("sdk/test/loader"); const options = require("@test/options"); @@ -46,14 +47,13 @@ exports.testOptionsValidator = function(test) { exports.testContentValidator = function(test) { test.waitUntilDone(); - Request({ + runMultipleURLs(null, test, { url: "data:text/html;charset=utf-8,response", content: { 'key1' : null, 'key2' : 'some value' }, onComplete: function(response) { test.assertEqual(response.text, "response?key1=null&key2=some+value"); - test.done(); } - }).get(); + }); }; // This is a request to a file that exists. @@ -82,15 +82,14 @@ exports.testStatus404 = function (test) { var srv = startServerAsync(port, basePath); test.waitUntilDone(); - Request({ + runMultipleURLs(srv, test, { // the following URL doesn't exist url: "http://localhost:" + port + "/test-request-404.txt", onComplete: function (response) { test.assertEqual(response.status, 404); test.assertEqual(response.statusText, "Not Found"); - srv.stop(function() test.done()); } - }).get(); + }); } // a simple file with a known header @@ -106,13 +105,12 @@ exports.testKnownHeader = function (test) { prepareFile(headerBasename, headerContent); test.waitUntilDone(); - Request({ + runMultipleURLs(srv, test, { url: "http://localhost:" + port + "/test-request-headers.txt", onComplete: function (response) { test.assertEqual(response.headers["x-jetpack-header"], "Jamba Juice"); - srv.stop(function() test.done()); } - }).get(); + }); } // complex headers @@ -132,15 +130,14 @@ exports.testComplexHeader = function (test) { } test.waitUntilDone(); - Request({ + runMultipleURLs(srv, test, { url: "http://localhost:" + port + "/test-request-complex-headers.sjs", onComplete: function (response) { for (k in headers) { test.assertEqual(response.headers[k], headers[k]); } - srv.stop(function() test.done()); } - }).get(); + }); } // Force Allow Third Party cookies @@ -199,13 +196,12 @@ exports.testSimpleJSON = function (test) { prepareFile(basename, JSON.stringify(json)); test.waitUntilDone(); - Request({ + runMultipleURLs(srv, test, { url: "http://localhost:" + port + "/" + basename, onComplete: function (response) { assertDeepEqual(test, response.json, json); - srv.stop(function() test.done()); } - }).get(); + }); } exports.testInvalidJSON = function (test) { @@ -214,13 +210,26 @@ exports.testInvalidJSON = function (test) { prepareFile(basename, '"this": "isn\'t JSON"'); test.waitUntilDone(); - Request({ + runMultipleURLs(srv, test, { url: "http://localhost:" + port + "/" + basename, onComplete: function (response) { test.assertEqual(response.json, null); - srv.stop(function() test.done()); } - }).get(); + }); +} + +function runMultipleURLs (srv, test, options) { + let urls = [options.url, URL(options.url)]; + let cb = options.onComplete; + let ran = 0; + let onComplete = function (res) { + cb(res); + if (++ran === urls.length) + srv ? srv.stop(function () test.done()) : test.done(); + } + urls.forEach(function (url) { + Request(extend(options, { url: url, onComplete: onComplete })).get(); + }); } // All tests below here require a network connection. They will be commented out diff --git a/addon-sdk/source/test/test-selection.js b/addon-sdk/source/test/test-selection.js index 211d7ffb84e4..4adcc74c15e5 100644 --- a/addon-sdk/source/test/test-selection.js +++ b/addon-sdk/source/test/test-selection.js @@ -28,7 +28,7 @@ const { setTimeout } = require("sdk/timers"); const { Cu } = require("chrome"); const { merge } = require("sdk/util/object"); const { isPrivate } = require("sdk/private-browsing"); - +const events = require("sdk/system/events"); // General purpose utility functions /** @@ -161,10 +161,16 @@ function hideAndShowFrame(window) { Cu.forceGC(); - setTimeout(function(){ - iframe.style.display = ""; + setTimeout(function() { + events.on("document-shown", function shown(event) { + if (iframe.contentWindow !== event.subject.defaultView) + return; - setTimeout(resolve, 500, window); + events.off("document-shown", shown); + setTimeout(resolve, 0, window); + }, true); + + iframe.style.display = ""; }, 0) return promise; @@ -828,6 +834,8 @@ exports["test Selection Listener on frame"] = function(assert, done) { selection.once("select", function() { assert.equal(selection.text, "fo"); + close(); + loader.unload(); done(); }); @@ -836,8 +844,7 @@ exports["test Selection Listener on frame"] = function(assert, done) { then(getFrameWindow). then(selectContentFirstDiv). then(dispatchSelectionEvent). - then(close). - then(loader.unload, assert.fail); + then(null, assert.fail); }; exports["test Textarea onSelect Listener on frame"] = function(assert, done) { @@ -846,6 +853,8 @@ exports["test Textarea onSelect Listener on frame"] = function(assert, done) { selection.once("select", function() { assert.equal(selection.text, "noodles"); + close(); + loader.unload(); done(); }); @@ -854,8 +863,7 @@ exports["test Textarea onSelect Listener on frame"] = function(assert, done) { then(getFrameWindow). then(selectTextarea). then(dispatchOnSelectEvent). - then(close). - then(loader.unload, assert.fail); + then(null, assert.fail); }; diff --git a/addon-sdk/source/test/test-system-events.js b/addon-sdk/source/test/test-system-events.js index 84cf477f7620..38d12af3c247 100644 --- a/addon-sdk/source/test/test-system-events.js +++ b/addon-sdk/source/test/test-system-events.js @@ -191,7 +191,7 @@ exports["test emit to nsIObserverService observers"] = function(assert) { "event.subject is notification subject"); assert.equal(lastData, customData, "event.data is notification data"); - nsIObserverService.removeObserver(nsIObserver, topic, false); + nsIObserverService.removeObserver(nsIObserver, topic); events.emit(topic, { data: "more data" }); diff --git a/addon-sdk/source/test/test-url.js b/addon-sdk/source/test/test-url.js index 9c82c752c130..6f7a2398bcd0 100644 --- a/addon-sdk/source/test/test-url.js +++ b/addon-sdk/source/test/test-url.js @@ -255,6 +255,12 @@ exports.testIsInvalidURI = function (test) { }); }; +exports.testURLFromURL = function(test) { + let aURL = url.URL('http://mozilla.org'); + let bURL = url.URL(aURL); + test.assertEqual(aURL.toString(), bURL.toString(), 'Making a URL from a URL works'); +}; + function validURIs() { return [ 'http://foo.com/blah_blah',