diff options
Diffstat (limited to 'tools/addon-sdk-1.4/packages')
283 files changed, 49601 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.4/packages/addon-kit/README.md b/tools/addon-sdk-1.4/packages/addon-kit/README.md new file mode 100644 index 0000000..9a68554 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/README.md @@ -0,0 +1,8 @@ +The addon-kit package provides high-level APIs for add-on developers. +Most of the needs of most add-on developers should be served by the modules +found here. Modules in this packages don't require any special privileges to +run. + +The modules in the addon-kit package are relatively stable. We intend to add +new APIs here and extend existing ones, but will avoid making incompatible +changes to them unless absolutely necessary. diff --git a/tools/addon-sdk-1.4/packages/addon-kit/data/moz_favicon.ico b/tools/addon-sdk-1.4/packages/addon-kit/data/moz_favicon.ico Binary files differnew file mode 100644 index 0000000..d444389 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/data/moz_favicon.ico diff --git a/tools/addon-sdk-1.4/packages/addon-kit/data/test-page-mod.html b/tools/addon-sdk-1.4/packages/addon-kit/data/test-page-mod.html new file mode 100644 index 0000000..9211698 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/data/test-page-mod.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>Page Mod test</title> +</head> +<body> + <p id="paragraph">Lorem ipsum dolor sit amet.</p> +</body> +</html> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/data/test-page-worker.html b/tools/addon-sdk-1.4/packages/addon-kit/data/test-page-worker.html new file mode 100644 index 0000000..e89a283 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/data/test-page-worker.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>Page Worker test</title> +</head> +<body> + <p id="paragraph">Lorem ipsum dolor sit amet.</p> +</body> +</html> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/data/test-page-worker.js b/tools/addon-sdk-1.4/packages/addon-kit/data/test-page-worker.js new file mode 100644 index 0000000..d8effb9 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/data/test-page-worker.js @@ -0,0 +1,25 @@ + +// get title directly +self.postMessage(["assertEqual", document.title, "Page Worker test", + "Correct page title accessed directly"]); + +// get <p> directly +let p = document.getElementById("paragraph"); +self.postMessage(["assert", !!p, "<p> can be accessed directly"]); +self.postMessage(["assertEqual", p.firstChild.nodeValue, + "Lorem ipsum dolor sit amet.", + "Correct text node expected"]); + +// Modify page +let div = document.createElement("div"); +div.setAttribute("id", "block"); +div.appendChild(document.createTextNode("Test text created")); +document.body.appendChild(div); + +// Check back the modification +div = document.getElementById("block"); +self.postMessage(["assert", !!div, "<div> can be accessed directly"]); +self.postMessage(["assertEqual", div.firstChild.nodeValue, + "Test text created", "Correct text node expected"]); +self.postMessage(["done"]); + diff --git a/tools/addon-sdk-1.4/packages/addon-kit/data/test-request-invalid.json b/tools/addon-sdk-1.4/packages/addon-kit/data/test-request-invalid.json new file mode 100644 index 0000000..35f9ff6 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/data/test-request-invalid.json @@ -0,0 +1 @@ +"this": "isn't JSON" diff --git a/tools/addon-sdk-1.4/packages/addon-kit/data/test-request.json b/tools/addon-sdk-1.4/packages/addon-kit/data/test-request.json new file mode 100644 index 0000000..8c850a5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/data/test-request.json @@ -0,0 +1 @@ +{ "foo": "bar" } diff --git a/tools/addon-sdk-1.4/packages/addon-kit/data/test-request.txt b/tools/addon-sdk-1.4/packages/addon-kit/data/test-request.txt new file mode 100644 index 0000000..390125d --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/data/test-request.txt @@ -0,0 +1 @@ +Look ma, no hands! diff --git a/tools/addon-sdk-1.4/packages/addon-kit/data/test.html b/tools/addon-sdk-1.4/packages/addon-kit/data/test.html new file mode 100644 index 0000000..39de477 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/data/test.html @@ -0,0 +1,8 @@ +<html> + <head> + <title>foo</title> + </head> + <body> + <p>bar</p> + </body> +</html> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/clipboard.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/clipboard.md new file mode 100644 index 0000000..7292017 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/clipboard.md @@ -0,0 +1,58 @@ +<!-- contributed by Dietrich Ayala [dietrich@mozilla.com] --> + +The `clipboard` module allows callers to interact with the system clipboard, +setting and retrieving its contents. + +You can optionally specify the type of the data to set and retrieve. +The following types are supported: + +* `text` (plain text) +* `html` (a string of HTML) + +If no data type is provided, then the module will detect it for you. + +Examples +-------- + +Set and get the contents of the clipboard. + + let clipboard = require("clipboard"); + clipboard.set("Lorem ipsum dolor sit amet"); + let contents = clipboard.get(); + +Set the clipboard contents to some HTML. + + let clipboard = require("clipboard"); + clipboard.set("<blink>Lorem ipsum dolor sit amet</blink>", "html"); + +If the clipboard contains HTML content, open it in a new tab. + + let clipboard = require("clipboard"); + if (clipboard.currentFlavors.indexOf("html") != -1) + require("tabs").open("data:text/html," + clipboard.get("html")); + +<api name="set"> +@function + Replace the contents of the user's clipboard with the provided data. +@param data {string} + The data to put on the clipboard. +@param [datatype] {string} + The type of the data (optional). +</api> + +<api name="get"> +@function + Get the contents of the user's clipboard. +@param [datatype] {string} + Retrieve the clipboard contents only if matching this type (optional). + The function will return null if the contents of the clipboard do not match + the supplied type. +</api> + +<api name="currentFlavors"> +@property {array} + Data on the clipboard is sometimes available in multiple types. For example, + HTML data might be available as both a string of HTML (the `html` type) + and a string of plain text (the `text` type). This function returns an array + of all types in which the data currently on the clipboard is available. +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/context-menu.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/context-menu.md new file mode 100644 index 0000000..f2bb5b3 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/context-menu.md @@ -0,0 +1,702 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `context-menu` module lets you add items to Firefox's page context menu. + + +Introduction +------------ + +The `context-menu` API provides a simple, declarative way to add items to the +page's context menu. You can add items that perform an action when clicked, +submenus, and menu separators. + +Instead of manually adding items when particular contexts occur and then +removing them when those contexts go away, you *bind* items to contexts, and the +adding and removing is automatically handled for you. Items are bound to +contexts in much the same way that event listeners are bound to events. When +the user invokes the context menu, all of the items bound to the current context +are automatically added to the menu. If no items are bound, none are added. +Likewise, any items that were previously in the menu but are not bound to the +current context are automatically removed from the menu. You never need to +manually remove your items from the menu unless you want them to never appear +again. + +For example, if your add-on needs to add a context menu item whenever the +user visits a certain page, don't create the item when that page loads, and +don't remove it when the page unloads. Rather, create your item only once and +supply a context that matches the target URL. + + +Specifying Contexts +------------------- + +As its name implies, the context menu should be reserved for the occurrence of +specific contexts. Contexts can be related to page content or the page itself, +but they should never be external to the page. + +For example, a good use of the menu would be to show an "Edit Image" item when +the user right-clicks an image in the page. A bad use would be to show a +submenu that listed all the user's tabs, since tabs aren't related to the page +or the node the user clicked to open the menu. + +### The Page Context + +First of all, you may not need to specify a context at all. When an item does +not specify a context, the page context applies. + +The *page context* occurs when the user invokes the context menu on a +non-interactive portion of the page. Try right-clicking a blank spot in this +page, or on text. Make sure that no text is selected. The menu that appears +should contain the items "Back", "Forward", "Reload", "Stop", and so on. This +is the page context. + +The page context is appropriate when your item acts on the page as a whole. It +does not occur when the user invokes the context menu on a link, image, or other +non-text node, or while a selection exists. + +### Declarative Contexts + +You can specify some simple, declarative contexts when you create a menu item by +setting the `context` property of the options object passed to its constructor, +like this: + + var cm = require("context-menu"); + cm.Item({ + label: "My Menu Item", + context: cm.URLContext("*.mozilla.org") + }); + +These contexts may be specified by calling the following constructors. Each is +exported by the `context-menu` module. + +<table> + <tr> + <th>Constructor</th> + <th>Description</th> + </tr> + <tr> + <td><code> + PageContext() + </code></td> + <td> + The page context. + </td> + </tr> + <tr> + <td><code> + SelectionContext() + </code></td> + <td> + This context occurs when the menu is invoked on a page in which the user + has made a selection. + </td> + </tr> + <tr> + <td><code> + SelectorContext(selector) + </code></td> + <td> + This context occurs when the menu is invoked on a node that either matches + <code>selector</code>, a CSS selector, or has an ancestor that matches. + <code>selector</code> may include multiple selectors separated by commas, + e.g., <code>"a[href], img"</code>. + </td> + </tr> + <tr> + <td><code> + URLContext(matchPattern) + </code></td> + <td> + This context occurs when the menu is invoked on pages with particular + URLs. <code>matchPattern</code> is a match pattern string or an array of + match pattern strings. When <code>matchPattern</code> is an array, the + context occurs when the menu is invoked on a page whose URL matches any of + the patterns. These are the same match pattern strings that you use with + the <a href="packages/addon-kit/docs/page-mod.html"><code>page-mod</code></a> + <code>include</code> property. + <a href="packages/api-utils/docs/match-pattern.html">Read more about patterns</a>. + </td> + </tr> + <tr> + <td> + array + </td> + <td> + An array of any of the other types. This context occurs when all contexts + in the array occur. + </td> + </tr> +</table> + +Menu items also have a `context` property that can be used to add and remove +declarative contexts after construction. For example: + + var context = require("context-menu").SelectorContext("img"); + myMenuItem.context.add(context); + myMenuItem.context.remove(context); + +When a menu item is bound to more than one context, it appears in the menu when +all of those contexts occur. + +### In Content Scripts + +The declarative contexts are handy but not very powerful. For instance, you +might want your menu item to appear for any page that has at least one image, +but declarative contexts won't help you there. + +When you need more control control over the context in which your menu items are +shown, you can use content scripts. Like other APIs in the SDK, the +`context-menu` API uses +[content scripts](dev-guide/addon-development/web-content.html) to let your +add-on interact with pages in the browser. Each menu item you create in the +top-level context menu can have a content script. + +A special event named `"context"` is emitted in your content scripts whenever +the context menu is about to be shown. If you register a listener function for +this event and it returns true, the menu item associated with the listener's +content script is shown in the menu. + +For example, this item appears whenever the context menu is invoked on a page +that contains at least one image: + + require("context-menu").Item({ + label: "This Page Has Images", + contentScript: 'self.on("context", function (node) {' + + ' return !!document.querySelector("img");' + + '});' + }); + +Note that the listener function has a parameter called `node`. This is the node +in the page that the user context-clicked to invoke the menu. You can use it to +determine whether your item should be shown. + +You can both specify declarative contexts and listen for contexts in a content +script. In that case, the declarative contexts are evaluated first. If they +are not current, then your context listener is never called. + +This example takes advantage of that fact. The listener can be assured that +`node` will always be an image: + + require("context-menu").Item({ + label: "A Mozilla Image", + context: contextMenu.SelectorContext("img"), + contentScript: 'self.on("context", function (node) {' + + ' return /mozilla/.test(node.src);' + + '});' + }); + +Your item is shown only when all declarative contexts are current and your +context listener returns true. + + +Handling Menu Item Clicks +------------------------- + +In addition to using content scripts to listen for the `"context"` event as +described above, you can use content scripts to handle item clicks. When the +user clicks your menu item, an event named `"click"` is emitted in the item's +content script. + +Therefore, to handle an item click, listen for the `"click"` event in that +item's content script like so: + + require("context-menu").Item({ + label: "My Item", + contentScript: 'self.on("click", function (node, data) {' + + ' console.log("Item clicked!");' + + '});' + }); + +Note that the listener function has parameters called `node` and `data`. `node` +is the node that the user context-clicked to invoke the menu. You can use it +when performing some action. `data` is the `data` property of the menu item +that was clicked. Since only top-level menu items have content scripts, this +comes in handy for determining which item in a `Menu` was clicked: + + var cm = require("context-menu"); + cm.Menu({ + label: "My Menu", + contentScript: 'self.on("click", function (node, data) {' + + ' console.log("You clicked " + data);' + + '});', + items: [ + cm.Item({ label: "Item 1", data: "item1" }), + cm.Item({ label: "Item 2", data: "item2" }), + cm.Item({ label: "Item 3", data: "item3" }) + ] + }); + +Often you will need to collect some kind of information in the click listener +and perform an action unrelated to content. To communicate to the menu item +associated with the content script, the content script can call the +`postMessage` function attached to the global `self` object, passing it some +JSON-able data. The menu item's `"message"` event listener will be called with +that data. + + require("context-menu").Item({ + label: "Edit Image", + context: contextMenu.SelectorContext("img"), + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage(node.src);' + + '});', + onMessage: function (imgSrc) { + openImageEditor(imgSrc); + } + }); + + +Updating a Menu Item's Label +---------------------------- + +Each menu item must be created with a label, but you can change its label later +using a couple of methods. + +The simplest method is to set the menu item's `label` property. This example +updates the item's label based on the number of times it's been clicked: + + var numClicks = 0; + var myItem = require("context-menu").Item({ + label: "Click Me: " + numClicks, + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + numClicks++; + this.label = "Click Me: " + numClicks; + // Setting myItem.label is equivalent. + } + }); + +Sometimes you might want to update the label based on the context. For +instance, if your item performs a search with the user's selected text, it would +be nice to display the text in the item to provide feedback to the user. In +these cases you can use the second method. Recall that your content scripts can +listen for the `"context"` event and if your listeners return true, the items +associated with the content scripts are shown in the menu. In addition to +returning true, your `"context"` listeners can also return strings. When a +`"context"` listener returns a string, it becomes the item's new label. + +This item implements the aforementioned search example: + + var cm = require("context-menu"); + cm.Item({ + label: "Search Google", + context: cm.SelectionContext(), + contentScript: 'self.on("context", function () {' + + ' var text = window.getSelection().toString();' + + ' if (text.length > 20)' + + ' text = text.substr(0, 20) + "...";' + + ' return "Search Google for " + text;' + + '});' + }); + +The `"context"` listener gets the window's current selection, truncating it if +it's too long, and includes it in the returned string. When the item is shown, +its label will be "Search Google for `text`", where `text` is the truncated +selection. + + +More Examples +------------- + +For conciseness, these examples create their content scripts as strings and use +the `contentScript` property. In your own add-on, you will probably want to +create your content scripts in separate files and pass their URLs using the +`contentScriptFile` property. See +[Working with Content Scripts](dev-guide/addon-development/web-content.html) +for more information. + +Show an "Edit Page Source" item when the user right-clicks a non-interactive +part of the page: + + require("context-menu").Item({ + label: "Edit Page Source", + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage(document.URL);' + + '});', + onMessage: function (pageURL) { + editSource(pageURL); + } + }); + +Show an "Edit Image" item when the menu is invoked on an image: + + require("context-menu").Item({ + label: "Edit Image", + context: contextMenu.SelectorContext("img"), + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage(node.src);' + + '});', + onMessage: function (imgSrc) { + openImageEditor(imgSrc); + } + }); + +Show an "Edit Mozilla Image" item when the menu is invoked on an image in a +mozilla.org or mozilla.com page: + + var cm = require("context-menu"); + cm.Item({ + label: "Edit Mozilla Image", + context: [ + cm.URLContext(["*.mozilla.org", "*.mozilla.com"]), + cm.SelectorContext("img") + ], + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage(node.src);' + + '});', + onMessage: function (imgSrc) { + openImageEditor(imgSrc); + } + }); + +Show an "Edit Page Images" item when the page contains at least one image: + + require("context-menu").Item({ + label: "Edit Page Images", + // This ensures the item only appears during the page context. + context: contextMenu.PageContext(), + contentScript: 'self.on("context", function (node) {' + + ' var pageHasImgs = !!document.querySelector("img");' + + ' return pageHasImgs;' + + '});' + + 'self.on("click", function (node, data) {' + + ' var imgs = document.querySelectorAll("img");' + + ' var imgSrcs = [];' + + ' for (var i = 0 ; i < imgs.length; i++)' + + ' imgSrcs.push(imgs[i].src);' + + ' self.postMessage(imgSrcs);' + + '});', + onMessage: function (imgSrcs) { + openImageEditor(imgSrcs); + } + }); + +Show a "Search With" menu when the user right-clicks an anchor that searches +Google or Wikipedia with the text contained in the anchor: + + var cm = require("context-menu"); + var googleItem = cm.Item({ + label: "Google", + data: "http://www.google.com/search?q=" + }); + var wikipediaItem = cm.Item({ + label: "Wikipedia", + data: "http://en.wikipedia.org/wiki/Special:Search?search=" + }); + var searchMenu = cm.Menu({ + label: "Search With", + context: contextMenu.SelectorContext("a[href]"), + contentScript: 'self.on("click", function (node, data) {' + + ' var searchURL = data + node.textContent;' + + ' window.location.href = searchURL;' + + '});', + items: [googleItem, wikipediaItem] + }); + + +<api name="Item"> +@class +A labeled menu item that can perform an action when clicked. +<api name="Item"> +@constructor + Creates a labeled menu item that can perform an action when clicked. +@param options {object} + An object with the following keys: + @prop label {string} + The item's label. It must either be a string or an object that implements + `toString()`. + @prop [image] {string} + The item's icon, a string URL. The URL can be remote, a reference to an + image in the add-on's `data` directory, or a data URI. + @prop [data] {string} + An optional arbitrary value to associate with the item. It must be either a + string or an object that implements `toString()`. It will be passed to + click listeners. + @prop [context] {value} + If the item is contained in the top-level context menu, this declaratively + specifies the context under which the item will appear; see Specifying + Contexts above. Ignored if the item is contained in a submenu. + @prop [contentScript] {string,array} + If the item is contained in the top-level context menu, this is the content + script or an array of content scripts that the item can use to interact with + the page. Ignored if the item is contained in a submenu. + @prop [contentScriptFile] {string,array} + If the item is contained in the top-level context menu, this is the local + file URL of the content script or an array of such URLs that the item can + use to interact with the page. Ignored if the item is contained in a + submenu. + @prop [onMessage] {function} + If the item is contained in the top-level context menu, this function will + be called when the content script calls `self.postMessage`. It will be + passed the data that was passed to `postMessage`. Ignored if the item is + contained in a submenu. +</api> + +<api name="label"> +@property {string} + The menu item's label. You can set this after creating the item to update its + label later. +</api> + +<api name="image"> +@property {string} + The item's icon, a string URL. The URL can be remote, a reference to an image + in the add-on's `data` directory, or a data URI. You can set this after + creating the item to update its image later. To remove the item's image, set + it to `null`. +</api> + +<api name="data"> +@property {string} + An optional arbitrary value to associate with the item. It must be either a + string or an object that implements `toString()`. It will be passed to + click listeners. You can set this after creating the item to update its data + later. +</api> + +<api name="context"> +@property {list} + A list of declarative contexts for which the menu item will appear in the + context menu. Contexts can be added by calling `context.add()` and removed by + called `context.remove()`. This property is meaningful only for items + contained in the top-level context menu. +</api> + +<api name="parentMenu"> +@property {Menu} + The item's parent `Menu`, or `null` if the item is contained in the top-level + context menu. This property is read-only. To add the item to a new menu, + call that menu's `addItem()` method. +</api> + +<api name="contentScript"> +@property {string,array} + The content script or the array of content scripts associated with the menu + item during creation. This property is meaningful only for items contained in + the top-level context menu. +</api> + +<api name="contentScriptFile"> +@property {string,array} + The URL of a content script or the array of such URLs associated with the menu + item during creation. This property is meaningful only for items contained in + the top-level context menu. +</api> + +<api name="destroy"> +@method + Permanently removes the item from its parent menu and frees its resources. + The item must not be used afterward. If you need to remove the item from its + parent menu but use it afterward, call `removeItem()` on the parent menu + instead. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this menu item. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the menu item's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/addon-development/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +</api> + +<api name="Menu"> +@class +A labeled menu item that expands into a submenu. + +<api name="Menu"> +@constructor + Creates a labeled menu item that expands into a submenu. +@param options {object} + An object with the following keys: + @prop label {string} + The item's label. It must either be a string or an object that implements + `toString()`. + @prop items {array} + An array of menu items that the menu will contain. Each must be an `Item`, + `Menu`, or `Separator`. + @prop [image] {string} + The menu's icon, a string URL. The URL can be remote, a reference to an + image in the add-on's `data` directory, or a data URI. + @prop [context] {value} + If the menu is contained in the top-level context menu, this declaratively + specifies the context under which the menu will appear; see Specifying + Contexts above. Ignored if the menu is contained in a submenu. + @prop [contentScript] {string,array} + If the menu is contained in the top-level context menu, this is the content + script or an array of content scripts that the menu can use to interact with + the page. Ignored if the menu is contained in a submenu. + @prop [contentScriptFile] {string,array} + If the menu is contained in the top-level context menu, this is the local + file URL of the content script or an array of such URLs that the menu can + use to interact with the page. Ignored if the menu is contained in a + submenu. + @prop [onMessage] {function} + If the menu is contained in the top-level context menu, this function will + be called when the content script calls `self.postMessage`. It will be + passed the data that was passed to `postMessage`. Ignored if the item is + contained in a submenu. +</api> + +<api name="label"> +@property {string} + The menu's label. You can set this after creating the menu to update its + label later. +</api> + +<api name="items"> +@property {array} + An array containing the items in the menu. The array is read-only, meaning + that modifications to it will not affect the menu. However, setting this + property to a new array will replace all the items currently in the menu with + the items in the new array. +</api> + +<api name="image"> +@property {string} + The menu's icon, a string URL. The URL can be remote, a reference to an image + in the add-on's `data` directory, or a data URI. You can set this after + creating the menu to update its image later. To remove the menu's image, set + it to `null`. +</api> + +<api name="context"> +@property {list} + A list of declarative contexts for which the menu will appear in the context + menu. Contexts can be added by calling `context.add()` and removed by called + `context.remove()`. This property is meaningful only for menus contained in + the top-level context menu. +</api> + +<api name="parentMenu"> +@property {Menu} + The menu's parent `Menu`, or `null` if the menu is contained in the top-level + context menu. This property is read-only. To add the menu to a new menu, + call that menu's `addItem()` method. +</api> + +<api name="contentScript"> +@property {string,array} + The content script or the array of content scripts associated with the menu + during creation. This property is meaningful only for menus contained in the + top-level context menu. +</api> + +<api name="contentScriptFile"> +@property {string,array} + The URL of a content script or the array of such URLs associated with the menu + during creation. This property is meaningful only for menus contained in the + top-level context menu. +</api> + +<api name="addItem"> +@method + Appends a menu item to the end of the menu. If the item is already contained + in another menu or in the top-level context menu, it's automatically removed + first. +@param item {Item,Menu,Separator} + The `Item`, `Menu`, or `Separator` to add to the menu. +</api> + +<api name="removeItem"> +@method + Removes the given menu item from the menu. If the menu does not contain the + item, this method does nothing. +@param item {Item,Menu,Separator} + The menu item to remove from the menu. +</api> + +<api name="destroy"> +@method + Permanently removes the menu from its parent menu and frees its resources. + The menu must not be used afterward. If you need to remove the menu from its + parent menu but use it afterward, call `removeItem()` on the parent menu + instead. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this menu item. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the menu item's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/addon-development/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +</api> + +<api name="Separator"> +@class +A menu separator. Separators can be contained only in `Menu`s, not in the +top-level context menu. + +<api name="Separator"> +@constructor + Creates a menu separator. +</api> + +<api name="parentMenu"> +@property {Menu} + The separator's parent `Menu`. This property is read-only. To add the + separator to a new menu, call that menu's `addItem()` method. +</api> + +<api name="destroy"> +@method + Permanently removes the separator from its parent menu and frees its + resources. The separator must not be used afterward. If you need to remove + the separator from its parent menu but use it afterward, call `removeItem()` + on the parent menu instead. +</api> + +</api> + +<api name="PageContext"> +@class +<api name="PageContext"> +@constructor + Creates a page context. See Specifying Contexts above. +</api> +</api> + +<api name="SelectionContext"> +@class +<api name="SelectionContext"> +@constructor + Creates a context that occurs when a page contains a selection. See + Specifying Contexts above. +</api> +</api> + +<api name="SelectorContext"> +@class +<api name="SelectorContext"> +@constructor + Creates a context that matches a given CSS selector. See Specifying Contexts + above. +@param selector {string} + A CSS selector. +</api> +</api> + +<api name="URLContext"> +@class +<api name="URLContext"> +@constructor + Creates a context that matches pages with particular URLs. See Specifying + Contexts above. +@param matchPattern {string,array} + A [match pattern](packages/api-utils/docs/match-pattern.html) string or an array of + match pattern strings. +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/hotkeys.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/hotkeys.md new file mode 100644 index 0000000..dfbe572 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/hotkeys.md @@ -0,0 +1,74 @@ +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +Some add-ons may wish to define keyboard shortcuts for certain operations. This +module exposes an API to create those. + +<api name="Hotkey"> +@class + +Module exports `Hotkey` constructor allowing users to create a `hotkey` for the +host application. + +<api name="Hotkey"> +@constructor +Creates a hotkey who's `onPress` listener method is invoked when key combination +defined by `hotkey` is pressed. + +Please note: If more than one `hotkey` is created for the same key +combination, the listener is executed only on the last one created + +@param options {Object} + Options for the hotkey, with the following keys: + +@prop combo {String} +Any function key: `"f1, f2, ..., f24"` or key combination in the format +of `'modifier-key'`: + + "accel-s" + "meta-shift-i" + "control-alt-d" + +Modifier keynames: + +- **shift**: The Shift key. +- **alt**: The Alt key. On the Macintosh, this is the Option key. On + Macintosh this can only be used in conjunction with another modifier, + since `Alt-Letter` combinations are reserved for entering special + characters in text. +- **meta**: The Meta key. On the Macintosh, this is the Command key. +- **control**: The Control key. +- **accel**: The key used for keyboard shortcuts on the user's platform, + which is Control on Windows and Linux, and Command on Mac. Usually, this + would be the value you would use. + +@prop onPress {Function} +Function that is invoked when the key combination `hotkey` is pressed. + +</api> +<api name="destroy"> +@method +Stops this instance of `Hotkey` from reacting on the key combinations. Once +destroyed it can no longer be used. +</api> +</api> + +## Example ## + + // Define keyboard shortcuts for showing and hiding a custom panel. + var { Hotkey } = require("hotkeys"); + + var showHotKey = Hotkey({ + combo: "accel-shift-o", + onPress: function() { + showMyPanel(); + } + }); + var hideHotKey = Hotkey({ + combo: "accel-alt-shift-o", + onPress: function() { + hideMyPanel(); + } + }); + +[Mozilla keyboard planning FAQ]:http://www.mozilla.org/access/keyboard/ +[keyboard shortcuts]:https://developer.mozilla.org/en/XUL_Tutorial/Keyboard_Shortcuts diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/notifications.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/notifications.md new file mode 100644 index 0000000..5e76c1b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/notifications.md @@ -0,0 +1,60 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> + +The `notifications` module allows you to display transient, +[toaster](http://en.wikipedia.org/wiki/Toast_%28computing%29)-style +desktop messages to the user. + +This API supports desktop notifications on Windows, OS X using +[Growl](http://growl.info/), and Linux using libnotify. If the user's system +does not support desktop notifications or if its notifications service is not +running, then notifications made with this API are logged to Firefox's error +console and, if the user launched Firefox from the command line, the terminal. + +Examples +-------- + +Here's a typical example. When the message is clicked, a string is logged to +the console. + + var notifications = require("notifications"); + notifications.notify({ + title: "Jabberwocky", + text: "'Twas brillig, and the slithy toves", + data: "did gyre and gimble in the wabe", + onClick: function (data) { + console.log(data); + // console.log(this.data) would produce the same result. + } + }); + +This one displays an icon that's stored in the add-on's `data` directory. (See +the [`self`](packages/addon-kit/docs/self.html) module documentation for more information.) + + var notifications = require("notifications"); + var self = require("self"); + var myIconURL = self.data.url("myIcon.png"); + notifications.notify({ + text: "I have an icon!", + iconURL: myIconURL + }); + + +<api name="notify"> +@function + Displays a transient notification to the user. +@param options {object} + An object with the following keys. Each is optional. + @prop [title] {string} + A string to display as the message's title. + @prop [text] {string} + A string to display as the body of the message. + @prop [iconURL] {string} + The URL of an icon to display inside the message. It may be a remote URL, + a data URI, or a URL returned by the [`self`](packages/addon-kit/docs/self.html) + module. + @prop [onClick] {function} + A function to be called when the user clicks the message. It will be passed + the value of `data`. + @prop [data] {string} + A string that will be passed to `onClick`. +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/page-mod.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/page-mod.md new file mode 100644 index 0000000..e523d48 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/page-mod.md @@ -0,0 +1,367 @@ +<!-- contributed by Nickolay Ponomarev [asqueella@gmail.com] --> +<!-- contributed by Myk Melez [myk@mozilla.org] --> +<!-- contributed by Irakli Gozalishvil [gozala@mozilla.com] --> + +Overview +-------- +The page-mod module enables add-on developers to execute scripts in the context +of specific web pages. Most obviously you could use page-mod to dynamically +modify the content of certain pages. + +The module exports a constructor function `PageMod` which creates a new page +modification (or "mod" for short). + +A page mod does not modify its pages until those pages are loaded or reloaded. +In other words, if your add-on is loaded while the user's browser is open, the +user will have to reload any open pages that match the mod for the mod to affect +them. + +To stop a page mod from making any more modifications, call its `destroy` +method. + +Like all modules that interact with web content, page-mod uses content +scripts that execute in the content process and defines a messaging API to +communicate between the content scripts and the main add-on script. For more +details on content scripting see the tutorial on [interacting with web +content](dev-guide/addon-development/web-content.html). + +To create a PageMod the add-on developer supplies: + +* a set of rules to select the desired subset of web pages based on their URL. +Each rule is specified using the +[match-pattern](packages/api-utils/docs/match-pattern.html) syntax. + +* a set of content scripts to execute in the context of the desired pages. + +* a value for the onAttach option: this value is a function which will be +called when a page is loaded that matches the ruleset. This is used to set up a +communication channel between the add-on code and the content script. + +All these parameters are optional except for the ruleset, which must include +at least one rule. + +The following add-on displays an alert whenever a page matching the ruleset is +loaded: + + var pageMod = require("page-mod"); + pageMod.PageMod({ + include: "*.org", + contentScript: 'window.alert("Page matches ruleset");' + }); + +If you specify a value of "ready" or "end" for `contentScriptWhen`, +as opposed to "start", +then the content script can interact with the DOM itself: + + var pageMod = require("page-mod"); + pageMod.PageMod({ + include: "*.org", + contentScriptWhen: 'end', + contentScript: 'document.body.innerHTML = ' + + ' "<h1>Page matches ruleset</h1>";' + }); + +### Using `contentScriptFile` ### + +Most of the examples in this page define content scripts as strings, +and use the `contentScript` option to assign them to page mods. + +In your code you will more often create content scripts in separate files +under your add-on's `data` directory. Then you can use the +[`self`](packages/addon-kit/docs/self.html) module to retrieve a URL pointing +to the file, and assign this to the page-mod's `contentScriptFile` +property. + +For example, if you save the content script +file in your `data` directory as "myScript.js", you would assign it using +code like: + + var data = require("self").data; + + var pageMod = require("page-mod"); + pageMod.PageMod({ + include: "*.org", + contentScriptWhen: 'end', + contentScriptFile: data.url("myScript.js") + }); + +## Communicating With Content Scripts ## + +When a matching page is loaded the `PageMod` will call the function that the +add-on code supplied to `onAttach`. The `PageMod` supplies one argument to +this function: a `worker` object. + +The worker can be thought of as the add-on's end of +a communication channel between the add-on code and the content scripts that +have been attached to this page. + +Thus the add-on can pass messages to the content scripts by calling the +worker's `postMessage` function and can receive messages from the content +scripts by registering a function as a listener to the worker's `on` function. + +Note that if multiple matching pages are loaded simultaneously then each page +is loaded into its own execution context with its own copy of the content +scripts. In this case `onAttach` is called once for each loaded page, and the +add-on code will have a separate worker for each page: + +![Multiple workers](static-files/media/multiple-workers.jpg) + +This is demonstrated in the following example: + + var pageMod = require("page-mod"); + var tabs = require("tabs"); + + var workers = []; + + pageMod.PageMod({ + include: ["http://www.mozilla*"], + contentScriptWhen: 'end', + contentScript: "onMessage = function onMessage(message) {" + + " window.alert(message);};", + onAttach: function onAttach(worker) { + if (workers.push(worker) == 3) { + workers[0].postMessage("The first worker!"); + workers[1].postMessage("The second worker!"); + workers[2].postMessage("The third worker!"); + } + } + }); + + tabs.open("http://www.mozilla.com"); + tabs.open("http://www.mozilla.org"); + tabs.open("http://www.mozilla-europe.org"); + +Here we specify a ruleset to match any URLs starting with +"http://www.mozilla". When a page matches we add the supplied worker to +an array, and when we have three workers in the array we send a message to +each worker in turn, telling it the order in which it was attached. The +worker just displays the message in an alert box. + +This shows that separate pages execute in separate contexts and that each +context has its own communication channel with the add-on script. + +Note though that while there is a separate worker for each execution context, +the worker is shared across all the content scripts associated with a single +execution context. In the following example we pass two content scripts into +the `PageMod`: these content scripts will share a worker instance. + +In the example each content script identifies itself to the add-on script +by sending it a message using the global `postMessage` function. In the +`onAttach` function the add-on code logs the fact that a new page is +attached and registers a listener function that simply logs the message: + + + var pageMod = require("page-mod"); + var data = require("self").data; + var tabs = require("tabs"); + + pageMod.PageMod({ + include: ["http://www.mozilla*"], + contentScriptWhen: 'end', + contentScript: ["postMessage('Content script 1 is attached to '+ " + + "document.URL);", + "postMessage('Content script 2 is attached to '+ " + + "document.URL);"], + onAttach: function onAttach(worker) { + console.log("Attaching content scripts") + worker.on('message', function(data) { + console.log(data); + }); + } + }); + + tabs.open("http://www.mozilla.com"); + +The console output of this add-on is: + +<pre> + info: Attaching content scripts + info: Content script 1 is attached to http://www.mozilla.com/en-US/ + info: Content script 2 is attached to http://www.mozilla.com/en-US/ +</pre> + +### Mapping workers to tabs ### + +The [`worker`](packages/api-utils/docs/content/worker.html) has a `tab` +property which returns the tab associated with this worker. You can use this +to access the [`tabs API`](packages/addon-kit/docs/tabs.html) for the tab +associated with a specific page: + + var pageMod = require("page-mod"); + var tabs = require("tabs"); + + pageMod.PageMod({ + include: ["*"], + onAttach: function onAttach(worker) { + console.log(worker.tab.title); + } + }); + +### Attaching content scripts to tabs ### + +We've seen that the page mod API attaches content scripts to pages based on +their URL. Sometimes, though, we don't care about the URL: we just want +to execute a script on demand in the context of a particular tab. + +For example, we might want to run a script in the context of the currently +active tab when the user clicks a widget: to block certain content, to +change the font style, or to display the page's DOM structure. + +Using the `attach` method of the [`tab`](packages/addon-kit/docs/tabs.html) +object, you can attach a set of content scripts to a particular tab. The +scripts are executed immediately. + +The following add-on creates a widget which, when clicked, highlights all the +`div` elements in the page loaded into the active tab: + + var widgets = require("widget"); + var tabs = require("tabs"); + + var widget = widgets.Widget({ + id: "div-show", + label: "Show divs", + contentURL: "http://www.mozilla.org/favicon.ico", + onClick: function() { + tabs.activeTab.attach({ + contentScript: + 'var divs = document.getElementsByTagName("div");' + + 'for (var i = 0; i < divs.length; ++i) {' + + 'divs[i].setAttribute("style", "border: solid red 1px;");' + + '}' + }); + } + }); + +## Destroying Workers ## + +Workers generate a `detach` event when their associated page is closed: that +is, when the tab is closed or the tab's location changes. If +you are maintaining a list of workers belonging to a page mod, you can use +this event to remove workers that are no longer valid. + +For example, if you maintain a list of workers attached to a page mod: + + var workers = []; + + var pageMod = require("page-mod").PageMod({ + include: ['*'], + contentScriptWhen: 'ready', + contentScriptFile: data.url('pagemod.js'), + onAttach: function(worker) { + workers.push(worker); + } + }); + +You can remove workers when they are no longer valid by listening to `detach`: + + var workers = []; + + function detachWorker(worker, workerArray) { + var index = workerArray.indexOf(worker); + if(index != -1) { + workerArray.splice(index, 1); + } + } + + var pageMod = require("page-mod").PageMod({ + include: ['*'], + contentScriptWhen: 'ready', + contentScriptFile: data.url('pagemod.js'), + onAttach: function(worker) { + workers.push(worker); + worker.on('detach', function () { + detachWorker(this, workers); + }); + } + }); + +<api name="PageMod"> +@class +A PageMod object. Once activated a page mod will execute the supplied content +scripts in the context of any pages matching the pattern specified by the +'include' property. +<api name="PageMod"> +@constructor +Creates a PageMod. +@param options {object} + Options for the PageMod, with the following keys: + @prop include {string,array} + A match pattern string or an array of match pattern strings. These define + the pages to which the PageMod applies. See the + [match-pattern](packages/api-utils/docs/match-pattern.html) module for + a description of match pattern syntax. + At least one match pattern must be supplied. + + @prop [contentScriptFile] {string,array} + The local file URLs of content scripts to load. Content scripts specified + by this option are loaded *before* those specified by the `contentScript` + option. Optional. + @prop [contentScript] {string,array} + The texts of content scripts to load. Content scripts specified by this + option are loaded *after* those specified by the `contentScriptFile` option. + Optional. + @prop [contentScriptWhen="end"] {string} + When to load the content scripts. This may take one of the following + values: + + * "start": load content scripts immediately after the document + element for the page is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the page has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + + This property is optional and defaults to "end". + + @prop [onAttach] {function} +A function to call when the PageMod attaches content scripts to +a matching page. The function will be called with one argument, a `worker` +object which the add-on script can use to communicate with the content scripts +attached to the page in question. + +</api> + +<api name="include"> +@property {List} +A [list](packages/api-utils/docs/list.html) of match pattern strings. These +define the pages to which the page mod applies. See the +[match-pattern](packages/api-utils/docs/match-pattern.html) module for a +description of match patterns. Rules can be added to the list by calling its +`add` method and removed by calling its `remove` method. + +</api> + +<api name="destroy"> +@method +Stops the page mod from making any more modifications. Once destroyed the page +mod can no longer be used. Note that modifications already made to open pages +will not be undone. +</api> + +<api name="attach"> +@event +This event is emitted this event when the page-mod's content scripts are +attached to a page whose URL matches the page-mod's `include` filter. + +@argument {Worker} +The listener function is passed a [`Worker`](packages/api-utils/docs/content/worker.html) object that can be used to communicate +with any content scripts attached to this page. +</api> + +<api name="error"> +@event +This event is emitted when an uncaught runtime error occurs in one of the page +mod's content scripts. + +@argument {Error} +Listeners are passed a single argument, the +[Error](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error) +object. +</api> + +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/page-worker.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/page-worker.md new file mode 100644 index 0000000..06097ae --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/page-worker.md @@ -0,0 +1,218 @@ +<!-- contributed by Felipe Gomes [felipc@gmail.com] --> + +The `page-worker` module provides a way to create a permanent, invisible page +and access its DOM. + +Introduction +------------ + +The module exports a constructor function `Page`, which constructs a new page +worker. A page worker may be destroyed, after which its memory is freed, and +you must create a new instance to load another page. + +Page workers have associated content scripts, which are JavaScript scripts that +have access to the content loaded into the pages. You can specify scripts to +load for a page worker, and you communicate with those scripts over an +asynchronous JSON pipe. For more information on content scripts, see +[Working with Content Scripts](dev-guide/addon-development/web-content.html). + +Examples +-------- + +For conciseness, these examples create their content scripts as strings and use +the `contentScript` property. In your own add-ons, you will probably want to +create your content scripts in separate files and pass their URLs using the +`contentScriptFile` property. See +[Working with Content Scripts](dev-guide/addon-development/web-content.html) +for more information. + +### Print all header titles from a Wikipedia article ### + + var pageWorkers = require("page-worker"); + + // This content script sends header titles from the page to the add-on: + var script = "var elements = document.querySelectorAll('h2 > span'); " + + "for (var i = 0; i < elements.length; i++) { " + + " postMessage(elements[i].textContent) " + + "}"; + + // Create a page worker that loads Wikipedia: + pageWorkers.Page({ + contentURL: "http://en.wikipedia.org/wiki/Internet", + contentScript: script, + contentScriptWhen: "ready", + onMessage: function(message) { + console.log(message); + } + }); + +The page worker's "message" event listener, specified by `onMessage`, will print +all the titles it receives from the content script. + +<api name="Page"> +@class +A `Page` object loads the page specified by its `contentURL` option and +executes any content scripts that have been supplied to it in the +`contentScript` and `contentScriptFile` options. + +The page is not displayed to the user. + +The page worker is loaded as soon as the `Page` object is created and stays +loaded until its `destroy` method is called or the add-on is unloaded. + +<api name="Page"> +@constructor + Creates an uninitialized page worker instance. +@param [options] {object} + The *`options`* parameter is optional, and if given it should be an object + with any of the following keys: + @prop [contentURL] {string} + The URL of the content to load in the panel. + @prop [allow] {object} + An object with keys to configure the permissions on the page worker. The + boolean key `script` controls if scripts from the page are allowed to run. + `script` defaults to true. + @prop [contentScriptFile] {string,array} + A local file URL or an array of local file URLs of content scripts to load. + Content scripts specified by this option are loaded *before* those specified + by the `contentScript` option. See + [Working with Content Scripts](dev-guide/addon-development/web-content.html) + for help on setting this property. + @prop [contentScript] {string,array} + A string or an array of strings containing the texts of content scripts to + load. Content scripts specified by this option are loaded *after* those + specified by the `contentScriptFile` option. + @prop [contentScriptWhen="end"] {string} + When to load the content scripts. This may take one of the following + values: + + * "start": load content scripts immediately after the document + element for the page is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the page has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + + This property is optional and defaults to "end". + + @prop [onMessage] {function} + Use this to add a listener to the page worker's `message` event. +</api> + +<api name="port"> +@property {EventEmitter} +[EventEmitter](packages/api-utils/docs/events.html) object that allows you to: + +* send events to the content script using the `port.emit` function +* receive events from the content script using the `port.on` function + +See the guide to +<a href="dev-guide/addon-development/content-scripts/using-port.html"> +communicating using <code>port</code></a> for details. +</api> + +<api name="contentURL"> +@property {string} +The URL of the content loaded. +</api> + +<api name="allow"> +@property {object} + A object describing permissions for the content. It contains a single key + named `script` whose value is a boolean that indicates whether or not to + execute script in the content. `script` defaults to true. +</api> + +<api name="contentScriptFile"> +@property {string,array} +A local file URL or an array of local file URLs of content scripts to load. +</api> + +<api name="contentScript"> +@property {string,array} +A string or an array of strings containing the texts of content scripts to +load. +</api> + +<api name="contentScriptWhen"> +@property {string} + When to load the content scripts. This may have one of the following + values: + + * "start": load content scripts immediately after the document + element for the page is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the page has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + +</api> + +<api name="destroy"> +@method +Unloads the page worker. After you destroy a page worker, its memory is freed +and you must create a new instance if you need to load another page. +</api> + +<api name="postMessage"> +@method +Sends a message to the content scripts. +@param message {value} +The message to send. Must be JSON-able. +</api> + +<api name="on"> +@method +Registers an event listener with the page worker. See +[Working with Events](dev-guide/addon-development/events.html) for help with +events. +@param type {string} +The type of event to listen for. +@param listener {function} +The listener function that handles the event. +</api> + +<api name="removeListener"> +@method +Unregisters an event listener from the page worker. +@param type {string} +The type of event for which `listener` was registered. +@param listener {function} +The listener function that was registered. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this page worker. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the page worker's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/addon-development/content-scripts/using-port.html#json_serializable">JSON-serializable value</a> +</api> + +<api name="error"> +@event +This event is emitted when an uncaught runtime error occurs in one of the +page worker's content scripts. + +@argument {Error} +Listeners are passed a single argument, the +[Error](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error) +object. +</api> + +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/panel.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/panel.md new file mode 100644 index 0000000..45eea2f --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/panel.md @@ -0,0 +1,301 @@ +<!-- contributed by Myk Melez [myk@mozilla.org] --> +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `panel` module creates floating modal "popup dialogs" that appear on top of +web content and browser chrome and persist until dismissed by users or programs. +Panels are useful for presenting temporary interfaces to users in a way that is +easier for users to ignore and dismiss than a modal dialog, since panels are +hidden the moment users interact with parts of the application interface outside +them. + +The module exports a single constructor function `Panel` which constructs a +new panel. + +A panel's content is loaded as soon as it is created, before the panel is shown, +and the content remains loaded when a panel is hidden, so it is possible +to keep a panel around in the background, updating its content as appropriate +in preparation for the next time it is shown. + +Your add-on can receive notifications when a panel is shown or hidden by +listening to its `show` and `hide` events. + +Panels have associated content scripts, which are JavaScript scripts that have +access to the content loaded into the panels. An add-on can specify one or +more content scripts to load for a panel, and the add-on can communicate with +those scripts either using the `message` event or by using user-defined +events. See +[Working with Content Scripts](dev-guide/addon-development/web-content.html) +for more information. + +The panel's default style is different for each operating system. +For example, suppose a panel's content is specified with the following HTML: + +<script type="syntaxhighlighter" class="brush: html"><![CDATA[ +<h1>Default Style</h1> + +This is what a panel with no custom styling looks like. +]]> +</script> + +On OS X it will look like this: + +<img class="image-center" src="static-files/media/screenshots/default-panel-osx.png" +alt="OS X panel default style"> +<br> + +On Windows 7 it will look like this: + +<img class="image-center" src="static-files/media/screenshots/default-panel-windows.png" +alt="Windows 7 panel default style"> +<br> + +On Ubuntu it will look like this: + +<img class="image-center" src="static-files/media/screenshots/default-panel-ubuntu.png" +alt="Ubuntu panel default style"> +<br> + +This helps to ensure that the panel's style is consistent with the dialogs +displayed by Firefox and other applications, but means you need to take care +when applying your own styles. For example, if you set the panel's +`background-color` property to `white` and do not set the `color` property, +then the panel's text will be invisible on OS X although it looks fine on Ubuntu. + +Examples +-------- + +Create and show a simple panel with content from the `data/` directory: + + var data = require("self").data; + var panel = require("panel").Panel({ + contentURL: data.url("foo.html") + }); + + panel.show(); + +The tutorial section on +[web content](dev-guide/addon-development/web-content.html) has +a more complex example using panels. + +<api name="Panel"> +@class +The Panel object represents a floating modal dialog that can by an add-on to +present user interface content. + +Once a panel object has been created it can be shown and hidden using its +`show()` and `hide()` methods. Once a panel is no longer needed it can be +deactivated using `destroy()`. + +The content of a panel is specified using the `contentURL` option. An add-on +can interact with the content of a panel using content scripts which it +supplies in the `contentScript` and/or `contentScriptFile` options. For example, +a content script could create a menu and send the user's selection to the +add-on. + +<api name="Panel"> +@constructor +Creates a panel. +@param options {object} + Options for the panel, with the following keys: + @prop [width] {number} + The width of the panel in pixels. Optional. + @prop [height] {number} + The height of the panel in pixels. Optional. + @prop [contentURL] {string} + The URL of the content to load in the panel. + @prop [allow] {object} + An optional object describing permissions for the content. It should + contain a single key named `script` whose value is a boolean that indicates + whether or not to execute script in the content. `script` defaults to true. + @prop [contentScriptFile] {string,array} + A local file URL or an array of local file URLs of content scripts to load. + Content scripts specified by this property are loaded *before* those + specified by the `contentScript` property. + @prop [contentScript] {string,array} + A string or an array of strings containing the texts of content scripts to + load. Content scripts specified by this property are loaded *after* those + specified by the `contentScriptFile` property. + @prop [contentScriptWhen="end"] {string} + When to load the content scripts. This may take one of the following + values: + + * "start": load content scripts immediately after the document + element for the panel is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the panel has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + + This property is optional and defaults to "end". + + @prop [onMessage] {function} + Include this to listen to the panel's `message` event. + @prop [onShow] {function} + Include this to listen to the panel's `show` event. + @prop [onHide] {function} + Include this to listen to the panel's `hide` event. +</api> + +<api name="port"> +@property {EventEmitter} +[EventEmitter](packages/api-utils/docs/events.html) object that allows you to: + +* send events to the content script using the `port.emit` function +* receive events from the content script using the `port.on` function + +See the guide to +<a href="dev-guide/addon-development/content-scripts/using-port.html"> +communicating using <code>port</code></a> for details. +</api> + +<api name="isShowing"> +@property {boolean} +Tells if the panel is currently shown or not. This property is read-only. +</api> + +<api name="height"> +@property {number} +The height of the panel in pixels. +</api> + +<api name="width"> +@property {number} +The width of the panel in pixels. +</api> + +<api name="contentURL"> +@property {string} +The URL of the content loaded in the panel. +</api> + +<api name="allow"> +@property {object} +An object describing permissions for the content. It contains a single key +named `script` whose value is a boolean that indicates whether or not to execute +script in the content. +</api> + +<api name="contentScriptFile"> +@property {string,array} +A local file URL or an array of local file URLs of content scripts to load. +Content scripts specified by this property are loaded *before* those +specified by the `contentScript` property. +</api> + +<api name="contentScript"> +@property {string,array} +A string or an array of strings containing the texts of content scripts to +load. Content scripts specified by this property are loaded *after* those +specified by the `contentScriptFile` property. +</api> + +<api name="contentScriptWhen"> +@property {string} +When to load the content scripts. This may have one of the following +values: + +* "start": load content scripts immediately after the document +element for the panel is inserted into the DOM, but before the DOM content +itself has been loaded +* "ready": load content scripts once DOM content has been loaded, +corresponding to the +[DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) +event +* "end": load content scripts once all the content (DOM, JS, CSS, +images) for the panel has been loaded, at the time the +[window.onload event](https://developer.mozilla.org/en/DOM/window.onload) +fires + +</api> + +<api name="destroy"> +@method +Destroys the panel, unloading any content that was loaded in it. Once +destroyed, the panel can no longer be used. If you just want to hide +the panel and might show it later, use `hide` instead. +</api> + +<api name="postMessage"> +@method +Sends a message to the content scripts. +@param message {value} +The message to send. Must be stringifiable to JSON. +</api> + +<api name="show"> +@method +Displays the panel. +</api> + +<api name="hide"> +@method +Stops displaying the panel. +</api> + +<api name="resize"> +@method +Resizes the panel. +@param width {number} +The new width of the panel in pixels. +@param height {number} +The new height of the panel in pixels. +</api> + +<api name="on"> +@method + Registers an event listener with the panel. +@param type {string} + The type of event to listen for. +@param listener {function} + The listener function that handles the event. +</api> + +<api name="removeListener"> +@method + Unregisters an event listener from the panel. +@param type {string} + The type of event for which `listener` was registered. +@param listener {function} + The listener function that was registered. +</api> + +<api name="show"> +@event +This event is emitted when the panel is shown. +</api> + +<api name="hide"> +@event +This event is emitted when the panel is hidden. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this panel. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the panel's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/addon-development/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="error"> +@event +This event is emitted when an uncaught runtime error occurs in one of the +panel's content scripts. + +@argument {Error} +Listeners are passed a single argument, the +[Error](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error) +object. +</api> + +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/passwords.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/passwords.md new file mode 100644 index 0000000..6a4b246 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/passwords.md @@ -0,0 +1,564 @@ +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `passwords` module allows add-ons to interact with Firefox's +[Password Manager](http://support.mozilla.com/en-US/kb/Remembering%20passwords) +to add, retrieve and remove stored credentials. + +A _credential_ is the set of information a user supplies to authenticate +herself with a service. Typically a credential consists of a username and a +password. + +Using this module you can: + +1. **Search** for credentials which have been stored in the Password Manager. + You can then use the credentials to access their related service (for + example, by logging into a web site). + +2. **Store** credentials in the Password Manager. You can store different sorts + of credentials, as outlined in the "Credentials" section below. + +3. **Remove** stored credentials from the Password Manager. + +## Credentials ## + +In this API, credentials are represented by objects. + +You create credential objects to pass into the API, and the API also returns +credential objects to you. The sections below explain both the properties you +should define on credential objects and the properties you can expect on +credential objects returned by the API. + +All credential objects include `username` and `password` properties. Different +sorts of stored credentials include various additional properties, as +outlined in this section. + +You can use the Passwords API with three sorts of credentials: + +* Add-on credentials +* HTML form credentials +* HTTP Authentication credentials + +### Add-on Credential ### + +These are associated with your add-on rather than a particular web site. +They contain the following properties: + +<table> +<colgroup> +<col width="25%"> +</colgroup> +<tr> + <td> + <code>username</code> + </td> + <td> + The username. + </td> +</tr> + +<tr> + <td> + <code>password</code> + </td> + <td> + The password. + </td> +</tr> + +<tr> + <td> + <code>url</code> + </td> + <td> + <p>For an add-on credential, this property is of the form:<br><code> + addon:<addon-id></code>, where <code><addon-id></code> + is the add-on's + <a href="dev-guide/addon-development/program-id.html"> + Program ID</a>.</p> + <p>You don't supply this value when storing an add-on credential: it is + automatically generated for you. However, you can use it to work out + which stored credentials belong to your add-on by comparing it with the + <code>uri</code> property of the + <a href="packages/addon-kit/docs/self.html"><code>self</code></a> + module.</p> + </td> +</tr> + +<tr> + <td> + <code>realm</code> + </td> + <td> + <p>You can use this as a name for the credential, to distinguish + it from any other credentials you've stored.</p> + <p>The realm is displayed + in Firefox's Password Manager, under "Site", in brackets after the URL. + For example, if the realm for a credential is "User Registration", then + its "Site" field will look something like:</p> + <code>addon:jid0-01mBBFyu0ZAXCFuB1JYKooSTKIc (User Registration)</code> + </td> +</tr> + +</table> + +### HTML Form Credential ### + +If a web service uses HTML forms to authenticate its users, then the +corresponding credential is an HTML Form credential. + +It contains the following properties: + +<table> +<colgroup> +<col width="25%"> +</colgroup> +<tr> + <td> + <code>username</code> + </td> + <td> + The username. + </td> +</tr> + +<tr> + <td> + <code>password</code> + </td> + <td> + The password. + </td> +</tr> + +<tr> + <td> + <code>url</code> + </td> + <td> + The URL for the web service which requires the credential. + You should omit anything after the hostname and (optional) port. + </td> +</tr> + +<tr> + <td> + <code>formSubmitURL</code> + </td> + <td> + The value of the form's "action" attribute. + You should omit anything after the hostname and (optional) port. + If the form doesn't contain an "action" attribute, this property should + match the <code>url</code> property. + </td> +</tr> + +<tr> + <td> + <code>usernameField</code> + </td> + <td> + The value of the "name" attribute for the form's username field. + </td> +</tr> + +<tr> + <td> + <code>passwordField</code> + </td> + <td> + The value of the "name" attribute for the form's password field. + </td> +</tr> + +</table> + +So: given a form at `http://www.example.com/login` +with the following HTML: + +<script type="syntaxhighlighter" class="brush: html"><![CDATA[ +<form action="http://login.example.com/foo/authenticate.cgi"> + <div>Please log in.</div> + <label>Username:</label> <input type="text" name="uname"> + <label>Password:</label> <input type="password" name="pword"> +</form> +]]> +</script> + +The corresponding values for the credential (excluding username and password) +should be: + +<pre> + url: "http://www.example.com" + formSubmitURL: "http://login.example.com" + usernameField: "uname" + passwordField: "pword" +</pre> + +Note that for both `url` and `formSubmitURL`, the portion of the URL after the +hostname is omitted. + +### HTTP Authentication Credential ### + +These are used to authenticate the user to a web site +which uses HTTP Authentication, as detailed in +[RFC 2617](http://tools.ietf.org/html/rfc2617). +They contain the following properties: + +<table> +<colgroup> +<col width="25%"> +</colgroup> +<tr> + <td> + <code>username</code> + </td> + <td> + The username. + </td> +</tr> + +<tr> + <td> + <code>password</code> + </td> + <td> + The password. + </td> +</tr> + +<tr> + <td> + <code>url</code> + </td> + <td> + The URL for the web service which requires the credential. + You should omit anything after the hostname and (optional) port. + </td> +</tr> + +<tr> + <td> + <code>realm</code> + </td> + <td> + <p>The WWW-Authenticate response header sent by the server may include a + "realm" field as detailed in + <a href="http://tools.ietf.org/html/rfc2617">RFC 2617</a>. If it does, + this property contains the value for the "realm" field. Otherwise, it is + omitted.</p> + <p>The realm is displayed in Firefox's Password Manager, under "Site", + in brackets after the URL.</p> + </td> +</tr> + +</table> + +So: if a web server at `http://www.example.com` requested authentication with +a response code like this: + +<pre> + HTTP/1.0 401 Authorization Required + Server: Apache/1.3.27 + WWW-Authenticate: Basic realm="ExampleCo Login" +</pre> + +The corresponding values for the credential (excluding username and password) +should be: + +<pre> + url: "http://www.example.com" + realm: "ExampleCo Login" +</pre> + +## onComplete and onError ## + +This API is explicitly asynchronous, so all its functions take two callback +functions as additional options: `onComplete` and `onError`. + +`onComplete` is called when the operation has completed successfully and +`onError` is called when the function encounters an error. + +Because the `search` function is expected to return a list of matching +credentials, its `onComplete` option is mandatory. Because the other functions +don't return a value in case of success their `onComplete` options are +optional. + +For all functions, `onError` is optional. + +<api name="search"> +@function + +This function is used to retrieve a credential, or a list of credentials, +stored in the Password Manager. + +You pass it any subset of the possible properties a credential can contain. +Credentials which match all the properties you supplied are returned as an +argument to the `onComplete` callback. + +So if you pass in an empty set of properties, all stored credentials are +returned: + + function show_all_passwords() { + require("passwords").search({ + onComplete: function onComplete(credentials) { + credentials.forEach(function(credential) { + console.log(credential.username); + console.log(credential.password); + }); + } + }); + } + +If you pass it a single property, only credentials matching that property are +returned: + + function show_passwords_for_joe() { + require("passwords").search({ + username: "joe", + onComplete: function onComplete(credentials) { + credentials.forEach(function(credential) { + console.log(credential.username); + console.log(credential.password); + }); + } + }); + } + +If you pass more than one property, returned credentials must match all of +them: + + function show_google_password_for_joe() { + require("passwords").search({ + username: "joe", + url: "https://www.google.com", + onComplete: function onComplete(credentials) { + credentials.forEach(function(credential) { + console.log(credential.username); + console.log(credential.password); + }); + } + }); + } + +To retrieve only credentials associated with your add-on, use the `url` +property, initialized from `self.uri`: + + function show_my_addon_passwords() { + require("passwords").search({ + url: require("self").uri, + onComplete: function onComplete(credentials) { + credentials.forEach(function(credential) { + console.log(credential.username); + console.log(credential.password); + }); + } + }); + } + +@param options {object} +The `options` object may contain any credential properties. It is used to +restrict the set of credentials returned by the `search` function. + +See "Credentials" above for details on what these properties should be. + +Additionally, `options` must contain a function assigned to its `onComplete` +property: this is called when the function completes and is passed the set of +credentials retrieved. + +`options` may contain a function assigned to its `onError` property, which is +called if the function encounters an error. `onError` is passed the error as an +[nsIException](https://developer.mozilla.org/en/nsIException) object. + +@prop [username] {string} +The username for the credential. + +@prop [password] {string} +The password for the credential. + +@prop [url] {string} +The URL associated with the credential. + +@prop [formSubmitURL] {string} +The URL an HTML form credential is submitted to. + +@prop [realm] {string} +For HTTP Authentication credentials, the realm for which the credential was +requested. For add-on credentials, a name for the credential. + +@prop [usernameField] {string} +The value of the `name` attribute for the user name input field in a form. + +@prop [passwordField] {string} +The value of the `name` attribute for the password input field in a form. + +@prop onComplete {function} +The callback function that is called once the function completes successfully. +It is passed all the matching credentials as a list. This is the only +mandatory option. + +@prop [onError] {function} +The callback function that is called if the function failed. The +callback is passed an `error` containing a reason of a failure: this is an +[nsIException](https://developer.mozilla.org/en/nsIException) object. + +</api> + +<api name="store"> +@function + +This function is used to store a credential in the Password Manager. + +It takes an `options` object as an argument: this contains all the properties +for the new credential. + +As different sorts of credentials contain different properties, the +appropriate options differ depending on the sort of credential being stored. + +To store an add-on credential: + + require("passwords").store({ + realm: "User Registration", + username: "joe", + password: "SeCrEt123", + }); + +To store an HTML form credential: + + require("passwords").store({ + url: "http://www.example.com", + formSubmitURL: "http://login.example.com", + username: "joe", + usernameField: "uname", + password: "SeCrEt123", + passwordField: "pword" + }); + +To store an HTTP Authentication credential: + + require("passwords").store({ + url: "http://www.example.com", + realm: "ExampleCo Login", + username: "joe", + password: "SeCrEt123", + }); + +See "Credentials" above for more details on how to set these properties. + +The options parameter may also include `onComplete` and `onError` +callback functions, which are called when the function has completed +successfully and when it encounters an error, respectively. These options +are both optional. + +@param options {object} +An object containing the properties of the credential to be stored, and +optional `onComplete` and `onError` callback functions. + +@prop username {string} +The username for the credential. + +@prop password {string} +The password for the credential. + +@prop [url] {string} +The URL to which the credential applies. Omitted for add-on +credentials. + +@prop [formSubmitURL] {string} +The URL a form-based credential was submitted to. Omitted for add-on +credentials and HTTP Authentication credentials. + +@prop [realm] {string} +For HTTP Authentication credentials, the realm for which the credential was +requested. For add-on credentials, a name for the credential. + +@prop [usernameField] {string} +The value of the `name` attribute for the username input in a form. +Omitted for add-on credentials and HTTP Authentication credentials. + +@prop [passwordField] {string} +The value of the `name` attribute for the password input in a form. +Omitted for add-on credentials and HTTP Authentication credentials. + +@prop [onComplete] {function} +The callback function that is called once the function completes successfully. + +@prop [onError] {function} +The callback function that is called if the function failed. The +callback is passed an `error` argument: this is an +[nsIException](https://developer.mozilla.org/en/nsIException) object. + +</api> + +<api name="remove"> +@function + +Removes a stored credential. You supply it all the properties of the credential +to remove, along with optional `onComplete` and `onError` callbacks. + +Because you must supply all the credential's properties, it may be convenient +to call `search` first, and use its output as the input to `remove`. For +example, to remove all of joe's stored credentials: + + require("passwords").search({ + username: "joe", + onComplete: function onComplete(credentials) { + credentials.forEach(require("passwords").remove); + }) + }); + +To change an existing credential just call `store` after `remove` succeeds: + + require("passwords").remove({ + realm: "User Registration", + username: "joe", + password: "SeCrEt123" + onComplete: function onComplete() { + require("passwords").store({ + realm: "User Registration", + username: "joe", + password: "{{new password}}" + }) + } + }); + +@param options {object} + +An object containing all the properties of the credential to be removed, +and optional `onComplete` and `onError` callback functions. + +@prop username {string} +The username for the credential. + +@prop password {string} +The password for the credential. + +@prop [url] {string} +The URL to which the credential applies. Omitted for add-on +credentials. + +@prop [formSubmitURL] {string} +The URL a form-based credential was submitted to. Omitted for add-on +credentials and HTTP Authentication credentials. + +@prop [realm] {string} +For HTTP Authentication credentials, the realm for which the credential was +requested. For add-on credentials, a name for the credential. + +@prop [usernameField] {string} +The value of the `name` attribute for the username input in a form. +Omitted for add-on credentials and HTTP Authentication credentials. + +@prop [passwordField] {string} +The value of the `name` attribute for the password input in a form. +Omitted for add-on credentials and HTTP Authentication credentials. + +@prop [onComplete] {function} +The callback function that is called once the function has completed +successfully. + +@prop [onError] {function} +The callback function that is called if the function failed. The +callback is passed an `error` argument: this is an +[nsIException](https://developer.mozilla.org/en/nsIException) object. + +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/private-browsing.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/private-browsing.md new file mode 100644 index 0000000..cad6166 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/private-browsing.md @@ -0,0 +1,46 @@ +<!-- contributed by Paul O'Shannessy [paul@oshannessy.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `private-browsing` module allows you to access Firefox's private browsing +mode, detecting if it is active and when its state changes. + +This module is available in all applications. However, only Firefox will ever +transition into or out of private browsing mode. For all other applications, +`pb.isActive` will always be `false`, and none of the events will be emitted. + +<api name="isActive"> +@property {boolean} + This read-only boolean is true if private browsing mode is turned on. +</api> + +<api name="activate"> +@function + Turns on private browsing mode. +</api> + +<api name="deactivate"> +@function + Turns off private browsing mode. +</api> + +<api name="start"> +@event +Emitted immediately after the browser enters private browsing mode. + + var pb = require("private-browsing"); + pb.on("start", function() { + // Do something when the browser starts private browsing mode. + }); + +</api> + +<api name="stop"> +@event +Emitted immediately after the browser exits private browsing mode. + + var pb = require("private-browsing"); + pb.on("stop", function() { + // Do something when the browser stops private browsing mode. + }); +</api>
\ No newline at end of file diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/request.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/request.md new file mode 100644 index 0000000..77bea43 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/request.md @@ -0,0 +1,192 @@ +The `request` module lets you make simple yet powerful network requests. + +<api name="Request"> +@class +The `Request` object is used to make `GET` or `POST` network requests. It is +constructed with a URL to which the request is sent. Optionally the user may +specify a collection of headers and content to send alongside the request and +a callback which will be executed once the request completes. + +Once a `Request` object has been created a `GET` request can be executed by +calling its `get()` method, or a `POST` request by calling its `post()` method. + +When the server completes the request, the `Request` object emits a "complete" +event. Registered event listeners are passed a `Response` object. + +Each `Request` object is designed to be used once. Once `GET` or `POST` are +called, attempting to call either will throw an error. + +Since the request is not being made by any particular website, requests made +here are not subject to the same-domain restriction that requests made in web +pages are subject to. + +With the exception of `response`, all of a `Request` object's properties +correspond with the options in the constructor. Each can be set by simply +performing an assignment. However, keep in mind that the same validation rules +that apply to `options` in the constructor will apply during assignment. Thus, +each can throw if given an invalid value. + +The example below shows how to use Request to get the most recent public tweet. + + var Request = require("request").Request; + var latestTweetRequest = Request({ + url: "http://api.twitter.com/1/statuses/public_timeline.json", + onComplete: function (response) { + var tweet = response.json[0]; + console.log("User: " + tweet.user.screen_name); + console.log("Tweet: " + tweet.text); + } + }); + + // Be a good consumer and check for rate limiting before doing more. + Request({ + url: "http://api.twitter.com/1/account/rate_limit_status.json", + onComplete: function (response) { + if (response.json.remaining_hits) { + latestTweetRequest.get(); + } else { + console.log("You have been rate limited!"); + } + } + }).get(); + +<api name="Request"> +@constructor +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 [onComplete] {function} + This function will be called when the request has received a response (or in + terms of XHR, when `readyState == 4`). The function is passed a `Response` + object. + + @prop [headers] {object} + An unordered collection of name/value pairs representing headers to send + with the request. + + @prop [content] {string,object} + The content to send to the server. If `content` is a string, it should be + URL-encoded (use `encodeURIComponent`). If `content` is an object, it + should be a collection of name/value pairs. Nested objects & arrays should + encode safely. + + For `GET` requests, the query string (`content`) will be appended to the + URL. For `POST` requests, the query string will be sent as the body of the + request. + + @prop [contentType] {string} + The type of content to send to the server. This explicitly sets the + `Content-Type` header. The default value is `application/x-www-form-urlencoded`. + + @prop [overrideMimeType] {string} + Use this string to override the MIME type returned by the server in the + response's Content-Type header. You can use this to treat the content as a + different MIME type, or to force text to be interpreted using a specific + character. + + For example, if you're retrieving text content which was encoded as + ISO-8859-1 (Latin 1), it will be given a content type of "utf-8" and + certain characters will not display correctly. To force the response to + be interpreted as Latin-1, use `overrideMimeType`: + + var Request = require("request").Request; + var quijote = Request({ + url: "http://www.latin1files.org/quijote.txt", + overrideMimeType: "text/plain; charset=latin1", + onComplete: function (response) { + console.log(response.text); + } + }); + + quijote.get(); + +</api> + +<api name="url"> +@property {string} +</api> + +<api name="headers"> +@property {object} +</api> + +<api name="content"> +@property {string,object} +</api> + +<api name="contentType"> +@property {string} +</api> + +<api name="response"> +@property {Response} +</api> + +<api name="get"> +@method +Make a `GET` request. +@returns {Request} +</api> + +<api name="post"> +@method +Make a `POST` request. +@returns {Request} +</api> + +<api name="complete"> +@event +The `Request` object emits this event when the request has completed and a +response has been received. + +@argument {Response} +Listener functions are passed the response to the request as a `Response` object. +</api> + +</api> + + +<api name="Response"> +@class +The Response object contains the response to a network request issued using a +`Request` object. It is returned by the `get()` or `post()` method of a +`Request` object. + +All members of a `Response` object are read-only. +<api name="text"> +@property {string} +The content of the response as plain text. +</api> + +<api name="json"> +@property {object} +The content of the response as a JavaScript object. The value will be `null` +if the document cannot be processed by `JSON.parse`. +</api> + +<api name="status"> +@property {string} +The HTTP response status code (e.g. *200*). +</api> + +<api name="statusText"> +@property {string} +The HTTP response status line (e.g. *OK*). +</api> + +<api name="headers"> +@property {object} +The HTTP response headers represented as key/value pairs. + +To print all the headers you can do something like this: + + for (var headerName in response.headers) { + console.log(headerName + " : " + response.headers[headerName]); + } + +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/selection.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/selection.md new file mode 100644 index 0000000..62a3d64 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/selection.md @@ -0,0 +1,86 @@ +<!-- contributed by Eric H. Jung [eric.jung@yahoo.com] --> +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `selection` module provides a means to get and set text and HTML selections +in the current Firefox page. It can also observe new selections. + +Registering for Selection Notifications +--------------------------------------- + +To be notified when the user makes a selection, register a listener for the +"select" event. Each listener will be called after a selection is made. + + function myListener() { + console.log("A selection has been made."); + } + var selection = require("selection"); + selection.on('select', myListener); + + // You can remove listeners too. + selection.removeListener('select', myListener); + +Iterating Over Discontiguous Selections +--------------------------------------- + +Discontiguous selections can be accessed by iterating over the `selection` +module itself. Each iteration yields a `Selection` object from which `text`, +`html`, and `isContiguous` properties can be accessed. + + +Examples +-------- + +Log the current contiguous selection as text: + + var selection = require("selection"); + if (selection.text) + console.log(selection.text); + +Log the current discontiguous selections as HTML: + + var selection = require("selection"); + if (!selection.isContiguous) { + for (var subselection in selection) { + console.log(subselection.html); + } + } + +Surround HTML selections with delimiters: + + var selection = require("selection"); + selection.on('select', function () { + selection.html = "\\\" + selection.html + "///"; + }); + +<api name="text"> +@property {string} + Gets or sets the current selection as plain text. Setting the selection + removes all current selections, inserts the specified text at the location of + the first selection, and selects the new text. Getting the selection when + there is no current selection returns `null`. Setting the selection when there + is no current selection throws an exception. Getting the selection when + `isContiguous` is `true` returns the text of the first selection. +</api> + +<api name="html"> +@property {string} + Gets or sets the current selection as HTML. Setting the selection removes all + current selections, inserts the specified text at the location of the first + selection, and selects the new text. Getting the selection when there is no + current selection returns `null`. Setting the selection when there is no + current selection throws an exception. Getting the selection when + `isContiguous` is `true` returns the text of the first selection. +</api> + +<api name="isContiguous"> +@property {boolean} + `true` if the current selection is a single, contiguous selection, and `false` + if there are two or more discrete selections, each of which may or may not be + spatially adjacent. (Discontiguous selections can be created by the user with + Ctrl+click-and-drag.) +</api> + +<api name="select"> +@event + This event is emitted whenever the user makes a new selection in a page. +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/self.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/self.md new file mode 100644 index 0000000..e76ad50 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/self.md @@ -0,0 +1,73 @@ +The `self` module provides access to data that is bundled with the add-on +as a whole. It also provides access to the +[Program ID](dev-guide/addon-development/program-id.html), a value which is +unique for each add-on. + +Note that the `self` module is completely different from the global `self` +object accessible to content scripts, which is used by a content script to +[communicate with the add-on code](dev-guide/addon-development/content-scripts/using-port.html). + +<api name="id"> +@property {string} +This property is a printable string that is unique for each add-on. It comes +from the `id` property set in the `package.json` file in the main package +(i.e. the package in which you run `cfx xpi`). While not generally of use to +add-on code directly, it can be used by internal API code to index local +storage and other resources that are associated with a particular add-on. +Eventually, this ID will be unspoofable (see +[JEP 118](https://wiki.mozilla.org/Labs/Jetpack/Reboot/JEP/118) for details). +</api> + +<api name="name"> +@property {string} +This property contains the add-on's short name. It comes from the `name` +property in the main package's `package.json` file. +</api> + +<api name="version"> +@property {string} +This property contains the add-on's version string. It comes from the +`version` property set in the `package.json` file in the main package. +</api> + +<api name="data"> +@property {object} +The `data` object is used to access data that was bundled with the add-on. +This data lives in the main package's `data/` directory, immediately below +the `package.json` file. All files in this directory will be copied into the +XPI and made available through the `data` object. + +The [Package Specification](dev-guide/addon-development/package-spec.html) +section explains the `package.json` file. + +<api name="data.load"> +@method +The `data.load(NAME)` method returns the contents of an embedded data file, +as a string. It is most useful for data that will be modified or parsed in +some way, such as JSON, XML, plain text, or perhaps an HTML template. For +data that can be displayed directly in a content frame, use `data.url(NAME)`. +@param name {string} The filename to be read, relative to the + package's `data` directory. Each package that uses the `self` module + will see its own `data` directory. +@returns {string} +</api> + +<api name="data.url"> +@method +The `data.url(NAME)` method returns a URL instance that points at an embedded +data file. It is most useful for data that can be displayed directly in a +content frame. The URL instance can be passed to a content frame constructor, +such as the Panel: + + var self = require("self"); + var myPanel = require("panel").Panel({ + contentURL: self.data.url("my-panel-content.html") + }); + myPanel.show(); + +@param name {string} The filename to be read, relative to the + package's `data` directory. Each package that uses the `self` module + will see its own `data` directory. +@returns {URL} +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/simple-prefs.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/simple-prefs.md new file mode 100644 index 0000000..824a151 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/simple-prefs.md @@ -0,0 +1,70 @@ +<!-- contributed by Erik Vold [erikvvold@gmail.com] --> + +#### *Experimental* + +The `simple-prefs` module lets you easily and persistently store preferences +across application restarts, which can be configured by users in the +Add-ons Manager. + +Introduction +------------ + +With the simple preferences module you can store booleans, integers, and string +values. + + +Inline Options & Default Values +------------------------------- + +In order to have a `options.xul` (for inline options) generated, or a +`defaults/preferences/prefs.js` for default preferences, you will need to +define the preferences in your `package.json`, like so: + + { + "fullName": "Example Add-on", + ... + "preferences": [{ + "name": "somePreference", + "title": "Some preference title", + "type": "string", + "value": "this is the default string value" + }] + } + + +<api name="prefs"> +@property {object} + *experimental* A persistent object private to your add-on. Properties with boolean, + number, and string values will be persisted in the Mozilla preferences system. +</api> + + +<api name="on"> +@function + *experimental* Registers an event `listener` that will be called when a preference is changed. + +**Example:** + + function onPrefChange(prefName) { + console.log("The " + prefName + " preference changed."); + } + require("simple-prefs").on("somePreference", onPrefChange); + require("simple-prefs").on("someOtherPreference", onPrefChange); + + +@param prefName {String} + The name of the preference to watch for changes. +@param listener {Function} + The listener function that processes the event. +</api> + +<api name="removeListener"> +@function + *experimental* Unregisters an event `listener` for the specified preference. + +@param prefName {String} + The name of the preference to watch for changes. +@param listener {Function} + The listener function that processes the event. +</api> + diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/simple-storage.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/simple-storage.md new file mode 100644 index 0000000..e34e333 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/simple-storage.md @@ -0,0 +1,125 @@ +The `simple-storage` module lets you easily and persistently store data across +application restarts. If you're familiar with [DOM storage][] on the Web, it's +kind of like that, but for add-ons. + +[DOM storage]: https://developer.mozilla.org/en/DOM/Storage + + +Introduction +------------ + +The simple storage module exports an object called `storage` that is persistent +and private to your add-on. It's a normal JavaScript object, and you can treat +it as you would any other. + +To store a value, just assign it to a property on `storage`: + + var ss = require("simple-storage"); + ss.storage.myArray = [1, 1, 2, 3, 5, 8, 13]; + ss.storage.myBoolean = true; + ss.storage.myNull = null; + ss.storage.myNumber = 3.1337; + ss.storage.myObject = { a: "foo", b: { c: true }, d: null }; + ss.storage.myString = "O frabjous day!"; + +You can store array, boolean, number, object, null, and string values. If you'd +like to store other types of values, you'll first have to convert them to +strings or another one of these types. + +Be careful to set properties on the `storage` object and not the module itself: + + // This is no good! + var ss = require("simple-storage"); + ss.foo = "I will not be saved! :("; + + +Quotas +------ + +The simple storage available to your add-on is limited. Currently this limit is +about five megabytes (5,242,880 bytes). You can choose to be notified when you +go over quota, and you should respond by reducing the amount of data in storage. +If the user quits the application while you are over quota, all data stored +since the last time you were under quota will not be persisted. You should not +let that happen. + +To listen for quota notifications, register a listener for the `"OverQuota"` +event. It will be called when your storage goes over quota. + + function myOnOverQuotaListener() { + console.log("Uh oh."); + } + ss.on("OverQuota", myOnOverQuotaListener); + +Listeners can also be removed: + + ss.removeListener("OverQuota", myOnOverQuotaListener); + +To find out how much of your quota you're using, check the module's `quotaUsage` +property. It indicates the percentage of quota your storage occupies. If +you're within your quota, it's a number from 0 to 1, inclusive, and if you're +over, it's a number greater than 1. + +Therefore, when you're notified that you're over quota, respond by removing +storage until your `quotaUsage` is less than or equal to 1. Which particular +data you remove is up to you. For example: + + ss.storage.myList = [ /* some long array */ ]; + ss.on("OverQuota", function () { + while (ss.quotaUsage > 1) + ss.storage.myList.pop(); + }); + + +Private Browsing +---------------- + +*This section applies only to add-ons running on Firefox.* + +If your storage is related to your users' Web history, personal information, or +other sensitive data, your add-on should respect [private browsing mode][SUMO]. +While private browsing mode is active, you should not store any sensitive data. + +Because any kind of data can be placed into simple storage, support for private +browsing is not built into the module. Instead, use the +[`private-browsing`](packages/addon-kit/docs/private-browsing.html) module to +check private browsing status and respond accordingly. + +For example, the URLs your users visit should not be stored during private +browsing. If your add-on records the URL of the selected tab, here's how you +might handle that: + + ss.storage.history = []; + var privateBrowsing = require("private-browsing"); + if (!privateBrowsing.active) { + var url = getSelectedTabURL(); + ss.storage.history.push(url); + } + +For more information on supporting private browsing, see its [Mozilla Developer +Network documentation][MDN]. While that page does not apply specifically to +SDK-based add-ons (and its code samples don't apply at all), you should follow +its guidance on best practices and policies. + +[SUMO]: http://support.mozilla.com/en-US/kb/Private+Browsing +[MDN]: https://developer.mozilla.org/En/Supporting_private_browsing_mode + + +<api name="storage"> +@property {object} + A persistent object private to your add-on. Properties with array, boolean, + number, object, null, and string values will be persisted. +</api> + +<api name="quotaUsage"> +@property {number} + A number in the range [0, Infinity) that indicates the percentage of quota + occupied by storage. A value in the range [0, 1] indicates that the storage + is within quota. A value greater than 1 indicates that the storage exceeds + quota. +</api> + +<api name="OverQuota"> +@event +The module emits this event when your add-on's storage goes over its quota. +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/tabs.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/tabs.md new file mode 100644 index 0000000..a6a858f --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/tabs.md @@ -0,0 +1,381 @@ +<!-- contributed by Dietrich Ayala [dietrich@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `tabs` module provides easy access to tabs and tab-related events. + +The module itself can be used like a basic list of all opened +tabs across all windows. In particular, you can enumerate it: + + var tabs = require('tabs'); + for each (var tab in tabs) + console.log(tab.title); + +You can also access individual tabs by index: + + var tabs = require('tabs'); + + tabs.on('ready', function () { + console.log('first: ' + tabs[0].title); + console.log('last: ' + tabs[tabs.length-1].title); + }); + +You can open a new tab, specifying various properties including location: + + var tabs = require("tabs"); + tabs.open("http://www.example.com"); + +You can register event listeners to be notified when tabs open, close, finish +loading DOM content, or are made active or inactive: + + var tabs = require("tabs"); + + // Listen for tab openings. + tabs.on('open', function onOpen(tab) { + myOpenTabs.push(tab); + }); + + // Listen for tab content loads. + tabs.on('ready', function(tab) { + console.log('tab is loaded', tab.title, tab.url) + }); + +You can get and set various properties of tabs (but note that properties + relating to the tab's content, such as the URL, will not contain valid +values until after the tab's `ready` event fires). By setting the `url` +property you can load a new page in the tab: + + var tabs = require("tabs"); + tabs.on('activate', function(tab) { + tab.url = "http://www.example.com"; + }); + +You can attach a [content script](dev-guide/addon-development/web-content.html) +to the page hosted in a tab, and use that to access and manipulate the page's +content: + + var tabs = require("tabs"); + + tabs.on('activate', function(tab) { + tab.attach({ + contentScript: 'self.postMessage(document.body.innerHTML);', + onMessage: function (message) { + console.log(message); + } + }); + }); + +<api name="activeTab"> +@property {Tab} + +The currently active tab in the active window. This property is read-only. To +activate a `Tab` object, call its `activate` method. + +**Example** + + // Get the active tab's title. + var tabs = require("tabs"); + console.log("title of active tab is " + tabs.activeTab.title); +</api> + +<api name="length"> +@property {number} +The number of open tabs across all windows. +</api> + +<api name="open"> +@function +Opens a new tab. The new tab will open in the active window or in a new window, +depending on the `inNewWindow` option. + +**Example** + + var tabs = require("tabs"); + + // Open a new tab on active window and make tab active. + tabs.open("http://www.mysite.com"); + + // Open a new tab in a new window and make it active. + tabs.open({ + url: "http://www.mysite.com", + inNewWindow: true + }); + + // Open a new tab on active window in the background. + tabs.open({ + url: "http://www.mysite.com", + inBackground: true + }); + + // Open a new tab as an app tab and do something once it's open. + tabs.open({ + url: "http://www.mysite.com", + isPinned: true, + onOpen: function onOpen(tab) { + // do stuff like listen for content + // loading. + } + }); + +@param options {object} +An object containing configurable options for how and where the tab will be +opened, as well as a listeners for the tab events. + +If the only option being used is `url`, then a bare string URL can be passed to +`open` instead of adding at a property of the `options` object. + +@prop [url] {string} +String URL to be opened in the new tab. +This is a required property. + +@prop [inNewWindow] {boolean} +If present and true, a new browser window will be opened and the URL will be +opened in the first tab in that window. This is an optional property. + +@prop [inBackground] {boolean} +If present and true, the new tab will be opened to the right of the active tab +and will not be active. This is an optional property. + +@prop [isPinned] {boolean} +If present and true, then the new tab will be pinned as an +[app tab](http://support.mozilla.com/en-US/kb/what-are-app-tabs). + +@prop [onOpen] {function} +A callback function that will be registered for 'open' event. +This is an optional property. +@prop [onClose] {function} +A callback function that will be registered for 'close' event. +This is an optional property. +@prop [onReady] {function} +A callback function that will be registered for 'ready' event. +This is an optional property. +@prop [onActivate] {function} +A callback function that will be registered for 'activate' event. +This is an optional property. +@prop [onDeactivate] {function} +A callback function that will be registered for 'deactivate' event. +This is an optional property. +</api> + +<api name="Tab"> +@class +A `Tab` instance represents a single open tab. It contains various tab +properties, several methods for manipulation, as well as per-tab event +registration. + +Tabs emit all the events described in the Events section. Listeners are +passed the `Tab` object that triggered the event. + +<api name="title"> +@property {string} +The title of the page currently loaded in the tab. +This property can be set to change the tab title. +</api> + +<api name="url"> +@property {String} +The URL of the page currently loaded in the tab. +This property can be set to load a different URL in the tab. +</api> + +<api name="favicon"> +@property {string} +The URL of the favicon for the page currently loaded in the tab. +This property is read-only. +</api> + +<api name="index"> +@property {integer} +The index of the tab relative to other tabs in the application window. +This property can be set to change its relative position. +</api> + +<api name="isPinned"> +@property {boolean} +Whether or not tab is pinned as an [app tab][]. +This property is read-only. +[app tab]:http://support.mozilla.com/en-US/kb/what-are-app-tabs +</api> + +<api name="getThumbnail"> +@property {method} +Returns thumbnail data URI of the page currently loaded in this tab. +</api> + +<api name="pin"> +@method +Pins this tab as an [app tab][]. +[app tab]:http://support.mozilla.com/en-US/kb/what-are-app-tabs +</api> + +<api name="unpin"> +@method +Unpins this tab. +</api> + +<api name="close"> +@method +Closes this tab. + +@param [callback] {function} +A function to be called when the tab finishes its closing process. +This is an optional argument. +</api> + +<api name="reload"> +@method +Reloads this tab. +</api> + +<api name="activate"> +@method +Makes this tab active, which will bring this tab to the foreground. +</api> + +<api name="attach"> +@method + Create a page mod and attach it to the document in the tab. + +**Example** + + var tabs = require("tabs"); + + tabs.on('ready', function(tab) { + tab.attach({ + contentScript: + 'document.body.style.border = "5px solid red";' + }); + }); + +@param options {object} + Options for the page mod, with the following keys: + +@prop [contentScriptFile] {string,array} + The local file URLs of content scripts to load. Content scripts specified + by this option are loaded *before* those specified by the `contentScript` + option. Optional. +@prop [contentScript] {string,array} + The texts of content scripts to load. Content scripts specified by this + option are loaded *after* those specified by the `contentScriptFile` option. + Optional. +@prop [onMessage] {function} + A function called when the page mod receives a message from content scripts. + Listeners are passed a single argument, the message posted from the + content script. + +@returns {Worker} + See [Content Scripts guide](dev-guide/addon-development/web-content.html) + to learn how to use the `Worker` object to communicate with the content script. + +</api> + +<api name="close"> +@event + +This event is emitted when the tab is closed. It's also emitted when the +tab's window is closed. + +@argument {Tab} +Listeners are passed the tab object. +</api> + +<api name="ready"> +@event + +This event is emitted when the DOM for the tab's content is ready. It is +equivalent to the `DOMContentLoaded` event for the given content page. + +A single tab will emit this event every time the DOM is loaded: so it will be +emitted again if the tab's location changes or the content is reloaded. + +After this event has been emitted, all properties relating to the tab's +content can be used. + +@argument {Tab} +Listeners are passed the tab object. +</api> + +<api name="activate"> +@event + +This event is emitted when the tab is made active. + +@argument {Tab} +Listeners are passed the tab object. +</api> + +<api name="deactivate"> +@event + +This event is emitted when the tab is made inactive. + +@argument {Tab} +Listeners are passed the tab object. +</api> + +</api> + +<api name="open"> +@event + +This event is emitted when a new tab is opened. This does not mean that +the content has loaded, only that the browser tab itself is fully visible +to the user. + +Properties relating to the tab's content (for example: `title`, `favicon`, +and `url`) will not be correct at this point. If you need to access these +properties, listen for the `ready` event: + + var tabs = require("tabs"); + tabs.on('open', function(tab){ + tab.on('ready', function(tab){ + console.log(tab.url); + }); + }); + +@argument {Tab} +Listeners are passed the tab object that just opened. +</api> + +<api name="close"> +@event + +This event is emitted when a tab is closed. When a window is closed +this event will be emitted for each of the open tabs in that window. + +@argument {Tab} +Listeners are passed the tab object that has closed. +</api> + +<api name="ready"> +@event + +This event is emitted when the DOM for a tab's content is ready. +It is equivalent to the `DOMContentLoaded` event for the given content page. + +A single tab will emit this event every time the DOM is loaded: so it will be +emitted again if the tab's location changes or the content is reloaded. + +After this event has been emitted, all properties relating to the tab's +content can be used. + +@argument {Tab} +Listeners are passed the tab object that has loaded. +</api> + +<api name="activate"> +@event + +This event is emitted when an inactive tab is made active. + +@argument {Tab} +Listeners are passed the tab object that has become active. +</api> + +<api name="deactivate"> +@event + +This event is emitted when the active tab is made inactive. + +@argument {Tab} +Listeners are passed the tab object that has become inactive. +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/timers.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/timers.md new file mode 100644 index 0000000..bc36750 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/timers.md @@ -0,0 +1,48 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> +<!-- contributed by Irakli Gozalishvil [gozala@mozilla.com] --> + +The `timers` module provides access to web-like timing functionality. + +<api name="setTimeout"> +@function + Schedules `callback` to be called in `ms` milliseconds. Any additional + arguments are passed straight through to the callback. +@returns {integer} + An ID that can later be used to undo this scheduling, if `callback` hasn't yet + been called. +@param callback {function} + Function to be called. +@param ms {integer} + Interval in milliseconds after which the function will be called. +</api> + +<api name="clearTimeout"> +@function + Given an ID returned from `setTimeout()`, prevents the callback with the ID + from being called (if it hasn't yet been called). +@param ID {integer} + An ID returned from `setTimeout()`. +</api> + +<api name="setInterval"> +@function + Schedules `callback` to be called repeatedly every `ms` milliseconds. Any + additional arguments are passed straight through to the callback. +@returns {integer} + An ID that can later be used to unschedule the callback. +@param callback {function} + Function to be called. +@param ms {integer} + Interval in milliseconds at which the function will be called. +</api> + +<api name="clearInterval"> +@function + Given an ID returned from `setInterval()`, prevents the callback with the ID + from being called again. +@param ID {integer} + An ID returned from `setInterval()`. +</api> + diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/widget.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/widget.md new file mode 100644 index 0000000..aa12cbe --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/widget.md @@ -0,0 +1,692 @@ +<!-- contributed by Dietrich Ayala [dietrich@mozilla.com] --> +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `widget` module provides your add-on with a simple user interface that is +consistent with other add-ons and blends in well with Firefox. + +## Introduction ## + +"Widgets" are small pieces of content that live in the Firefox 4 +[add-on bar](https://developer.mozilla.org/en/The_add-on_bar). +They can be simple icons or complex web pages. You can attach +[panels](packages/addon-kit/docs/panel.html) to them that open when they're +clicked, or you can define a custom click handler to perform some other action, +like opening a web page in a tab. + +There are a few advantages to using widgets over an ad hoc user interface. +First, your users will be accustomed to interacting with add-ons via widgets and +the add-on bar. Second, it allows Firefox to treat your interface as a +first-class citizen. For example, in the future Firefox may allow the user to +drag widgets from the add-on bar to other toolbars. By exposing your interface +as a widget, your add-on would automatically inherit such functionality. + +## Creation and Content ## + +Widgets can contain images or arbitrary web content. You can include this +content inline as a string by using the `content` property, or point to content +using a URL with the `contentURL` property. + +For example, this widget contains an image, so it looks like a simple icon: + + require("widget").Widget({ + id: "mozilla-icon", + label: "My Mozilla Widget", + contentURL: "http://www.mozilla.org/favicon.ico" + }); + +Upon creation, the widget is automatically added to the add-on bar. +You can set the width of a widget, but the height is fixed so as to fit in the +add-on bar. If the content is an image, it is automatically scaled to be 16x16 +pixels. + +This widget contains an entire web page: + + require("widget").Widget({ + id: "hello-display", + label: "My Hello Widget", + content: "Hello!", + width: 50 + }); + +Widgets are quite small by default, so this example used the `width` property to +grow it in order to show all the text. + +As with many SDK APIs, communication with the content inside your widgets is +handled by [content scripts](dev-guide/addon-development/web-content.html). +So, for example, to be notified when your widget's content has loaded, you can +make a small script that calls back to the widget when it finishes loading. + +## Attaching Panels to Widgets ## + +You can supply a [panel](packages/addon-kit/docs/panel.html) to the widget's +constructor: if you do this, the panel is automatically displayed when the +user clicks the widget. + +<!-- The icon the widget displays, shown in the screenshot, is taken from the +Circular icon set, http://prothemedesign.com/circular-icons/ which is made +available under the Creative Commons Attribution 2.5 Generic License: +http://creativecommons.org/licenses/by/2.5/ --> + +<img class="image-right" src="static-files/media/screenshots/widget-panel-clock.png" +alt="Panel attached to a widget"> + + data = require("self").data + + var clockPanel = require("panel").Panel({ + width:215, + height:160, + contentURL: data.url("clock.html") + }); + + require("widget").Widget({ + id: "open-clock-btn", + label: "Clock", + contentURL: data.url("History.png"), + panel: clockPanel + }); + +Note that this is, at the moment, the only way you can attach a panel to a widget. + +You must supply the panel in the widget's constructor for it to work. If you +assign the panel to the widget after construction, the panel can still be shown +but will not be anchored to the widget: + + data = require("self").data + + var clockPanel = require("panel").Panel({ + width:215, + height:160, + contentURL: data.url("clock.html") + }); + + widget = require("widget").Widget({ + id: "open-clock-btn", + label: "Clock", + contentURL: data.url("History.png") + }); + + widget.panel = clockPanel; + + // Will not be anchored + widget.panel.show(); + +Also, if you try to call `panel.show()` inside your widget's `click` event +listener, the panel will not be anchored: + + data = require("self").data + + var clockPanel = require("panel").Panel({ + width:215, + height:160, + contentURL: data.url("clock.html") + }); + + require("widget").Widget({ + id: "open-clock-btn", + label: "Clock", + contentURL: data.url("History.png"), + panel: clockPanel, + onClick: function() { + // Will not be anchored + this.panel.show(); + } + }); + +See [bug 638142](https://bugzilla.mozilla.org/show_bug.cgi?id=638142). + +## Examples ## + +For conciseness, these examples create their content scripts as strings and use +the `contentScript` property. In your own add-ons, you will probably want to +create your content scripts in separate files and pass their URLs using the +`contentScriptFile` property. See +[Working with Content Scripts](dev-guide/addon-development/web-content.html) for more +information. + + var widgets = require("widget"); + + // A basic click-able image widget. + widgets.Widget({ + id: "google-link", + label: "Widget with an image and a click handler", + contentURL: "http://www.google.com/favicon.ico", + onClick: function() { + require("tabs").activeTab.url = "http://www.google.com/"; + } + }); +<br> + + // A widget that changes display on mouseover. + widgets.Widget({ + id: "mouseover-effect", + label: "Widget with changing image on mouseover", + contentURL: "http://www.yahoo.com/favicon.ico", + onMouseover: function() { + this.contentURL = "http://www.bing.com/favicon.ico"; + }, + onMouseout: function() { + this.contentURL = "http://www.yahoo.com/favicon.ico"; + } + }); +<br> + + // A widget that updates content on a timer. + widgets.Widget({ + id: "auto-update-widget", + label: "Widget that updates content on a timer", + content: "0", + contentScript: 'setTimeout(function() {' + + ' document.body.innerHTML++;' + + '}, 2000)', + contentScriptWhen: "ready" + }); +<br> + + // A widget that loads a random Flickr photo every 5 minutes. + widgets.Widget({ + id: "random-flickr", + label: "Random Flickr Photo Widget", + contentURL: "http://www.flickr.com/explore/", + contentScriptWhen: "ready", + contentScript: 'postMessage(document.querySelector(".pc_img").src);' + + 'setTimeout(function() {' + + ' document.location = "http://www.flickr.com/explore/";' + + '}, 5 * 60 * 1000);', + onMessage: function(imgSrc) { + this.contentURL = imgSrc; + }, + onClick: function() { + require("tabs").activeTab.url = this.contentURL; + } + }); +<br> + + // A widget created with a specified width, that grows. + let myWidget = widgets.Widget({ + id: "widget-effect", + label: "Wide widget that grows wider on a timer", + content: "I'm getting longer.", + width: 50, + }); + require("timers").setInterval(function() { + myWidget.width += 10; + }, 1000); +<br> + + // A widget communicating bi-directionally with a content script. + let widget = widgets.Widget({ + id: "message-test", + label: "Bi-directional communication!", + content: "<foo>bar</foo>", + contentScriptWhen: "ready", + contentScript: 'on("message", function(message) {' + + ' alert("Got message: " + message);' + + '});' + + 'postMessage("ready");', + onMessage: function(message) { + if (message == "ready") + widget.postMessage("me too"); + } + }); + +<api-name="Widget"> +@class +Represents a widget object. + +<api name="Widget"> +@constructor {options} + Creates a new widget. The widget is immediately added to the add-on bar. + +@param options {object} + An object with the following keys: + + @prop label {string} + A required string description of the widget used for accessibility, + title bars, and error reporting. + + @prop id {string} + Mandatory string used to identify your widget in order to save its + location when the user moves it in the browser. + This string has to be unique and must not be changed over time. + + @prop [content] {string} + An optional string value containing the displayed content of the widget. + It may contain HTML. Widgets must have either the `content` property or the + `contentURL` property set. + + If the content is an image, it is automatically scaled to be 16x16 pixels. + + @prop [contentURL] {string} + An optional string URL to content to load into the widget. This can be + [local content](dev-guide/addon-development/web-content.html) or remote + content, an image or web content. Widgets must have either the `content` + property or the `contentURL` property set. + + If the content is an image, it is automatically scaled to be 16x16 pixels. + + @prop [panel] {Panel} + An optional [panel](packages/addon-kit/docs/panel.html) to open when the + user clicks on the widget. Note: If you also register a "click" listener, + it will be called instead of the panel being opened. However, you can show + the panel from the listener by calling `this.panel.show()`. + + @prop [width] {integer} + Optional width in pixels of the widget. If not given, a default width is + used. + + @prop [onClick] {function} + Include this to listen to the widget's `click` event. + + @prop [onMessage] {function} + Include this to listen to the widget's `message` event. + + @prop [onMouseover] {function} + Include this to listen to the widget's `mouseover` event. + + @prop [onMouseout] {function} + Include this to listen to the widget's `mouseout` event. + + @prop [onAttach] {function} + Include this to listen to the widget's `attach` event. + + @prop [tooltip] {string} + Optional text to show when the user's mouse hovers over the widget. If not + given, the `label` is used. + + @prop [allow] {object} + An optional object describing permissions for the content. It should + contain a single key named `script` whose value is a boolean that indicates + whether or not to execute script in the content. `script` defaults to true. + + @prop [contentScriptFile] {string,array} + A local file URL or an array of local file URLs of content scripts to load. + Content scripts specified by this property are loaded *before* those + specified by the `contentScript` property. + + @prop [contentScript] {string,array} + A string or an array of strings containing the texts of content scripts to + load. Content scripts specified by this property are loaded *after* those + specified by the `contentScriptFile` property. + + @prop [contentScriptWhen="end"] {string} + When to load the content scripts. This may take one of the following + values: + + * "start": load content scripts immediately after the document + element for the widget is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the widget has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + + This property is optional and defaults to "end". + +</api> + +<api name="destroy"> +@method + Removes the widget from the add-on bar. +</api> + +<api name="postMessage"> +@method + Sends a message to the widget's content scripts. +@param data {value} + The message to send. + The message can be any +<a href = "dev-guide/addon-development/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="on"> +@method + Registers an event listener with the widget. +@param type {string} + The type of event to listen for. +@param listener {function} + The listener function that handles the event. +</api> + +<api name="removeListener"> +@method + Unregisters an event listener from the widget. +@param type {string} + The type of event for which `listener` was registered. +@param listener {function} + The listener function that was registered. +</api> + +<api name="getView"> +@method + Retrieve a `WidgetView` instance of this widget relative to a browser window. +@param window {BrowserWindow} + The [BrowserWindow](packages/addon-kit/docs/windows.html) instance to match. +@returns {WidgetView} + A `WidgetView` instance associated with the browser window. Any changes + subsequently applied to this object will only be applied to the widget + attached to that window. +</api> + +<api name="label"> +@property {string} + The widget's label. Read-only. +</api> + +<api name="content"> +@property {string} + A string containing the widget's content. It can contain HTML. Setting it + updates the widget's appearance immediately. However, if the widget was + created using `contentURL`, then this property is meaningless, and setting it + has no effect. +</api> + +<api name="contentURL"> +@property {string} + The URL of content to load into the widget. This can be + [local content](dev-guide/addon-development/web-content.html) or remote + content, an image or web content. Setting it updates the widget's appearance + immediately. However, if the widget was created using `content`, then this + property is meaningless, and setting it has no effect. +</api> + +<api name="panel"> +@property {Panel} + A [panel](packages/addon-kit/docs/panel.html) to open when the user clicks on + the widget. +</api> + +<api name="width"> +@property {number} + The widget's width in pixels. Setting it updates the widget's appearance + immediately. +</api> + +<api name="tooltip"> +@property {string} + The text of the tooltip that appears when the user hovers over the widget. +</api> + +<api name="allow"> +@property {object} + A object describing permissions for the content. It contains a single key + named `script` whose value is a boolean that indicates whether or not to + execute script in the content. +</api> + +<api name="contentScriptFile"> +@property {string,array} + A local file URL or an array of local file URLs of content scripts to load. +</api> + +<api name="contentScript"> +@property {string,array} + A string or an array of strings containing the texts of content scripts to + load. +</api> + +<api name="contentScriptWhen"> +@property {string} + When to load the content scripts. This may have one of the following + values: + + * "start": load content scripts immediately after the document + element for the widget is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the widget has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + +</api> + +<api name="port"> +@property {EventEmitter} +[EventEmitter](packages/api-utils/docs/events.html) object that allows you to: + +* send events to the content script using the `port.emit` function +* receive events from the content script using the `port.on` function + +See the guide to +<a href="dev-guide/addon-development/content-scripts/using-port.html"> +communicating using <code>port</code></a> for details. +</api> + +<api name="attach"> +@event +This event is emitted when a new `WidgetView` object is created using the +`getView()` function. +</api> + +<api name="click"> +@event +This event is emitted when the widget is clicked. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this widget. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the widget's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/addon-development/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="mouseover"> +@event +This event is emitted when the user moves the mouse over the widget. +</api> + +<api name="mouseout"> +@event +This event is emitted when the user moves the mouse away from the widget. +</api> + +</api> + + +<api-name="WidgetView"> +@class +Represents a widget instance specific to one browser window. + +Anything you do to an instance of this object will only be applied to the +instance attached to its browser window: widget instances attached to other +browser windows will be unaffected. + +By contrast, any changes you make to an instance of the normal `Widget` class +will be applied across all browser windows. + +This class has all the same methods, attributes and events as the `Widget` +class except for the `getView` method and the `attach` event. + +In this example `WidgetView` is used to display different content for +`http` and `https` schemes: + + // A widget that update its content specifically to each window. + let tabs = require("tabs"); + let windows = require("windows").browserWindows; + let widget = widgets.Widget({ + id: "window-specific-test", + label: "Widget with content specific to each window", + content: " ", + width: 50 + }); + // Observe tab switch or document changes in each existing tab: + function updateWidgetState(tab) { + let view = widget.getView(tab.window); + if (!view) return; + // Update widget displayed text: + view.content = tab.url.match(/^https/) ? "Secured" : "Unsafe"; + } + tabs.on('ready', updateWidgetState); + tabs.on('activate', updateWidgetState); + +<api name="destroy"> +@method + Removes the widget view from the add-on bar. +</api> + +<api name="postMessage"> +@method + Sends a message to the widget view's content scripts. +@param data {value} + The message to send. The message can be any +<a href = "dev-guide/addon-development/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="on"> +@method + Registers an event listener with the widget view. +@param type {string} + The type of event to listen for. +@param listener {function} + The listener function that handles the event. +</api> + +<api name="removeListener"> +@method + Unregisters an event listener from the widget view. +@param type {string} + The type of event for which `listener` was registered. +@param listener {function} + The listener function that was registered. +</api> + +<api name="label"> +@property {string} + The widget view's label. Read-only. +</api> + +<api name="content"> +@property {string} + A string containing the widget view's content. It can contain HTML. + Setting it updates the widget view's appearance immediately. However, + if the widget view was created using `contentURL`, then this property + is meaningless, and setting it has no effect. +</api> + +<api name="contentURL"> +@property {string} + The URL of content to load into the widget view. This can be + [local content](dev-guide/addon-development/web-content.html) or remote + content, an image or web content. Setting it updates the widget view's + appearance immediately. However, if the widget view was created using + `content`, then this property is meaningless, and setting it has no effect. +</api> + +<api name="panel"> +@property {Panel} + A [panel](packages/addon-kit/docs/panel.html) to open when the user clicks on + the widget view. +</api> + +<api name="width"> +@property {number} + The widget view's width in pixels. Setting it updates the widget view's + appearance immediately. +</api> + +<api name="tooltip"> +@property {string} + The text of the tooltip that appears when the user hovers over the widget + view. +</api> + +<api name="allow"> +@property {object} + A object describing permissions for the content. It contains a single key + named `script` whose value is a boolean that indicates whether or not to + execute script in the content. +</api> + +<api name="contentScriptFile"> +@property {string,array} + A local file URL or an array of local file URLs of content scripts to load. +</api> + +<api name="contentScript"> +@property {string,array} + A string or an array of strings containing the texts of content scripts to + load. +</api> + +<api name="contentScriptWhen"> +@property {string} + When to load the content scripts. This may have one of the following + values: + + * "start": load content scripts immediately after the document + element for the widget view is inserted into the DOM, but before the DOM + content itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the widget view has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + +</api> + +<api name="port"> +@property {EventEmitter} +[EventEmitter](packages/api-utils/docs/events.html) object that allows you to: + +* send events to the content script using the `port.emit` function +* receive events from the content script using the `port.on` + +See the guide to +<a href="dev-guide/addon-development/content-scripts/using-port.html"> +communicating using <code>port</code></a> for details. +</api> + +<api name="detach"> +@event +The `detach` event is fired when the widget view is removed from its related +window. +This can occur if the window is closed, Firefox exits, or the add-on is +disabled. +</api> + +<api name="click"> +@event +This event is emitted when the widget view is clicked. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this widget view. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the widget view's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/addon-development/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="mouseover"> +@event +This event is emitted when the user moves the mouse over the widget view. +</api> + +<api name="mouseout"> +@event +This event is emitted when the user moves the mouse away from the widget view. +</api> + +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/docs/windows.md b/tools/addon-sdk-1.4/packages/addon-kit/docs/windows.md new file mode 100644 index 0000000..a5bed95 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/docs/windows.md @@ -0,0 +1,187 @@ +<!-- contributed by Felipe Gomes [felipc@gmail.com] --> + + +The `windows` module provides easy access to browser windows, their +tabs, and open/close related functions and events. + +This module currently only supports browser windows and does not provide +access to non-browser windows such as the Bookmarks Library, preferences +or other non-browser windows created via add-ons. + +<api name="browserWindows"> +@property {List} +An object that contains various properties and methods to access +functionality from browser windows, such as opening new windows, accessing +their tabs or switching the current active window. + +`browserWindows` provides access to all the currently open browser windows: + + var windows = require("windows"); + for each (var window in windows.browserWindows) { + console.log(window.title); + } + + console.log(windows.browserWindows.length); + +Object emits all the events listed under "Events" section. + +####Examples#### + + var windows = require("windows").browserWindows; + + // add a listener to the 'open' event + windows.on('open', function(window) { + myOpenWindows.push(window); + }); + + // add a listener to the 'close' event + windows.on('close', function(window) { + console.log("A window was closed."); + }); + +<api name="activeWindow"> +@property {BrowserWindow} + +The currently active window. This property is read-only. + +**Example** + + // get + var windows = require("windows"); + console.log("title of active window is " + + windows.browserWindows.activeWindow.title); + + anotherWindow.activate(); + // set + windows.activeWindow == anotherWindow // true +</api> + +</api> + +<api name="open"> +@function +Open a new window. + + var windows = require("windows").browserWindows; + + // Open a new window. + windows.open("http://www.example.com"); + + // Open a new window and set a listener for "open" event. + windows.open({ + url: "http://www.example.com", + onOpen: function(window) { + // do stuff like listen for content + // loading. + } + }); + +Returns the window that was opened: + + var widgets = require("widget"); + var windows = require("windows").browserWindows; + + var example = windows.open("http://www.example.com"); + + var widget = widgets.Widget({ + id: "close-window", + label: "Close window", + contentURL: "http://www.mozilla.org/favicon.ico", + onClick: function() { + example.close(); + } + }); + +@param options {object} +An object containing configurable options for how this window will be opened, +as well as a callback for being notified when the window has fully opened. + +If the only option being used is `url`, then a bare string URL can be passed to +`open` instead of specifying it as a property of the `options` object. + +@prop url {string} +String URL to be opened in the new window. +This is a required property. + +@prop [onOpen] {function} +A callback function that is called when the window has opened. This does not +mean that the URL content has loaded, only that the window itself is fully +functional and its properties can be accessed. This is an optional property. + +@prop [onClose] {function} +A callback function that is called when the window will be called. +This is an optional property. + +@returns {BrowserWindow} +</api> + +<api name="BrowserWindow"> +@class +A `BrowserWindow` instance represents a single open window. They can be +retrieved from the `browserWindows` property exported by this module. + + var windows = require("windows").browserWindows; + + //Print how many tabs the current window has + console.log("The active window has " + + windows.activeWindow.tabs.length + + " tabs."); + + // Print the title of all browser windows + for each (var window in windows) { + console.log(window.title); + } + + // close the active window + windows.activeWindow.close(); + + windows.activeWindow.close(function() { + console.log("The active window was closed"); + }); + +<api name="title"> +@property {string} +The current title of the window. Usually the title of the active tab, +plus an app identifier. +This property is read-only. +</api> + +<api name="tabs"> +@property {TabList} +A live list of tabs in this window. This object has the same interface as the +[`tabs` API](packages/addon-kit/docs/tabs.html), except it contains only the +tabs in this window, not all tabs in all windows. This property is read-only. +</api> + +<api name="activate"> +@method +Makes window active, which will focus that window and bring it to the +foreground. +</api> + +<api name="close"> +@method +Close the window. + +@param [callback] {function} +A function to be called when the window finishes its closing process. +This is an optional argument. +</api> + +</api> + +<api name="open"> +@event +Event emitted when a new window is open. +This does not mean that the content has loaded, only that the browser window +itself is fully visible to the user. +@argument {Window} +Listeners are passed the `window` object that triggered the event. +</api> + +<api name="close"> +@event +Event emitted when a window is closed. +@argument {Window} +Listeners are passed the `window` object that triggered the event. +</api> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/clipboard.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/clipboard.js new file mode 100644 index 0000000..24f4641 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/clipboard.js @@ -0,0 +1,266 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Paul O’Shannessy <paul@oshannessy.com> (Original Author) + * Dietrich Ayala <dietrich@mozilla.com> + * Myk Melez <myk@mozilla.org> + * Erik Vold <erikvvold@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci} = require("chrome"); +const errors = require("api-utils/errors"); +const apiUtils = require("api-utils/api-utils"); + +/* +While these data flavors resemble Internet media types, they do +no directly map to them. +*/ +const kAllowableFlavors = [ + "text/unicode", + "text/html" + /* CURRENTLY UNSUPPORTED FLAVORS + "text/plain", + "image/png", + "image/jpg", + "image/gif" + "text/x-moz-text-internal", + "AOLMAIL", + "application/x-moz-file", + "text/x-moz-url", + "text/x-moz-url-data", + "text/x-moz-url-desc", + "text/x-moz-url-priv", + "application/x-moz-nativeimage", + "application/x-moz-nativehtml", + "application/x-moz-file-promise-url", + "application/x-moz-file-promise-dest-filename", + "application/x-moz-file-promise", + "application/x-moz-file-promise-dir" + */ +]; + +/* +Aliases for common flavors. Not all flavors will +get an alias. New aliases must be approved by a +Jetpack API druid. +*/ +const kFlavorMap = [ + { short: "text", long: "text/unicode" }, + { short: "html", long: "text/html" } + // Images are currently unsupported. + //{ short: "image", long: "image/png" }, +]; + +let clipboardService = Cc["@mozilla.org/widget/clipboard;1"]. + getService(Ci.nsIClipboard); + +let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + + +exports.set = function(aData, aDataType) { + let options = { + data: aData, + datatype: aDataType || "text" + }; + options = apiUtils.validateOptions(options, { + data: { + is: ["string"] + }, + datatype: { + is: ["string"] + } + }); + + var flavor = fromJetpackFlavor(options.datatype); + + if (!flavor) + throw new Error("Invalid flavor"); + + // Additional checks for using the simple case + if (flavor == "text/unicode") { + clipboardHelper.copyString(options.data); + return true; + } + + // Below are the more complex cases where we actually have to work with a + // nsITransferable object + var xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + if (!xferable) + throw new Error("Couldn't set the clipboard due to an internal error " + + "(couldn't create a Transferable object)."); + + switch (flavor) { + case "text/html": + // add text/html flavor + let (str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString)) + { + str.data = options.data; + xferable.addDataFlavor(flavor); + xferable.setTransferData(flavor, str, str.data.length * 2); + } + + // add a text/unicode flavor (html converted to plain text) + let (str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString), + converter = Cc["@mozilla.org/feed-textconstruct;1"]. + createInstance(Ci.nsIFeedTextConstruct)) + { + converter.type = "html"; + converter.text = options.data; + str.data = converter.plainText(); + xferable.addDataFlavor("text/unicode"); + xferable.setTransferData("text/unicode", str, str.data.length * 2); + } + break; + // TODO: images! + default: + throw new Error("Unable to handle the flavor " + flavor + "."); + } + + // TODO: Not sure if this will ever actually throw. -zpao + try { + clipboardService.setData( + xferable, + null, + clipboardService.kGlobalClipboard + ); + } catch (e) { + throw new Error("Couldn't set clipboard data due to an internal error: " + e); + } + return true; +}; + + +exports.get = function(aDataType) { + let options = { + datatype: aDataType || "text" + }; + options = apiUtils.validateOptions(options, { + datatype: { + is: ["string"] + } + }); + + var xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + if (!xferable) + throw new Error("Couldn't set the clipboard due to an internal error " + + "(couldn't create a Transferable object)."); + + var flavor = fromJetpackFlavor(options.datatype); + + // Ensure that the user hasn't requested a flavor that we don't support. + if (!flavor) + throw new Error("Getting the clipboard with the flavor '" + flavor + + "' is > not supported."); + + // TODO: Check for matching flavor first? Probably not worth it. + + xferable.addDataFlavor(flavor); + + // Get the data into our transferable. + clipboardService.getData( + xferable, + clipboardService.kGlobalClipboard + ); + + var data = {}; + var dataLen = {}; + try { + xferable.getTransferData(flavor, data, dataLen); + } catch (e) { + // Clipboard doesn't contain data in flavor, return null. + return null; + } + + // There's no data available, return. + if (data.value === null) + return null; + + // TODO: Add flavors here as we support more in kAllowableFlavors. + switch (flavor) { + case "text/unicode": + case "text/html": + data = data.value.QueryInterface(Ci.nsISupportsString).data; + break; + default: + data = null; + } + + return data; +}; + +exports.__defineGetter__("currentFlavors", function() { + // Loop over kAllowableFlavors, calling hasDataMatchingFlavors for each. + // This doesn't seem like the most efficient way, but we can't get + // confirmation for specific flavors any other way. This is supposed to be + // an inexpensive call, so performance shouldn't be impacted (much). + var currentFlavors = []; + for each (var flavor in kAllowableFlavors) { + var matches = clipboardService.hasDataMatchingFlavors( + [flavor], + 1, + clipboardService.kGlobalClipboard + ); + if (matches) + currentFlavors.push(toJetpackFlavor(flavor)); + } + return currentFlavors; +}); + +// SUPPORT FUNCTIONS //////////////////////////////////////////////////////// + +function toJetpackFlavor(aFlavor) { + for each (let flavorMap in kFlavorMap) + if (flavorMap.long == aFlavor) + return flavorMap.short; + // Return null in the case where we don't match + return null; +} + +function fromJetpackFlavor(aJetpackFlavor) { + // TODO: Handle proper flavors better + for each (let flavorMap in kFlavorMap) + if (flavorMap.short == aJetpackFlavor || flavorMap.long == aJetpackFlavor) + return flavorMap.long; + // Return null in the case where we don't match. + return null; +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/context-menu.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/context-menu.js new file mode 100644 index 0000000..c9f00f9 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/context-menu.js @@ -0,0 +1,1526 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * Irakli Gozalishvili <gozala@mozilla.com> + * Matteo Ferretti <zer0@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Ci} = require("chrome"); + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The context-menu module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +const apiUtils = require("api-utils/api-utils"); +const collection = require("api-utils/collection"); +const { Worker } = require("api-utils/content"); +const url = require("api-utils/url"); +const { MatchPattern } = require("api-utils/match-pattern"); +const { EventEmitterTrait: EventEmitter } = require("api-utils/events"); +const observerServ = require("api-utils/observer-service"); +const jpSelf = require("self"); +const winUtils = require("api-utils/window-utils"); +const { Trait } = require("api-utils/light-traits"); +const { Cortex } = require("api-utils/cortex"); +const timer = require("timer"); + +// All user items we add have this class name. +const ITEM_CLASS = "jetpack-context-menu-item"; + +// Items in the top-level context menu also have this class. +const TOPLEVEL_ITEM_CLASS = "jetpack-context-menu-item-toplevel"; + +// Items in the overflow submenu also have this class. +const OVERFLOW_ITEM_CLASS = "jetpack-context-menu-item-overflow"; + +// The ID of the menu separator that separates standard context menu items from +// our user items. +const SEPARATOR_ID = "jetpack-context-menu-separator"; + +// If more than this number of items are added to the context menu, all items +// overflow into a "Jetpack" submenu. +const OVERFLOW_THRESH_DEFAULT = 10; +const OVERFLOW_THRESH_PREF = + "extensions.addon-sdk.context-menu.overflowThreshold"; + +// The label of the overflow sub-xul:menu. +// +// TODO: Localize this. +const OVERFLOW_MENU_LABEL = "Add-ons"; + +// The ID of the overflow sub-xul:menu. +const OVERFLOW_MENU_ID = "jetpack-content-menu-overflow-menu"; + +// The ID of the overflow submenu's xul:menupopup. +const OVERFLOW_POPUP_ID = "jetpack-content-menu-overflow-popup"; + +// These are used by PageContext.isCurrent below. If the popupNode or any of +// its ancestors is one of these, Firefox uses a tailored context menu, and so +// the page context doesn't apply. +const NON_PAGE_CONTEXT_ELTS = [ + Ci.nsIDOMHTMLAnchorElement, + Ci.nsIDOMHTMLAppletElement, + Ci.nsIDOMHTMLAreaElement, + Ci.nsIDOMHTMLButtonElement, + Ci.nsIDOMHTMLCanvasElement, + Ci.nsIDOMHTMLEmbedElement, + Ci.nsIDOMHTMLImageElement, + Ci.nsIDOMHTMLInputElement, + Ci.nsIDOMHTMLMapElement, + Ci.nsIDOMHTMLMediaElement, + Ci.nsIDOMHTMLMenuElement, + Ci.nsIDOMHTMLObjectElement, + Ci.nsIDOMHTMLOptionElement, + Ci.nsIDOMHTMLSelectElement, + Ci.nsIDOMHTMLTextAreaElement, +]; + +// This is used to access private properties of Item and Menu instances. +const PRIVATE_PROPS_KEY = { + valueOf: function valueOf() "private properties key" +}; + +// Used as an internal ID for items and as part of a public ID for item DOM +// elements. Careful: This number is not necessarily unique to any one instance +// of the module. For each module instance, when the first item is created this +// number will be 0, when the second is created it will be 1, and so on. +let nextItemID = 0; + +// The number of items that haven't finished initializing yet. See +// AIT__finishActiveItemInit(). +let numItemsWithUnfinishedInit = 0; + +exports.Item = Item; +exports.Menu = Menu; +exports.Separator = Separator; + + +// A word about traits and privates. `this` inside of traits methods is an +// object private to the implementation. It should never be publicly leaked. +// We use Cortex in the exported menu item constructors to create public +// reflections of the private objects that hide private properties -- those +// prefixed with an underscore. Public reflections are attached to the private +// objects via the `_public` property. +// +// All item objects passed into the implementation by the client will be public +// reflections, not private objects. Likewise, all item objects passed out of +// the implementation to the client must be public, not private. Mixing up +// public and private is bad and easy to do, so not only are private objects +// restricted to the implementation, but as much as possible we try to restrict +// them to the Item, Menu, and Separator traits and constructors. Everybody +// else in the implementation should expect to be passed public reflections, and +// they must specifically request private objects via privateItem(). + +// Item, Menu, and Separator are composed of this trait. +const ItemBaseTrait = Trait({ + + _initBase: function IBT__initBase(opts, optRules, optsToNotSet) { + this._optRules = optRules; + for (let optName in optRules) + if (optsToNotSet.indexOf(optName) < 0) + this[optName] = opts[optName]; + optsToNotSet.forEach(function (opt) validateOpt(opts[opt], optRules[opt])); + this._isInited = true; + + this._id = nextItemID++; + this._parentMenu = null; + + // This makes the private properties accessible to anyone with access to + // PRIVATE_PROPS_KEY. Barring loader tricks, only this file has has access + // to it, so only this file has access to the private properties. + const self = this; + this.valueOf = function IBT_valueOf(key) { + return key === PRIVATE_PROPS_KEY ? self : self._public; + }; + }, + + destroy: function IBT_destroy() { + if (this._wasDestroyed) + return; + if (this.parentMenu) + this.parentMenu.removeItem(this._public); + else if (!(this instanceof Separator) && this._hasFinishedInit) + browserManager.removeTopLevelItem(this._public); + browserManager.unregisterItem(this._public); + this._wasDestroyed = true; + }, + + get parentMenu() { + return this._parentMenu; + }, + + set parentMenu(val) { + throw new Error("The 'parentMenu' property is not intended to be set. " + + "Use menu.addItem(item) instead."); + }, + + set _isTopLevel(val) { + if (val) + this._workerReg = new WorkerRegistry(this._public); + else { + this._workerReg.destroy(); + delete this._workerReg; + } + }, + + get _topLevelItem() { + let topLevelItem = this._public; + let parentMenu = this.parentMenu; + while (parentMenu) { + topLevelItem = parentMenu; + parentMenu = parentMenu.parentMenu; + } + return topLevelItem; + } +}); + +// Item and Menu are composed of this trait. +const ActiveItemTrait = Trait.compose(ItemBaseTrait, EventEmitter, Trait({ + + _initActiveItem: function AIT__initActiveItem(opts, optRules, optsToNotSet) { + this._initBase(opts, optRules, + optsToNotSet.concat(["onMessage", "context"])); + + if ("onMessage" in opts) + this.on("message", opts.onMessage); + + // When a URL context is removed (by calling context.remove(urlContext)), we + // may need to create workers for windows containing pages that the item now + // matches. Likewise, when a URL context is added, we need to destroy + // workers for windows containing pages that the item now does not match. + // + // collection doesn't provide a way to listen for removals. utils/registry + // does, but it doesn't allow its elements to be enumerated. So as a hack, + // use a collection for item.context and replace its add and remove methods. + collection.addCollectionProperty(this, "context"); + if (opts.context) + this.context.add(opts.context); + + const self = this; + + let add = this.context.add; + this.context.add = function itemContextAdd() { + let args = Array.slice(arguments); + add.apply(self.context, args); + if (self._workerReg && args.some(function (a) a instanceof URLContext)) + self._workerReg.destroyUnneededWorkers(); + }; + + let remove = this.context.remove; + this.context.remove = function itemContextRemove() { + let args = Array.slice(arguments); + remove.apply(self.context, args); + if (self._workerReg && args.some(function (a) a instanceof URLContext)) + self._workerReg.createNeededWorkers(); + }; + }, + + // Workers are only created for top-level menu items. When a top-level item + // is later added to a Menu, its workers are destroyed. Well, all items start + // out as top-level because there is, unfortunately, no contextMenu.add(). So + // when an item is created and immediately added to a Menu, workers for it are + // needlessly created and destroyed. The point of this timeout is to avoid + // that. Items that are created and added to Menus in the same turn of the + // event loop won't have workers created for them. + _finishActiveItemInit: function AIT__finishActiveItemInit() { + numItemsWithUnfinishedInit++; + const self = this; + timer.setTimeout(function AIT__finishActiveItemInitTimeout() { + if (!self.parentMenu && !self._wasDestroyed) + browserManager.addTopLevelItem(self._public); + self._hasFinishedInit = true; + numItemsWithUnfinishedInit--; + }, 0); + }, + + get label() { + return this._label; + }, + + set label(val) { + this._label = validateOpt(val, this._optRules.label); + if (this._isInited) + browserManager.setItemLabel(this, this._label); + return this._label; + }, + + get image() { + return this._image; + }, + + set image(val) { + this._image = validateOpt(val, this._optRules.image); + if (this._isInited) + browserManager.setItemImage(this, this._image); + return this._image; + }, + + get contentScript() { + return this._contentScript; + }, + + set contentScript(val) { + this._contentScript = validateOpt(val, this._optRules.contentScript); + return this._contentScript; + }, + + get contentScriptFile() { + return this._contentScriptFile; + }, + + set contentScriptFile(val) { + this._contentScriptFile = + validateOpt(val, this._optRules.contentScriptFile); + return this._contentScriptFile; + } +})); + +// Item is composed of this trait. +const ItemTrait = Trait.compose(ActiveItemTrait, Trait({ + + _initItem: function IT__initItem(opts, optRules) { + this._initActiveItem(opts, optRules, []); + }, + + get data() { + return this._data; + }, + + set data(val) { + this._data = validateOpt(val, this._optRules.data); + if (this._isInited) + browserManager.setItemData(this, this._data); + return this._data; + }, + + toString: function IT_toString() { + return '[object Item "' + this.label + '"]'; + } +})); + +// The exported Item constructor. +function Item(options) { + let optRules = optionsRules(); + optRules.data = { + map: function (v) v.toString(), + is: ["string", "undefined"] + }; + + let item = ItemTrait.create(Item.prototype); + item._initItem(options, optRules); + + item._public = Cortex(item); + browserManager.registerItem(item._public); + item._finishActiveItemInit(); + + return item._public; +} + +// Menu is composed of this trait. +const MenuTrait = Trait.compose( + ActiveItemTrait.resolve({ destroy: "_destroyThisItem" }), + Trait({ + + _initMenu: function MT__initMenu(opts, optRules, optsToNotSet) { + this._items = []; + this._initActiveItem(opts, optRules, optsToNotSet); + }, + + destroy: function MT_destroy() { + while (this.items.length) + this.items[0].destroy(); + this._destroyThisItem(); + }, + + get items() { + return this._items; + }, + + set items(val) { + let newItems = validateOpt(val, this._optRules.items); + while (this.items.length) + this.items[0].destroy(); + newItems.forEach(function (i) this.addItem(i), this); + return newItems; + }, + + addItem: function MT_addItem(item) { + // First, remove the item from its current parent. + let privates = privateItem(item); + if (item.parentMenu) + item.parentMenu.removeItem(item); + else if (!(item instanceof Separator) && privates._hasFinishedInit) + browserManager.removeTopLevelItem(item); + + // Now add the item to this menu. + this._items.push(item); + privates._parentMenu = this._public; + browserManager.addItemToMenu(item, this._public); + }, + + removeItem: function MT_removeItem(item) { + let idx = this._items.indexOf(item); + if (idx < 0) + return; + this._items.splice(idx, 1); + privateItem(item)._parentMenu = null; + browserManager.removeItemFromMenu(item, this._public); + }, + + toString: function MT_toString() { + return '[object Menu "' + this.label + '"]'; + } +})); + +// The exported Menu constructor. +function Menu(options) { + let optRules = optionsRules(); + optRules.items = { + is: ["array"], + ok: function (v) { + return v.every(function (item) { + return (item instanceof Item) || + (item instanceof Menu) || + (item instanceof Separator); + }); + }, + msg: "items must be an array, and each element in the array must be an " + + "Item, Menu, or Separator." + }; + + let menu = MenuTrait.create(Menu.prototype); + + // We can't rely on _initBase to set the `items` property, because the menu + // needs to be registered with and added to the browserManager before any + // child items are added to it. + menu._initMenu(options, optRules, ["items"]); + + menu._public = Cortex(menu); + browserManager.registerItem(menu._public); + menu.items = options.items; + menu._finishActiveItemInit(); + + return menu._public; +} + +// The exported Separator constructor. +function Separator() { + let sep = ItemBaseTrait.create(Separator.prototype); + sep._initBase({}, {}, []); + + sep._public = Cortex(sep); + browserManager.registerItem(sep._public); + sep._hasFinishedInit = true; + return sep._public; +} + + +function Context() {} + +function PageContext() { + this.isCurrent = function PageContext_isCurrent(popupNode) { + let win = popupNode.ownerDocument.defaultView; + if (win && !win.getSelection().isCollapsed) + return false; + + let cursor = popupNode; + while (cursor && !(cursor instanceof Ci.nsIDOMHTMLHtmlElement)) { + if (NON_PAGE_CONTEXT_ELTS.some(function (iface) cursor instanceof iface)) + return false; + cursor = cursor.parentNode; + } + return true; + }; +} + +PageContext.prototype = new Context(); + +function SelectorContext(selector) { + let opts = apiUtils.validateOptions({ selector: selector }, { + selector: { + is: ["string"], + msg: "selector must be a string." + } + }); + + this.adjustPopupNode = function SelectorContext_adjustPopupNode(node) { + return closestMatchingAncestor(node); + }; + + this.isCurrent = function SelectorContext_isCurrent(popupNode) { + return !!closestMatchingAncestor(popupNode); + }; + + // Returns node if it matches selector, or the closest ancestor of node that + // matches, or null if node and none of its ancestors matches. + function closestMatchingAncestor(node) { + let cursor = node; + while (cursor) { + if (cursor.mozMatchesSelector(selector)) + return cursor; + if (cursor instanceof Ci.nsIDOMHTMLHtmlElement) + break; + cursor = cursor.parentNode; + } + return null; + } +} + +SelectorContext.prototype = new Context(); + +function SelectionContext() { + this.isCurrent = function SelectionContext_isCurrent(popupNode) { + let win = popupNode.ownerDocument.defaultView; + if (!win) + return false; + + let hasSelection = !win.getSelection().isCollapsed; + if (!hasSelection) { + // window.getSelection doesn't return a selection for text selected in a + // form field (see bug 85686), so before returning false we want to check + // if the popupNode is a text field. + let { selectionStart, selectionEnd } = popupNode; + hasSelection = !isNaN(selectionStart) && + !isNaN(selectionEnd) && + selectionStart !== selectionEnd; + } + return hasSelection; + }; +} + +SelectionContext.prototype = new Context(); + +function URLContext(patterns) { + let opts = apiUtils.validateOptions({ patterns: patterns }, { + patterns: { + map: function (v) apiUtils.getTypeOf(v) === "array" ? v : [v], + ok: function (v) v.every(function (p) typeof(p) === "string"), + msg: "patterns must be a string or an array of strings." + } + }); + try { + patterns = opts.patterns.map(function (p) new MatchPattern(p)); + } + catch (err) { + console.error("Error creating URLContext match pattern:"); + throw err; + } + + const self = this; + + this.isCurrent = function URLContext_isCurrent(popupNode) { + return self.isCurrentForURL(popupNode.ownerDocument.URL); + }; + + this.isCurrentForURL = function URLContext_isCurrentForURL(url) { + return patterns.some(function (p) p.test(url)); + }; +} + +URLContext.prototype = new Context(); + +exports.PageContext = apiUtils.publicConstructor(PageContext); +exports.SelectorContext = apiUtils.publicConstructor(SelectorContext); +exports.SelectionContext = apiUtils.publicConstructor(SelectionContext); +exports.URLContext = apiUtils.publicConstructor(URLContext); + + +// Returns a version of opt validated against the given rule. +function validateOpt(opt, rule) { + return apiUtils.validateOptions({ opt: opt }, { opt: rule }).opt; +} + +// Returns rules for apiUtils.validateOptions() common to Item and Menu. +function optionsRules() { + return { + context: { + is: ["undefined", "object", "array"], + ok: function (v) { + if (!v) + return true; + let arr = apiUtils.getTypeOf(v) === "array" ? v : [v]; + return arr.every(function (o) o instanceof Context); + }, + msg: "The 'context' option must be a Context object or an array of " + + "Context objects." + }, + label: { + map: function (v) v.toString(), + is: ["string"], + ok: function (v) !!v, + msg: "The item must have a non-empty string label." + }, + image: { + map: function (v) v.toString(), + is: ["string", "undefined", "null"] + }, + contentScript: { + is: ["string", "array", "undefined"], + ok: function (v) { + return apiUtils.getTypeOf(v) !== "array" || + v.every(function (s) typeof(s) === "string"); + } + }, + contentScriptFile: { + is: ["string", "array", "undefined"], + ok: function (v) { + if (!v) + return true; + let arr = apiUtils.getTypeOf(v) === "array" ? v : [v]; + try { + return arr.every(function (s) { + return apiUtils.getTypeOf(s) === "string" && url.toFilename(s); + }); + } + catch (err) {} + return false; + }, + msg: "The 'contentScriptFile' option must be a local file URL or " + + "an array of local file URLs." + }, + onMessage: { + is: ["function", "undefined"] + } + }; +} + +// Does a binary search on elts, a NodeList, and returns the DOM element +// before which an item with targetLabel should be inserted. null is returned +// if the new item should be inserted at the end. +function insertionPoint(targetLabel, elts) { + let from = 0; + let to = elts.length - 1; + + while (from <= to) { + let i = Math.floor((from + to) / 2); + let comp = targetLabel.localeCompare(elts[i].getAttribute("label")); + if (comp < 0) + to = i - 1; + else if (comp > 0) + from = i + 1; + else + return elts[i]; + } + return elts[from] || null; +} + +// Builds an ID suitable for a DOM element from the given item ID. +// isInOverflowSubtree should be true if the returned element will be inserted +// into the DOM subtree rooted at the overflow menu. +function domEltIDFromItemID(itemID, isInOverflowSubtree) { + let suffix = isInOverflowSubtree ? "-overflow" : ""; + return jpSelf.id + "-context-menu-item-" + itemID + suffix; +} + +// Parses the item ID out of the given DOM element ID and returns it. If the +// element's ID is malformed or it indicates that the element was not created by +// the instance of the module calling this function, returns -1. +function itemIDFromDOMEltID(domEltID) { + let match = /^(.+?)-context-menu-item-([0-9]+)[-a-z]*$/.exec(domEltID); + return !match || match[1] !== jpSelf.id ? -1 : match[2]; +} + +// Returns the private version of the given public reflection. +function privateItem(publicItem) { + return publicItem.valueOf(PRIVATE_PROPS_KEY); +} + + +// A type of Worker tailored to our uses. +const ContextMenuWorker = Worker.compose({ + destroy: Worker.required, + + // Returns true if any context listeners are defined in the worker's port. + anyContextListeners: function CMW_anyContextListeners() { + return this._contentWorker._listeners("context").length > 0; + }, + + // Returns the first string or truthy value returned by a context listener in + // the worker's port. If none return a string or truthy value or if there are + // no context listeners, returns false. popupNode is the node that was + // context-clicked. + isAnyContextCurrent: function CMW_isAnyContextCurrent(popupNode) { + let listeners = this._contentWorker._listeners("context"); + for (let i = 0; i < listeners.length; i++) { + try { + let val = listeners[i].call(this._contentWorker._sandbox, popupNode); + if (typeof(val) === "string" || val) + return val; + } + catch (err) { + console.exception(err); + } + } + return false; + }, + + // Emits a click event in the worker's port. popupNode is the node that was + // context-clicked, and clickedItemData is the data of the item that was + // clicked. + fireClick: function CMW_fireClick(popupNode, clickedItemData) { + this._contentWorker._asyncEmit("click", popupNode, clickedItemData); + } +}); + + +// This class creates and stores content workers for pairs of menu items and +// content windows. Use one instance for each item. Not all pairs need a +// worker: if an item has a URL context that does not match a window's page, +// then no worker is created for the pair. +function WorkerRegistry(item) { + this.item = item; + + // inner window ID => { win, worker } + this.winWorkers = {}; + + // inner window ID => content window + this.winsWithoutWorkers = {}; +} + +WorkerRegistry.prototype = { + + // Registers a content window, creating a worker for it if it needs one. + registerContentWin: function WR_registerContentWin(win) { + let innerWinID = winUtils.getInnerId(win); + if ((innerWinID in this.winWorkers) || + (innerWinID in this.winsWithoutWorkers)) + return; + if (this._doesURLNeedWorker(win.document.URL)) + this.winWorkers[innerWinID] = { win: win, worker: this._makeWorker(win) }; + else + this.winsWithoutWorkers[innerWinID] = win; + }, + + // Unregisters a content window, destroying its related worker if it has one. + unregisterContentWin: function WR_unregisterContentWin(innerWinID) { + if (innerWinID in this.winWorkers) { + let winWorker = this.winWorkers[innerWinID]; + winWorker.worker.destroy(); + delete winWorker.worker; + delete winWorker.win; + delete this.winWorkers[innerWinID]; + } + else + delete this.winsWithoutWorkers[innerWinID]; + }, + + // Creates a worker for each window that needs a worker but doesn't have one. + createNeededWorkers: function WR_createNeededWorkers() { + for (let [innerWinID, win] in Iterator(this.winsWithoutWorkers)) { + delete this.winsWithoutWorkers[innerWinID]; + this.registerContentWin(win); + } + }, + + // Destroys the worker for each window that has a worker but doesn't need it. + destroyUnneededWorkers: function WR_destroyUnneededWorkers() { + for (let [innerWinID, winWorker] in Iterator(this.winWorkers)) { + if (!this._doesURLNeedWorker(winWorker.win.document.URL)) { + this.unregisterContentWin(innerWinID); + this.winsWithoutWorkers[innerWinID] = winWorker.win; + } + } + }, + + // Returns the worker for the item-window pair or null if none exists. + find: function WR_find(contentWin) { + let innerWinID = winUtils.getInnerId(contentWin); + return (innerWinID in this.winWorkers) ? + this.winWorkers[innerWinID].worker : + null; + }, + + // Unregisters all content windows from the registry, which destroys all + // workers. + destroy: function WR_destroy() { + for (let innerWinID in this.winWorkers) + this.unregisterContentWin(innerWinID); + for (let innerWinID in this.winsWithoutWorkers) + this.unregisterContentWin(innerWinID); + }, + + // Returns false if the item has a URL context that does not match the given + // URL. + _doesURLNeedWorker: function WR__doesURLNeedWorker(url) { + for (let ctxt in this.item.context) + if ((ctxt instanceof URLContext) && !ctxt.isCurrentForURL(url)) + return false; + return true; + }, + + _makeWorker: function WR__makeWorker(win) { + let worker = ContextMenuWorker({ + window: win, + contentScript: this.item.contentScript, + contentScriptFile: this.item.contentScriptFile, + onError: function (err) console.exception(err) + }); + let item = this.item; + worker.on("message", function workerOnMessage(msg) { + try { + privateItem(item)._emitOnObject(item, "message", msg); + } + catch (err) { + console.exception(err); + } + }); + return worker; + } +}; + + +// Mirrors state across all browser windows. Also responsible for detecting +// all content window loads and unloads. +let browserManager = { + topLevelItems: [], + browserWins: [], + + // inner window ID => content window + contentWins: {}, + + // Call this when a new item is created, top-level or not. + registerItem: function BM_registerItem(item) { + this.browserWins.forEach(function (w) w.registerItem(item)); + }, + + // Call this when an item is destroyed and won't be used again, top-level or + // not. + unregisterItem: function BM_unregisterItem(item) { + this.browserWins.forEach(function (w) w.unregisterItem(item)); + }, + + addTopLevelItem: function BM_addTopLevelItem(item) { + this.topLevelItems.push(item); + this.browserWins.forEach(function (w) w.addTopLevelItem(item)); + + // Create the item's worker registry and register all currently loaded + // content windows with it. + let privates = privateItem(item); + privates._isTopLevel = true; + for each (let win in this.contentWins) + privates._workerReg.registerContentWin(win); + }, + + removeTopLevelItem: function BM_removeTopLevelItem(item) { + let idx = this.topLevelItems.indexOf(item); + if (idx < 0) + throw new Error("Internal error: item not in top-level menu: " + item); + this.topLevelItems.splice(idx, 1); + this.browserWins.forEach(function (w) w.removeTopLevelItem(item)); + privateItem(item)._isTopLevel = false; + }, + + addItemToMenu: function BM_addItemToMenu(item, parentMenu) { + this.browserWins.forEach(function (w) w.addItemToMenu(item, parentMenu)); + }, + + removeItemFromMenu: function BM_removeItemFromMenu(item, parentMenu) { + this.browserWins.forEach(function (w) w.removeItemFromMenu(item, + parentMenu)); + }, + + setItemLabel: function BM_setItemLabel(item, label) { + this.browserWins.forEach(function (w) w.setItemLabel(item, label)); + }, + + setItemImage: function BM_setItemImage(item, imageURL) { + this.browserWins.forEach(function (w) w.setItemImage(item, imageURL)); + }, + + setItemData: function BM_setItemData(item, data) { + this.browserWins.forEach(function (w) w.setItemData(item, data)); + }, + + // Note that calling this method will cause onTrack to be called immediately + // for each currently open browser window. + init: function BM_init() { + require("api-utils/unload").ensure(this); + let windowTracker = new winUtils.WindowTracker(this); + + // Register content windows on content-document-global-created and + // unregister them on inner-window-destroyed. For rationale, see bug 667957 + // for the former and bug 642004 for the latter. + observerServ.add("content-document-global-created", + this._onDocGlobalCreated, this); + observerServ.add("inner-window-destroyed", + this._onInnerWinDestroyed, this); + }, + + _onDocGlobalCreated: function BM__onDocGlobalCreated(contentWin) { + let doc = contentWin.document; + if (doc.readyState == "loading") { + const self = this; + doc.addEventListener("readystatechange", function onReadyStateChange(e) { + if (e.target != doc || doc.readyState != "complete") + return; + doc.removeEventListener("readystatechange", onReadyStateChange, false); + self._registerContentWin(contentWin); + }, false); + } + else if (doc.readyState == "complete") + this._registerContentWin(contentWin); + }, + + _onInnerWinDestroyed: function BM__onInnerWinDestroyed(subj) { + this._unregisterContentWin( + subj.QueryInterface(Ci.nsISupportsPRUint64).data); + }, + + // Stores the given content window with the manager and registers it with each + // top-level item's worker registry. + _registerContentWin: function BM__registerContentWin(win) { + let innerID = winUtils.getInnerId(win); + + // It's an error to call this method for the same window more than once, but + // we allow it in one case: when onTrack races _onDocGlobalCreated. (See + // the comment in onTrack.) Make sure the window is registered only once. + if (innerID in this.contentWins) + return; + + this.contentWins[innerID] = win; + this.topLevelItems.forEach(function (item) { + privateItem(item)._workerReg.registerContentWin(win); + }); + }, + + // Removes the given content window from the manager and unregisters it from + // each top-level item's worker registry. + _unregisterContentWin: function BM__unregisterContentWin(innerID) { + delete this.contentWins[innerID]; + this.topLevelItems.forEach(function (item) { + privateItem(item)._workerReg.unregisterContentWin(innerID); + }); + }, + + unload: function BM_unload() { + // The window tracker is unloaded at the same time this method is called, + // which causes onUntrack to be called for each open browser window, so + // there's no need to clean up browser windows here. + + while (this.topLevelItems.length) { + let item = this.topLevelItems[0]; + this.removeTopLevelItem(item); + this.unregisterItem(item); + } + delete this.contentWins; + }, + + // Registers a browser window with the manager. This is a WindowTracker + // callback. Note that this is called in two cases: for each newly opened + // chrome window, and for each chrome window that is open when the loader + // loads this module. + onTrack: function BM_onTrack(window) { + if (!this._isBrowserWindow(window)) + return; + + let browserWin = new BrowserWindow(window); + this.browserWins.push(browserWin); + + // Register all loaded content windows in the browser window. Be sure to + // include frames and iframes. If onTrack is called as a result of a new + // browser window being opened, as opposed to the module being loaded, then + // this will race the content-document-global-created notification. That's + // OK, since _registerContentWin will not register the same content window + // more than once. + window.gBrowser.browsers.forEach(function (browser) { + let topContentWin = browser.contentWindow; + let allContentWins = Array.slice(topContentWin.frames); + allContentWins.push(topContentWin); + allContentWins.forEach(function (contentWin) { + if (contentWin.document.readyState == "complete") + this._registerContentWin(contentWin); + }, this); + }, this); + + // Add all top-level items and, recursively, their child items to the new + // browser window. + function addItemTree(item, parentMenu) { + browserWin.registerItem(item); + if (parentMenu) + browserWin.addItemToMenu(item, parentMenu); + else + browserWin.addTopLevelItem(item); + if (item instanceof Menu) + item.items.forEach(function (subitem) addItemTree(subitem, item)); + } + this.topLevelItems.forEach(function (item) addItemTree(item, null)); + }, + + // Unregisters a browser window from the manager. This is a WindowTracker + // callback. Note that this is called in two cases: for each newly closed + // chrome window, and for each chrome window that is open when this module is + // unloaded. + onUntrack: function BM_onUntrack(window) { + if (!this._isBrowserWindow(window)) + return; + + // Remove the window from the window list. + let idx = 0; + for (; idx < this.browserWins.length; idx++) + if (this.browserWins[idx].window == window) + break; + if (idx == this.browserWins.length) + throw new Error("Internal error: browser window not found"); + let browserWin = this.browserWins.splice(idx, 1)[0]; + + // Remove all top-level items from the window. + this.topLevelItems.forEach(function (i) browserWin.removeTopLevelItem(i)); + browserWin.destroy(); + }, + + _isBrowserWindow: function BM__isBrowserWindow(win) { + let winType = win.document.documentElement.getAttribute("windowtype"); + return winType === "navigator:browser"; + } +}; + + +// Responsible for creating and managing context menu item DOM elements for a +// browser window. Also responsible for providing a description of the window's +// current context and determining whether an item matches the current context. +// +// TODO: If other apps besides Firefox want to support the context menu in +// whatever way is appropriate for them, plugging in a substitute for or an +// adapter to this class should be the way to do it. Make it easy for them. +// See bug 560716. +function BrowserWindow(window) { + this.window = window; + this.doc = window.document; + + let popupDOMElt = this.doc.getElementById("contentAreaContextMenu"); + if (!popupDOMElt) + throw new Error("Internal error: Context menu popup not found."); + this.contextMenuPopup = new ContextMenuPopup(popupDOMElt, this); + + // item ID => { item, domElt, overflowDOMElt, popup, overflowPopup } + // item may or may not be top-level. domElt is the item's DOM element + // contained in the subtree rooted in the top-level context menu. + // overflowDOMElt is the item's DOM element contained in the subtree rooted in + // the overflow submenu. popup and overflowPopup are only defined if the item + // is a Menu; they're the Popup instances containing the Menu's child items, + // with the aforementioned distinction between top-level and overflow + // subtrees. + this.items = {}; +} + +BrowserWindow.prototype = { + + // Creates and stores DOM elements for the given item, top-level or not. + registerItem: function BW_registerItem(item) { + // this.items[id] is referenced by _makeMenu, so it needs to be defined + // before _makeDOMElt is called. + let props = { item: item }; + this.items[privateItem(item)._id] = props; + props.domElt = this._makeDOMElt(item, false); + props.overflowDOMElt = this._makeDOMElt(item, true); + }, + + // Removes the given item's DOM elements from the store. + unregisterItem: function BW_unregisterItem(item) { + delete this.items[privateItem(item)._id]; + }, + + addTopLevelItem: function BW_addTopLevelItem(item) { + this.contextMenuPopup.addItem(item); + }, + + removeTopLevelItem: function BW_removeTopLevelItem(item) { + this.contextMenuPopup.removeItem(item); + }, + + addItemToMenu: function BW_addItemToMenu(item, parentMenu) { + let { popup, overflowPopup } = this.items[privateItem(parentMenu)._id]; + popup.addItem(item); + overflowPopup.addItem(item); + }, + + removeItemFromMenu: function BW_removeItemFromMenu(item, parentMenu) { + let { popup, overflowPopup } = this.items[privateItem(parentMenu)._id]; + popup.removeItem(item); + overflowPopup.removeItem(item); + }, + + setItemLabel: function BW_setItemLabel(item, label) { + let privates = privateItem(item); + let { domElt, overflowDOMElt } = this.items[privates._id]; + this._setDOMEltLabel(domElt, label); + this._setDOMEltLabel(overflowDOMElt, label); + if (!item.parentMenu && privates._hasFinishedInit) + this.contextMenuPopup.itemLabelDidChange(item); + }, + + _setDOMEltLabel: function BW__setDOMEltLabel(domElt, label) { + domElt.setAttribute("label", label); + }, + + setItemImage: function BW_setItemImage(item, imageURL) { + let { domElt, overflowDOMElt } = this.items[privateItem(item)._id]; + let isMenu = item instanceof Menu; + this._setDOMEltImage(domElt, imageURL, isMenu); + this._setDOMEltImage(overflowDOMElt, imageURL, isMenu); + }, + + _setDOMEltImage: function BW__setDOMEltImage(domElt, imageURL, isMenu) { + if (!imageURL) { + domElt.removeAttribute("image"); + domElt.classList.remove("menu-iconic"); + domElt.classList.remove("menuitem-iconic"); + } + else { + domElt.setAttribute("image", imageURL); + domElt.classList.add(isMenu ? "menu-iconic" : "menuitem-iconic"); + } + }, + + setItemData: function BW_setItemData(item, data) { + let { domElt, overflowDOMElt } = this.items[privateItem(item)._id]; + this._setDOMEltData(domElt, data); + this._setDOMEltData(overflowDOMElt, data); + }, + + _setDOMEltData: function BW__setDOMEltData(domElt, data) { + domElt.setAttribute("value", data); + }, + + // The context specified for a top-level item may not match exactly the real + // context that triggers it. For example, if the user context-clicks a span + // inside an anchor, we want items that specify an anchor context to be + // triggered, but the real context will indicate that the span was clicked, + // not the anchor. Where the real context and an item's context conflict, + // clients should be given the item's context, and this method can be used to + // make such adjustments. Returns an adjusted popupNode. + adjustPopupNode: function BW_adjustPopupNode(popupNode, topLevelItem) { + for (let ctxt in topLevelItem.context) { + if (typeof(ctxt.adjustPopupNode) === "function") { + let ctxtNode = ctxt.adjustPopupNode(popupNode); + if (ctxtNode) { + popupNode = ctxtNode; + break; + } + } + } + return popupNode; + }, + + // Returns true if all of item's contexts are current in the window. + areAllContextsCurrent: function BW_areAllContextsCurrent(item, popupNode) { + let win = popupNode.ownerDocument.defaultView; + let worker = privateItem(item)._workerReg.find(win); + + // If the worker for the item-window pair doesn't exist (e.g., because the + // page hasn't loaded yet), we can't really make a good decision since the + // content script may have a context listener. So just don't show the item + // at all. + if (!worker) + return false; + + // If there are no contexts given at all, the page context applies. + let hasContentContext = worker.anyContextListeners(); + if (!hasContentContext && !item.context.length) + return new PageContext().isCurrent(popupNode); + + // Otherwise, determine if all given contexts are current. Evaluate the + // declarative contexts first and the worker's context listeners last. That + // way the listener might be able to avoid some work. + let curr = true; + for (let ctxt in item.context) { + curr = curr && ctxt.isCurrent(popupNode); + if (!curr) + return false; + } + return !hasContentContext || worker.isAnyContextCurrent(popupNode); + }, + + // Sets this.popupNode to the node the user context-clicked to invoke the + // context menu. For Gecko 2.0 and later, triggerNode is this node; if it's + // falsey, document.popupNode is used. Returns the popupNode. + capturePopupNode: function BW_capturePopupNode(triggerNode) { + this.popupNode = triggerNode || this.doc.popupNode; + if (!this.popupNode) + console.warn("popupNode is null."); + return this.popupNode; + }, + + destroy: function BW_destroy() { + this.contextMenuPopup.destroy(); + delete this.window; + delete this.doc; + delete this.items; + }, + + // Emits a click event in the port of the content worker related to given top- + // level item and popupNode's content window. Listeners will be passed + // popupNode and clickedItemData. + fireClick: function BW_fireClick(topLevelItem, popupNode, clickedItemData) { + let win = popupNode.ownerDocument.defaultView; + let worker = privateItem(topLevelItem)._workerReg.find(win); + if (worker) + worker.fireClick(popupNode, clickedItemData); + }, + + _makeDOMElt: function BW__makeDOMElt(item, isInOverflowSubtree) { + let elt = item instanceof Item ? this._makeMenuitem(item) : + item instanceof Menu ? this._makeMenu(item, isInOverflowSubtree) : + item instanceof Separator ? this._makeSeparator() : + null; + if (!elt) + throw new Error("Internal error: can't make element, unknown item type"); + + elt.id = domEltIDFromItemID(privateItem(item)._id, isInOverflowSubtree); + elt.classList.add(ITEM_CLASS); + return elt; + }, + + // Returns a new xul:menu representing the menu. + _makeMenu: function BW__makeMenu(menu, isInOverflowSubtree) { + let menuElt = this.doc.createElement("menu"); + this._setDOMEltLabel(menuElt, menu.label); + if (menu.image) + this._setDOMEltImage(menuElt, menu.image, true); + let popupElt = this.doc.createElement("menupopup"); + menuElt.appendChild(popupElt); + + let popup = new Popup(popupElt, this, isInOverflowSubtree); + let props = this.items[privateItem(menu)._id]; + if (isInOverflowSubtree) + props.overflowPopup = popup; + else + props.popup = popup; + + return menuElt; + }, + + // Returns a new xul:menuitem representing the item. + _makeMenuitem: function BW__makeMenuitem(item) { + let elt = this.doc.createElement("menuitem"); + this._setDOMEltLabel(elt, item.label); + if (item.image) + this._setDOMEltImage(elt, item.image, false); + if (item.data) + this._setDOMEltData(elt, item.data); + return elt; + }, + + // Returns a new xul:menuseparator. + _makeSeparator: function BW__makeSeparator() { + return this.doc.createElement("menuseparator"); + } +}; + + +// Responsible for adding DOM elements to and removing them from poupDOMElt. +function Popup(popupDOMElt, browserWin, isInOverflowSubtree) { + this.popupDOMElt = popupDOMElt; + this.browserWin = browserWin; + this.isInOverflowSubtree = isInOverflowSubtree; +} + +Popup.prototype = { + + addItem: function Popup_addItem(item) { + let props = this.browserWin.items[privateItem(item)._id]; + let elt = this.isInOverflowSubtree ? props.overflowDOMElt : props.domElt; + this.popupDOMElt.appendChild(elt); + }, + + removeItem: function Popup_removeItem(item) { + let props = this.browserWin.items[privateItem(item)._id]; + let elt = this.isInOverflowSubtree ? props.overflowDOMElt : props.domElt; + this.popupDOMElt.removeChild(elt); + } +}; + + +// Represents a browser window's context menu popup. Responsible for hiding and +// showing items according to the browser window's current context and for +// handling item clicks. +function ContextMenuPopup(popupDOMElt, browserWin) { + this.popupDOMElt = popupDOMElt; + this.browserWin = browserWin; + this.doc = popupDOMElt.ownerDocument; + + // item ID => item + // Calling this variable "topLevelItems" is redundant, since Popup and + // ContextMenuPopup are only responsible for their child items, not all their + // descendant items. But calling it "items" might encourage one to believe + // otherwise, so topLevelItems it is. + this.topLevelItems = {}; + + popupDOMElt.addEventListener("popupshowing", this, false); + popupDOMElt.addEventListener("command", this, false); +} + +ContextMenuPopup.prototype = { + + addItem: function CMP_addItem(item) { + this._ensureStaticEltsExist(); + let itemID = privateItem(item)._id; + this.topLevelItems[itemID] = item; + let props = this.browserWin.items[itemID]; + props.domElt.classList.add(TOPLEVEL_ITEM_CLASS); + props.overflowDOMElt.classList.add(OVERFLOW_ITEM_CLASS); + this._insertItemInSortedOrder(item); + }, + + removeItem: function CMP_removeItem(item) { + let itemID = privateItem(item)._id; + delete this.topLevelItems[itemID]; + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + domElt.classList.remove(TOPLEVEL_ITEM_CLASS); + overflowDOMElt.classList.remove(OVERFLOW_ITEM_CLASS); + this.popupDOMElt.removeChild(domElt); + this._overflowPopup.removeChild(overflowDOMElt); + }, + + // Call this after the item's label changes. This re-inserts the item into + // the popup so that it remains in sorted order. + itemLabelDidChange: function CMP_itemLabelDidChange(item) { + let itemID = privateItem(item)._id; + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + this.popupDOMElt.removeChild(domElt); + this._overflowPopup.removeChild(overflowDOMElt); + this._insertItemInSortedOrder(item); + }, + + destroy: function CMP_destroy() { + // If there are no more items from any instance of the module, remove the + // separator and overflow submenu, if they exist. + let elts = this._topLevelElts; + if (!elts.length) { + let submenu = this._overflowMenu; + if (submenu) + this.popupDOMElt.removeChild(submenu); + + let sep = this._separator; + if (sep) + this.popupDOMElt.removeChild(sep); + } + + this.popupDOMElt.removeEventListener("popupshowing", this, false); + this.popupDOMElt.removeEventListener("command", this, false); + }, + + handleEvent: function CMP_handleEvent(event) { + try { + if (event.type === "command") + this._handleClick(event.target); + else if (event.type === "popupshowing" && + event.target === this.popupDOMElt) + this._handlePopupShowing(); + } + catch (err) { + console.exception(err); + } + }, + + // command events bubble to the context menu's top-level xul:menupopup and are + // caught here. + _handleClick: function CMP__handleClick(clickedDOMElt) { + if (!clickedDOMElt.classList.contains(ITEM_CLASS)) + return; + let itemID = itemIDFromDOMEltID(clickedDOMElt.id); + if (itemID < 0) + return; + let { item, domElt, overflowDOMElt } = this.browserWin.items[itemID]; + + // Bail if the DOM element was not created by this module instance. In + // real-world add-ons, the itemID < 0 check above is sufficient, but for the + // unit test the JID never changes, making this necessary. + if (clickedDOMElt != domElt && clickedDOMElt != overflowDOMElt) + return; + + let topLevelItem = privateItem(item)._topLevelItem; + let popupNode = this.browserWin.adjustPopupNode(this.browserWin.popupNode, + topLevelItem); + this.browserWin.fireClick(topLevelItem, popupNode, item.data); + }, + + // popupshowing is used to show top-level items that match the browser + // window's current context and hide items that don't. Each module instance + // is responsible for showing and hiding the items it owns. + _handlePopupShowing: function CMP__handlePopupShowing() { + // If there are items queued up to finish initializing, let them go first. + // Otherwise the overflow submenu and menu separator may end up in an + // inappropriate state when those items are later added to the menu. + if (numItemsWithUnfinishedInit) { + const self = this; + timer.setTimeout(function popupShowingTryAgain() { + self._handlePopupShowing(); + }, 0); + return; + } + + // popupDOMElt.triggerNode was added in Gecko 2.0 by bug 383930. The || is + // to avoid a Spidermonkey strict warning on earlier versions. + let triggerNode = this.popupDOMElt.triggerNode || undefined; + let popupNode = this.browserWin.capturePopupNode(triggerNode); + + // Show and hide items. Set a "jetpackContextCurrent" property on the + // DOM elements to signal which of our items match the current context. + for (let [itemID, item] in Iterator(this.topLevelItems)) { + let areContextsCurr = + this.browserWin.areAllContextsCurrent(item, popupNode); + + // Change the item's label if the return value was a string. + if (typeof(areContextsCurr) === "string") { + item.label = areContextsCurr; + areContextsCurr = true; + } + + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + domElt.jetpackContextCurrent = areContextsCurr; + domElt.hidden = !areContextsCurr; + overflowDOMElt.jetpackContextCurrent = areContextsCurr; + overflowDOMElt.hidden = !areContextsCurr; + } + + // Get the total number of items that match the current context. It's a + // little tricky: There may be other instances of this module loaded, + // each hiding and showing their items. So we can't base this number on + // only our items, or on the hidden state of items. That's why we set + // the jetpackContextCurrent property above. The last instance to run + // will leave the menupopup in the correct state. + let elts = this._topLevelElts; + let numShown = Array.reduce(elts, function (total, elt) { + return total + (elt.jetpackContextCurrent ? 1 : 0); + }, 0); + + // If too many items are shown, show the submenu and hide the top-level + // items. Otherwise, hide the submenu and show the top-level items. + let overflow = numShown > this._overflowThreshold; + if (overflow) + Array.forEach(elts, function (e) e.hidden = true); + + let submenu = this._overflowMenu; + if (submenu) + submenu.hidden = !overflow; + + // If no items are shown, hide the menu separator. + let sep = this._separator; + if (sep) + sep.hidden = numShown === 0; + }, + + // Adds the menu separator and overflow submenu if they don't exist. + _ensureStaticEltsExist: function CMP__ensureStaticEltsExist() { + let sep = this._separator; + if (!sep) { + sep = this._makeSeparator(); + this.popupDOMElt.appendChild(sep); + } + + let submenu = this._overflowMenu; + if (!submenu) { + submenu = this._makeOverflowMenu(); + submenu.hidden = true; + this.popupDOMElt.insertBefore(submenu, sep.nextSibling); + } + }, + + // Inserts the given item's DOM element into the popup in sorted order. + _insertItemInSortedOrder: function CMP__insertItemInSortedOrder(item) { + let props = this.browserWin.items[privateItem(item)._id]; + this.popupDOMElt.insertBefore( + props.domElt, insertionPoint(item.label, this._topLevelElts)); + this._overflowPopup.insertBefore( + props.overflowDOMElt, insertionPoint(item.label, this._overflowElts)); + }, + + // Creates and returns the xul:menu that's shown when too many items are added + // to the popup. + _makeOverflowMenu: function CMP__makeOverflowMenu() { + let submenu = this.doc.createElement("menu"); + submenu.id = OVERFLOW_MENU_ID; + submenu.setAttribute("label", OVERFLOW_MENU_LABEL); + let popup = this.doc.createElement("menupopup"); + popup.id = OVERFLOW_POPUP_ID; + submenu.appendChild(popup); + return submenu; + }, + + // Creates and returns the xul:menuseparator that separates the standard + // context menu items from our items. + _makeSeparator: function CMP__makeSeparator() { + let elt = this.doc.createElement("menuseparator"); + elt.id = SEPARATOR_ID; + return elt; + }, + + // Returns the item elements contained in the overflow menu, a NodeList. + get _overflowElts() { + return this._overflowPopup.getElementsByClassName(OVERFLOW_ITEM_CLASS); + }, + + // Returns the overflow xul:menu. + get _overflowMenu() { + return this.doc.getElementById(OVERFLOW_MENU_ID); + }, + + // Returns the overflow xul:menupopup. + get _overflowPopup() { + return this.doc.getElementById(OVERFLOW_POPUP_ID); + }, + + // Returns the OVERFLOW_THRESH_PREF pref value if it exists or + // OVERFLOW_THRESH_DEFAULT if it doesn't. + get _overflowThreshold() { + let prefs = require("api-utils/preferences-service"); + return prefs.get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); + }, + + // Returns the xul:menuseparator. + get _separator() { + return this.doc.getElementById(SEPARATOR_ID); + }, + + // Returns the item elements contained in the top-level menu, a NodeList. + get _topLevelElts() { + return this.popupDOMElt.getElementsByClassName(TOPLEVEL_ITEM_CLASS); + } +}; + + +// Init the browserManager only after setting prototypes and such above, because +// it will cause browserManager.onTrack to be called immediately if there are +// open windows. +browserManager.init(); diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/hotkeys.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/hotkeys.js new file mode 100644 index 0000000..f8c6434 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/hotkeys.js @@ -0,0 +1,71 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * Henri Wiechers <hwiechers@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const INVALID_HOTKEY = "Hotkey must have at least one modifier."; + +const { toJSON: jsonify, toString: stringify, + isFunctionKey } = require("api-utils/keyboard/utils"); +const { register, unregister } = require("api-utils/keyboard/hotkeys"); + +const Hotkey = exports.Hotkey = function Hotkey(options) { + if (!(this instanceof Hotkey)) + return new Hotkey(options); + + // Parsing key combination string. + let hotkey = jsonify(options.combo); + if (!isFunctionKey(hotkey.key) && !hotkey.modifiers.length) { + throw new TypeError(INVALID_HOTKEY); + } + + this.onPress = options.onPress; + this.toString = stringify.bind(null, hotkey); + // Registering listener on keyboard combination enclosed by this hotkey. + // Please note that `this.toString()` is a normalized version of + // `options.combination` where order of modifiers is sorted and `accel` is + // replaced with platform specific key. + register(this.toString(), this.onPress); + // We freeze instance before returning it in order to make it's properties + // read-only. + return Object.freeze(this); +}; +Hotkey.prototype.destroy = function destroy() { + unregister(this.toString(), this.onPress); +}; diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/notifications.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/notifications.js new file mode 100644 index 0000000..f2a4ea2 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/notifications.js @@ -0,0 +1,112 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Cc, Ci, Cr } = require("chrome"); +const apiUtils = require("api-utils/api-utils"); +const errors = require("api-utils/errors"); + +try { + let alertServ = Cc["@mozilla.org/alerts-service;1"]. + getService(Ci.nsIAlertsService); + + // The unit test sets this to a mock notification function. + var notify = alertServ.showAlertNotification.bind(alertServ); +} +catch (err) { + // An exception will be thrown if the platform doesn't provide an alert + // service, e.g., if Growl is not installed on OS X. In that case, use a + // mock notification function that just logs to the console. + notify = notifyUsingConsole; +} + +exports.notify = function notifications_notify(options) { + let valOpts = validateOptions(options); + let clickObserver = !valOpts.onClick ? null : { + observe: function notificationClickObserved(subject, topic, data) { + if (topic === "alertclickcallback") + errors.catchAndLog(valOpts.onClick).call(exports, valOpts.data); + } + }; + function notifyWithOpts(notifyFn) { + notifyFn(valOpts.iconURL, valOpts.title, valOpts.text, !!clickObserver, + valOpts.data, clickObserver); + } + try { + notifyWithOpts(notify); + } + catch (err if err instanceof Ci.nsIException && + err.result == Cr.NS_ERROR_FILE_NOT_FOUND) { + console.warn("The notification icon named by " + valOpts.iconURL + + " does not exist. A default icon will be used instead."); + delete valOpts.iconURL; + notifyWithOpts(notify); + } + catch (err) { + notifyWithOpts(notifyUsingConsole); + } +}; + +function notifyUsingConsole(iconURL, title, text) { + title = title ? "[" + title + "]" : ""; + text = text || ""; + let str = [title, text].filter(function (s) s).join(" "); + console.log(str); +} + +function validateOptions(options) { + return apiUtils.validateOptions(options, { + data: { + is: ["string", "undefined"] + }, + iconURL: { + is: ["string", "undefined"] + }, + onClick: { + is: ["function", "undefined"] + }, + text: { + is: ["string", "undefined"] + }, + title: { + is: ["string", "undefined"] + } + }); +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/page-mod.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/page-mod.js new file mode 100644 index 0000000..d511464 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/page-mod.js @@ -0,0 +1,229 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack Packages. + * + * The Initial Developer of the Original Code is Nickolay Ponomarev. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Nickolay Ponomarev <asqueella@gmail.com> (Original Author) + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const observers = require("api-utils/observer-service"); +const { Worker, Loader } = require('api-utils/content'); +const { EventEmitter } = require('api-utils/events'); +const { List } = require('api-utils/list'); +const { Registry } = require('api-utils/utils/registry'); +const xulApp = require("api-utils/xul-app"); +const { MatchPattern } = require('api-utils/match-pattern'); + +// Whether or not the host application dispatches a document-element-inserted +// notification when the document element is inserted into the DOM of a page. +// The notification was added in Gecko 2.0b6, it's a better time to attach +// scripts with contentScriptWhen "start" than content-document-global-created, +// since libraries like jQuery assume the presence of the document element. +const HAS_DOCUMENT_ELEMENT_INSERTED = + xulApp.versionInRange(xulApp.platformVersion, "2.0b6", "*"); +const ON_CONTENT = HAS_DOCUMENT_ELEMENT_INSERTED ? 'document-element-inserted' : + 'content-document-global-created'; + +// Workaround bug 642145: document-element-inserted is fired multiple times. +// This bug is fixed in Firefox 4.0.1, but we want to keep FF 4.0 compatibility +// Tracking bug 641457. To be removed when 4.0 has disappeared from earth. +const HAS_BUG_642145_FIXED = + xulApp.versionInRange(xulApp.platformVersion, "2.0.1", "*"); + +// rules registry +const RULES = {}; + +const Rules = EventEmitter.resolve({ toString: null }).compose(List, { + add: function() Array.slice(arguments).forEach(function onAdd(rule) { + if (this._has(rule)) + return; + // registering rule to the rules registry + if (!(rule in RULES)) + RULES[rule] = new MatchPattern(rule); + this._add(rule); + this._emit('add', rule); + }.bind(this)), + remove: function() Array.slice(arguments).forEach(function onRemove(rule) { + if (!this._has(rule)) + return; + this._remove(rule); + this._emit('remove', rule); + }.bind(this)), +}); + +/** + * PageMod constructor (exported below). + * @constructor + */ +const PageMod = Loader.compose(EventEmitter, { + on: EventEmitter.required, + _listeners: EventEmitter.required, + contentScript: Loader.required, + contentScriptFile: Loader.required, + contentScriptWhen: Loader.required, + include: null, + constructor: function PageMod(options) { + this._onContent = this._onContent.bind(this); + options = options || {}; + + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + if ('onAttach' in options) + this.on('attach', options.onAttach); + if ('onError' in options) + this.on('error', options.onError); + + let include = options.include; + let rules = this.include = Rules(); + rules.on('add', this._onRuleAdd = this._onRuleAdd.bind(this)); + rules.on('remove', this._onRuleRemove = this._onRuleRemove.bind(this)); + + if (Array.isArray(include)) + rules.add.apply(null, include); + else + rules.add(include); + + this.on('error', this._onUncaughtError = this._onUncaughtError.bind(this)); + pageModManager.add(this._public); + + this._loadingWindows = []; + }, + + destroy: function destroy() { + for each (let rule in this.include) + this.include.remove(rule); + pageModManager.remove(this._public); + this._loadingWindows = []; + }, + + _loadingWindows: [], + + _onContent: function _onContent(window) { + // not registered yet + if (!pageModManager.has(this)) + return; + + if (!HAS_BUG_642145_FIXED) { + if (this._loadingWindows.indexOf(window) != -1) + return; + this._loadingWindows.push(window); + } + + if ('start' == this.contentScriptWhen) { + this._createWorker(window); + return; + } + + let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded'; + let self = this; + window.addEventListener(eventName, function onReady(event) { + if (event.target.defaultView != window) + return; + window.removeEventListener(eventName, onReady, true); + + self._createWorker(window); + }, true); + }, + _createWorker: function _createWorker(window) { + let worker = Worker({ + window: window, + contentScript: this.contentScript, + contentScriptFile: this.contentScriptFile, + onError: this._onUncaughtError + }); + this._emit('attach', worker); + let self = this; + worker.once('detach', function detach() { + worker.destroy(); + + if (!HAS_BUG_642145_FIXED) { + let idx = self._loadingWindows.indexOf(window); + if (idx != -1) + self._loadingWindows.splice(idx, 1); + } + }); + }, + _onRuleAdd: function _onRuleAdd(url) { + pageModManager.on(url, this._onContent); + }, + _onRuleRemove: function _onRuleRemove(url) { + pageModManager.off(url, this._onContent); + }, + _onUncaughtError: function _onUncaughtError(e) { + if (this._listeners('error').length == 1) + console.exception(e); + } +}); +exports.PageMod = function(options) PageMod(options) +exports.PageMod.prototype = PageMod.prototype; + +const PageModManager = Registry.resolve({ + constructor: '_init', + _destructor: '_registryDestructor' +}).compose({ + constructor: function PageModRegistry(constructor) { + this._init(PageMod); + observers.add( + ON_CONTENT, this._onContentWindow = this._onContentWindow.bind(this) + ); + }, + _destructor: function _destructor() { + observers.remove(ON_CONTENT, this._onContentWindow); + for (let rule in RULES) { + this._removeAllListeners(rule); + delete RULES[rule]; + } + this._registryDestructor(); + }, + _onContentWindow: function _onContentWindow(domObj) { + let window = HAS_DOCUMENT_ELEMENT_INSERTED ? domObj.defaultView : domObj; + // XML documents don't have windows, and we don't yet support them. + if (!window) + return; + for (let rule in RULES) + if (RULES[rule].test(window.document.URL)) + this._emit(rule, window); + }, + off: function off(topic, listener) { + this.removeListener(topic, listener); + if (!this._listeners(topic).length) + delete RULES[topic]; + } +}); +const pageModManager = PageModManager(); diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/page-worker.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/page-worker.js new file mode 100644 index 0000000..93750a1 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/page-worker.js @@ -0,0 +1,101 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Felipe Gomes <felipc@gmail.com> (Original Author) + * Myk Melez <myk@mozilla.org> + * Irakli Gozalishvili <gozala@mozilla.com> + * Drew Willcoxon <adw@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const { Symbiont } = require("api-utils/content"); +const { Trait } = require("api-utils/traits"); + +if (!require("api-utils/xul-app").isOneOf(["Firefox", "Thunderbird"])) { + throw new Error([ + "The page-worker module currently supports only Firefox and Thunderbird. ", + "In the future, we would like it to support other applications, however. ", + "Please see https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more ", + "information." + ].join("")); +} + +const Page = Trait.compose( + Symbiont.resolve({ + constructor: '_initSymbiont' + }), + { + _frame: Trait.required, + _initFrame: Trait.required, + postMessage: Symbiont.required, + on: Symbiont.required, + destroy: Symbiont.required, + + constructor: function Page(options) { + options = options || {}; + + this.contentURL = 'contentURL' in options ? options.contentURL + : 'about:blank'; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('allow' in options) + this.allow = options.allow; + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + + this.on('propertyChange', this._onChange.bind(this)); + + this._initSymbiont(); + }, + + _onChange: function _onChange(e) { + if ('contentURL' in e && this._frame) { + // Cleanup the worker before injecting the content script in the new + // document + this._workerCleanup(); + this._initFrame(this._frame); + } + } + } +); +exports.Page = function(options) Page(options); +exports.Page.prototype = Page.prototype; diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/panel.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/panel.js new file mode 100644 index 0000000..442f6a9 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/panel.js @@ -0,0 +1,403 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez <myk@mozilla.org> (Original Author) + * Irakli Gozalishvili <gozala@mazilla.com> + * Mihai Sucan <mihai.sucan@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The panel module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps ", + "for more information." + ].join("")); +} + +const { Cc, Ci } = require("chrome"); + +const { validateOptions: valid } = require("api-utils/api-utils"); +const { Symbiont } = require("api-utils/content"); +const { EventEmitter } = require('api-utils/events'); +const timer = require("api-utils/timer"); +const runtime = require("api-utils/runtime"); + +const windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + ON_SHOW = 'popupshown', + ON_HIDE = 'popuphidden', + validNumber = { is: ['number', 'undefined', 'null'] }; + +/** + * Emits show and hide events. + */ +const Panel = Symbiont.resolve({ + constructor: '_init', + _onInit: '_onSymbiontInit', + destroy: '_symbiontDestructor', + _documentUnload: '_workerDocumentUnload' +}).compose({ + _frame: Symbiont.required, + _init: Symbiont.required, + _onSymbiontInit: Symbiont.required, + _symbiontDestructor: Symbiont.required, + _emit: Symbiont.required, + _asyncEmit: Symbiont.required, + on: Symbiont.required, + removeListener: Symbiont.required, + + _inited: false, + + /** + * If set to `true` frame loaders between xul panel frame and + * hidden frame are swapped. If set to `false` frame loaders are + * set back to normal. Setting the value that was already set will + * have no effect. + */ + set _frameLoadersSwapped(value) { + if (this.__frameLoadersSwapped == value) return; + this._frame.QueryInterface(Ci.nsIFrameLoaderOwner) + .swapFrameLoaders(this._viewFrame); + this.__frameLoadersSwapped = value; + }, + __frameLoadersSwapped: false, + + constructor: function Panel(options) { + this._onShow = this._onShow.bind(this); + this._onHide = this._onHide.bind(this); + this.on('inited', this._onSymbiontInit.bind(this)); + + options = options || {}; + if ('onShow' in options) + this.on('show', options.onShow); + if ('onHide' in options) + this.on('hide', options.onHide); + if ('width' in options) + this.width = options.width; + if ('height' in options) + this.height = options.height; + if ('contentURL' in options) + this.contentURL = options.contentURL; + + this._init(options); + }, + _destructor: function _destructor() { + this.hide(); + this._removeAllListeners('show'); + // defer cleanup to be performed after panel gets hidden + this._xulPanel = null; + this._symbiontDestructor(this); + this._removeAllListeners(this, 'hide'); + }, + destroy: function destroy() { + this._destructor(); + }, + /* Public API: Panel.width */ + get width() this._width, + set width(value) + this._width = valid({ $: value }, { $: validNumber }).$ || this._width, + _width: 320, + /* Public API: Panel.height */ + get height() this._height, + set height(value) + this._height = valid({ $: value }, { $: validNumber }).$ || this._height, + _height: 240, + + /* Public API: Panel.isShowing */ + get isShowing() !!this._xulPanel && this._xulPanel.state == "open", + + /* Public API: Panel.show */ + show: function show(anchor) { + anchor = anchor || null; + let document = getWindow(anchor).document; + let xulPanel = this._xulPanel; + if (!xulPanel) { + xulPanel = this._xulPanel = document.createElementNS(XUL_NS, 'panel'); + xulPanel.setAttribute("type", "arrow"); + + // One anonymous node has a big padding that doesn't work well with + // Jetpack, as we would like to display an iframe that completely fills + // the panel. + // -> Use a XBL wrapper with inner stylesheet to remove this padding. + let css = ".panel-inner-arrowcontent, .panel-arrowcontent {padding: 0;}"; + let originalXBL = "chrome://global/content/bindings/popup.xml#arrowpanel"; + let binding = + '<bindings xmlns="http://www.mozilla.org/xbl">' + + '<binding id="id" extends="' + originalXBL + '">' + + '<resources>' + + '<stylesheet src="data:text/css,' + + document.defaultView.encodeURIComponent(css) + '"/>' + + '</resources>' + + '</binding>' + + '</bindings>'; + xulPanel.style.MozBinding = 'url("data:text/xml,' + + 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:,"); + + xulPanel.appendChild(frame); + document.getElementById("mainPopupSet").appendChild(xulPanel); + } + let { width, height } = this, x, y, position; + + if (!anchor) { + // Open the popup in the middle of the window. + x = document.documentElement.clientWidth / 2 - width / 2; + y = document.documentElement.clientHeight / 2 - height / 2; + position = null; + } + else { + // Open the popup by the anchor. + let rect = anchor.getBoundingClientRect(); + + let window = anchor.ownerDocument.defaultView; + + let zoom = window.mozScreenPixelsPerCSSPixel; + let screenX = rect.left + window.mozInnerScreenX * zoom; + let screenY = rect.top + window.mozInnerScreenY * zoom; + + // Set up the vertical position of the popup relative to the anchor + // (always display the arrow on anchor center) + let horizontal, vertical; + if (screenY > window.screen.availHeight / 2 + height) + vertical = "top"; + else + vertical = "bottom"; + + if (screenY > window.screen.availWidth / 2 + width) + horizontal = "left"; + else + horizontal = "right"; + + let verticalInverse = vertical == "top" ? "bottom" : "top"; + position = vertical + "center " + verticalInverse + horizontal; + + // Allow panel to flip itself if the panel can't be displayed at the + // specified position (useful if we compute a bad position or if the + // user moves the window and panel remains visible) + xulPanel.setAttribute("flip","both"); + } + + // Resize the iframe instead of using panel.sizeTo + // because sizeTo doesn't work with arrow panels + xulPanel.firstChild.style.width = width + "px"; + xulPanel.firstChild.style.height = height + "px"; + + // Wait for the XBL binding to be constructed + function waitForBinding() { + if (!xulPanel.openPopup) { + timer.setTimeout(waitForBinding, 50); + return; + } + xulPanel.openPopup(anchor, position, x, y); + } + waitForBinding(); + + return this._public; + }, + /* Public API: Panel.hide */ + hide: function hide() { + // The popuphiding handler takes care of swapping back the frame loaders + // and removing the XUL panel from the application window, we just have to + // trigger it by hiding the popup. + // XXX Sometimes I get "TypeError: xulPanel.hidePopup is not a function" + // when quitting the host application while a panel is visible. To suppress + // them, this now checks for "hidePopup" in xulPanel before calling it. + // It's not clear if there's an actual issue or the error is just normal. + let xulPanel = this._xulPanel; + if (xulPanel && "hidePopup" in xulPanel) + xulPanel.hidePopup(); + return this._public; + }, + + /* Public API: Panel.resize */ + resize: function resize(width, height) { + this.width = width; + this.height = height; + // Resize the iframe instead of using panel.sizeTo + // because sizeTo doesn't work with arrow panels + let xulPanel = this._xulPanel; + if (xulPanel) { + xulPanel.firstChild.style.width = width + "px"; + xulPanel.firstChild.style.height = height + "px"; + } + }, + + // While the panel is visible, this is the XUL <panel> we use to display it. + // Otherwise, it's null. + get _xulPanel() this.__xulPanel, + set _xulPanel(value) { + let xulPanel = this.__xulPanel; + if (value === xulPanel) return; + if (xulPanel) { + xulPanel.removeEventListener(ON_HIDE, this._onHide, false); + xulPanel.removeEventListener(ON_SHOW, this._onShow, false); + xulPanel.parentNode.removeChild(xulPanel); + } + if (value) { + value.addEventListener(ON_HIDE, this._onHide, false); + value.addEventListener(ON_SHOW, this._onShow, false); + } + this.__xulPanel = value; + }, + __xulPanel: null, + get _viewFrame() this.__xulPanel.children[0], + /** + * When the XUL panel becomes hidden, we swap frame loaders back to move + * the content of the panel to the hidden frame & remove panel element. + */ + _onHide: function _onHide() { + try { + this._frameLoadersSwapped = false; + this._xulPanel = null; + this._emit('hide'); + } catch(e) { + this._emit('error', e); + } + }, + /** + * 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; + + // 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. + let win = this._xulPanel.ownerDocument.defaultView; + let node = win.document.getAnonymousElementByAttribute(this._xulPanel, + "class", "panel-inner-arrowcontent"); + let textColor = win.getComputedStyle(node).getPropertyValue("color"); + let doc = this._xulPanel.firstChild.contentDocument; + let style = doc.createElement("style"); + style.textContent = "body { color: " + textColor + "; }"; + let container = doc.head ? doc.head : doc.documentElement; + + if (container.firstChild) + container.insertBefore(style, container.firstChild); + else + container.appendChild(style); + + this._emit('show'); + } + } catch(e) { + this._emit('error', e); + } + }, + /** + * Notification that panel was fully initialized. + */ + _onInit: function _onInit() { + this._inited = true; + + // Avoid panel document from resizing the browser window + // New platform capability added through bug 635673 + if ("allowWindowControl" in this._frame.docShell) + this._frame.docShell.allowWindowControl = false; + + // perform all deferred tasks like initSymbiont, show, hide ... + // TODO: We're publicly exposing a private event here; this + // 'inited' event should really be made private, somehow. + this._emit('inited'); + }, + + // Catch document unload event in order to rebind load event listener with + // Symbiont._initFrame if Worker._documentUnload destroyed the worker + _documentUnload: function(subject, topic, data) { + if (this._workerDocumentUnload(subject, topic, data)) { + this._initFrame(this._frame); + return true; + } + return false; + } +}); +exports.Panel = function(options) Panel(options) +exports.Panel.prototype = Panel.prototype; + +function getWindow(anchor) { + let window; + + if (anchor) { + let anchorWindow = anchor.ownerDocument.defaultView.top; + let anchorDocument = anchorWindow.document; + + let enumerator = windowMediator.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + let enumWindow = enumerator.getNext(); + + // Check if the anchor is in this browser window. + if (enumWindow == anchorWindow) { + window = anchorWindow; + break; + } + + // Check if the anchor is in a browser tab in this browser window. + let browser = enumWindow.gBrowser.getBrowserForDocument(anchorDocument); + if (browser) { + window = enumWindow; + break; + } + + // Look in other subdocuments (sidebar, etc.)? + } + } + + // If we didn't find the anchor's window (or we have no anchor), + // return the most recent browser window. + if (!window) + window = windowMediator.getMostRecentWindow("navigator:browser"); + + return window; +} + diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/passwords.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/passwords.js new file mode 100644 index 0000000..8225da9 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/passwords.js @@ -0,0 +1,92 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; +const { Trait } = require("api-utils/light-traits"); +const utils = require("api-utils/passwords/utils"); +const defer = require("api-utils/utils/function").Enqueued; + +/** + * Utility function that returns `onComplete` and `onError` callbacks form the + * given `options` objects. Also properties are removed from the passed + * `options` objects. + * @param {Object} options + * Object that is passed to the exported functions of this module. + * @returns {Function[]} + * Array with two elements `onComplete` and `onError` functions. + */ +function getCallbacks(options) { + let value = [ + 'onComplete' in options ? options.onComplete : null, + 'onError' in options ? defer(options.onError) : console.exception + ]; + + delete options.onComplete; + delete options.onError; + + return value; +}; + +/** + * Creates a wrapper function that tries to call `onComplete` with a return + * value of the wrapped function or falls back to `onError` if wrapped function + * throws an exception. + */ +function createWrapperMethod(wrapped) { + return function (options) { + let [ onComplete, onError ] = getCallbacks(options); + try { + let value = wrapped(options); + if (onComplete) { + defer(function() { + try { + onComplete(value); + } catch (exception) { + onError(exception); + } + })(); + } + } catch (exception) { + onError(exception); + } + }; +} + +exports.search = createWrapperMethod(utils.search); +exports.store = createWrapperMethod(utils.store); +exports.remove = createWrapperMethod(utils.remove); diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/private-browsing.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/private-browsing.js new file mode 100644 index 0000000..8336e7b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/private-browsing.js @@ -0,0 +1,102 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Paul O’Shannessy <paul@oshannessy.com> + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci} = require("chrome"); +const observers = require("api-utils/observer-service"); +const { EventEmitter } = require("api-utils/events"); +const { setTimeout } = require("api-utils/timer"); +const unload = require("api-utils/unload"); + +const ON_START = "start"; +const ON_STOP = "stop"; +const ON_TRANSITION = "private-browsing-transition-complete"; + +let pbService; +// Currently, only Firefox implements the private browsing service. +if (require("api-utils/xul-app").is("Firefox")) { + pbService = Cc["@mozilla.org/privatebrowsing;1"]. + getService(Ci.nsIPrivateBrowsingService); +} + +function toggleMode(value) pbService.privateBrowsingEnabled = !!value + +const privateBrowsing = EventEmitter.compose({ + constructor: function PrivateBrowsing() { + // Binding method to instance since it will be used with `setTimeout`. + this._emitOnObject = this._emitOnObject.bind(this); + this.unload = this.unload.bind(this); + // Report unhandled errors from listeners + this.on("error", console.exception.bind(console)); + unload.ensure(this); + // We only need to add observers if `pbService` exists. + if (pbService) { + observers.add(ON_TRANSITION, this.onTransition.bind(this)); + this._isActive = pbService.privateBrowsingEnabled; + } + }, + unload: function _destructor() { + this._removeAllListeners(ON_START); + this._removeAllListeners(ON_STOP); + }, + // We don't need to do anything with cancel here. + onTransition: function onTransition() { + let isActive = this._isActive = pbService.privateBrowsingEnabled; + setTimeout(this._emitOnObject, 0, exports, isActive ? ON_START : ON_STOP); + }, + get isActive() this._isActive, + set isActive(value) { + if (pbService) + // We toggle private browsing mode asynchronously in order to work around + // bug 659629. Since private browsing transitions are asynchronous + // anyway, this doesn't significantly change the behavior of the API. + setTimeout(toggleMode, 0, value); + }, + _isActive: false +})() + +Object.defineProperty(exports, "isActive", { + get: function() privateBrowsing.isActive +}); +exports.activate = function activate() privateBrowsing.isActive = true; +exports.deactivate = function deactivate() privateBrowsing.isActive = false; +exports.on = privateBrowsing.on; +exports.once = privateBrowsing.once; +exports.removeListener = privateBrowsing.removeListener; + diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/request.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/request.js new file mode 100644 index 0000000..a72b28c --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/request.js @@ -0,0 +1,309 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Paul O’Shannessy <paul@oshannessy.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; +const xpcom = require("api-utils/xpcom"); +const xhr = require("api-utils/xhr"); +const errors = require("api-utils/errors"); +const apiUtils = require("api-utils/api-utils"); + +// Ugly but will fix with: https://bugzilla.mozilla.org/show_bug.cgi?id=596248 +const EventEmitter = require('api-utils/events').EventEmitter.compose({ + constructor: function EventEmitter() this +}); + +// Instead of creating a new validator for each request, just make one and reuse it. +const validator = new OptionsValidator({ + url: { + //XXXzpao should probably verify that url is a valid url as well + is: ["string"] + }, + headers: { + map: function (v) v || {}, + is: ["object"], + }, + content: { + map: function (v) v || null, + is: ["string", "object", "null"], + }, + contentType: { + map: function (v) v || "application/x-www-form-urlencoded", + is: ["string"], + }, + overrideMimeType: { + map: function(v) v || null, + is: ["string", "null"], + } +}); + +const REUSE_ERROR = "This request object has been used already. You must " + + "create a new one to make a new request." + +function Request(options) { + const self = EventEmitter(), + _public = self._public; + // request will hold the actual XHR object + let request; + let response; + + if ('onComplete' in options) + self.on('complete', options.onComplete) + options = validator.validateOptions(options); + + // function to prep the request since it's the same between GET and POST + function makeRequest(mode) { + // If this request has already been used, then we can't reuse it. Throw an error. + if (request) { + throw new Error(REUSE_ERROR); + } + + request = new xhr.XMLHttpRequest(); + + let url = options.url; + // Build the data to be set. For GET requests, we want to append that to + // the URL before opening the request. + let data = makeQueryString(options.content); + if (mode == "GET" && data) { + // If the URL already has ? in it, then we want to just use & + url = url + (/\?/.test(url) ? "&" : "?") + data; + } + + // open the request + request.open(mode, url); + + // request header must be set after open, but before send + request.setRequestHeader("Content-Type", options.contentType); + + // set other headers + for (let k in options.headers) { + request.setRequestHeader(k, options.headers[k]); + } + + // set overrideMimeType + if (options.overrideMimeType) { + request.overrideMimeType(options.overrideMimeType); + } + + // handle the readystate, create the response, and call the callback + request.onreadystatechange = function () { + if (request.readyState == 4) { + response = new Response(request); + errors.catchAndLog(function () { + self._emit('complete', response); + })(); + } + } + + // actually send the request. we only want to send data on POST requests + request.send(mode == "POST" ? data : null); + } + + // Map these setters/getters to the options + ["url", "headers", "content", "contentType"].forEach(function (k) { + _public.__defineGetter__(k, function () options[k]); + _public.__defineSetter__(k, function (v) { + // This will automatically rethrow errors from apiUtils.validateOptions. + return options[k] = validator.validateSingleOption(k, v); + }); + }); + + // response should be available as a getter + _public.__defineGetter__("response", function () response); + + _public.get = function () { + makeRequest("GET"); + return this; + }; + + _public.post = function () { + makeRequest("POST"); + return this; + }; + + return _public; +} +exports.Request = Request; + +// Converts an object of unordered key-vals to a string that can be passed +// as part of a request +function makeQueryString(content) { + // Explicitly return null if we have null, and empty string, or empty object. + if (!content) { + return null; + } + + // If content is already a string, just return it as is. + if (typeof(content) == "string") { + return content; + } + + // At this point we have a k:v object. Iterate over it and encode each value. + // Arrays and nested objects will get encoded as needed. For example... + // + // { foo: [1, 2, { omg: "bbq", "all your base!": "are belong to us" }], bar: "baz" } + // + // will be encoded as + // + // foo[0]=1&foo[1]=2&foo[2][omg]=bbq&foo[2][all+your+base!]=are+belong+to+us&bar=baz + // + // Keys (including "[" and "]") and values will be encoded with + // fixedEncodeURIComponent before returning. + // + // Execution was inspired by jQuery, but some details have changed and numeric + // array keys are included (whereas they are not in jQuery). + + let encodedContent = []; + function add(key, val) { + encodedContent.push(fixedEncodeURIComponent(key) + "=" + + fixedEncodeURIComponent(val)); + } + + function make(key, val) { + if (typeof(val) === "object" && val !== null) { + for ([k, v] in Iterator(val)) { + make(key + "[" + k + "]", v); + } + } + else { + add(key, val) + } + } + for ([k, v] in Iterator(content)) { + make(k, v); + } + return encodedContent.join("&"); + + //XXXzpao In theory, we can just use a FormData object on 1.9.3, but I had + // trouble getting that working. It would also be nice to stay + // backwards-compat as long as possible. Keeping this in for now... + // let formData = Cc["@mozilla.org/files/formdata;1"]. + // createInstance(Ci.nsIDOMFormData); + // for ([k, v] in Iterator(content)) { + // formData.append(k, v); + // } + // return formData; +} + + +// encodes a string safely for application/x-www-form-urlencoded +// adheres to RFC 3986 +// see https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Functions/encodeURIComponent +function fixedEncodeURIComponent (str) { + return encodeURIComponent(str).replace(/%20/g, "+").replace(/!/g, "%21"). + replace(/'/g, "%27").replace(/\(/g, "%28"). + replace(/\)/g, "%29").replace(/\*/g, "%2A"); +} + +function Response(request) { + // Define the straight mappings of our value to original request value + xpcom.utils.defineLazyGetter(this, "text", function () request.responseText); + xpcom.utils.defineLazyGetter(this, "xml", function () { + throw new Error("Sorry, the 'xml' property is no longer available. " + + "see bug 611042 for more information."); + }); + xpcom.utils.defineLazyGetter(this, "status", function () request.status); + xpcom.utils.defineLazyGetter(this, "statusText", function () request.statusText); + + // this.json should be the JS object, so we need to attempt to parse it. + xpcom.utils.defineLazyGetter(this, "json", function () { + let _json = null; + try { + _json = JSON.parse(this.text); + } + catch (e) {} + return _json; + }); + + // this.headers also should be a JS object, so we need to split up the raw + // headers string provided by the request. + xpcom.utils.defineLazyGetter(this, "headers", function () { + let _headers = {}; + let lastKey; + // Since getAllResponseHeaders() will return null if there are no headers, + // defend against it by defaulting to "" + let rawHeaders = request.getAllResponseHeaders() || ""; + rawHeaders.split("\n").forEach(function (h) { + // According to the HTTP spec, the header string is terminated by an empty + // line, so we can just skip it. + if (!h.length) { + return; + } + + let index = h.indexOf(":"); + // The spec allows for leading spaces, so instead of assuming a single + // leading space, just trim the values. + let key = h.substring(0, index).trim(), + val = h.substring(index + 1).trim(); + + // For empty keys, that means that the header value spanned multiple lines. + // In that case we should append the value to the value of lastKey with a + // new line. We'll assume lastKey will be set because there should never + // be an empty key on the first pass. + if (key) { + _headers[key] = val; + lastKey = key; + } + else { + _headers[lastKey] += "\n" + val; + } + }); + return _headers; + }) +} + +// apiUtils.validateOptions doesn't give the ability to easily validate single +// options, so this is a wrapper that provides that ability. +function OptionsValidator(rules) { + this.rules = rules; + + this.validateOptions = function (options) { + return apiUtils.validateOptions(options, this.rules); + } + + this.validateSingleOption = function (field, value) { + // We need to create a single rule object from our listed rules. To avoid + // JavaScript String warnings, check for the field & default to an empty object. + let singleRule = {}; + if (field in this.rules) { + singleRule[field] = this.rules[field]; + } + let singleOption = {}; + singleOption[field] = value; + // This should throw if it's invalid, which will bubble up & out. + return apiUtils.validateOptions(singleOption, singleRule)[field]; + } +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/selection.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/selection.js new file mode 100644 index 0000000..48db7fc --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/selection.js @@ -0,0 +1,448 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Eric H. Jung <eric.jung@yahoo.com> + * Irakli Gozalishivili <gozala@mozilla.com> + * Matteo Ferretti <zer0@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The selection module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +let { Ci } = require("chrome"), + { setTimeout } = require("api-utils/timer"), + { EventEmitter } = require("api-utils/events"); + +// The selection type HTML +const HTML = 0x01; + +// The selection type TEXT +const TEXT = 0x02; + +// The selection type DOM (internal use only) +const DOM = 0x03; + +// A more developer-friendly message than the caught exception when is not +// possible change a selection. +const ERR_CANNOT_CHANGE_SELECTION = + "It isn't possible to change the selection, as there isn't currently a selection"; + +/** + * Creates an object from which a selection can be set, get, etc. Each + * object has an associated with a range number. Range numbers are the + * 0-indexed counter of selection ranges as explained at + * https://developer.mozilla.org/en/DOM/Selection. + * + * @param rangeNumber + * The zero-based range index into the selection + */ +function Selection(rangeNumber) { + + // In order to hide the private rangeNumber argument from API consumers while + // still enabling Selection getters/setters to access it, the getters/setters + // are defined as lexical closures in the Selector constructor. + + this.__defineGetter__("text", function () getSelection(TEXT, rangeNumber)); + this.__defineSetter__("text", function (str) setSelection(str, rangeNumber)); + + this.__defineGetter__("html", function () getSelection(HTML, rangeNumber)); + this.__defineSetter__("html", function (str) setSelection(str, rangeNumber)); + + this.__defineGetter__("isContiguous", function () { + let sel = getSelection(DOM); + + // If there are multiple ranges, the selection is definitely discontiguous. + // It returns `false` also if there are no selection; and `true` if there is + // a single non empty range, or a selection in a text field - contiguous or + // not (text field selection APIs doesn't support multiple selections). + + if (sel.rangeCount > 1) + return false; + + return !!(safeGetRange(sel, 0) || getElementWithSelection()); + }); +} + +require("api-utils/xpcom").utils.defineLazyServiceGetter(this, "windowMediator", + "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"); + +/** + * Returns the most recent content window + */ +function context() { + // Overlay names should probably go into the xul-app module instead of here + return windowMediator.getMostRecentWindow("navigator:browser").document. + commandDispatcher.focusedWindow; +} + +/** + * Returns the current selection from most recent content window. Depending on + * the specified |type|, the value returned can be a string of text, stringified + * HTML, or a DOM selection object as described at + * https://developer.mozilla.org/en/DOM/Selection. + * + * @param type + * Specifies the return type of the selection. Valid values are the one + * of the constants HTML, TEXT, or DOM. + * + * @param rangeNumber + * Specifies the zero-based range index of the returned selection. + */ +function getSelection(type, rangeNumber) { + let window, selection; + try { + window = context(); + selection = window.getSelection(); + } + catch (e) { + return null; + } + + // Get the selected content as the specified type + if (type == DOM) + return selection; + else if (type == TEXT) { + let range = safeGetRange(selection, rangeNumber); + + if (range) + return range.toString(); + + let node = getElementWithSelection(window); + + if (!node) + return null; + + return node.value.substring(node.selectionStart, node.selectionEnd); + } + else if (type == HTML) { + let range = safeGetRange(selection, rangeNumber); + // Another way, but this includes the xmlns attribute for all elements in + // Gecko 1.9.2+ : + // return Cc["@mozilla.org/xmlextras/xmlserializer;1"]. + // createInstance(Ci.nsIDOMSerializer).serializeToSTring(range. + // cloneContents()); + if (!range) + return null; + let node = window.document.createElement("span"); + node.appendChild(range.cloneContents()); + return node.innerHTML; + } + throw new Error("Type " + type + " is unrecognized."); +} + +/** + * Returns the specified range in a selection without throwing an exception. + * + * @param selection + * A selection object as described at + * https://developer.mozilla.org/en/DOM/Selection + * + * @param rangeNumber + * Specifies the zero-based range index of the returned selection. + */ +function safeGetRange(selection, rangeNumber) { + try { + let range = selection.getRangeAt(rangeNumber); + if (!range || range.toString() == "") + return null; + return range; + } + catch (e) { + return null; + } +} + +/** + * Returns a reference of the DOM's active element for the window given, if it + * supports the text field selection API and has a text selected. + * + * Note: + * we need this method because window.getSelection doesn't return a selection + * for text selected in a form field (see bug 85686) + * + * @param {nsIWindow} [window] + * A reference to a window + */ +function getElementWithSelection(window) { + let element; + + try { + element = (window || context()).document.activeElement; + } + catch (e) { + element = null; + } + + if (!element) + return null; + + let { value, selectionStart, selectionEnd } = element; + + let hasSelection = typeof value === "string" && + !isNaN(selectionStart) && + !isNaN(selectionEnd) && + selectionStart !== selectionEnd; + + return hasSelection ? element : null; +} +/** + * Sets the current selection of the most recent content document by changing + * the existing selected text/HTML range to the specified value. + * + * @param val + * The value for the new selection + * + * @param rangeNumber + * The zero-based range index of the selection to be set + * + */ +function setSelection(val, rangeNumber) { + // Make sure we have a window context & that there is a current selection. + // Selection cannot be set unless there is an existing selection. + let window, selection; + + try { + window = context(); + selection = window.getSelection(); + } + catch (e) { + throw new Error(ERR_CANNOT_CHANGE_SELECTION); + } + + let range = safeGetRange(selection, rangeNumber); + + if (range) { + // Get rid of the current selection and insert our own + range.deleteContents(); + let node = window.document.createElement("span"); + range.surroundContents(node); + + // Some relevant JEP-111 requirements: + + // Setting the text property replaces the selection with the value to + // which the property is set and sets the html property to the same value + // to which the text property is being set. + + // Setting the html property replaces the selection with the value to + // which the property is set and sets the text property to the text version + // of the HTML value. + + // This sets both the HTML and text properties. + node.innerHTML = val; + } else { + let node = getElementWithSelection(window); + + if (!node) + throw new Error(ERR_CANNOT_CHANGE_SELECTION); + + let { value, selectionStart, selectionEnd } = node; + + let newSelectionEnd = selectionStart + val.length; + + node.value = value.substring(0, selectionStart) + + val + + value.substring(selectionEnd, value.length); + + node.setSelectionRange(selectionStart, newSelectionEnd); + } +} + +function onLoad(event) { + SelectionListenerManager.onLoad(event); +} + +function onUnload(event) { + SelectionListenerManager.onUnload(event); +} + +function onSelect() { + SelectionListenerManager.onSelect(); +} + +let SelectionListenerManager = { + QueryInterface: require("api-utils/xpcom").utils. + generateQI([Ci.nsISelectionListener]), + + // The collection of listeners wanting to be notified of selection changes + listeners: EventEmitter.compose({ + emit: function emit(type) this._emitOnObject(exports, type), + off: function() this._removeAllListeners.apply(this, arguments) + })(), + /** + * This is the nsISelectionListener implementation. This function is called + * by Gecko when a selection is changed interactively. + * + * We only pay attention to the SELECTALL, KEYPRESS, and MOUSEUP selection + * reasons. All reasons are listed here: + * + * http://mxr.mozilla.org/mozilla1.9.2/source/content/base/public/ + * nsISelectionListener.idl + * + * The other reasons (NO_REASON, DRAG_REASON, MOUSEDOWN_REASON) aren't + * applicable to us. + */ + notifySelectionChanged: function notifySelectionChanged(document, selection, + reason) { + if (!["SELECTALL", "KEYPRESS", "MOUSEUP"].some(function(type) reason & + Ci.nsISelectionListener[type + "_REASON"]) || selection.toString() == "") + return; + + this.onSelect(); + }, + + onSelect : function onSelect() { + setTimeout(this.listeners.emit, 0, "select"); + }, + + /** + * Part of the Tracker implementation. This function is called by the + * tabs module when a browser is being tracked. Often, that means a new tab + * has been opened, but it can also mean an addon has been installed while + * tabs are already opened. In that case, this function is called for those + * already-opened tabs. + * + * @param browser + * The browser being tracked + */ + onTrack: function onTrack(browser) { + browser.addEventListener("load", onLoad, true); + browser.addEventListener("unload", onUnload, true); + }, + + onLoad: function onLoad(event) { + // Nothing to do without a useful window + let window = event.target.defaultView; + if (!window) + return; + + // Wrap the add selection call with some number of setTimeout 0 because some + // reason it's possible to add a selection listener "too early". 2 sometimes + // works for gmail, and more consistently with 3, so make it 5 to be safe. + let count = 0; + let self = this; + function wrap(count, func) { + if (count-- > 0) + require("api-utils/timer").setTimeout(wrap, 0); + else + self.addSelectionListener(window); + } + wrap(); + }, + + addSelectionListener: function addSelectionListener(window) { + if (window.jetpack_core_selection_listener) + return; + let selection = window.getSelection(); + if (selection instanceof Ci.nsISelectionPrivate) + selection.addSelectionListener(this); + + // nsISelectionListener implementation seems not fire a notification if + // a selection is in a text field, therefore we need to add a listener to + // window.onselect, that is fired only for text fields. + // https://developer.mozilla.org/en/DOM/window.onselect + window.addEventListener("select", onSelect, true); + + window.jetpack_core_selection_listener = true; + }, + + onUnload: function onUnload(event) { + // Nothing to do without a useful window + let window = event.target.defaultView; + if (!window) + return; + this.removeSelectionListener(window); + this.listeners.off('error'); + this.listeners.off('selection'); + }, + + removeSelectionListener: function removeSelectionListener(window) { + if (!window.jetpack_core_selection_listener) + return; + let selection = window.getSelection(); + if (selection instanceof Ci.nsISelectionPrivate) + selection.removeSelectionListener(this); + + window.removeEventListener("select", onSelect); + + window.jetpack_core_selection_listener = false; + }, + + /** + * Part of the TabTracker implementation. This function is called by the + * tabs module when a browser is being untracked. Usually, that means a tab + * has been closed. + * + * @param browser + * The browser being untracked + */ + onUntrack: function onUntrack(browser) { + browser.removeEventListener("load", onLoad, true); + browser.removeEventListener("unload", onUnload, true); + } +}; +SelectionListenerManager.listeners.on('error', console.error); + +/** + * Install |SelectionListenerManager| as tab tracker in order to watch + * tab opening/closing + */ +require("api-utils/tab-browser").Tracker(SelectionListenerManager); + +/** + * Exports an iterator so that discontiguous selections can be iterated. + * + * If discontiguous selections are in a text field, only the first one + * is returned because the text field selection APIs doesn't support + * multiple selections. + */ +exports.__iterator__ = function __iterator__() { + let sel = getSelection(DOM); + let rangeCount = sel.rangeCount || (getElementWithSelection() ? 1 : 0); + + for (let i = 0; i < rangeCount; i++) + yield new Selection(i); +}; + +exports.on = SelectionListenerManager.listeners.on; +exports.removeListener = SelectionListenerManager.listeners.removeListener; + +// Export the Selection singleton. Its rangeNumber is always zero. +Selection.call(exports, 0); + diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/simple-prefs.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/simple-prefs.js new file mode 100644 index 0000000..c7a059b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/simple-prefs.js @@ -0,0 +1,107 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Hernan Rodriguez Colmeiro <colmeiro@gmail.com> (Original Author) + * Erik Vold <erikvvold@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const { Cc, Ci } = require("chrome"); +const observers = require("observer-service"); +const { EventEmitter } = require("events"); +const unload = require("unload"); +const prefService = require("preferences-service"); +const { jetpackID } = require("@packaging"); + +const ADDON_BRANCH = "extensions." + jetpackID + "."; +const BUTTON_PRESSED = jetpackID + "-cmdPressed"; + +// XXX Currently, only Firefox implements the inline preferences. +if (!require("xul-app").is("Firefox")) + throw Error("This API is only supported in Firefox"); + +let branch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(ADDON_BRANCH). + QueryInterface(Ci.nsIPrefBranch2); + +const events = EventEmitter.compose({ + constructor: function Prefs() { + // Log unhandled errors. + this.on("error", console.exception.bind(console)); + + // Make sure we remove all the listeners + unload.ensure(this); + + this._prefObserver = this._prefObserver.bind(this); + this._buttonObserver = this._buttonObserver.bind(this); + + // Listen to changes in the preferences + branch.addObserver("", this._prefObserver, false); + + // Listen to clicks on buttons + observers.add(BUTTON_PRESSED, this._buttonObserver, this); + }, + _prefObserver: function PrefsPrefObserver(subject, topic, prefName) { + if (topic == "nsPref:changed") { + this._emit(prefName, prefName); + } + }, + _buttonObserver: function PrefsButtonObserver(subject, data) { + this._emit(data); + }, + unload: function manager_unload() { + this._removeAllListeners("error"); + branch.removeObserver("", this._prefObserver); + }, +})(); + +const simple = Proxy.create({ + get: function(receiver, pref) { + return prefService.get(ADDON_BRANCH + pref); + }, + set: function(receiver, pref, val) { + prefService.set(ADDON_BRANCH + pref, val); + }, + delete: function(pref) { + prefService.reset(ADDON_BRANCH + pref); + return true; + }, + has: function(pref) { + return prefService.has(ADDON_BRANCH + pref); + } +}); + +exports.on = events.on; +exports.removeListener = events.removeListener; +exports.prefs = simple; diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/simple-storage.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/simple-storage.js new file mode 100644 index 0000000..b719e27 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/simple-storage.js @@ -0,0 +1,263 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci} = require("chrome"); +const file = require("api-utils/file"); +const prefs = require("api-utils/preferences-service"); +const jpSelf = require("self"); +const timer = require("api-utils/timer"); +const unload = require("api-utils/unload"); +const { EventEmitter } = require("api-utils/events"); +const { Trait } = require("api-utils/traits"); + +const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod"; +const WRITE_PERIOD_DEFAULT = 300000; // 5 minutes + +const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota"; +const QUOTA_DEFAULT = 5242880; // 5 MiB + +const JETPACK_DIR_BASENAME = "jetpack"; + + +// simpleStorage.storage +exports.__defineGetter__("storage", function () manager.root); +exports.__defineSetter__("storage", function (val) manager.root = val); + +// simpleStorage.quotaUsage +exports.__defineGetter__("quotaUsage", function () manager.quotaUsage); + +// A generic JSON store backed by a file on disk. This should be isolated +// enough to move to its own module if need be... +function JsonStore(options) { + this.filename = options.filename; + this.quota = options.quota; + this.writePeriod = options.writePeriod; + this.onOverQuota = options.onOverQuota; + this.onWrite = options.onWrite; + + unload.ensure(this); + + this.writeTimer = timer.setInterval(this.write.bind(this), + this.writePeriod); +} + +JsonStore.prototype = { + // The store's root. + get root() { + return this.isRootInited ? this._root : {}; + }, + + // Performs some type checking. + set root(val) { + let types = ["array", "boolean", "null", "number", "object", "string"]; + if (types.indexOf(typeof(val)) < 0) { + throw new Error("storage must be one of the following types: " + + types.join(", ")); + } + this._root = val; + return val; + }, + + // True if the root has ever been set (either via the root setter or by the + // backing file's having been read). + get isRootInited() { + return this._root !== undefined; + }, + + // Percentage of quota used, as a number [0, Inf). > 1 implies over quota. + // Undefined if there is no quota. + get quotaUsage() { + return this.quota > 0 ? + JSON.stringify(this.root).length / this.quota : + undefined; + }, + + // Removes the backing file and all empty subdirectories. + purge: function JsonStore_purge() { + try { + // This'll throw if the file doesn't exist. + file.remove(this.filename); + let parentPath = this.filename; + do { + parentPath = file.dirname(parentPath); + // This'll throw if the dir isn't empty. + file.rmdir(parentPath); + } while (file.basename(parentPath) !== JETPACK_DIR_BASENAME); + } + catch (err) {} + }, + + // Initializes the root by reading the backing file. + read: function JsonStore_read() { + try { + let str = file.read(this.filename); + + // Ideally we'd log the parse error with console.error(), but logged + // errors cause tests to fail. Supporting "known" errors in the test + // harness appears to be non-trivial. Maybe later. + this.root = JSON.parse(str); + } + catch (err) { + this.root = {}; + } + }, + + // If the store is under quota, writes the root to the backing file. + // Otherwise quota observers are notified and nothing is written. + write: function JsonStore_write() { + if (this.quotaUsage > 1) + this.onOverQuota(this); + else + this._write(); + }, + + // Cleans up on unload. If unloading because of uninstall, the store is + // purged; otherwise it's written. + unload: function JsonStore_unload(reason) { + timer.clearInterval(this.writeTimer); + this.writeTimer = null; + + if (reason === "uninstall") + this.purge(); + else + this._write(); + }, + + // True if the root is an empty object. + get _isEmpty() { + if (this.root && typeof(this.root) === "object") { + let empty = true; + for (let key in this.root) { + empty = false; + break; + } + return empty; + } + return false; + }, + + // Writes the root to the backing file, notifying write observers when + // complete. If the store is over quota or if it's empty and the store has + // never been written, nothing is written and write observers aren't notified. + _write: function JsonStore__write() { + // Don't write if the root is uninitialized or if the store is empty and the + // backing file doesn't yet exist. + if (!this.isRootInited || (this._isEmpty && !file.exists(this.filename))) + return; + + // If the store is over quota, don't write. The current under-quota state + // should persist. + if (this.quotaUsage > 1) + return; + + // Finally, write. + let stream = file.open(this.filename, "w"); + try { + stream.writeAsync(JSON.stringify(this.root), function writeAsync(err) { + if (err) + console.error("Error writing simple storage file: " + this.filename); + else if (this.onWrite) + this.onWrite(this); + }.bind(this)); + } + catch (err) { + // writeAsync closes the stream after it's done, so only close on error. + stream.close(); + } + } +}; + + +// This manages a JsonStore singleton and tailors its use to simple storage. +// The root of the JsonStore is lazy-loaded: The backing file is only read the +// first time the root's gotten. +let manager = Trait.compose(EventEmitter, Trait.compose({ + jsonStore: null, + + // The filename of the store, based on the profile dir and extension ID. + get filename() { + let storeFile = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + storeFile.append(JETPACK_DIR_BASENAME); + storeFile.append(jpSelf.id); + storeFile.append("simple-storage"); + file.mkpath(storeFile.path); + storeFile.append("store.json"); + return storeFile.path; + }, + + get quotaUsage() { + return this.jsonStore.quotaUsage; + }, + + get root() { + if (!this.jsonStore.isRootInited) + this.jsonStore.read(); + return this.jsonStore.root; + }, + + set root(val) { + return this.jsonStore.root = val; + }, + + unload: function manager_unload() { + this._removeAllListeners("OverQuota"); + this._removeAllListeners("error"); + }, + + constructor: function manager_constructor() { + // Log unhandled errors. + this.on("error", console.exception.bind(console)); + unload.ensure(this); + + this.jsonStore = new JsonStore({ + filename: this.filename, + writePeriod: prefs.get(WRITE_PERIOD_PREF, WRITE_PERIOD_DEFAULT), + quota: prefs.get(QUOTA_PREF, QUOTA_DEFAULT), + onOverQuota: this._emitOnObject.bind(this, exports, "OverQuota") + }); + } +}))(); + +exports.on = manager.on; +exports.removeListener = manager.removeListener; diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/tabs.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/tabs.js new file mode 100644 index 0000000..81681d6 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/tabs.js @@ -0,0 +1,62 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dietrich Ayala <dietrich@mozilla.com> (Original author) + * Felipe Gomes <felipc@gmail.com> + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The tabs module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +const { browserWindows } = require("./windows"); +const { tabs } = require("api-utils/windows/tabs"); + +Object.defineProperties(tabs, { + open: { value: function open(options) { + if (options.inNewWindow) + // `tabs` option is under review and may be removed. + return browserWindows.open({ tabs: [ options ] }); + // Open in active window if new window was not required. + return browserWindows.activeWindow.tabs.open(options); + }} +}); +// It's a hack but we will be able to remove it once will implement CommonJS +// feature that would allow us to override exports. +exports.__proto__ = tabs; diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/timers.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/timers.js new file mode 100644 index 0000000..9373dea --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/timers.js @@ -0,0 +1,40 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +// This module just proxies to the low level equivalent "timer" in "api-utils". +module.exports = require("api-utils/timer"); diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/widget.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/widget.js new file mode 100644 index 0000000..9a169f4 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/widget.js @@ -0,0 +1,947 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dietrich Ayala <dietrich@mozilla.com> (Original Author) + * Drew Willcoxon <adw@mozilla.com> + * Irakli Gozalishvili <gozala@mozilla.com> + * Alexandre Poirot <apoirot@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc, Ci} = require("chrome"); + +// Widget content types +const CONTENT_TYPE_URI = 1; +const CONTENT_TYPE_HTML = 2; +const CONTENT_TYPE_IMAGE = 3; + +const ERR_CONTENT = "No content or contentURL property found. Widgets must " + + "have one or the other.", + ERR_LABEL = "The widget must have a non-empty label property.", + ERR_ID = "You have to specify a unique value for the id property of " + + "your widget in order for the application to remember its " + + "position.", + ERR_DESTROYED = "The widget has been destroyed and can no longer be used."; + +// Supported events, mapping from DOM event names to our event names +const EVENTS = { + "click": "click", + "mouseover": "mouseover", + "mouseout": "mouseout", +}; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The widget module currently supports only Firefox. In the future ", + "it will support other applications. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +const { validateOptions } = require("api-utils/api-utils"); +const panels = require("./panel"); +const { EventEmitter, EventEmitterTrait } = require("api-utils/events"); +const { Trait } = require("api-utils/traits"); +const LightTrait = require('api-utils/light-traits').Trait; +const { Loader, Symbiont } = require("api-utils/content"); +const timer = require("api-utils/timer"); +const { Cortex } = require('api-utils/cortex'); +const windowsAPI = require("./windows"); +const unload = require("api-utils/unload"); + +// Data types definition +const valid = { + number: { is: ["null", "undefined", "number"] }, + string: { is: ["null", "undefined", "string"] }, + id: { + is: ["string"], + ok: function (v) v.length > 0, + msg: ERR_ID, + readonly: true + }, + label: { + is: ["string"], + ok: function (v) v.length > 0, + msg: ERR_LABEL + }, + panel: { + is: ["null", "undefined", "object"], + ok: function(v) !v || v instanceof panels.Panel + }, + width: { + is: ["null", "undefined", "number"], + map: function (v) { + if (null === v || undefined === v) v = 16; + return v; + }, + defaultValue: 16 + }, + allow: { + is: ["null", "undefined", "object"], + map: function (v) { + if (!v) v = { script: true }; + return v; + }, + get defaultValue() ({ script: true }) + }, +}; + +// Widgets attributes definition +let widgetAttributes = { + label: valid.label, + id: valid.id, + tooltip: valid.string, + width: valid.width, + content: valid.string, + panel: valid.panel, + allow: valid.allow +}; + +// Import data definitions from loader, but don't compose with it as Model +// functions allow us to recreate easily all Loader code. +let loaderAttributes = require("api-utils/content/loader").validationAttributes; +for (let i in loaderAttributes) + widgetAttributes[i] = loaderAttributes[i]; + +widgetAttributes.contentURL.optional = true; + +// Widgets public events list, that are automatically binded in options object +const WIDGET_EVENTS = [ + "click", + "mouseover", + "mouseout", + "error", + "message", + "attach" +]; + +// `Model` utility functions that help creating these various Widgets objects +let model = { + + // Validate one attribute using api-utils.js:validateOptions function + _validate: function _validate(name, suspect, validation) { + let $1 = {}; + $1[name] = suspect; + let $2 = {}; + $2[name] = validation; + return validateOptions($1, $2)[name]; + }, + + /** + * This method has two purposes: + * 1/ Validate and define, on a given object, a set of attribute + * 2/ Emit a "change" event on this object when an attribute is changed + * + * @params {Object} object + * Object on which we can bind attributes on and watch for their changes. + * This object must have an EventEmitter interface, or, at least `_emit` + * method + * @params {Object} attrs + * Dictionary of attributes definition following api-utils:validateOptions + * scheme + * @params {Object} values + * Dictionary of attributes default values + */ + setAttributes: function setAttributes(object, attrs, values) { + let properties = {}; + for (let name in attrs) { + let value = values[name]; + let req = attrs[name]; + + // Retrieve default value from typedef if the value is not defined + if ((typeof value == "undefined" || value == null) && req.defaultValue) + value = req.defaultValue; + + // Check for valid value if value is defined or mandatory + if (!req.optional || typeof value != "undefined") + value = model._validate(name, value, req); + + // In any case, define this property on `object` + let property = null; + if (req.readonly) { + property = { + value: value, + writable: false, + enumerable: true, + configurable: false + }; + } + else { + property = model._createWritableProperty(name, value); + } + + properties[name] = property; + } + Object.defineProperties(object, properties); + }, + + // Generate ES5 property definition for a given attribute + _createWritableProperty: function _createWritableProperty(name, value) { + return { + get: function () { + return value; + }, + set: function (newValue) { + value = newValue; + // The main goal of all this Model stuff is here: + // We want to forward all changes to some listeners + this._emit("change", name, value); + }, + enumerable: true, + configurable: false + }; + }, + + /** + * Automagically register listeners in options dictionary + * by detecting listener attributes with name starting with `on` + * + * @params {Object} object + * Target object that need to follow EventEmitter interface, or, at least, + * having `on` method. + * @params {Array} events + * List of events name to automatically bind. + * @params {Object} listeners + * Dictionary of event listener functions to register. + */ + setEvents: function setEvents(object, events, listeners) { + for (let i = 0, l = events.length; i < l; i++) { + let name = events[i]; + let onName = "on" + name[0].toUpperCase() + name.substr(1); + if (!listeners[onName]) + continue; + object.on(name, listeners[onName].bind(object)); + } + } + +}; + + +/** + * Main Widget class: entry point of the widget API + * + * Allow to control all widget across all existing windows with a single object. + * Widget.getView allow to retrieve a WidgetView instance to control a widget + * specific to one window. + */ +const WidgetTrait = LightTrait.compose(EventEmitterTrait, LightTrait({ + + _initWidget: function _initWidget(options) { + model.setAttributes(this, widgetAttributes, options); + + browserManager.validate(this); + + // We must have at least content or contentURL defined + if (!(this.content || this.contentURL)) + throw new Error(ERR_CONTENT); + + this._views = []; + + // Set tooltip to label value if we don't have tooltip defined + if (!this.tooltip) + this.tooltip = this.label; + + model.setEvents(this, WIDGET_EVENTS, options); + + this.on('change', this._onChange.bind(this)); + + let self = this; + this._port = EventEmitterTrait.create({ + emit: function () { + let args = arguments; + self._views.forEach(function(v) v.port.emit.apply(v.port, args)); + } + }); + // expose wrapped port, that exposes only public properties. + this._port._public = Cortex(this._port); + + // Register this widget to browser manager in order to create new widget on + // all new windows + browserManager.addItem(this); + }, + + _onChange: function _onChange(name, value) { + // Set tooltip to label value if we don't have tooltip defined + if (name == 'tooltip' && !value) { + // we need to change tooltip again in order to change the value of the + // attribute itself + this.tooltip = this.label; + return; + } + + // Forward attributes changes to WidgetViews + if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) { + this._views.forEach(function(v) v[name] = value); + } + }, + + _onEvent: function _onEvent(type, eventData) { + this._emit(type, eventData); + }, + + _createView: function _createView() { + // Create a new WidgetView instance + let view = WidgetView(this); + + // Keep a reference to it + this._views.push(view); + + // Emit an `attach` event with a WidgetView instance without private attrs + this._emit("attach", view._public); + + return view; + }, + + // a WidgetView instance is destroyed + _onViewDestroyed: function _onViewDestroyed(view) { + let idx = this._views.indexOf(view); + this._views.splice(idx, 1); + }, + + /** + * Called on browser window closed, to destroy related WidgetViews + * @params {ChromeWindow} window + * Window that has been closed + */ + _onWindowClosed: function _onWindowClosed(window) { + for each (let view in this._views) { + if (view._isInChromeWindow(window)) { + view.destroy(); + break; + } + } + }, + + /** + * Get the WidgetView instance related to a BrowserWindow instance + * @params {BrowserWindow} window + * BrowserWindow reference from "windows" module + */ + getView: function getView(window) { + for each (let view in this._views) { + if (view._isInWindow(window)) { + return view._public; + } + } + return null; + }, + + get port() this._port._public, + set port(v) {}, // Work around Cortex failure with getter without setter + // See bug 653464 + _port: null, + + postMessage: function postMessage(message) { + this._views.forEach(function(v) v.postMessage(message)); + }, + + destroy: function destroy() { + if (this.panel) + this.panel.destroy(); + + // Dispatch destroy calls to views + // we need to go backward as we remove items from this array in + // _onViewDestroyed + for (let i = this._views.length - 1; i >= 0; i--) + this._views[i].destroy(); + + // Unregister widget to stop creating it over new windows + // and allow creation of new widget with same id + browserManager.removeItem(this); + } + +})); + +// Widget constructor +const Widget = function Widget(options) { + let w = WidgetTrait.create(Widget.prototype); + w._initWidget(options); + + // Return a Cortex of widget in order to hide private attributes like _onEvent + let _public = Cortex(w); + unload.ensure(_public, "destroy"); + return _public; +} +exports.Widget = Widget; + + + +/** + * WidgetView is an instance of a widget for a specific window. + * + * This is an external API that can be retrieved by calling Widget.getView or + * by watching `attach` event on Widget. + */ +const WidgetViewTrait = LightTrait.compose(EventEmitterTrait, LightTrait({ + + // Reference to the matching WidgetChrome + // set right after constructor call + _chrome: null, + + // Public interface of the WidgetView, passed in `attach` event or in + // Widget.getView + _public: null, + + _initWidgetView: function WidgetView__initWidgetView(baseWidget) { + this._baseWidget = baseWidget; + + model.setAttributes(this, widgetAttributes, baseWidget); + + this.on('change', this._onChange.bind(this)); + + let self = this; + this._port = EventEmitterTrait.create({ + emit: function () { + if (!self._chrome) + throw new Error(ERR_DESTROYED); + self._chrome.update(self._baseWidget, "emit", arguments); + } + }); + // expose wrapped port, that exposes only public properties. + this._port._public = Cortex(this._port); + + this._public = Cortex(this); + }, + + _onChange: function WidgetView__onChange(name, value) { + if (name == 'tooltip' && !value) { + this.tooltip = this.label; + return; + } + + // Forward attributes changes to WidgetChrome instance + if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) { + this._chrome.update(this._baseWidget, name, value); + } + }, + + _onEvent: function WidgetView__onEvent(type, eventData, domNode) { + // Dispatch event in view + this._emit(type, eventData); + + // And forward it to the main Widget object + if ("click" == type || type.indexOf("mouse") == 0) + this._baseWidget._onEvent(type, this._public); + else + this._baseWidget._onEvent(type, eventData); + + // Special case for click events: if the widget doesn't have a click + // handler, but it does have a panel, display the panel. + if ("click" == type && !this._listeners("click").length && this.panel) + this.panel.show(domNode); + }, + + _isInWindow: function WidgetView__isInWindow(window) { + return windowsAPI.BrowserWindow({ + window: this._chrome.window + }) == window; + }, + + _isInChromeWindow: function WidgetView__isInChromeWindow(window) { + return this._chrome.window == window; + }, + + _onPortEvent: function WidgetView__onPortEvent(args) { + let port = this._port; + port._emit.apply(port, args); + let basePort = this._baseWidget._port; + basePort._emit.apply(basePort, args); + }, + + get port() this._port._public, + set port(v) {}, // Work around Cortex failure with getter without setter + // See bug 653464 + _port: null, + + postMessage: function WidgetView_postMessage(message) { + if (!this._chrome) + throw new Error(ERR_DESTROYED); + this._chrome.update(this._baseWidget, "postMessage", message); + }, + + destroy: function WidgetView_destroy() { + this._chrome.destroy(); + delete this._chrome; + this._baseWidget._onViewDestroyed(this); + this._emit("detach"); + } + +})); + +const WidgetView = function WidgetView(baseWidget) { + let w = WidgetViewTrait.create(WidgetView.prototype); + w._initWidgetView(baseWidget); + return w; +} + + + +/** + * Keeps track of all browser windows. + * Exposes methods for adding/removing widgets + * across all open windows (and future ones). + * Create a new instance of BrowserWindow per window. + */ +let browserManager = { + items: [], + windows: [], + + // Registers the manager to listen for window openings and closings. Note + // that calling this method can cause onTrack to be called immediately if + // there are open windows. + init: function () { + let windowTracker = new (require("api-utils/window-utils").WindowTracker)(this); + unload.ensure(windowTracker); + }, + + // Registers a window with the manager. This is a WindowTracker callback. + onTrack: function browserManager_onTrack(window) { + if (this._isBrowserWindow(window)) { + let win = new BrowserWindow(window); + win.addItems(this.items); + this.windows.push(win); + } + }, + + // Unregisters a window from the manager. It's told to undo all + // modifications. This is a WindowTracker callback. Note that when + // WindowTracker is unloaded, it calls onUntrack for every currently opened + // window. The browserManager therefore doesn't need to specially handle + // unload itself, since unloading the browserManager means untracking all + // currently opened windows. + onUntrack: function browserManager_onUntrack(window) { + if (this._isBrowserWindow(window)) { + this.items.forEach(function(i) i._onWindowClosed(window)); + for (let i = 0; i < this.windows.length; i++) { + if (this.windows[i].window == window) { + this.windows.splice(i, 1)[0]; + return; + } + } + + } + }, + + // Used to validate widget by browserManager before adding it, + // in order to check input very early in widget constructor + validate : function (item) { + let idx = this.items.indexOf(item); + if (idx > -1) + throw new Error("The widget " + item + " has already been added."); + if (item.id) { + let sameId = this.items.filter(function(i) i.id == item.id); + if (sameId.length > 0) + throw new Error("This widget ID is already used: " + item.id); + } else { + item.id = this.items.length; + } + }, + + // Registers an item with the manager. It's added to all currently registered + // windows, and when new windows are registered it will be added to them, too. + addItem: function browserManager_addItem(item) { + this.items.push(item); + this.windows.forEach(function (w) w.addItems([item])); + }, + + // Unregisters an item from the manager. It's removed from all windows that + // are currently registered. + removeItem: function browserManager_removeItem(item) { + let idx = this.items.indexOf(item); + if (idx > -1) + this.items.splice(idx, 1); + }, + + _isBrowserWindow: function browserManager__isBrowserWindow(win) { + let winType = win.document.documentElement.getAttribute("windowtype"); + return winType === "navigator:browser"; + } +}; + + + +/** + * Keeps track of a single browser window. + * + * This is where the core of how a widget's content is added to a window lives. + */ +function BrowserWindow(window) { + this.window = window; + this.doc = window.document; +} + +BrowserWindow.prototype = { + + // Adds an array of items to the window. + addItems: function BW_addItems(items) { + items.forEach(this._addItemToWindow, this); + }, + + _addItemToWindow: function BW__addItemToWindow(baseWidget) { + // Create a WidgetView instance + let widget = baseWidget._createView(); + + // Create a WidgetChrome instance + let item = new WidgetChrome({ + widget: widget, + doc: this.doc, + window: this.window + }); + + widget._chrome = item; + + this._insertNodeInToolbar(item.node); + + // We need to insert Widget DOM Node before finishing widget view creation + // (because fill creates an iframe and tries to access its docShell) + item.fill(); + }, + + _insertNodeInToolbar: function BW__insertNodeInToolbar(node) { + // Add to the customization palette + let toolbox = this.doc.getElementById("navigator-toolbox"); + let palette = toolbox.palette; + palette.appendChild(node); + + // Search for widget toolbar by reading toolbar's currentset attribute + let container = null; + let toolbars = this.doc.getElementsByTagName("toolbar"); + let id = node.getAttribute("id"); + for (let i = 0, l = toolbars.length; i < l; i++) { + let toolbar = toolbars[i]; + if (toolbar.getAttribute("currentset").indexOf(id) == -1) + continue; + container = toolbar; + } + + // if widget isn't in any toolbar, add it to the addon-bar + // TODO: we may want some "first-launch" module to do this only on very + // first execution + if (!container) { + container = this.doc.getElementById("addon-bar"); + // TODO: find a way to make the following code work when we use "cfx run": + // http://mxr.mozilla.org/mozilla-central/source/browser/base/content/browser.js#8586 + // until then, force display of addon bar directly from sdk code + // https://bugzilla.mozilla.org/show_bug.cgi?id=627484 + if (container.collapsed) + this.window.toggleAddonBar(); + } + + // Now retrieve a reference to the next toolbar item + // by reading currentset attribute on the toolbar + let nextNode = null; + let currentSet = container.getAttribute("currentset"); + let ids = (currentSet == "__empty") ? [] : currentSet.split(","); + let idx = ids.indexOf(id); + if (idx != -1) { + for (let i = idx; i < ids.length; i++) { + nextNode = this.doc.getElementById(ids[i]); + if (nextNode) + break; + } + } + + // Finally insert our widget in the right toolbar and in the right position + container.insertItem(id, nextNode, null, false); + + // Update DOM in order to save position if we remove/readd the widget + container.setAttribute("currentset", container.currentSet); + // Save DOM attribute in order to save position on new window opened + this.window.document.persist(container.id, "currentset"); + } +} + + +/** + * Final Widget class that handles chrome DOM Node: + * - create initial DOM nodes + * - receive instruction from WidgetView through update method and update DOM + * - watch for DOM events and forward them to WidgetView + */ +function WidgetChrome(options) { + this.window = options.window; + this._doc = options.doc; + this._widget = options.widget; + this._symbiont = null; // set later + this.node = null; // set later + + this._createNode(); +} + +// Update a property of a widget. +WidgetChrome.prototype.update = function WC_update(updatedItem, property, value) { + switch(property) { + case "contentURL": + case "content": + this.setContent(); + break; + case "width": + this.node.style.minWidth = value + "px"; + this.node.querySelector("iframe").style.width = value + "px"; + break; + case "tooltip": + this.node.setAttribute("tooltiptext", value); + break; + case "postMessage": + this._symbiont.postMessage(value); + break; + case "emit": + let port = this._symbiont.port; + port.emit.apply(port, value); + break; + } +} + +// Add a widget to this window. +WidgetChrome.prototype._createNode = function WC__createNode() { + // XUL element container for widget + let node = this._doc.createElement("toolbaritem"); + let guid = require("api-utils/xpcom").makeUuid().toString(); + + // Temporary work around require("self") failing on unit-test execution ... + let jetpackID = "testID"; + try { + jetpackID = require("self").id; + } catch(e) {} + + // Compute an unique and stable widget id with jetpack id and widget.id + let id = "widget:" + jetpackID + "-" + this._widget.id; + node.setAttribute("id", id); + node.setAttribute("label", this._widget.label); + node.setAttribute("tooltiptext", this._widget.tooltip); + node.setAttribute("align", "center"); + + // TODO move into a stylesheet, configurable by consumers. + // Either widget.style, exposing the style object, or a URL + // (eg, can load local stylesheet file). + node.setAttribute("style", [ + "overflow: hidden; margin: 1px 2px 1px 2px; padding: 0px;", + "min-height: 16px;", + ].join("")); + + node.style.minWidth = this._widget.width + "px"; + + this.node = node; +} + +// Initial population of a widget's content. +WidgetChrome.prototype.fill = function WC_fill() { + // Create element + var iframe = this._doc.createElement("iframe"); + iframe.setAttribute("type", "content"); + iframe.setAttribute("transparent", "transparent"); + iframe.style.overflow = "hidden"; + iframe.style.height = "16px"; + iframe.style.maxHeight = "16px"; + iframe.style.width = this._widget.width + "px"; + iframe.setAttribute("flex", "1"); + iframe.style.border = "none"; + iframe.style.padding = "0px"; + + // Do this early, because things like contentWindow are null + // until the node is attached to a document. + this.node.appendChild(iframe); + + // add event handlers + this.addEventHandlers(); + + // set content + this.setContent(); +} + +// Get widget content type. +WidgetChrome.prototype.getContentType = function WC_getContentType() { + if (this._widget.content) + return CONTENT_TYPE_HTML; + return (this._widget.contentURL && /\.(jpg|gif|png|ico)$/.test(this._widget.contentURL)) + ? CONTENT_TYPE_IMAGE : CONTENT_TYPE_URI; +} + +// Set widget content. +WidgetChrome.prototype.setContent = function WC_setContent() { + let type = this.getContentType(); + let contentURL = null; + + switch (type) { + case CONTENT_TYPE_HTML: + contentURL = "data:text/html," + encodeURIComponent(this._widget.content); + break; + case CONTENT_TYPE_URI: + contentURL = this._widget.contentURL; + break; + case CONTENT_TYPE_IMAGE: + let imageURL = this._widget.contentURL; + contentURL = "data:text/html,<html><body><img src='" + + encodeURI(imageURL) + "'></body></html>"; + break; + default: + throw new Error("The widget's type cannot be determined."); + } + + let iframe = this.node.firstElementChild; + + let self = this; + // Cleanup previously created symbiont (in case we are update content) + if (this._symbiont) + this._symbiont.destroy(); + + this._symbiont = Trait.compose(Symbiont.resolve({ + _onContentScriptEvent: "_onContentScriptEvent-not-used" + }), { + _onContentScriptEvent: function () { + // Redirect events to WidgetView + self._widget._onPortEvent(arguments); + } + })({ + frame: iframe, + contentURL: contentURL, + contentScriptFile: this._widget.contentScriptFile, + contentScript: this._widget.contentScript, + contentScriptWhen: this._widget.contentScriptWhen, + allow: this._widget.allow, + onMessage: function(message) { + timer.setTimeout(function() { + self._widget._onEvent("message", message); + }, 0); + } + }); +} + +// Detect if document consists of a single image. +WidgetChrome._isImageDoc = function WC__isImageDoc(doc) { + return doc.body.childNodes.length == 1 && + doc.body.firstElementChild && + doc.body.firstElementChild.tagName == "IMG"; +} + +// Set up all supported events for a widget. +WidgetChrome.prototype.addEventHandlers = function WC_addEventHandlers() { + let contentType = this.getContentType(); + + let self = this; + let listener = function(e) { + // Ignore event firings that target the iframe. + if (e.target == self.node.firstElementChild) + return; + + // The widget only supports left-click for now, + // so ignore right-clicks. + if (e.type == "click" && e.button == 2) + return; + + // Proxy event to the widget + timer.setTimeout(function() { + self._widget._onEvent(EVENTS[e.type], null, self.node); + }, 0); + }; + + this.eventListeners = {}; + let iframe = this.node.firstElementChild; + for (let [type, method] in Iterator(EVENTS)) { + iframe.addEventListener(type, listener, true, true); + + // Store listeners for later removal + this.eventListeners[type] = listener; + } + + // On document load, make modifications required for nice default + // presentation. + let self = this; + function loadListener(e) { + // Ignore event firings that target the iframe + if (e.target == iframe) + return; + // Ignore about:blank loads + if (e.type == "load" && e.target.location == "about:blank") + return; + + // We may have had an unload event before that cleaned up the symbiont + if (!self._symbiont) + self.setContent(); + + let doc = e.target; + if (contentType == CONTENT_TYPE_IMAGE || WidgetChrome._isImageDoc(doc)) { + // Force image content to size. + // Add-on authors must size their images correctly. + doc.body.firstElementChild.style.width = self._widget.width + "px"; + doc.body.firstElementChild.style.height = "16px"; + } + + // Allow all content to fill the box by default. + doc.body.style.margin = "0"; + } + iframe.addEventListener("load", loadListener, true); + this.eventListeners["load"] = loadListener; + + // Register a listener to unload symbiont if the toolbaritem is moved + // on user toolbars customization + function unloadListener(e) { + if (e.target.location == "about:blank") + return; + self._symbiont.destroy(); + self._symbiont = null; + // This may fail but not always, it depends on how the node is + // moved or removed + try { + self.setContent(); + } catch(e) {} + + } + + iframe.addEventListener("unload", unloadListener, true); + this.eventListeners["unload"] = unloadListener; +} + +// Remove and unregister the widget from everything +WidgetChrome.prototype.destroy = function WC_destroy(removedItems) { + // remove event listeners + for (let [type, listener] in Iterator(this.eventListeners)) + this.node.firstElementChild.removeEventListener(type, listener, true); + // remove dom node + this.node.parentNode.removeChild(this.node); + // cleanup symbiont + this._symbiont.destroy(); + // cleanup itself + this.eventListeners = null; + this._widget = null; + this._symbiont = null; +} + +// Init the browserManager only after setting prototypes and such above, because +// it will cause browserManager.onTrack to be called immediately if there are +// open windows. +browserManager.init(); diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/windows.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/windows.js new file mode 100644 index 0000000..e9e097b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/windows.js @@ -0,0 +1,238 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Felipe Gomes <felipc@gmail.com> (Original author) + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The windows module currently supports only Firefox. In the future", + " we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=571449 for more information." + ].join("")); +} + +const { Cc, Ci } = require('chrome'), + { Trait } = require('api-utils/traits'), + { List } = require('api-utils/list'), + { EventEmitter } = require('api-utils/events'), + { WindowTabs, WindowTabTracker } = require('api-utils/windows/tabs'), + { WindowDom } = require('api-utils/windows/dom'), + { WindowLoader } = require('api-utils/windows/loader'), + { WindowTrackerTrait } = require('api-utils/window-utils'), + { Options } = require('api-utils/tabs/tab'), + { utils } = require('api-utils/xpcom'), + apiUtils = require('api-utils/api-utils'), + unload = require('api-utils/unload'), + + WM = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator), + + BROWSER = 'navigator:browser'; + +/** + * Window trait composes safe wrappers for browser window that are E10S + * compatible. + */ +const BrowserWindowTrait = Trait.compose( + EventEmitter, + WindowDom.resolve({ close: '_close' }), + WindowTabs, + WindowTabTracker, + WindowLoader, + /* WindowSidebars, */ + Trait.compose({ + _emit: Trait.required, + _close: Trait.required, + _load: Trait.required, + /** + * Constructor returns wrapper of the specified chrome window. + * @param {nsIWindow} window + */ + constructor: function BrowserWindow(options) { + // Register this window ASAP, in order to avoid loop that would try + // to create this window instance over and over (see bug 648244) + windows.push(this); + + // make sure we don't have unhandled errors + this.on('error', console.exception.bind(console)); + + if ('onOpen' in options) + this.on('open', options.onOpen); + if ('onClose' in options) + this.on('close', options.onClose); + if ('window' in options) + this._window = options.window; + if ('tabs' in options) { + this._tabOptions = Array.isArray(options.tabs) ? + options.tabs.map(Options) : + [ Options(options.tabs) ]; + } + else if ('url' in options) { + this._tabOptions = [ Options(options.url) ]; + } + this._load(); + return this; + }, + _tabOptions: [], + _onLoad: function() { + try { + this._initWindowTabTracker(); + } catch(e) { + this._emit('error', e) + } + this._emitOnObject(browserWindows, 'open', this._public); + }, + _onUnload: function() { + this._destroyWindowTabTracker(); + this._emitOnObject(browserWindows, 'close', this._public); + this._window = null; + // Removing reference from the windows array. + windows.splice(windows.indexOf(this), 1); + this._removeAllListeners('close'); + this._removeAllListeners('open'); + this._removeAllListeners('ready'); + }, + close: function close(callback) { + // maybe we should deprecate this with message ? + if (callback) this.on('close', callback); + return this._close(); + } + }) +); +/** + * Wrapper for `BrowserWindowTrait`. Creates new instance if wrapper for + * window doesn't exists yet. If wrapper already exists then returns it + * instead. + * @params {Object} options + * Options that are passed to the the `BrowserWindowTrait` + * @returns {BrowserWindow} + * @see BrowserWindowTrait + */ +function BrowserWindow(options) { + let chromeWindow = options.window; + for each (let window in windows) { + if (chromeWindow == window._window) + return window._public + } + let window = BrowserWindowTrait(options); + return window._public; +} +// to have proper `instanceof` behavior will go away when #596248 is fixed. +BrowserWindow.prototype = BrowserWindowTrait.prototype; +exports.BrowserWindow = BrowserWindow +const windows = []; +/** + * `BrowserWindows` trait is composed out of `List` trait and it represents + * "live" list of currently open browser windows. Instance mutates itself + * whenever new browser window gets opened / closed. + */ +// Very stupid to resolve all `toStrings` but this will be fixed by #596248 +const browserWindows = Trait.resolve({ toString: null }).compose( + List.resolve({ constructor: '_initList' }), + EventEmitter.resolve({ toString: null }), + WindowTrackerTrait.resolve({ constructor: '_initTracker', toString: null }), + Trait.compose({ + _emit: Trait.required, + _add: Trait.required, + _remove: Trait.required, + + // public API + + /** + * Constructor creates instance of `Windows` that represents live list of open + * windows. + */ + constructor: function BrowserWindows() { + this._trackedWindows = []; + this._initList(); + this._initTracker(); + unload.ensure(this, "_destructor"); + }, + _destructor: function _destructor() { + this._removeAllListeners('open'); + this._removeAllListeners('close'); + }, + /** + * This property represents currently active window. + * Property is non-enumerable, in order to preserve array like enumeration. + * @type {Window|null} + */ + get activeWindow() { + let window = WM.getMostRecentWindow(BROWSER); + return this._isBrowser(window) ? BrowserWindow({ window: window }) : null; + }, + open: function open(options) { + if (typeof options === "string") + // `tabs` option is under review and may be removed. + options = { tabs: [Options(options)] }; + return BrowserWindow(options); + }, + /** + * Returns true if specified window is a browser window. + * @param {nsIWindow} window + * @returns {Boolean} + */ + _isBrowser: function _isBrowser(window) + BROWSER === window.document.documentElement.getAttribute("windowtype") + , + /** + * Internal listener which is called whenever new window gets open. + * Creates wrapper and adds to this list. + * @param {nsIWindow} chromeWindow + */ + _onTrack: function _onTrack(chromeWindow) { + if (!this._isBrowser(chromeWindow)) return; + let window = BrowserWindow({ window: chromeWindow }); + this._add(window); + this._emit('open', window); + }, + /** + * Internal listener which is called whenever window gets closed. + * Cleans up references and removes wrapper from this list. + * @param {nsIWindow} window + */ + _onUntrack: function _onUntrack(chromeWindow) { + if (!this._isBrowser(chromeWindow)) return; + let window = BrowserWindow({ window: chromeWindow }); + // `_onUnload` method of the `BrowserWindow` will remove `chromeWindow` + // from the `windows` array. + this._remove(window); + this._emit('close', window); + } + }).resolve({ toString: null }) +)(); +exports.browserWindows = browserWindows; + diff --git a/tools/addon-sdk-1.4/packages/addon-kit/package.json b/tools/addon-sdk-1.4/packages/addon-kit/package.json new file mode 100644 index 0000000..9c62ae8 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/package.json @@ -0,0 +1,12 @@ +{ + "name": "addon-kit", + "description": "Add-on development made easy.", + "keywords": ["javascript", "engine", "platform", "xulrunner"], + "author": "Atul Varma (http://toolness.com/) <atul@mozilla.com>", + "contributors": [ + "Myk Melez (http://melez.com/) <myk@mozilla.org>", + "Daniel Aquino <mr.danielaquino@gmail.com>" + ], + "license": "MPL 1.1/GPL 2.0/LGPL 2.1", + "dependencies": ["api-utils"] +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/helpers.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/helpers.js new file mode 100644 index 0000000..bbfe911 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/helpers.js @@ -0,0 +1,19 @@ +"use strict"; + +const { Loader } = require("@loader"); + +exports.Loader = function(module, globals) { + var options = JSON.parse(JSON.stringify(require("@packaging"))); + options.globals = globals; + let loader = Loader.new(options); + return Object.create(loader, { + require: { value: Loader.require.bind(loader, module.path) }, + sandbox: { value: function sandbox(id) { + let path = options.manifest[module.path].requirements[id].path; + return loader.sandboxes[path].sandbox; + }}, + unload: { value: function unload(reason, callback) { + loader.unload(reason, callback); + }} + }) +}; diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/pagemod-test-helpers.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/pagemod-test-helpers.js new file mode 100644 index 0000000..7e87a85 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/pagemod-test-helpers.js @@ -0,0 +1,63 @@ +"use strict"; + +const {Cc,Ci} = require("chrome"); +const timer = require("timer"); +const xulApp = require("xul-app"); +const { Loader } = require('./helpers'); + +/** + * A helper function that creates a PageMod, then opens the specified URL + * and checks the effect of the page mod on 'onload' event via testCallback. + */ +exports.testPageMod = function testPageMod(test, testURL, pageModOptions, + testCallback, timeout) { + if (!xulApp.versionInRange(xulApp.platformVersion, "1.9.3a3", "*") && + !xulApp.versionInRange(xulApp.platformVersion, "1.9.2.7", "1.9.2.*")) { + test.pass("Note: not testing PageMod, as it doesn't work on this platform version"); + return null; + } + + var wm = Cc['@mozilla.org/appshell/window-mediator;1'] + .getService(Ci.nsIWindowMediator); + var browserWindow = wm.getMostRecentWindow("navigator:browser"); + if (!browserWindow) { + test.pass("page-mod tests: could not find the browser window, so " + + "will not run. Use -a firefox to run the pagemod tests.") + return null; + } + + if (timeout !== undefined) + test.waitUntilDone(timeout); + else + test.waitUntilDone(); + + let loader = Loader(module); + let pageMod = loader.require("page-mod"); + + var pageMods = [new pageMod.PageMod(opts) for each(opts in pageModOptions)]; + + var tabBrowser = browserWindow.gBrowser; + var newTab = tabBrowser.addTab(testURL); + tabBrowser.selectedTab = newTab; + var b = tabBrowser.getBrowserForTab(newTab); + + function onPageLoad() { + b.removeEventListener("load", onPageLoad, true); + // Delay callback execute as page-mod content scripts may be executed on + // load event. So page-mod actions may not be already done. + // If we delay even more contentScriptWhen:'end', we may want to modify + // this code again. + timer.setTimeout(testCallback, 0, + b.contentWindow.wrappedJSObject, + function done() { + pageMods.forEach(function(mod) mod.destroy()); + // XXX leaks reported if we don't close the tab? + tabBrowser.removeTab(newTab); + loader.unload(); + test.done(); + }); + } + b.addEventListener("load", onPageLoad, true); + + return pageMods; +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-clipboard.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-clipboard.js new file mode 100644 index 0000000..5f61d48 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-clipboard.js @@ -0,0 +1,60 @@ + +// Test the typical use case, setting & getting with no flavors specified +exports.testWithNoFlavor = function(test) { + var contents = "hello there"; + var flavor = "text"; + var fullFlavor = "text/unicode"; + var clip = require("clipboard"); + // Confirm we set the clipboard + test.assert(clip.set(contents)); + // Confirm flavor is set + test.assertEqual(clip.currentFlavors[0], flavor); + // Confirm we set the clipboard + test.assertEqual(clip.get(), contents); + // Confirm we can get the clipboard using the flavor + test.assertEqual(clip.get(flavor), contents); + // Confirm we can still get the clipboard using the full flavor + test.assertEqual(clip.get(fullFlavor), contents); +}; + +// Test the slightly less common case where we specify the flavor +exports.testWithFlavor = function(test) { + var contents = "<b>hello there</b>"; + var contentsText = "hello there"; + var flavor = "html"; + var fullFlavor = "text/html"; + var unicodeFlavor = "text"; + var unicodeFullFlavor = "text/unicode"; + var clip = require("clipboard"); + test.assert(clip.set(contents, flavor)); + test.assertEqual(clip.currentFlavors[0], unicodeFlavor); + test.assertEqual(clip.currentFlavors[1], flavor); + test.assertEqual(clip.get(), contentsText); + test.assertEqual(clip.get(flavor), contents); + test.assertEqual(clip.get(fullFlavor), contents); + test.assertEqual(clip.get(unicodeFlavor), contentsText); + test.assertEqual(clip.get(unicodeFullFlavor), contentsText); +}; + +// Test that the typical case still works when we specify the flavor to set +exports.testWithRedundantFlavor = function(test) { + var contents = "<b>hello there</b>"; + var flavor = "text"; + var fullFlavor = "text/unicode"; + var clip = require("clipboard"); + test.assert(clip.set(contents, flavor)); + test.assertEqual(clip.currentFlavors[0], flavor); + test.assertEqual(clip.get(), contents); + test.assertEqual(clip.get(flavor), contents); + test.assertEqual(clip.get(fullFlavor), contents); +}; + +exports.testNotInFlavor = function(test) { + var contents = "hello there"; + var flavor = "html"; + var clip = require("clipboard"); + test.assert(clip.set(contents)); + // If there's nothing on the clipboard with this flavor, should return null + test.assertEqual(clip.get(flavor), null); +}; +// TODO: Test error cases. diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-context-menu.html b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-context-menu.html new file mode 100644 index 0000000..a0fb5cb --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-context-menu.html @@ -0,0 +1,78 @@ +<!-- ***** BEGIN LICENSE BLOCK ***** + - Version: MPL 1.1/GPL 2.0/LGPL 2.1 + - + - The contents of this file are subject to the Mozilla Public License Version + - 1.1 (the "License"); you may not use this file except in compliance with + - the License. You may obtain a copy of the License at + - http://www.mozilla.org/MPL/ + - + - Software distributed under the License is distributed on an "AS IS" basis, + - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + - for the specific language governing rights and limitations under the + - License. + - + - The Original Code is Jetpack. + - + - The Initial Developer of the Original Code is + - the Mozilla Foundation. + - Portions created by the Initial Developer are Copyright (C) 2010 + - the Initial Developer. All Rights Reserved. + - + - Contributor(s): + - Drew Willcoxon <adw@mozilla.com> (Original Author) + - + - Alternatively, the contents of this file may be used under the terms of + - either the GNU General Public License Version 2 or later (the "GPL"), or + - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + - in which case the provisions of the GPL or the LGPL are applicable instead + - of those above. If you wish to allow use of your version of this file only + - under the terms of either the GPL or the LGPL, and not to allow others to + - use your version of this file under the terms of the MPL, indicate your + - decision by deleting the provisions above and replace them with the notice + - and other provisions required by the LGPL or the GPL. If you do not delete + - the provisions above, a recipient may use your version of this file under + - the terms of any one of the MPL, the GPL or the LGPL. + - + - ***** END LICENSE BLOCK ***** --> + +<html> + <head> + <title>Context menu test</title> + </head> + <body> + <p> + <img id="image" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="> + </p> + + <p> + <a id="link" href=""> + A simple link. + </a> + </p> + + <p> + <a href=""> + <span id="span-link"> + A span inside a link. + </span> + </a> + </p> + + <p id="text"> + Some text. + </p> + + <p> + <textarea id="textfield"> + A text field, + with some text. + </textarea> + </p> + + <p> + <iframe id="iframe" src="data:text/html,An iframe." + width="200" height="100"> + </iframe> + </p> + </body> +</html> diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-context-menu.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-context-menu.js new file mode 100644 index 0000000..157edf1 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-context-menu.js @@ -0,0 +1,2075 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * Matteo Ferretti <zer0@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +let {Cc,Ci} = require("chrome"); +const { Loader } = require('./helpers'); + +// These should match the same constants in the module. +const ITEM_CLASS = "jetpack-context-menu-item"; +const SEPARATOR_ID = "jetpack-context-menu-separator"; +const OVERFLOW_THRESH_DEFAULT = 10; +const OVERFLOW_THRESH_PREF = + "extensions.addon-sdk.context-menu.overflowThreshold"; +const OVERFLOW_MENU_ID = "jetpack-content-menu-overflow-menu"; +const OVERFLOW_POPUP_ID = "jetpack-content-menu-overflow-popup"; + +const TEST_DOC_URL = module.uri.replace(/\.js$/, ".html"); + + +// Destroying items that were previously created should cause them to be absent +// from the menu. +exports.testConstructDestroy = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + // Create an item. + let item = new loader.cm.Item({ label: "item" }); + test.assertEqual(item.parentMenu, null, "item's parent menu should be null"); + + test.showMenu(null, function (popup) { + + // It should be present when the menu is shown. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Destroy the item. Multiple destroys should be harmless. + item.destroy(); + item.destroy(); + test.showMenu(null, function (popup) { + + // It should be removed from the menu. + test.checkMenu([], [], [item]); + test.done(); + }); + }); +}; + + +// Destroying an item twice should not cause an error. +exports.testDestroyTwice = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + item.destroy(); + item.destroy(); + + test.pass("Destroying an item twice should not cause an error."); + test.done(); +}; + + +// CSS selector contexts should cause their items to be present in the menu +// when the menu is invoked on nodes that match the selectors. +exports.testSelectorContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("img") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("image"), function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// CSS selector contexts should cause their items to be present in the menu +// when the menu is invoked on nodes that have ancestors that match the +// selectors. +exports.testSelectorAncestorContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("a[href]") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("span-link"), function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// CSS selector contexts should cause their items to be absent from the menu +// when the menu is not invoked on nodes that match or have ancestors that +// match the selectors. +exports.testSelectorContextNoMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); +}; + + +// Page contexts should cause their items to be present in the menu when the +// menu is not invoked on an active element. +exports.testPageContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 0" + }), + new loader.cm.Item({ + label: "item 1", + context: undefined + }), + new loader.cm.Item({ + label: "item 2", + context: loader.cm.PageContext() + }), + new loader.cm.Item({ + label: "item 3", + context: [loader.cm.PageContext()] + }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Page contexts should cause their items to be absent from the menu when the +// menu is invoked on an active element. +exports.testPageContextNoMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 0" + }), + new loader.cm.Item({ + label: "item 1", + context: undefined + }), + new loader.cm.Item({ + label: "item 2", + context: loader.cm.PageContext() + }), + new loader.cm.Item({ + label: "item 3", + context: [loader.cm.PageContext()] + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("image"), function (popup) { + test.checkMenu([], items, []); + test.done(); + }); + }); +}; + + +// Selection contexts should cause items to appear when a selection exists. +exports.testSelectionContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + window.getSelection().selectAllChildren(doc.body); + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Selection contexts should cause items to appear when a selection exists in +// a text field. +exports.testSelectionContextMatchInTextField = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + let textfield = doc.getElementById("textfield"); + textfield.setSelectionRange(0, textfield.value.length); + test.showMenu(textfield, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Selection contexts should not cause items to appear when a selection does +// not exist in a text field. +exports.testSelectionContextNoMatchInTextField = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + let textfield = doc.getElementById("textfield"); + textfield.setSelectionRange(0, 0); + test.showMenu(textfield, function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); + }); +}; + + +// Selection contexts should not cause items to appear when a selection does +// not exist. +exports.testSelectionContextNoMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.showMenu(null, function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); +}; + + +// URL contexts should cause items to appear on pages that match. +exports.testURLContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + loader.cm.Item({ + label: "item 0", + context: loader.cm.URLContext(TEST_DOC_URL) + }), + loader.cm.Item({ + label: "item 1", + context: loader.cm.URLContext([TEST_DOC_URL, "*.bogus.com"]) + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// URL contexts should not cause items to appear on pages that do not match. +exports.testURLContextNoMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + loader.cm.Item({ + label: "item 0", + context: loader.cm.URLContext("*.bogus.com") + }), + loader.cm.Item({ + label: "item 1", + context: loader.cm.URLContext(["*.bogus.com", "*.gnarly.com"]) + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu([], items, []); + test.done(); + }); + }); +}; + + +// Removing a non-matching URL context after its item is created and the page is +// loaded should cause the item's content script to be evaluated. +exports.testURLContextRemove = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let shouldBeEvaled = false; + let context = loader.cm.URLContext("*.bogus.com"); + let item = loader.cm.Item({ + label: "item", + context: context, + contentScript: 'self.postMessage("ok");', + onMessage: function (msg) { + test.assert(shouldBeEvaled, + "content script should be evaluated when expected"); + shouldBeEvaled = false; + test.done(); + } + }); + + test.withTestDoc(function (window, doc) { + shouldBeEvaled = true; + item.context.remove(context); + }); +}; + + +// Adding a non-matching URL context after its item is created and the page is +// loaded should cause the item's worker to be destroyed. +exports.testURLContextAdd = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ label: "item" }); + + test.withTestDoc(function (window, doc) { + let privatePropsKey = loader.globalScope.PRIVATE_PROPS_KEY; + let workerReg = item.valueOf(privatePropsKey)._workerReg; + + let found = false; + for each (let winWorker in workerReg.winWorkers) { + if (winWorker.win === window) { + found = true; + break; + } + } + this.test.assert(found, "window should be present in worker registry"); + + item.context.add(loader.cm.URLContext("*.bogus.com")); + + for each (let winWorker in workerReg.winWorkers) + this.test.assertNotEqual(winWorker.win, window, + "window should not be present in worker registry"); + + test.done(); + }); +}; + + +// Content contexts that return true should cause their items to be present +// in the menu. +exports.testContentContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () true);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// Content contexts that return false should cause their items to be absent +// from the menu. +exports.testContentContextNoMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () false);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); +}; + + +// Content contexts that return a string should cause their items to be present +// in the menu and the items' labels to be updated. +exports.testContentContextMatchString = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "first label", + contentScript: 'self.on("context", function () "second label");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.assertEqual(item.label, "second label", + "item's label should be updated"); + test.done(); + }); +}; + + +// The args passed to context listeners should be correct. +exports.testContentContextArgs = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + let callbacks = 0; + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function (node) {' + + ' let Ci = Components.interfaces;' + + ' self.postMessage(node instanceof Ci.nsIDOMHTMLElement);' + + ' return false;' + + '});', + onMessage: function (isElt) { + test.assert(isElt, "node should be an HTML element"); + if (++callbacks == 2) test.done(); + } + }); + + test.showMenu(null, function () { + if (++callbacks == 2) test.done(); + }); +}; + +// Multiple contexts imply intersection, not union, and content context +// listeners should not be called if all declarative contexts are not current. +exports.testMultipleContexts = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + context: [loader.cm.SelectorContext("a[href]"), loader.cm.PageContext()], + contentScript: 'self.on("context", function () self.postMessage());', + onMessage: function () { + test.fail("Context listener should not be called"); + } + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("span-link"), function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); + }); +}; + + +// Once a context is removed, it should no longer cause its item to appear. +exports.testRemoveContext = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let ctxt = loader.cm.SelectorContext("img"); + let item = new loader.cm.Item({ + label: "item", + context: ctxt + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("image"), function (popup) { + + // The item should be present at first. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Remove the img context and check again. + item.context.remove(ctxt); + test.showMenu(doc.getElementById("image"), function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); + }); + }); +}; + + +// Lots of items should overflow into the overflow submenu. +exports.testOverflow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT + 1; i++) { + let item = new loader.cm.Item({ label: "item " + i }); + items.push(item); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Module unload should cause all items to be removed. +exports.testUnload = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + + // The menu should contain the item. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Unload the module. + loader.unload(); + test.showMenu(null, function (popup) { + + // The item should be removed from the menu. + test.checkMenu([], [], [item]); + test.done(); + }); + }); +}; + + +// Using multiple module instances to add items without causing overflow should +// work OK. Assumes OVERFLOW_THRESH_DEFAULT <= 2. +exports.testMultipleModulesAdd = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + // Use each module to add an item, then unload each module in turn. + let item0 = new loader0.cm.Item({ label: "item 0" }); + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain both items. + test.checkMenu([item0, item1], [], []); + popup.hidePopup(); + + // Unload the first module. + loader0.unload(); + test.showMenu(null, function (popup) { + + // The first item should be removed from the menu. + test.checkMenu([item1], [], [item0]); + popup.hidePopup(); + + // Unload the second module. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to add items causing overflow should work OK. +exports.testMultipleModulesAddOverflow = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + // Use module 0 to add OVERFLOW_THRESH_DEFAULT items. + let items0 = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i++) { + let item = new loader0.cm.Item({ label: "item 0 " + i }); + items0.push(item); + } + + // Use module 1 to add one item. + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let allItems = items0.concat(item1); + + test.showMenu(null, function (popup) { + + // The menu should contain all items in overflow. + test.checkMenu(allItems, [], []); + popup.hidePopup(); + + // Unload the first module. + loader0.unload(); + test.showMenu(null, function (popup) { + + // The first items should be removed from the menu, which should not + // overflow. + test.checkMenu([item1], [], items0); + popup.hidePopup(); + + // Unload the second module. + loader1.unload(); + test.showMenu(null, function (popup) { + + // All items should be removed from the menu. + test.checkMenu([], [], allItems); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader0 create item -> loader1 create item -> loader0.unload -> +// loader1.unload +exports.testMultipleModulesDiffContexts1 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item1], [item0], []); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // item0 should be removed from the menu. + test.checkMenu([item1], [], [item0]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader1 create item -> loader0 create item -> loader0.unload -> +// loader1.unload +exports.testMultipleModulesDiffContexts2 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item1], [item0], []); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // item0 should be removed from the menu. + test.checkMenu([item1], [], [item0]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader0 create item -> loader1 create item -> loader1.unload -> +// loader0.unload +exports.testMultipleModulesDiffContexts3 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item1], [item0], []); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // item1 should be removed from the menu. + test.checkMenu([], [item0], [item1]); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader1 create item -> loader0 create item -> loader1.unload -> +// loader0.unload +exports.testMultipleModulesDiffContexts4 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item1], [item0], []); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // item1 should be removed from the menu. + test.checkMenu([], [item0], [item1]); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Test interactions between a loaded module, unloading another module, and the +// menu separator and overflow submenu. +exports.testMultipleModulesAddRemove = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item = new loader0.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + + // The menu should contain the item. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Remove the item. + item.destroy(); + test.showMenu(null, function (popup) { + + // The item should be removed from the menu. + test.checkMenu([], [], [item]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // There shouldn't be any errors involving the menu separator or + // overflow submenu. + test.checkMenu([], [], [item]); + test.done(); + }); + }); + }); +}; + + +// An item's click listener should work. +exports.testItemClick = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item data", + contentScript: 'self.on("click", function (node, data) {' + + ' let Ci = Components.interfaces;' + + ' self.postMessage({' + + ' isElt: node instanceof Ci.nsIDOMHTMLElement,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + test.assertEqual(this, item, "`this` inside onMessage should be item"); + test.assert(data.isElt, "node should be an HTML element"); + test.assertEqual(data.data, item.data, "data should be item data"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + let elt = test.getItemElt(popup, item); + elt.click(); + }); +}; + + +// A menu's click listener should work and receive bubbling clicks from +// sub-items appropriately. This also tests menus and ensures that when a CSS +// selector context matches the clicked node's ancestor, the matching ancestor +// is passed to listeners as the clicked node. +exports.testMenuClick = function (test) { + // Create a top-level menu, submenu, and item, like this: + // topMenu -> submenu -> item + // Click the item and make sure the click bubbles. + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "submenu item", + data: "submenu item data" + }); + + let submenu = new loader.cm.Menu({ + label: "submenu", + items: [item] + }); + + let topMenu = new loader.cm.Menu({ + label: "top menu", + contentScript: 'self.on("click", function (node, data) {' + + ' let Ci = Components.interfaces;' + + ' self.postMessage({' + + ' isAnchor: node instanceof Ci.nsIDOMHTMLAnchorElement,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + test.assertEqual(this, topMenu, "`this` inside top menu should be menu"); + test.assert(data.isAnchor, "Clicked node should be anchor"); + test.assertEqual(data.data, item.data, + "Clicked item data should be correct"); + test.done(); + }, + items: [submenu], + context: loader.cm.SelectorContext("a") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("span-link"), function (popup) { + test.checkMenu([topMenu], [], []); + let topMenuElt = test.getItemElt(popup, topMenu); + let topMenuPopup = topMenuElt.firstChild; + let submenuElt = test.getItemElt(topMenuPopup, submenu); + let submenuPopup = submenuElt.firstChild; + let itemElt = test.getItemElt(submenuPopup, item); + itemElt.click(); + }); + }); +}; + + +// Click listeners should work when multiple modules are loaded. +exports.testItemClickMultipleModules = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = loader0.cm.Item({ + label: "loader 0 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.fail("loader 0 item should not emit click event"); + } + }); + let item1 = loader1.cm.Item({ + label: "loader 1 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.pass("loader 1 item clicked as expected"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item0, item1], [], []); + let item1Elt = test.getItemElt(popup, item1); + item1Elt.click(); + }); +}; + + +// Adding a separator to a submenu should work OK. +exports.testSeparator = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = new loader.cm.Menu({ + label: "submenu", + items: [new loader.cm.Separator()] + }); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Existing context menu modifications should apply to new windows. +exports.testNewWindow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.withNewWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// When a new window is opened, items added by an unloaded module should not +// be present in the menu. +exports.testNewWindowMultipleModules = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + loader.unload(); + test.withNewWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([], [], []); + test.done(); + }); + }); + }); +}; + + +// Items in the context menu should be sorted according to locale. +exports.testSorting = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + // Make an unsorted items list. It'll look like this: + // item 1, item 0, item 3, item 2, item 5, item 4, ... + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i += 2) { + items.push(new loader.cm.Item({ label: "item " + (i + 1) })); + items.push(new loader.cm.Item({ label: "item " + i })); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Items in the overflow menu should be sorted according to locale. +exports.testSortingOverflow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + // Make an unsorted items list. It'll look like this: + // item 1, item 0, item 3, item 2, item 5, item 4, ... + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT * 2; i += 2) { + items.push(new loader.cm.Item({ label: "item " + (i + 1) })); + items.push(new loader.cm.Item({ label: "item " + i })); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Multiple modules shouldn't interfere with sorting. +exports.testSortingMultipleModules = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let items0 = []; + let items1 = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i++) { + if (i % 2) { + let item = new loader0.cm.Item({ label: "item " + i }); + items0.push(item); + } + else { + let item = new loader1.cm.Item({ label: "item " + i }); + items1.push(item); + } + } + let allItems = items0.concat(items1); + + test.showMenu(null, function (popup) { + + // All items should be present and sorted. + test.checkMenu(allItems, [], []); + popup.hidePopup(); + loader0.unload(); + loader1.unload(); + test.showMenu(null, function (popup) { + + // All items should be removed. + test.checkMenu([], [], allItems); + test.done(); + }); + }); +}; + + +// The binary search of insertionPoint should work OK. +exports.testInsertionPoint = function (test) { + function mockElts(labels) { + return labels.map(function (label) { + return { label: label, getAttribute: function (l) label }; + }); + } + + test = new TestHelper(test); + let loader = test.newLoader(); + let insertionPoint = loader.globalScope.insertionPoint; + + let ip = insertionPoint("a", []); + test.assertStrictEqual(ip, null, "Insertion point should be null"); + + ip = insertionPoint("a", mockElts(["b"])); + test.assertEqual(ip.label, "b", "Insertion point should be 'b'"); + + ip = insertionPoint("c", mockElts(["b"])); + test.assertStrictEqual(ip, null, "Insertion point should be null"); + + ip = insertionPoint("b", mockElts(["a", "c"])); + test.assertEqual(ip.label, "c", "Insertion point should be 'c'"); + + ip = insertionPoint("c", mockElts(["a", "b", "d"])); + test.assertEqual(ip.label, "d", "Insertion point should be 'd'"); + + ip = insertionPoint("a", mockElts(["b", "c", "d"])); + test.assertEqual(ip.label, "b", "Insertion point should be 'b'"); + + ip = insertionPoint("d", mockElts(["a", "b", "c"])); + test.assertStrictEqual(ip, null, "Insertion point should be null"); + + test.done(); +}; + + +// Content click handlers and context handlers should be able to communicate, +// i.e., they're eval'ed in the same worker and sandbox. +exports.testContentCommunication = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'var potato;' + + 'self.on("context", function () {' + + ' potato = "potato";' + + ' return true;' + + '});' + + 'self.on("click", function () {' + + ' self.postMessage(potato);' + + '});', + }); + + item.on("message", function (data) { + test.assertEqual(data, "potato", "That's a lot of potatoes!"); + test.done(); + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + let elt = test.getItemElt(popup, item); + elt.click(); + }); +}; + + +// When the context menu is invoked on a tab that was already open when the +// module was loaded, it should contain the expected items and content workers +// should function as expected. +exports.testLoadWithOpenTab = function (test) { + test = new TestHelper(test); + test.withTestDoc(function (window, doc) { + let loader = test.newLoader(); + let item = new loader.cm.Item({ + label: "item", + contentScript: + 'self.on("click", function () self.postMessage("click"));', + onMessage: function (msg) { + if (msg === "click") + test.done(); + } + }); + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.getItemElt(popup, item).click(); + }); + }); +}; + + +// Setting an item's label before the menu is ever shown should correctly change +// its label and, if necessary, its order within the menu. +exports.testSetLabelBeforeShow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ] + items[0].label = "z"; + test.assertEqual(items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu([items[1], items[0]], [], []); + test.done(); + }); +}; + + +// Setting an item's label after the menu is shown should correctly change its +// label and, if necessary, its order within the menu. +exports.testSetLabelAfterShow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + popup.hidePopup(); + + items[0].label = "z"; + test.assertEqual(items[0].label, "z"); + test.showMenu(null, function (popup) { + test.checkMenu([items[1], items[0]], [], []); + test.done(); + }); + }); +}; + + +// Setting an item's label before the menu is ever shown should correctly change +// its label and, if necessary, its order within the menu if the item is in the +// overflow submenu. +exports.testSetLabelBeforeShowOverflow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let prefs = loader.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ] + items[0].label = "z"; + test.assertEqual(items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu([items[1], items[0]], [], []); + prefs.set(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); + test.done(); + }); +}; + + +// Setting an item's label after the menu is shown should correctly change its +// label and, if necessary, its order within the menu if the item is in the +// overflow submenu. +exports.testSetLabelAfterShowOverflow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let prefs = loader.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + popup.hidePopup(); + + items[0].label = "z"; + test.assertEqual(items[0].label, "z"); + test.showMenu(null, function (popup) { + test.checkMenu([items[1], items[0]], [], []); + prefs.set(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); + test.done(); + }); + }); +}; + + +// Setting the label of an item in a Menu should work. +exports.testSetLabelMenuItem = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [loader.cm.Item({ label: "a" })] + }); + menu.items[0].label = "z"; + + test.assertEqual(menu.items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Menu.addItem() should work. +exports.testMenuAddItem = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }) + ] + }); + menu.addItem(loader.cm.Item({ label: "item 1" })); + menu.addItem(loader.cm.Item({ label: "item 2" })); + + test.assertEqual(menu.items.length, 3, + "menu should have correct number of items"); + for (let i = 0; i < 3; i++) { + test.assertEqual(menu.items[i].label, "item " + i, + "item label should be correct"); + test.assertEqual(menu.items[i].parentMenu, menu, + "item's parent menu should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Adding the same item twice to a menu should work as expected. +exports.testMenuAddItemTwice = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [] + }); + let subitem = loader.cm.Item({ label: "item 1" }) + menu.addItem(subitem); + menu.addItem(loader.cm.Item({ label: "item 0" })); + menu.addItem(subitem); + + test.assertEqual(menu.items.length, 2, + "menu should have correct number of items"); + for (let i = 0; i < 2; i++) { + test.assertEqual(menu.items[i].label, "item " + i, + "item label should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Menu.removeItem() should work. +exports.testMenuRemoveItem = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item 1" }); + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }), + subitem, + loader.cm.Item({ label: "item 2" }) + ] + }); + + // Removing twice should be harmless. + menu.removeItem(subitem); + menu.removeItem(subitem); + + test.assertEqual(subitem.parentMenu, null, + "item's parent menu should be correct"); + + test.assertEqual(menu.items.length, 2, + "menu should have correct number of items"); + test.assertEqual(menu.items[0].label, "item 0", + "item label should be correct"); + test.assertEqual(menu.items[1].label, "item 2", + "item label should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Adding an item currently contained in one menu to another menu should work. +exports.testMenuItemSwap = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item" }); + let menu0 = loader.cm.Menu({ + label: "menu 0", + items: [subitem] + }); + let menu1 = loader.cm.Menu({ + label: "menu 1", + items: [] + }); + menu1.addItem(subitem); + + test.assertEqual(menu0.items.length, 0, + "menu should have correct number of items"); + + test.assertEqual(menu1.items.length, 1, + "menu should have correct number of items"); + test.assertEqual(menu1.items[0].label, "item", + "item label should be correct"); + + test.assertEqual(subitem.parentMenu, menu1, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu0, menu1], [], []); + test.done(); + }); +}; + + +// Destroying an item should remove it from its parent menu. +exports.testMenuItemDestroy = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item" }); + let menu = loader.cm.Menu({ + label: "menu", + items: [subitem] + }); + subitem.destroy(); + + test.assertEqual(menu.items.length, 0, + "menu should have correct number of items"); + test.assertEqual(subitem.parentMenu, null, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Setting Menu.items should work. +exports.testMenuItemsSetter = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "old item 0" }), + loader.cm.Item({ label: "old item 1" }) + ] + }); + menu.items = [ + loader.cm.Item({ label: "new item 0" }), + loader.cm.Item({ label: "new item 1" }), + loader.cm.Item({ label: "new item 2" }) + ]; + + test.assertEqual(menu.items.length, 3, + "menu should have correct number of items"); + for (let i = 0; i < 3; i++) { + test.assertEqual(menu.items[i].label, "new item " + i, + "item label should be correct"); + test.assertEqual(menu.items[i].parentMenu, menu, + "item's parent menu should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Setting Item.data should work. +exports.testItemDataSetter = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ label: "old item 0", data: "old" }); + item.data = "new"; + + test.assertEqual(item.data, "new", "item should have correct data"); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// Open the test doc, load the module, make sure items appear when context- +// clicking the iframe. +exports.testAlreadyOpenIframe = function (test) { + test = new TestHelper(test); + test.withTestDoc(function (window, doc) { + let loader = test.newLoader(); + let item = new loader.cm.Item({ + label: "item" + }); + test.showMenu(doc.getElementById("iframe"), function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Test image support. +exports.testItemImage = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let imageURL = require("self").data.url("moz_favicon.ico"); + let item = new loader.cm.Item({ label: "item", image: imageURL }); + let menu = new loader.cm.Menu({ label: "menu", image: imageURL, items: [] }); + + test.showMenu(null, function (popup) { + test.checkMenu([item, menu], [], []); + + let imageURL2 = require("self").data.url("dummy.ico"); + item.image = imageURL2; + menu.image = imageURL2; + test.checkMenu([item, menu], [], []); + + item.image = null; + menu.image = null; + test.checkMenu([item, menu], [], []); + + test.done(); + }); +}; + + +// Menu.destroy should destroy the item tree rooted at that menu. +exports.testMenuDestroy = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }), + loader.cm.Menu({ + label: "item 1", + items: [ + loader.cm.Item({ label: "subitem 0" }), + loader.cm.Item({ label: "subitem 1" }), + loader.cm.Item({ label: "subitem 2" }) + ] + }), + loader.cm.Item({ label: "item 2" }) + ] + }); + menu.destroy(); + + let numRegistryEntries = 0; + loader.globalScope.browserManager.browserWins.forEach(function (bwin) { + for (let itemID in bwin.items) + numRegistryEntries++; + }); + test.assertEqual(numRegistryEntries, 0, "All items should be unregistered."); + + test.showMenu(null, function (popup) { + test.checkMenu([], [], [menu]); + test.done(); + }); +}; + + +// NO TESTS BELOW THIS LINE! /////////////////////////////////////////////////// + +// Run only a dummy test if context-menu doesn't support the host app. +if (!require("xul-app").is("Firefox")) { + for (let [prop, val] in Iterator(exports)) + if (/^test/.test(prop) && typeof(val) === "function") + delete exports[prop]; + exports.testAppNotSupported = function (test) { + test.pass("context-menu does not support this application."); + }; +} + + +// This makes it easier to run tests by handling things like opening the menu, +// opening new windows, making assertions, etc. Methods on |test| can be called +// on instances of this class. Don't forget to call done() to end the test! +// WARNING: This looks up items in popups by comparing labels, so don't give two +// items the same label. +function TestHelper(test) { + // default waitUntilDone timeout is 10s, which is too short on the win7 + // buildslave + test.waitUntilDone(30*1000); + this.test = test; + this.loaders = []; + this.browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); +} + +TestHelper.prototype = { + get contextMenuPopup() { + return this.browserWindow.document.getElementById("contentAreaContextMenu"); + }, + + get contextMenuSeparator() { + return this.browserWindow.document.getElementById(SEPARATOR_ID); + }, + + get overflowPopup() { + return this.browserWindow.document.getElementById(OVERFLOW_POPUP_ID); + }, + + get overflowSubmenu() { + return this.browserWindow.document.getElementById(OVERFLOW_MENU_ID); + }, + + get tabBrowser() { + return this.browserWindow.gBrowser; + }, + + // Methods on the wrapped test can be called on this object. + __noSuchMethod__: function (methodName, args) { + this.test[methodName].apply(this.test, args); + }, + + // Asserts that absentItems -- an array of items that should not match the + // current context -- aren't present in the menu. + checkAbsentItems: function (presentItems, absentItems) { + for (let i = 0; i < absentItems.length; i++) { + let item = absentItems[i]; + let elt = this.getItemElt(this.contextMenuPopup, item); + + // The implementation actually hides items rather than removing or not + // adding them in the first place, but that's an implementation detail. + this.test.assert(!elt || elt.hidden, + "Item should not be present in top-level menu"); + + if (this.shouldOverflow(presentItems)) { + elt = getItemElt(this.overflowPopup, item); + this.test.assert(!elt || elt.hidden, + "Item should not be present in overflow submenu"); + } + } + }, + + // Asserts that elt, a DOM element representing item, looks OK. + checkItemElt: function (elt, item) { + let itemType = this.getItemType(item); + + switch (itemType) { + case "Item": + this.test.assertEqual(elt.localName, "menuitem", + "Item DOM element should be a xul:menuitem"); + if (typeof(item.data) === "string") { + this.test.assertEqual(elt.getAttribute("value"), item.data, + "Item should have correct data"); + } + break + case "Menu": + this.test.assertEqual(elt.localName, "menu", + "Menu DOM element should be a xul:menu"); + let subPopup = elt.firstChild; + this.test.assert(subPopup, "xul:menu should have a child"); + this.test.assertEqual(subPopup.localName, "menupopup", + "xul:menu's first child should be a menupopup"); + break; + case "Separator": + this.test.assertEqual(elt.localName, "menuseparator", + "Separator DOM element should be a xul:menuseparator"); + break; + } + + if (itemType === "Item" || itemType === "Menu") { + this.test.assertEqual(elt.getAttribute("label"), item.label, + "Item should have correct title"); + if (typeof(item.image) === "string") + this.test.assertEqual(elt.getAttribute("image"), item.image, + "Item should have correct image"); + else + this.test.assert(!elt.hasAttribute("image"), + "Item should not have image"); + } + }, + + // Asserts that the context menu looks OK given the arguments. presentItems + // are items that should match the current context. absentItems are items + // that shouldn't. removedItems are items that have been removed from the + // menu. + checkMenu: function (presentItems, absentItems, removedItems) { + this.checkSeparator(presentItems); + this.checkOverflow(presentItems); + this.checkPresentItems(presentItems); + this.checkAbsentItems(presentItems, absentItems); + this.checkRemovedItems(removedItems); + this.checkSort(presentItems); + }, + + // Asserts that the overflow submenu is present or absent as appropriate for + // presentItems. + checkOverflow: function (presentItems) { + let submenu = this.overflowSubmenu; + if (this.shouldOverflow(presentItems)) { + this.test.assert(submenu && !submenu.hidden, + "Overflow submenu should be present"); + this.test.assert(submenu.localName, "menu", + "Overflow submenu should be a <menu>"); + let overflowPopup = this.overflowPopup; + this.test.assert(overflowPopup, + "Overflow submenu popup should be present"); + this.test.assert(overflowPopup.localName, "menupopup", + "Overflow submenu popup should be a <menupopup>"); + } + else { + this.test.assert(!submenu || submenu.hidden, + "Overflow submenu should be absent"); + } + }, + + // Asserts that the items that are present in the menu because they match the + // current context look OK. + checkPresentItems: function (presentItems) { + function recurse(popup, items, isTopLevel) { + items.forEach(function (item) { + let elt = this.getItemElt(popup, item); + + if (isTopLevel) { + if (this.shouldOverflow(items)) { + this.test.assert(!elt || elt.hidden, + "Item should not be present in top-level menu"); + + let overflowPopup = this.overflowPopup; + this.test.assert(overflowPopup, + "Overflow submenu should be present"); + + elt = this.getItemElt(overflowPopup, item); + this.test.assert(elt && !elt.hidden, + "Item should be present in overflow submenu"); + } + else { + this.test.assert(elt && !elt.hidden, + "Item should be present in top-level menu"); + } + } + else { + this.test.assert(elt && !elt.hidden, + "Item should be present in menu"); + } + + this.checkItemElt(elt, item); + if (this.getItemType(item) === "Menu") + recurse.call(this, elt.firstChild, item.items, false); + }, this); + } + + recurse.call(this, this.contextMenuPopup, presentItems, true); + }, + + // Asserts that items that have been removed from the menu are really removed. + checkRemovedItems: function (removedItems) { + for (let i = 0; i < removedItems.length; i++) { + let item = removedItems[i]; + + let elt = this.getItemElt(this.contextMenuPopup, item); + this.test.assert(!elt, "Item should be removed from top-level menu"); + + let overflowPopup = this.overflowPopup; + if (overflowPopup) { + elt = this.getItemElt(overflowPopup, item); + this.test.assert(!elt, "Item should be removed from overflow submenu"); + } + } + }, + + // Asserts that the menu separator separating standard items from our items + // looks OK. + checkSeparator: function (presentItems) { + let sep = this.contextMenuSeparator; + if (presentItems.length) { + this.test.assert(sep && !sep.hidden, "Menu separator should be present"); + this.test.assertEqual(sep.localName, "menuseparator", + "Menu separator should be a <menuseparator>"); + } + else { + this.test.assert(!sep || sep.hidden, "Menu separator should be absent"); + } + }, + + // Asserts that our items are sorted. + checkSort: function (presentItems) { + // Get the first item in sorted order, get its elt, walk the nextSibling + // chain, making sure each is greater than the previous. + if (presentItems.length) { + let sorted = presentItems.slice(0). + sort(function (a, b) a.label.localeCompare(b.label)); + let elt = this.shouldOverflow(presentItems) ? + this.getItemElt(this.overflowPopup, sorted[0]) : + this.getItemElt(this.contextMenuPopup, sorted[0]); + let numElts = 1; + while (elt.nextSibling && + elt.nextSibling.className.split(/\s+/).indexOf(ITEM_CLASS) >= 0) { + let eltLabel = elt.getAttribute("label"); + let nextLabel = elt.nextSibling.getAttribute("label"); + this.test.assert(eltLabel.localeCompare(nextLabel) < 0, + "Item label should be < next item's label"); + elt = elt.nextSibling; + numElts++; + } + this.test.assertEqual(numElts, presentItems.length, + "The first item in sorted order should have the " + + "first element in sorted order"); + } + }, + + // Attaches an event listener to node. The listener is automatically removed + // when it's fired (so it's assumed it will fire), and callback is called + // after a short delay. Since the module we're testing relies on the same + // event listeners to do its work, this is to give them a little breathing + // room before callback runs. Inside callback |this| is this object. + delayedEventListener: function (node, event, callback, useCapture) { + const self = this; + node.addEventListener(event, function handler(evt) { + node.removeEventListener(event, handler, useCapture); + require("timer").setTimeout(function () { + try { + callback.call(self, evt); + } + catch (err) { + self.test.exception(err); + self.test.done(); + } + }, 20); + }, useCapture); + }, + + // Call to finish the test. + done: function () { + function commonDone() { + if (this.tab) { + this.tabBrowser.removeTab(this.tab); + this.tabBrowser.selectedTab = this.oldSelectedTab; + } + while (this.loaders.length) { + let browserManager = this.loaders[0].globalScope.browserManager; + let topLevelItems = browserManager.topLevelItems.slice(); + let privatePropsKey = this.loaders[0].globalScope.PRIVATE_PROPS_KEY; + let workerRegs = topLevelItems.map(function (item) { + return item.valueOf(privatePropsKey)._workerReg; + }); + + this.loaders[0].unload(); + + // Make sure the browser manager is cleaned up. + this.test.assertEqual(browserManager.browserWins.length, 0, + "browserManager should have no windows left"); + this.test.assertEqual(browserManager.topLevelItems.length, 0, + "browserManager should have no items left"); + this.test.assert(!("contentWins" in browserManager), + "browserManager should have no content windows left"); + + // Make sure the items' worker registries are cleaned up. + topLevelItems.forEach(function (item) { + this.test.assert(!("_workerReg" in item.valueOf(privatePropsKey)), + "item's worker registry should be removed"); + }, this); + workerRegs.forEach(function (workerReg) { + this.test.assertEqual(Object.keys(workerReg.winWorkers).length, 0, + "worker registry should be empty"); + this.test.assertEqual( + Object.keys(workerReg.winsWithoutWorkers).length, 0, + "worker registry list of windows without workers should be empty"); + }, this); + } + this.test.done(); + } + + function closeBrowserWindow() { + if (this.oldBrowserWindow) { + this.delayedEventListener(this.browserWindow, "unload", commonDone, + false); + this.browserWindow.close(); + this.browserWindow = this.oldBrowserWindow; + delete this.oldBrowserWindow; + } + else { + commonDone.call(this); + } + }; + + if (this.contextMenuPopup.state == "closed") { + closeBrowserWindow.call(this); + } + else { + this.delayedEventListener(this.contextMenuPopup, "popuphidden", + function () closeBrowserWindow.call(this), + false); + this.contextMenuPopup.hidePopup(); + } + }, + + // Returns the DOM element in popup corresponding to item. + // WARNING: The element is found by comparing labels, so don't give two items + // the same label. + getItemElt: function (popup, item) { + let nodes = popup.childNodes; + for (let i = nodes.length - 1; i >= 0; i--) { + if (this.getItemType(item) === "Separator") { + if (nodes[i].localName === "menuseparator") + return nodes[i]; + } + else if (nodes[i].getAttribute("label") === item.label) + return nodes[i]; + } + return null; + }, + + // Returns "Item", "Menu", or "Separator". + getItemType: function (item) { + // Could use instanceof here, but that would require accessing the loader + // that created the item, and I don't want to A) somehow search through the + // this.loaders list to find it, and B) assume there are any live loaders at + // all. + return /^\[object (Item|Menu|Separator)/.exec(item.toString())[1]; + }, + + // Returns a wrapper around a new loader: { loader, cm, unload, globalScope }. + // loader is a Cuddlefish sandboxed loader, cm is the context menu module, + // globalScope is the context menu module's global scope, and unload is a + // function that unloads the loader and associated resources. + newLoader: function () { + const self = this; + let loader = Loader(module); + let wrapper = { + loader: loader, + cm: loader.require("context-menu"), + globalScope: loader.sandbox("context-menu"), + unload: function () { + loader.unload(); + let idx = self.loaders.indexOf(wrapper); + if (idx < 0) + throw new Error("Test error: tried to unload nonexistent loader"); + self.loaders.splice(idx, 1); + } + }; + this.loaders.push(wrapper); + return wrapper; + }, + + // Returns true if the number of presentItems crosses the overflow threshold. + shouldOverflow: function (presentItems) { + return presentItems.length > + (this.loaders.length ? + this.loaders[0].loader.require("preferences-service"). + get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT) : + OVERFLOW_THRESH_DEFAULT); + }, + + // Opens the context menu on the current page. If targetNode is null, the + // menu is opened in the top-left corner. onShowncallback is passed the + // popup. + showMenu: function(targetNode, onshownCallback) { + function sendEvent() { + this.delayedEventListener(this.browserWindow, "popupshowing", + function (e) { + let popup = e.target; + onshownCallback.call(this, popup); + }, false); + + let rect = targetNode ? + targetNode.getBoundingClientRect() : + { left: 0, top: 0, width: 0, height: 0 }; + let contentWin = this.browserWindow.content; + contentWin. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils). + sendMouseEvent("contextmenu", + rect.left + (rect.width / 2), + rect.top + (rect.height / 2), + 2, 1, 0); + } + + // If a new tab or window has not yet been opened, open a new tab now. For + // some reason using the tab already opened when the test starts causes + // leaks. See bug 566351 for details. + if (!targetNode && !this.oldSelectedTab && !this.oldBrowserWindow) { + this.oldSelectedTab = this.tabBrowser.selectedTab; + this.tab = this.tabBrowser.addTab("about:blank"); + let browser = this.tabBrowser.getBrowserForTab(this.tab); + + this.delayedEventListener(browser, "load", function () { + this.tabBrowser.selectedTab = this.tab; + sendEvent.call(this); + }, true); + } + else + sendEvent.call(this); + }, + + // Opens a new browser window. The window will be closed automatically when + // done() is called. + withNewWindow: function (onloadCallback) { + let win = this.browserWindow.OpenBrowserWindow(); + this.delayedEventListener(win, "load", onloadCallback, true); + this.oldBrowserWindow = this.browserWindow; + this.browserWindow = win; + }, + + // Opens a new tab with our test page in the current window. The tab will + // be closed automatically when done() is called. + withTestDoc: function (onloadCallback) { + this.oldSelectedTab = this.tabBrowser.selectedTab; + this.tab = this.tabBrowser.addTab(TEST_DOC_URL); + let browser = this.tabBrowser.getBrowserForTab(this.tab); + + this.delayedEventListener(browser, "load", function () { + this.tabBrowser.selectedTab = this.tab; + onloadCallback.call(this, browser.contentWindow, browser.contentDocument); + }, true); + } +}; diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-hotkeys.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-hotkeys.js new file mode 100644 index 0000000..3b69a28 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-hotkeys.js @@ -0,0 +1,156 @@ +"use strict"; + +const { Hotkey } = require("hotkeys"); +const { keyDown } = require("dom/events/keys"); +const { Loader } = require('./helpers'); + +exports["test hotkey: function key"] = function(assert, done) { + var element = require("window-utils").activeBrowserWindow.document.documentElement; + var showHotKey = Hotkey({ + combo: "f1", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "f2"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "f2", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "f1"); +}; + +exports["test hotkey: accel alt shift"] = function(assert, done) { + var element = require("window-utils").activeBrowserWindow.document.documentElement; + var showHotKey = Hotkey({ + combo: "accel-shift-6", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "accel-alt-shift-6"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "accel-alt-shift-6", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "accel-shift-6"); +}; + +exports["test hotkey meta & control"] = function(assert, done) { + var element = require("window-utils").activeBrowserWindow.document.documentElement; + var showHotKey = Hotkey({ + combo: "meta-3", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "alt-control-shift-b"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "Ctrl-Alt-Shift-B", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "meta-3"); +}; + +exports["test hotkey: control-1 / meta--"] = function(assert, done) { + var element = require("window-utils").activeBrowserWindow.document.documentElement; + var showHotKey = Hotkey({ + combo: "control-1", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "meta--"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "meta--", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "control-1"); +}; + +exports["test invalid combos"] = function(assert) { + assert.throws(function() { + Hotkey({ + combo: "d", + onPress: function() {} + }); + }, "throws if no modifier is present"); + assert.throws(function() { + Hotkey({ + combo: "alt", + onPress: function() {} + }); + }, "throws if no key is present"); + assert.throws(function() { + Hotkey({ + combo: "alt p b", + onPress: function() {} + }); + }, "throws if more then one key is present"); +}; + +exports["test no exception on unmodified keypress"] = function(assert) { + var element = require("window-utils").activeBrowserWindow.document.documentElement; + var someHotkey = Hotkey({ + combo: "control-alt-1", + onPress: function() { + } + }); + keyDown(element, "a"); + assert.pass("No exception throw, unmodified keypress passed"); +}; + +exports["test hotkey: automatic destroy"] = function(assert, done) { + // Hacky way to be able to create unloadable modules via makeSandboxedLoader. + let loader = Loader(module); + + var called = false; + var element = loader.require("window-utils").activeBrowserWindow.document.documentElement; + var hotkey = loader.require("hotkeys").Hotkey({ + combo: "accel-shift-x", + onPress: function() { + called = true; + } + }); + + // Unload the module so that previous hotkey is automatically destroyed + loader.unload(); + + // Ensure that the hotkey is really destroyed + keyDown(element, "accel-shift-x"); + + require("timer").setTimeout(function () { + assert.ok(!called, "Hotkey is destroyed and not called."); + done(); + }, 0); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-module.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-module.js new file mode 100644 index 0000000..c62f923 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-module.js @@ -0,0 +1,33 @@ +"use strict"; + +/** Disabled because of Bug 672199 +exports["test module exports are frozen"] = function(assert) { + assert.ok(Object.isFrozen(require("addon-kit/hotkeys")), + "module exports are frozen"); +}; + +exports["test redefine exported property"] = function(assert) { + let hotkeys = require("addon-kit/hotkeys"); + let { Hotkey } = hotkeys; + try { Object.defineProperty(hotkeys, 'Hotkey', { value: {} }); } catch(e) {} + assert.equal(hotkeys.Hotkey, Hotkey, "exports can't be redefined"); +}; +*/ + +exports["test can't delete exported property"] = function(assert) { + let hotkeys = require("addon-kit/hotkeys"); + let { Hotkey } = hotkeys; + + try { delete hotkeys.Hotkey; } catch(e) {} + assert.equal(hotkeys.Hotkey, Hotkey, "exports can't be deleted"); +}; + +exports["test can't override exported property"] = function(assert) { + let hotkeys = require("addon-kit/hotkeys"); + let { Hotkey } = hotkeys; + + try { hotkeys.Hotkey = Object } catch(e) {} + assert.equal(hotkeys.Hotkey, Hotkey, "exports can't be overriden"); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-notifications.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-notifications.js new file mode 100644 index 0000000..1eb6190 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-notifications.js @@ -0,0 +1,79 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const { Loader } = require('./helpers'); + +exports.testOnClick = function (test) { + let [loader, mockAlertServ] = makeLoader(module); + let notifs = loader.require("notifications"); + let data = "test data"; + let opts = { + onClick: function (clickedData) { + test.assertEqual(this, notifs, "|this| should be notifications module"); + test.assertEqual(clickedData, data, + "data passed to onClick should be correct"); + }, + data: data, + title: "test title", + text: "test text", + iconURL: "test icon URL" + }; + notifs.notify(opts); + mockAlertServ.click(); + loader.unload(); +}; + +// Returns [loader, mockAlertService]. +function makeLoader(test) { + let loader = Loader(module); + let mockAlertServ = { + showAlertNotification: function (imageUrl, title, text, textClickable, + cookie, alertListener, name) { + this._cookie = cookie; + this._alertListener = alertListener; + }, + click: function () { + this._alertListener.observe(null, "alertclickcallback", this._cookie); + } + }; + loader.require("notifications"); + let scope = loader.sandbox("notifications"); + scope.notify = mockAlertServ.showAlertNotification.bind(mockAlertServ); + return [loader, mockAlertServ]; +}; diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-page-mod.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-page-mod.js new file mode 100644 index 0000000..2b38946 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-page-mod.js @@ -0,0 +1,416 @@ +"use strict"; + +var pageMod = require("page-mod"); +var testPageMod = require("pagemod-test-helpers").testPageMod; +const { Loader } = require('./helpers'); +const tabs = require("tabs"); + +/* XXX This can be used to delay closing the test Firefox instance for interactive + * testing or visual inspection. This test is registered first so that it runs + * the last. */ +exports.delay = function(test) { + if (false) { + test.waitUntilDone(60000); + require("timer").setTimeout(function() {test.done();}, 4000); + } else + test.pass(); +} + +/* Tests for the PageMod APIs */ + +exports.testPageMod1 = function(test) { + let mods = testPageMod(test, "about:", [{ + include: /about:/, + contentScriptWhen: 'end', + contentScript: 'new ' + function WorkerScope() { + window.document.body.setAttribute("JEP-107", "worked"); + }, + onAttach: function() { + test.assertEqual(this, mods[0], "The 'this' object is the page mod."); + } + }], + function(win, done) { + test.assertEqual( + win.document.body.getAttribute("JEP-107"), + "worked", + "PageMod.onReady test" + ); + done(); + } + ); +}; + +exports.testPageMod2 = function(test) { + testPageMod(test, "about:", [{ + include: "about:*", + contentScript: [ + 'new ' + function contentScript() { + window.AUQLUE = function() { return 42; } + try { + window.AUQLUE() + } + catch(e) { + throw new Error("PageMod scripts executed in order"); + } + document.documentElement.setAttribute("first", "true"); + }, + 'new ' + function contentScript() { + document.documentElement.setAttribute("second", "true"); + } + ] + }], function(win, done) { + test.assertEqual(win.document.documentElement.getAttribute("first"), + "true", + "PageMod test #2: first script has run"); + test.assertEqual(win.document.documentElement.getAttribute("second"), + "true", + "PageMod test #2: second script has run"); + test.assertEqual("AUQLUE" in win, false, + "PageMod test #2: scripts get a wrapped window"); + done(); + }); +}; + +exports.testPageModIncludes = function(test) { + var asserts = []; + function createPageModTest(include, expectedMatch) { + // Create an 'onload' test function... + asserts.push(function(test, win) { + var matches = include in win.localStorage; + test.assert(expectedMatch ? matches : !matches, + "'" + include + "' match test, expected: " + expectedMatch); + }); + // ...and corresponding PageMod options + return { + include: include, + contentScript: 'new ' + function() { + self.on("message", function(msg) { + window.localStorage[msg] = true; + }); + }, + // The testPageMod callback with test assertions is called on 'end', + // and we want this page mod to be attached before it gets called, + // so we attach it on 'start'. + contentScriptWhen: 'start', + onAttach: function(worker) { + worker.postMessage(this.include[0]); + } + }; + } + + testPageMod(test, "about:buildconfig", [ + createPageModTest("*", false), + createPageModTest("*.google.com", false), + createPageModTest("about:*", true), + createPageModTest("about:", false), + createPageModTest("about:buildconfig", true) + ], + function (win, done) { + test.waitUntil(function () win.localStorage["about:buildconfig"], + "about:buildconfig page-mod to be executed") + .then(function () { + asserts.forEach(function(fn) { + fn(test, win); + }); + done(); + }); + } + ); +}; + +exports.testPageModErrorHandling = function(test) { + test.assertRaises(function() { + new pageMod.PageMod(); + }, + 'pattern is undefined', + "PageMod() throws when 'include' option is not specified."); +}; + +/* Tests for internal functions. */ +exports.testCommunication1 = function(test) { + let workerDone = false, + callbackDone = null; + + testPageMod(test, "about:", [{ + include: "about:*", + contentScriptWhen: 'end', + contentScript: 'new ' + function WorkerScope() { + self.on('message', function(msg) { + document.body.setAttribute('JEP-107', 'worked'); + self.postMessage(document.body.getAttribute('JEP-107')); + }) + }, + onAttach: function(worker) { + worker.on('error', function(e) { + test.fail('Errors where reported'); + }); + worker.on('message', function(value) { + test.assertEqual( + "worked", + value, + "test comunication" + ); + workerDone = true; + if (callbackDone) + callbackDone(); + }); + worker.postMessage('do it!') + } + }], + function(win, done) { + (callbackDone = function() { + if (workerDone) { + test.assertEqual( + 'worked', + win.document.body.getAttribute('JEP-107'), + 'attribute should be modified' + ); + done(); + } + })(); + } + ); +}; + +exports.testCommunication2 = function(test) { + let callbackDone = null, + window; + + testPageMod(test, "about:", [{ + include: "about:*", + contentScriptWhen: 'start', + contentScript: 'new ' + function WorkerScope() { + document.documentElement.setAttribute('AUQLUE', 42); + window.addEventListener('load', function listener() { + self.postMessage('onload'); + }, false); + self.on("message", function() { + self.postMessage(document.documentElement.getAttribute("test")) + }); + }, + onAttach: function(worker) { + worker.on('error', function(e) { + test.fail('Errors where reported'); + }); + worker.on('message', function(msg) { + if ('onload' == msg) { + test.assertEqual( + '42', + window.document.documentElement.getAttribute('AUQLUE'), + 'PageMod scripts executed in order' + ); + window.document.documentElement.setAttribute('test', 'changes in window'); + worker.postMessage('get window.test') + } else { + test.assertEqual( + 'changes in window', + msg, + 'PageMod test #2: second script has run' + ) + callbackDone(); + } + }); + } + }], + function(win, done) { + window = win; + callbackDone = done; + } + ); +}; + +exports.testEventEmitter = function(test) { + let workerDone = false, + callbackDone = null; + + testPageMod(test, "about:", [{ + include: "about:*", + contentScript: 'new ' + function WorkerScope() { + self.port.on('addon-to-content', function(data) { + self.port.emit('content-to-addon', data); + }); + }, + onAttach: function(worker) { + worker.on('error', function(e) { + test.fail('Errors were reported : '+e); + }); + worker.port.on('content-to-addon', function(value) { + test.assertEqual( + "worked", + value, + "EventEmitter API works!" + ); + if (callbackDone) + callbackDone(); + else + workerDone = true; + }); + worker.port.emit('addon-to-content', 'worked'); + } + }], + function(win, done) { + if (workerDone) + done(); + else + callbackDone = done; + } + ); +}; + +// Execute two concurrent page mods on same document to ensure that their +// JS contexts are different +exports.testMixedContext = function(test) { + let doneCallback = null; + let messages = 0; + let modObject = { + include: "data:text/html,", + contentScript: 'new ' + function WorkerScope() { + // Both scripts will execute this, + // context is shared if one script see the other one modification. + let isContextShared = "sharedAttribute" in document; + self.postMessage(isContextShared); + document.sharedAttribute = true; + }, + onAttach: function(w) { + w.on("message", function (isContextShared) { + if (isContextShared) { + test.fail("Page mod contexts are mixed."); + doneCallback(); + } + else if (++messages == 2) { + test.pass("Page mod contexts are different."); + doneCallback(); + } + }); + } + }; + testPageMod(test, "data:text/html,", [modObject, modObject], + function(win, done) { + doneCallback = done; + } + ); +}; + +exports.testHistory = function(test) { + // We need a valid url in order to have a working History API. + // (i.e do not work on data: or about: pages) + // Test bug 679054. + let url = require("self").data.url("test-page-mod.html"); + let callbackDone = null; + testPageMod(test, url, [{ + include: url, + contentScriptWhen: 'end', + contentScript: 'new ' + function WorkerScope() { + history.pushState({}, "", "#"); + history.replaceState({foo: "bar"}, "", "#"); + self.postMessage(history.state); + }, + onAttach: function(worker) { + worker.on('message', function (data) { + test.assertEqual(JSON.stringify(data), JSON.stringify({foo: "bar"}), + "History API works!"); + callbackDone(); + }); + } + }], + function(win, done) { + callbackDone = done; + } + ); +}; + +exports.testRelatedTab = function(test) { + test.waitUntilDone(); + + let tab; + let { PageMod } = require("page-mod"); + let pageMod = new PageMod({ + include: "about:*", + onAttach: function(worker) { + test.assertEqual(tab, worker.tab, "Worker.tab is valid"); + pageMod.destroy(); + tab.close(); + test.done(); + } + }); + + tabs.open({ + url: "about:", + onOpen: function onOpen(t) { + tab = t; + } + }); + +}; + +exports['test tab worker on message'] = function(test) { + test.waitUntilDone(); + + let { browserWindows } = require("windows"); + let tabs = require("tabs"); + let { PageMod } = require("page-mod"); + + let url1 = "data:text/html,<title>tab1</title><h1>worker1.tab</h1>"; + let url2 = "data:text/html,<title>tab2</title><h1>worker2.tab</h1>"; + let worker1 = null; + + let mod = PageMod({ + include: "data:text/html,*", + contentScriptWhen: "ready", + contentScript: "self.postMessage('#1');", + onAttach: function onAttach(worker) { + worker.on("message", function onMessage() { + this.tab.attach({ + contentScriptWhen: "ready", + contentScript: "self.postMessage({ url: window.location.href, title: document.title });", + onMessage: function onMessage(data) { + test.assertEqual(this.tab.url, data.url, "location is correct"); + test.assertEqual(this.tab.title, data.title, "title is correct"); + if (this.tab.url === url1) { + worker1 = this; + tabs.open({ url: url2, inBackground: true }); + } + else if (this.tab.url === url2) { + mod.destroy(); + worker1.tab.close(); + worker1.destroy(); + worker.tab.close(); + worker.destroy(); + test.done(); + } + } + }); + }); + } + }); + + tabs.open(url1); +}; + +exports.testAutomaticDestroy = function(test) { + test.waitUntilDone(); + let loader = Loader(module); + + let pageMod = loader.require("page-mod").PageMod({ + include: "about:*", + contentScriptWhen: "start", + onAttach: function(w) { + test.fail("Page-mod should have been detroyed during module unload"); + } + }); + + // Unload the page-mod module so that our page mod is destroyed + loader.unload(); + + // Then create a second tab to ensure that it is correctly destroyed + let tabs = require("tabs"); + tabs.open({ + url: "about:", + onReady: function onReady(tab) { + test.pass("check automatic destroy"); + tab.close(); + test.done(); + } + }); + +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-page-worker.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-page-worker.js new file mode 100644 index 0000000..89cdf45 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-page-worker.js @@ -0,0 +1,362 @@ +let tests = {}, Pages, Page; +const { Loader } = require('./helpers'); + +const ERR_DESTROYED = + "The page has been destroyed and can no longer be used."; + +tests.testSimplePageCreation = function(test) { + test.waitUntilDone(); + + let page = new Page({ + contentScript: "self.postMessage(window.location.href)", + contentScriptWhen: "end", + onMessage: function (message) { + test.assertEqual(message, "about:blank", + "Page Worker should start with a blank page by default"); + test.assertEqual(this, page, "The 'this' object is the page itself."); + test.done(); + } + }); +} + +/* + * Tests that we can't be tricked by document overloads as we have access + * to wrapped nodes + */ +tests.testWrappedDOM = function(test) { + test.waitUntilDone(); + + let page = Page({ + allow: { script: true }, + contentURL: "data:text/html,<script>document.getElementById=3;window.scrollTo=3;</script>", + contentScript: "window.addEventListener('load', function () " + + "self.postMessage([typeof(document.getElementById), " + + "typeof(window.scrollTo)]), true)", + onMessage: function (message) { + test.assertEqual(message[0], + "function", + "getElementById from content script is the native one"); + + test.assertEqual(message[1], + "function", + "scrollTo from content script is the native one"); + + test.done(); + } + }); +} + +/* +// We do not offer unwrapped access to DOM since bug 601295 landed +// See 660780 to track progress of unwrap feature +tests.testUnwrappedDOM = function(test) { + test.waitUntilDone(); + + let page = Page({ + allow: { script: true }, + contentURL: "data:text/html,<script>document.getElementById=3;window.scrollTo=3;</script>", + contentScript: "window.addEventListener('load', function () " + + "self.postMessage([typeof(unsafeWindow.document.getElementById), " + + "typeof(unsafeWindow.scrollTo)]), true)", + onMessage: function (message) { + test.assertEqual(message[0], + "number", + "document inside page is free to be changed"); + + test.assertEqual(message[1], + "number", + "window inside page is free to be changed"); + + test.done(); + } + }); +} +*/ + +tests.testPageProperties = function(test) { + let page = new Page(); + + for each (let prop in ['contentURL', 'allow', 'contentScriptFile', + 'contentScript', 'contentScriptWhen', 'on', + 'postMessage', 'removeListener']) { + test.assert(prop in page, prop + " property is defined on page."); + } + + test.assert(function () page.postMessage("foo") || true, + "postMessage doesn't throw exception on page."); +} + +tests.testConstructorAndDestructor = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let Pages = loader.require("page-worker"); + let global = loader.sandbox("page-worker"); + + let pagesReady = 0; + + let page1 = Pages.Page({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: pageReady + }); + let page2 = Pages.Page({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: pageReady + }); + + test.assertNotEqual(page1, page2, + "Page 1 and page 2 should be different objects."); + + function pageReady() { + if (++pagesReady == 2) { + page1.destroy(); + page2.destroy(); + + test.assert(isDestroyed(page1), "page1 correctly unloaded."); + test.assert(isDestroyed(page2), "page2 correctly unloaded."); + + loader.unload(); + test.done(); + } + } +} + +tests.testAutoDestructor = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let Pages = loader.require("page-worker"); + + let page = Pages.Page({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: function() { + loader.unload(); + test.assert(isDestroyed(page), "Page correctly unloaded."); + test.done(); + } + }); +} + +tests.testValidateOptions = function(test) { + test.assertRaises( + function () Page({ contentURL: 'home' }), + "The `contentURL` option must be a valid URL.", + "Validation correctly denied a non-URL contentURL" + ); + + test.assertRaises( + function () Page({ onMessage: "This is not a function."}), + "The event listener must be a function.", + "Validation correctly denied a non-function onMessage." + ); + + test.pass("Options validation is working."); +} + +tests.testContentAndAllowGettersAndSetters = function(test) { + test.waitUntilDone(); + let content = "data:text/html,<script>window.localStorage.allowScript=3;</script>"; + let page = Page({ + contentURL: content, + contentScript: "self.postMessage(window.localStorage.allowScript)", + contentScriptWhen: "end", + onMessage: step0 + }); + + function step0(message) { + test.assertEqual(message, "3", + "Correct value expected for allowScript - 3"); + test.assertEqual(page.contentURL, content, + "Correct content expected"); + page.removeListener('message', step0); + page.on('message', step1); + page.allow = { script: false }; + page.contentURL = content = + "data:text/html,<script>window.localStorage.allowScript='f'</script>"; + } + + function step1(message) { + test.assertEqual(message, "3", + "Correct value expected for allowScript - 3"); + test.assertEqual(page.contentURL, content, "Correct content expected"); + page.removeListener('message', step1); + page.on('message', step2); + page.allow = { script: true }; + page.contentURL = content = + "data:text/html,<script>window.localStorage.allowScript='g'</script>"; + } + + function step2(message) { + test.assertEqual(message, "g", + "Correct value expected for allowScript - g"); + test.assertEqual(page.contentURL, content, "Correct content expected"); + page.removeListener('message', step2); + page.on('message', step3); + page.allow.script = false; + page.contentURL = content = + "data:text/html,<script>window.localStorage.allowScript=3</script>"; + } + + function step3(message) { + test.assertEqual(message, "g", + "Correct value expected for allowScript - g"); + test.assertEqual(page.contentURL, content, "Correct content expected"); + page.removeListener('message', step3); + page.on('message', step4); + page.allow.script = true; + page.contentURL = content = + "data:text/html,<script>window.localStorage.allowScript=4</script>"; + } + + function step4(message) { + test.assertEqual(message, "4", + "Correct value expected for allowScript - 4"); + test.assertEqual(page.contentURL, content, "Correct content expected"); + test.done(); + } + +} + +tests.testOnMessageCallback = function(test) { + test.waitUntilDone(); + + Page({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: function() { + test.pass("onMessage callback called"); + test.done(); + } + }); +} + +tests.testMultipleOnMessageCallbacks = function(test) { + test.waitUntilDone(); + + let count = 0; + let page = Page({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: function() count += 1 + }); + page.on('message', function() count += 2); + page.on('message', function() count *= 3); + page.on('message', function() + test.assertEqual(count, 9, "All callbacks were called, in order.")); + page.on('message', function() test.done()); + +} + +tests.testLoadContentPage = function(test) { + + test.waitUntilDone(); + + let page = Page({ + onMessage: function(message) { + // The message is an array whose first item is the test method to call + // and the rest of whose items are arguments to pass it. + test[message.shift()].apply(test, message); + }, + contentURL: require("self").data.url("test-page-worker.html"), + contentScriptFile: require("self").data.url("test-page-worker.js"), + contentScriptWhen: "ready" + }); + +} + +tests.testAllowScriptDefault = function(test) { + + test.waitUntilDone(); + + let page = Page({ + onMessage: function(message) { + test.assert(message, "Script is allowed to run by default."); + test.done(); + }, + contentURL: "data:text/html,<script>document.documentElement.setAttribute('foo', 3);</script>", + contentScript: "self.postMessage(document.documentElement.getAttribute('foo'))", + contentScriptWhen: "ready" + }); +} + +tests.testAllowScript = function(test) { + + test.waitUntilDone(); + + let page = Page({ + onMessage: function(message) { + test.assert(message, "Script runs when allowed to do so."); + test.done(); + }, + allow: { script: true }, + contentURL: "data:text/html,<script>document.documentElement.setAttribute('foo', 3);</script>", + contentScript: "self.postMessage(document.documentElement.hasAttribute('foo') && " + + " document.documentElement.getAttribute('foo') == 3)", + contentScriptWhen: "ready" + }); +} + +tests.testPingPong = function(test) { + test.waitUntilDone(); + let page = Page({ + contentURL: 'data:text/html,ping-pong', + contentScript: 'self.on("message", function(message) self.postMessage("pong"));' + + 'self.postMessage("ready");', + onMessage: function(message) { + if ('ready' == message) { + page.postMessage('ping'); + } + else { + test.assert(message, 'pong', 'Callback from contentScript'); + test.done(); + } + } + }); +}; + +tests.testMultipleDestroys = function(test) { + let page = Page(); + page.destroy(); + page.destroy(); + test.pass("Multiple destroys should not cause an error"); +}; + + +function isDestroyed(page) { + try { + page.postMessage("foo"); + } + catch (err if err.message == ERR_DESTROYED) { + return true; + } + return false; +} + + +let pageWorkerSupported = true; + +try { + Pages = require("page-worker"); + Page = Pages.Page; +} +catch (ex if ex.message == [ + "The page-worker module currently supports only Firefox and Thunderbird. ", + "In the future, we would like it to support other applications, however. ", + "Please see https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more ", + "information." + ].join("")) { + pageWorkerSupported = false; +} + +if (pageWorkerSupported) { + for (let test in tests) { + exports[test] = tests[test]; + } +} else { + exports.testPageWorkerNotSupported = function(test) { + test.pass("The page-worker module is not supported on this app."); + } +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-panel.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-panel.js new file mode 100644 index 0000000..3e240db --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-panel.js @@ -0,0 +1,437 @@ +let { Cc, Ci } = require("chrome"); +let panels = require('panel'); +let tests = {}, panels, Panel; +const { Loader } = require('./helpers'); + +tests.testPanel = function(test) { + test.waitUntilDone(); + let panel = Panel({ + contentURL: "about:buildconfig", + contentScript: "self.postMessage(1); self.on('message', function() self.postMessage(2));", + onMessage: function (message) { + test.assertEqual(this, panel, "The 'this' object is the panel."); + switch(message) { + case 1: + test.pass("The panel was loaded."); + panel.postMessage(''); + break; + case 2: + test.pass("The panel posted a message and received a response."); + panel.destroy(); + test.done(); + break; + } + } + }); +}; + +tests.testPanelEmit = function(test) { + test.waitUntilDone(); + let panel = Panel({ + contentURL: "about:buildconfig", + contentScript: "self.port.emit('loaded');" + + "self.port.on('addon-to-content', " + + " function() self.port.emit('received'));", + }); + panel.port.on("loaded", function () { + test.pass("The panel was loaded and sent a first event."); + panel.port.emit("addon-to-content"); + }); + panel.port.on("received", function () { + test.pass("The panel posted a message and received a response."); + panel.destroy(); + test.done(); + }); +}; + +tests.testPanelEmitEarly = function(test) { + test.waitUntilDone(); + let panel = Panel({ + contentURL: "about:buildconfig", + contentScript: "self.port.on('addon-to-content', " + + " function() self.port.emit('received'));", + }); + panel.port.on("received", function () { + test.pass("The panel posted a message early and received a response."); + panel.destroy(); + test.done(); + }); + panel.port.emit("addon-to-content"); +}; + +tests.testShowHidePanel = function(test) { + test.waitUntilDone(); + let panel = Panel({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: function (message) { + panel.show(); + }, + onShow: function () { + test.pass("The panel was shown."); + test.assertEqual(this, panel, "The 'this' object is the panel."); + test.assertEqual(this.isShowing, true, "panel.isShowing == true."); + panel.hide(); + }, + onHide: function () { + test.pass("The panel was hidden."); + test.assertEqual(this, panel, "The 'this' object is the panel."); + test.assertEqual(this.isShowing, false, "panel.isShowing == false."); + panel.destroy(); + test.done(); + } + }); +}; + +tests.testDocumentReload = function(test) { + test.waitUntilDone(); + let content = + "<script>" + + "setTimeout(function () {" + + " window.location = 'about:blank';" + + "}, 250);" + + "</script>"; + let messageCount = 0; + let panel = Panel({ + contentURL: "data:text/html," + encodeURIComponent(content), + contentScript: "self.postMessage(window.location.href)", + onMessage: function (message) { + messageCount++; + if (messageCount == 1) { + test.assertMatches(message, /data:text\/html,/, "First document had a content script"); + } + else if (messageCount == 2) { + test.assertEqual(message, "about:blank", "Second document too"); + panel.destroy(); + test.done(); + } + } + }); +}; + +tests.testParentResizeHack = function(test) { + let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); + let docShell = browserWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + if (!("allowWindowControl" in docShell)) { + // bug 635673 is not fixed in this firefox build + test.pass("allowWindowControl attribute that allow to fix browser window " + + "resize is not available on this build."); + return; + } + + test.waitUntilDone(30000); + + let previousWidth = browserWindow.outerWidth, previousHeight = browserWindow.outerHeight; + + let content = "<script>" + + "function contentResize() {" + + " resizeTo(200,200);" + + " resizeBy(200,200);" + + "}" + + "</script>" + + "Try to resize browser window"; + let panel = Panel({ + contentURL: "data:text/html," + encodeURIComponent(content), + contentScript: "self.on('message', function(message){" + + " if (message=='resize') " + + " unsafeWindow.contentResize();" + + "});", + contentScriptWhen: "ready", + onMessage: function (message) { + + }, + onShow: function () { + panel.postMessage('resize'); + require("timer").setTimeout(function () { + test.assertEqual(previousWidth,browserWindow.outerWidth,"Size doesn't change by calling resizeTo/By/..."); + test.assertEqual(previousHeight,browserWindow.outerHeight,"Size doesn't change by calling resizeTo/By/..."); + panel.destroy(); + test.done(); + },0); + } + }); + panel.show(); +} + +tests.testResizePanel = function(test) { + test.waitUntilDone(); + + // These tests fail on Linux if the browser window in which the panel + // is displayed is not active. And depending on what other tests have run + // before this one, it might not be (the untitled window in which the test + // runner executes is often active). So we make sure the browser window + // is focused by focusing it before running the tests. Then, to be the best + // possible test citizen, we refocus whatever window was focused before we + // started running these tests. + + let activeWindow = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher). + activeWindow; + let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); + + + function onFocus() { + browserWindow.removeEventListener("focus", onFocus, true); + + let panel = Panel({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + height: 10, + width: 10, + onMessage: function (message) { + panel.show(); + }, + onShow: function () { + panel.resize(100,100); + panel.hide(); + }, + onHide: function () { + test.assert((panel.width == 100) && (panel.height == 100), + "The panel was resized."); + if (activeWindow) + activeWindow.focus(); + test.done(); + } + }); + } + + if (browserWindow === activeWindow) { + onFocus(); + } + else { + browserWindow.addEventListener("focus", onFocus, true); + browserWindow.focus(); + } +}; + +tests.testHideBeforeShow = function(test) { + test.waitUntilDone(); + let showCalled = false; + let panel = Panel({ + onShow: function () { + showCalled = true; + }, + onHide: function () { + test.assert(!showCalled, 'must not emit show if was hidden before'); + test.done(); + } + }); + panel.show(); + panel.hide(); +}; + +tests.testSeveralShowHides = function(test) { + test.waitUntilDone(); + let hideCalled = 0; + let panel = panels.Panel({ + contentURL: "about:buildconfig", + onShow: function () { + panel.hide(); + }, + onHide: function () { + hideCalled++; + if (hideCalled < 3) + panel.show(); + else { + test.pass("onHide called three times as expected"); + test.done(); + } + } + }); + panel.on('error', function(e) { + test.fail('error was emitted:' + e.message + '\n' + e.stack); + }); + panel.show(); +}; + +tests.testAnchorAndArrow = function(test) { + test.waitUntilDone(20000); + let count = 0; + function newPanel(tab, anchor) { + let panel = panels.Panel({ + contentURL: "data:text/html,<html><body style='padding: 0; margin: 0; " + + "background: gray; text-align: center;'>Anchor: " + + anchor.id + "</body></html>", + width: 200, + height: 100, + onShow: function () { + count++; + panel.destroy(); + if (count==5) { + test.pass("All anchored panel test displayed"); + tab.close(function () { + test.done(); + }); + } + } + }); + panel.show(anchor); + } + + let tabs= require("tabs"); + let url = 'data:text/html,' + + '<html><head><title>foo</title></head><body>' + + '<style>div {background: gray; position: absolute; width: 300px; ' + + 'border: 2px solid black;}</style>' + + '<div id="tl" style="top: 0px; left: 0px;">Top Left</div>' + + '<div id="tr" style="top: 0px; right: 0px;">Top Right</div>' + + '<div id="bl" style="bottom: 0px; left: 0px;">Bottom Left</div>' + + '<div id="br" style="bottom: 0px; right: 0px;">Bottom right</div>' + + '</body></html>'; + + tabs.open({ + url: url, + onReady: function(tab) { + let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); + let window = browserWindow.content; + newPanel(tab, window.document.getElementById('tl')); + newPanel(tab, window.document.getElementById('tr')); + newPanel(tab, window.document.getElementById('bl')); + newPanel(tab, window.document.getElementById('br')); + let anchor = browserWindow.document.getElementById("identity-box"); + newPanel(tab, anchor); + } + }); + + + +}; + +tests.testPanelTextColor = function(test) { + test.waitUntilDone(); + let html = "<html><head><style>body {color: yellow}</style></head>" + + "<body><p>Foo</p></body></html>"; + let panel = Panel({ + contentURL: "data:text/html," + encodeURI(html), + contentScript: "self.port.emit('color', " + + "window.getComputedStyle(document.body.firstChild, null). " + + " getPropertyValue('color'));" + }); + panel.port.on("color", function (color) { + test.assertEqual(color, "rgb(255, 255, 0)", + "The panel text color style is preserved when a style exists."); + panel.destroy(); + test.done(); + }); +}; + +function makeEventOrderTest(options) { + let expectedEvents = []; + + return function(test) { + let panel = panels.Panel({ contentURL: "about:buildconfig" }); + + function expect(event, cb) { + expectedEvents.push(event); + panel.on(event, function() { + test.assertEqual(event, expectedEvents.shift()); + if (cb) + require("timer").setTimeout(cb, 1); + }); + return {then: expect}; + } + + test.waitUntilDone(); + options.test(test, expect, panel); + } +} + +tests.testAutomaticDestroy = function(test) { + let loader = Loader(module); + let panel = loader.require("panel").Panel({ + contentURL: "about:buildconfig", + contentScript: + "self.port.on('event', function() self.port.emit('event-back'));" + }); + + loader.unload(); + + panel.port.on("event-back", function () { + test.fail("Panel should have been destroyed on module unload"); + }); + panel.port.emit("event"); + test.pass("check automatic destroy"); +}; + +tests.testWaitForInitThenShowThenDestroy = makeEventOrderTest({ + test: function(test, expect, panel) { + expect('inited', function() { panel.show(); }). + then('show', function() { panel.destroy(); }). + then('hide', function() { test.done(); }); + } +}); + +tests.testShowThenWaitForInitThenDestroy = makeEventOrderTest({ + test: function(test, expect, panel) { + panel.show(); + expect('inited'). + then('show', function() { panel.destroy(); }). + then('hide', function() { test.done(); }); + } +}); + +tests.testShowThenHideThenDestroy = makeEventOrderTest({ + test: function(test, expect, panel) { + panel.show(); + expect('show', function() { panel.hide(); }). + then('hide', function() { panel.destroy(); test.done(); }); + } +}); + +tests.testContentURLOption = function(test) { + const URL_STRING = "about:buildconfig"; + const HTML_CONTENT = "<html><title>Test</title><p>This is a test.</p></html>"; + + let (panel = Panel({ contentURL: URL_STRING })) { + test.pass("contentURL accepts a string URL."); + test.assertEqual(panel.contentURL, URL_STRING, + "contentURL is the string to which it was set."); + } + + let dataURL = "data:text/html," + encodeURIComponent(HTML_CONTENT); + let (panel = Panel({ contentURL: dataURL })) { + test.pass("contentURL accepts a data: URL."); + } + + let (panel = Panel({})) { + test.assert(panel.contentURL == null, + "contentURL is undefined."); + } + + test.assertRaises(function () Panel({ contentURL: "foo" }), + "The `contentURL` option must be a valid URL.", + "Panel throws an exception if contentURL is not a URL."); +}; + +let panelSupported = true; + +try { + panels = require("panel"); + Panel = panels.Panel; +} +catch(ex if ex.message == [ + "The panel module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps ", + "for more information." + ].join("")) { + panelSupported = false; +} + +if (panelSupported) { + for (let test in tests) + exports[test] = tests[test]; +} +else { + exports.testPanelNotSupported = function(test) { + test.pass("The panel module is not supported on this app."); + } +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-passwords.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-passwords.js new file mode 100644 index 0000000..e799b48 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-passwords.js @@ -0,0 +1,277 @@ +"use strict"; + +const { store, search, remove } = require("passwords"); + +exports["test store requires `password` field"] = function(assert, done) { + store({ + username: "foo", + realm: "bar", + onComplete: function onComplete() { + assert.fail("onComplete should not be called"); + }, + onError: function onError() { + assert.pass("'`password` is required"); + done(); + } + }); +}; + +exports["test store requires `username` field"] = function(assert, done) { + store({ + password: "foo", + realm: "bar", + onComplete: function onComplete() { + assert.fail("onComplete should not be called"); + }, + onError: function onError() { + assert.pass("'`username` is required"); + done(); + } + }); +}; + +exports["test onComplete is optional"] = function(assert, done) { + store({ + realm: "bla", + username: "bla", + password: "bla", + onError: function onError() { + assert.fail("onError was called"); + } + }); + assert.pass("exception is not thrown if `onComplete is missing") + done(); +}; + +exports["test exceptions in onComplete are reported"] = function(assert, done) { + store({ + realm: "throws", + username: "error", + password: "boom!", + onComplete: function onComplete(error) { + throw new Error("Boom!") + }, + onError: function onError(error) { + assert.equal(error.message, "Boom!", "Error thrown is reported"); + done(); + } + }); +}; + +exports["test store requires `realm` field"] = function(assert, done) { + store({ + username: "foo", + password: "bar", + onComplete: function onComplete() { + assert.fail("onComplete should not be called"); + }, + onError: function onError() { + assert.pass("'`realm` is required"); + done(); + } + }); +}; + +exports["test can't store same login twice"] = function(assert, done) { + store({ + username: "user", + password: "pass", + realm: "realm", + onComplete: function onComplete() { + assert.pass("credential saved"); + + store({ + username: "user", + password: "pass", + realm: "realm", + onComplete: function onComplete() { + assert.fail("onComplete should not be called"); + }, + onError: function onError() { + assert.pass("re-saving credential failed"); + + remove({ + username: "user", + password: "pass", + realm: "realm", + onComplete: function onComplete() { + assert.pass("credential was removed"); + done(); + }, + onError: function onError() { + assert.fail("remove should not fail"); + } + }); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); +}; + +exports["test remove fails if no login found"] = function(assert, done) { + remove({ + username: "foo", + password: "bar", + realm: "baz", + onComplete: function onComplete() { + assert.fail("should not be able to remove unstored credentials"); + }, + onError: function onError() { + assert.pass("can't remove unstored credentials"); + done(); + } + }); +}; + +exports["test addon associated credentials"] = function(assert, done) { + store({ + username: "foo", + password: "bar", + realm: "baz", + onComplete: function onComplete() { + search({ + username: "foo", + password: "bar", + realm: "baz", + onComplete: function onComplete([credential]) { + assert.equal(credential.url.indexOf("addon:"), 0, + "`addon:` uri is used for add-on credentials"); + assert.equal(credential.username, "foo", + "username matches"); + assert.equal(credential.password, "bar", + "password matches"); + assert.equal(credential.realm, "baz", "realm matches"); + assert.equal(credential.formSubmitURL, null, + "`formSubmitURL` is `null` for add-on credentials"); + assert.equal(credential.usernameField, "", "usernameField is empty"); + assert.equal(credential.passwordField, "", "passwordField is empty"); + + remove({ + username: credential.username, + password: credential.password, + realm: credential.realm, + onComplete: function onComplete() { + assert.pass("credential is removed"); + done(); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); +}; + +exports["test web page associated credentials"] = function(assert, done) { + store({ + url: "http://bar.foo.com/authentication/?login", + formSubmitURL: "http://login.foo.com/authenticate.cgi", + username: "user", + password: "pass", + usernameField: "user-f", + passwordField: "pass-f", + onComplete: function onComplete() { + search({ + username: "user", + password: "pass", + url: "http://bar.foo.com", + formSubmitURL: "http://login.foo.com", + onComplete: function onComplete([credential]) { + assert.equal(credential.url, "http://bar.foo.com", "url matches"); + assert.equal(credential.username, "user", "username matches"); + assert.equal(credential.password, "pass", "password matches"); + assert.equal(credential.realm, null, "realm is null"); + assert.equal(credential.formSubmitURL, "http://login.foo.com", + "formSubmitURL matches"); + assert.equal(credential.usernameField, "user-f", + "usernameField is matches"); + assert.equal(credential.passwordField, "pass-f", + "passwordField matches"); + + remove({ + url: credential.url, + formSubmitURL: credential.formSubmitURL, + username: credential.username, + password: credential.password, + usernameField: credential.usernameField, + passwordField: credential.passwordField, + + onComplete: function onComplete() { + assert.pass("credential is removed"); + done(); + }, + onError: function onError(e) { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); +}; + +exports["test site authentication credentials"] = function(assert, done) { + store({ + url: "http://authentication.com", + username: "U", + password: "P", + realm: "R", + onComplete: function onComplete() { + search({ + url: "http://authentication.com", + username: "U", + password: "P", + realm: "R", + onComplete: function onComplete([credential]) { + assert.equal(credential.url,"http://authentication.com", + "url matches"); + assert.equal(credential.username, "U", "username matches"); + assert.equal(credential.password, "P", "password matches"); + assert.equal(credential.realm, "R", "realm matches"); + assert.equal(credential.formSubmitURL, null, "formSubmitURL is null"); + assert.equal(credential.usernameField, "", "usernameField is empty"); + assert.equal(credential.passwordField, "", "passwordField is empty"); + + remove({ + url: credential.url, + username: credential.username, + password: credential.password, + realm: credential.realm, + onComplete: function onComplete() { + assert.pass("credential is removed"); + done(); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-private-browsing.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-private-browsing.js new file mode 100644 index 0000000..a3c1519 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-private-browsing.js @@ -0,0 +1,238 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Paul O’Shannessy <paul@oshannessy.com> + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +let pb = require("private-browsing"); +let {Cc,Ci} = require("chrome"); +const { Loader } = require('./helpers'); + +let pbService; +// Currently, only Firefox implements the private browsing service. +if (require("xul-app").is("Firefox")) { + pbService = Cc["@mozilla.org/privatebrowsing;1"]. + getService(Ci.nsIPrivateBrowsingService); +} + +if (pbService) { + + // tests that isActive has the same value as the private browsing service + // expects + exports.testGetIsActive = function (test) { + test.assertEqual(pb.isActive, false, + "private-browsing.isActive is correct without modifying PB service"); + + pbService.privateBrowsingEnabled = true; + test.assert(pb.isActive, + "private-browsing.isActive is correct after modifying PB service"); + + // Switch back to normal mode. + pbService.privateBrowsingEnabled = false; + }; + + // tests that activating does put the browser into private browsing mode + exports.testActivateDeactivate = function (test) { + test.waitUntilDone(); + pb.once("start", function onStart() { + test.assertEqual(pbService.privateBrowsingEnabled, true, + "private browsing mode was activated"); + pb.deactivate(); + }); + pb.once("stop", function onStop() { + test.assertEqual(pbService.privateBrowsingEnabled, false, + "private browsing mode was deactivate"); + test.done(); + }); + pb.activate(); + }; + + exports.testStart = function(test) { + test.waitUntilDone(); + pb.on("start", function onStart() { + test.assertEqual(this, pb, "`this` should be private-browsing module"); + test.assert(pbService.privateBrowsingEnabled, + 'private mode is active when "start" event is emitted'); + test.assert(pb.isActive, + '`isActive` is `true` when "start" event is emitted'); + pb.removeListener("start", onStart); + test.done(); + }); + pb.activate(); + }; + + exports.testStop = function(test) { + test.waitUntilDone(); + pb.on("stop", function onStop() { + test.assertEqual(this, pb, "`this` should be private-browsing module"); + test.assertEqual(pbService.privateBrowsingEnabled, false, + "private mode is disabled when stop event is emitted"); + test.assertEqual(pb.isActive, false, + "`isActive` is `false` when stop event is emitted"); + pb.removeListener("stop", onStop); + test.done(); + }); + pb.activate(); + pb.deactivate(); + }; + + exports.testAutomaticUnload = function(test) { + test.waitUntilDone(); + // Create another private browsing instance and unload it + let loader = Loader(module); + let pb2 = loader.require("private-browsing"); + let called = false; + pb2.on("start", function onStart() { + called = true; + test.fail("should not be called:x"); + }); + loader.unload(); + + // Then switch to private mode in order to check that the previous instance + // is correctly destroyed + pb.activate(); + pb.once("start", function onStart() { + require("timer").setTimeout(function () { + test.assert(!called, + "First private browsing instance is destroyed and inactive"); + + // Must reset to normal mode, so that next test starts with it. + pb.deactivate(); + test.done(); + }, 0); + }); + }; + + exports.testBothListeners = function(test) { + test.waitUntilDone(); + let stop = false; + let start = false; + + function onStop() { + test.assertEqual(stop, false, + "stop callback must be called only once"); + test.assertEqual(pbService.privateBrowsingEnabled, false, + "private mode is disabled when stop event is emitted"); + test.assertEqual(pb.isActive, false, + "`isActive` is `false` when stop event is emitted"); + + pb.on("start", finish); + pb.removeListener("start", onStart); + pb.removeListener("start", onStart2); + pb.activate(); + stop = true; + } + + function onStart() { + test.assertEqual(false, start, + "stop callback must be called only once"); + test.assert(pbService.privateBrowsingEnabled, + "private mode is active when start event is emitted"); + test.assert(pb.isActive, + "`isActive` is `true` when start event is emitted"); + + pb.on("stop", onStop); + pb.deactivate(); + start = true; + } + + function onStart2() { + test.assert(start, "start listener must be called already"); + test.assertEqual(false, stop, "stop callback must not be called yet"); + } + + function finish() { + test.assert(pbService.privateBrowsingEnabled, true, + "private mode is active when start event is emitted"); + test.assert(pb.isActive, + "`isActive` is `true` when start event is emitted"); + + pb.removeListener("start", finish); + pb.removeListener("stop", onStop); + + pb.deactivate(); + pb.once("stop", function () { + test.assertEqual(pbService.privateBrowsingEnabled, false); + test.assertEqual(pb.isActive, false); + + test.done(); + }); + } + + pb.on("start", onStart); + pb.on("start", onStart2); + pbService.privateBrowsingEnabled = true; + }; + + exports["test activate private mode via handler"] = function(test) { + const tabs = require("tabs"); + + test.waitUntilDone(); + function onReady(tab) { + if (tab.url == "about:robots") + tab.close(function() pb.activate()); + } + function cleanup(tab) { + if (tab.url == "about:") { + tabs.removeListener("ready", cleanup); + tab.close(function onClose() { + test.done(); + }); + } + } + + tabs.on("ready", onReady); + pb.once("start", function onStart() { + test.pass("private mode was activated"); + pb.deactivate(); + }); + pb.once("stop", function onStop() { + test.pass("private mode was deactivated"); + tabs.removeListener("ready", onReady); + tabs.on("ready", cleanup); + }); + tabs.once("open", function onOpen() { + tabs.open("about:robots"); + }); + tabs.open("about:"); + }; +} +else { + // tests for the case where private browsing doesn't exist + exports.testNoImpl = function (test) { + test.assertEqual(pb.isActive, false, + "pb.isActive returns false when private browsing isn't supported"); + }; +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-request.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-request.js new file mode 100644 index 0000000..8b8300e --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-request.js @@ -0,0 +1,358 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Paul O’Shannessy <paul@oshannessy.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const Request = require("request").Request; + +var port = 8080; +var data = require("self").data; +var testFilePath = require("url").toFilename(data.url("test-request.txt")); +var basePath = require("file").dirname(testFilePath); + +var {startServerAsync} = require("httpd"); + +exports.testOptionsValidator = function(test) { + // First, a simple test to make sure we didn't break normal functionality. + test.assertRaises(function () { + Request({ + url: null + }); + }, 'The option "url" must be one of the following types: string'); + + // Next we'll have a Request that doesn't throw from c'tor, but from a setter. + let req = Request({ + url: "http://playground.zpao.com/jetpack/request/text.php", + onComplete: function () {} + }); + test.assertRaises(function () { + req.url = null; + }, 'The option "url" must be one of the following types: string'); + // The url shouldn't have changed, so check that + test.assertEqual(req.url, "http://playground.zpao.com/jetpack/request/text.php"); +} + +exports.testContentValidator = function(test) { + test.waitUntilDone(); + Request({ + url: "data:text/html,response", + content: { 'key1' : null, 'key2' : 'some value' }, + onComplete: function(response) { + test.assertEqual(response.text, "response?key1=null&key2=some+value"); + test.done(); + } + }).get(); +}; + +// All tests below here require a network connection. They will be commented out +// when checked in. If you'd like to run them, simply uncomment them. +// +// When we have the means, these tests will be converted so that they don't +// require an external server nor a network connection. + +// This is a request to a file that exists. +exports.testStatus200 = function (test) { + var srv = startServerAsync(port, basePath); + + test.waitUntilDone(); + var req = Request({ + url: "http://localhost:" + port + "/test-request.txt", + onComplete: function (response) { + test.assertEqual(this, req, "`this` should be request"); + test.assertEqual(response.status, 200); + test.assertEqual(response.statusText, "OK"); + test.assertEqual(response.headers["Content-Type"], "text/plain"); + test.assertEqual(response.text, "Look ma, no hands!\n"); + srv.stop(function() test.done()); + } + }).get(); +} + +// This tries to get a file that doesn't exist +exports.testStatus404 = function (test) { + var srv = startServerAsync(port, basePath); + + test.waitUntilDone(); + Request({ + // 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 +exports.testKnownHeader = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/headers.php", + onComplete: function (response) { + test.assertEqual(response.headers["x-zpao-header"], "Jamba Juice"); + test.done(); + } + }).get(); +} + +// complex headers +exports.testKnownHeader = function (test) { + let headers = { + "x-zpao-header": "Jamba Juice is: delicious", + "x-zpao-header-2": "foo, bar", + "Set-Cookie": "foo=bar\nbaz=foo" + } + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/complex_headers.php", + onComplete: function (response) { + for (k in headers) { + test.assertEqual(response.headers[k], headers[k]); + } + test.done(); + } + }).get(); +} +*/ + +exports.testSimpleJSON = function (test) { + var srv = startServerAsync(port, basePath); + + test.waitUntilDone(); + Request({ + url: "http://localhost:" + port + "/test-request.json", + onComplete: function (response) { + assertDeepEqual(test, response.json, { foo: "bar" }); + srv.stop(function() test.done()); + } + }).get(); +} + +exports.testInvalidJSON = function (test) { + var srv = startServerAsync(port, basePath); + + test.waitUntilDone(); + Request({ + url: "http://localhost:" + port + "/test-request-invalid.json", + onComplete: function (response) { + test.assertEqual(response.json, null); + srv.stop(function() test.done()); + } + }).get(); +} + +/* +exports.testGetWithParamsNotContent = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php?foo=bar", + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: "bar" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testGetWithContent = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: { foo: "bar" }, + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: "bar" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testGetWithParamsAndContent = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php?foo=bar", + content: { baz: "foo" }, + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: "bar", baz: "foo" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testSimplePost = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: { foo: "bar" }, + onComplete: function (response) { + let expected = { + "POST": { foo: "bar" }, + "GET" : [] + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).post(); +} + +exports.testEncodedContent = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: "foo=bar&baz=foo", + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: "bar", baz: "foo" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testEncodedContentWithSpaces = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: "foo=bar+hop!&baz=foo", + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: "bar hop!", baz: "foo" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testGetWithArray = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: { foo: [1, 2], baz: "foo" }, + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: [1, 2], baz: "foo" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testGetWithNestedArray = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: { foo: [1, 2, [3, 4]], bar: "baz" }, + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : this.content + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testGetWithNestedArray = function (test) { + test.waitUntilDone(); + let request = Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: { + foo: [1, 2, { + omg: "bbq", + "all your base!": "are belong to us" + }], + bar: "baz" + }, + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : request.content + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} +*/ + +// This is not a proper testing for deep equal, but it's good enough for my uses +// here. It will do type coercion to check equality, but that's good here. Data +// coming from the server will be stringified and so "0" should be equal to 0. +function assertDeepEqual(test, obj1, obj2, msg) { + function equal(o1, o2) { + // cover our non-object cases well enough + if (o1 == o2) + return true; + if (typeof(o1) != typeof(o2)) + return false; + if (typeof(o1) != "object") + return o1 == o2; + + let e = true; + for (let [key, val] in Iterator(o1)) { + e = e && key in o2 && equal(o2[key], val); + if (!e) + break; + } + for (let [key, val] in Iterator(o2)) { + e = e && key in o1 && equal(o1[key], val); + if (!e) + break; + } + return e; + } + msg = msg || "objects not equal - " + JSON.stringify(obj1) + " != " + + JSON.stringify(obj2); + test.assert(equal(obj1, obj2), msg); +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-selection.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-selection.js new file mode 100644 index 0000000..cd96072 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-selection.js @@ -0,0 +1,490 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Eric H. Jung <eric.jung@yahoo.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +let timer = require("timer"); +let {Cc,Ci} = require("chrome"); + +// Arbitrary delay needed to avoid weird behavior. +// TODO: We need to find all uses of this and replace them +// with more deterministic solutions. +const ARB_DELAY = 100; + +// Select all divs elements in an HTML document +function selectAllDivs(window) { + let divs = window.document.getElementsByTagName("div"); + let s = window.getSelection(); + if (s.rangeCount > 0) + s.removeAllRanges(); + for (let i = 0; i < divs.length; i++) { + let range = window.document.createRange(); + range.selectNode(divs[i]); + s.addRange(range); + } +} + +function selectTextarea(window, from, to) { + let textarea = window.document.getElementsByTagName("textarea")[0]; + + from = from || 0; + to = to || textarea.value.length; + + textarea.setSelectionRange(from, to); + textarea.focus(); +} + +function primeTestCase(html, test, callback) { + let tabBrowser = require("tab-browser"); + let dataURL = "data:text/html," + encodeURI(html); + let tracker = tabBrowser.whenContentLoaded( + function(window) { + if (window.document.location.href != dataURL) + return; + callback(window, test); + timer.setTimeout(function() { + tracker.unload(); + test.done(); + window.close(); + }, + ARB_DELAY); + } + ); + tabBrowser.addTab(dataURL); +} + +const DIV1 = '<div id="foo">bar</div>'; +const DIV2 = '<div>noodles</div>'; +const HTML_MULTIPLE = '<html><body>' + DIV1 + DIV2 + '</body></html>'; +const HTML_SINGLE = '<html><body>' + DIV1 + '</body></html>'; + +// Tests of contiguous + +exports.testContiguousMultiple = function testContiguousMultiple(test) { + let selection = require("selection"); + primeTestCase(HTML_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.isContiguous, false, + "selection.isContiguous multiple works."); + }); + + test.waitUntilDone(5000); +}; + +exports.testContiguousSingle = function testContiguousSingle(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.isContiguous, true, + "selection.isContiguous single works."); + }); + + test.waitUntilDone(5000); +}; + +exports.testContiguousWithoutSelection = + function testContiguousWithoutSelection(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + test.assertEqual(selection.isContiguous, false, + "selection.isContiguous without selection works."); + }); + + test.waitUntilDone(5000); +}; + +/** + * Test that setting the contiguous property has no effect. + */ +/*exports.testSetContiguous = function testSetContiguous(test) { + let selection = require("selection"); + primeTestCase(HTML_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + try { + selection.isContiguous = true; + test.assertEqual(selection.isContiguous, false, + "setting selection.isContiguous doesn't work (as expected)."); + } + catch (e) { + test.pass("setting selection.isContiguous doesn't work (as expected)."); + } + }); + + test.waitUntilDone(5000); +};*/ + + +// HTML tests + +exports.testGetHTMLSingleSelection = function testGetHTMLSingleSelection(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.html, DIV1, "get html selection works"); + }); + + test.waitUntilDone(5000); +}; + +/* Myk's comments: This is fine. However, it reminds me to figure out and + specify whether iteration is ordered. If so, we'll want to change this + test in the future to test that the discontiguous selections are returned in + the appropriate order. In the meantime, add a comment to that effect here */ +exports.testGetHTMLMultipleSelection = + function testGetHTMLMultipleSelection(test) { + let selection = require("selection"); + primeTestCase(HTML_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + let assertions = false; + for each (let i in selection) { + test.assertEqual(true, [DIV1, DIV2].some(function(t) t == i.html), + "get multiple selection html works"); + assertions = true; + } + // Ensure we ran at least one assertEqual() + test.assert(assertions, "No assertions were called"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetHTMLNull = function testGetHTMLNull(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + test.assertEqual(selection.html, null, "get html null works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetHTMLWeird = function testGetHTMLWeird(test) { + let selection = require("selection"); + // If the getter is used when there are contiguous selections, the first + // selection should be returned + primeTestCase(HTML_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.html, DIV1, "get html weird works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetHTMLNullInTextareaSelection = + function testGetHTMLNullInTextareaSelection(test) { + let selection = require("selection"); + + primeTestCase(TEXT_FIELD, test, function(window, test) { + selectTextarea(window); + + test.assertEqual(selection.html, null, "get html null in textarea works") + }); + + test.waitUntilDone(5000); +}; + +const REPLACEMENT_HTML = "<b>Lorem ipsum dolor sit amet</b>"; + +exports.testSetHTMLSelection = function testSetHTMLSelection(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + selectAllDivs(window); + selection.html = REPLACEMENT_HTML; + test.assertEqual(selection.html, "<span>" + REPLACEMENT_HTML + + "</span>", "selection html works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testSetHTMLException = function testSetHTMLException(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + try { + selection.html = REPLACEMENT_HTML; + test.fail("set HTML throws when there's no selection."); + } + catch (e) { + test.pass("set HTML throws when there's no selection."); + } + }); + + test.waitUntilDone(5000); +}; + +const TEXT1 = "foo"; +const TEXT2 = "noodles"; +const TEXT_MULTIPLE = "<html><body><div>" + TEXT1 + "</div><div>" + TEXT2 + + "</div></body></html>"; +const TEXT_SINGLE = "<html><body><div>" + TEXT1 + "</div></body></html>"; +const TEXT_FIELD = "<html><body><textarea>" + TEXT1 + "</textarea></body></html>"; + +// Text tests + +exports.testSetHTMLinTextareaSelection = + function testSetHTMLinTextareaSelection(test) { + let selection = require("selection"); + + primeTestCase(TEXT_FIELD, test, function(window, test) { + selectTextarea(window); + + // HTML string is set as plain text in textareas, that's because + // `selection.html` and `selection.text` are basically aliases when a + // value is set. See bug 677269 + selection.html = REPLACEMENT_HTML; + + test.assertEqual(selection.text, REPLACEMENT_HTML, + "set selection html in textarea works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextSingleSelection = + function testGetTextSingleSelection(test) { + let selection = require("selection"); + primeTestCase(TEXT_SINGLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.text, TEXT1, "get text selection works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextMultipleSelection = + function testGetTextMultipleSelection(test) { + let selection = require("selection"); + primeTestCase(TEXT_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + let assertions = false; + for each (let i in selection) { + test.assertEqual(true, [TEXT1, TEXT2].some(function(t) t == i.text), + "get multiple selection text works"); + assertions = true; + } + // Ensure we ran at least one assertEqual() + test.assert(assertions, "No assertions were called"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextNull = function testGetTextNull(test) { + let selection = require("selection"); + primeTestCase(TEXT_SINGLE, test, function(window, test) { + test.assertEqual(selection.text, null, "get text null works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextWeird = function testGetTextWeird(test) { + let selection = require("selection"); + // If the getter is used when there are contiguous selections, the first + // selection should be returned + primeTestCase(TEXT_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.text, TEXT1, "get text weird works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextNullInTextareaSelection = + function testGetTextInTextareaSelection(test) { + let selection = require("selection"); + + primeTestCase(TEXT_FIELD, test, function(window, test) { + test.assertEqual(selection.text, null, "get text null in textarea works") + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextInTextareaSelection = + function testGetTextInTextareaSelection(test) { + let selection = require("selection"); + + primeTestCase(TEXT_FIELD, test, function(window, test) { + selectTextarea(window); + + test.assertEqual(selection.text, TEXT1, "get text null in textarea works") + }); + + test.waitUntilDone(5000); +}; + +const REPLACEMENT_TEXT = "Lorem ipsum dolor sit amet"; + +exports.testSetTextSelection = function testSetTextSelection(test) { + let selection = require("selection"); + primeTestCase(TEXT_SINGLE, test, function(window, test) { + selectAllDivs(window); + selection.text = REPLACEMENT_TEXT; + test.assertEqual(selection.text, REPLACEMENT_TEXT, "selection text works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testSetHTMLException = function testSetHTMLException(test) { + let selection = require("selection"); + primeTestCase(TEXT_SINGLE, test, function(window, test) { + try { + selection.text = REPLACEMENT_TEXT; + test.fail("set HTML throws when there's no selection."); + } + catch (e) { + test.pass("set HTML throws when there's no selection."); + } + }); + + test.waitUntilDone(5000); +}; + +exports.testSetTextInTextareaSelection = + function testSetTextInTextareaSelection(test) { + let selection = require("selection"); + + primeTestCase(TEXT_FIELD, test, function(window, test) { + selectTextarea(window); + + selection.text = REPLACEMENT_TEXT; + + test.assertEqual(selection.text, REPLACEMENT_TEXT, + "set selection text in textarea works"); + }); + + test.waitUntilDone(5000); +}; + +// Iterator tests + +exports.testIterator = function testIterator(test) { + let selection = require("selection"); + let selectionCount = 0; + primeTestCase(TEXT_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + for each (let i in selection) + selectionCount++; + test.assertEqual(2, selectionCount, "iterator works."); + }); + + test.waitUntilDone(5000); +}; + +exports.testIteratorWithTextareaSelection = + function testIteratorWithTextareaSelection(test) { + let selection = require("selection"); + let selectionCount = 0; + + primeTestCase(TEXT_FIELD, test, function(window, test) { + selectTextarea(window); + + for each (let i in selection) + selectionCount++; + + test.assertEqual(1, selectionCount, "iterator works in textarea."); + }); + + test.waitUntilDone(5000); +}; + +/* onSelect tests */ + +/* +function sendSelectionSetEvent(window) { + const Ci = Components.interfaces; + let utils = window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); + if (!utils.sendSelectionSetEvent(0, 1, false)) + dump("**** sendSelectionSetEvent did not select anything\n"); + else + dump("**** sendSelectionSetEvent succeeded\n"); +} + +// testOnSelect() requires nsIDOMWindowUtils, which is only available in +// Firefox 3.7+. +exports.testOnSelect = function testOnSelect(test) { + let selection = require("selection"); + let callbackCount = 0; + primeTestCase(TEXT_SINGLE, test, function(window, test) { + selection.onSelect = function() {callbackCount++}; + // Now simulate the user selecting stuff + sendSelectionSetEvent(window); + selection.text = REPLACEMENT_TEXT; + test.assertEqual(1, callbackCount, "onSelect text listener works."); + //test.pass(); + //test.done(); + }); + + test.waitUntilDone(5000); +}; + +// testOnSelectExceptionNoBubble() requires nsIDOMWindowUtils, which is only +// available in Firefox 3.7+. +exports.testOnSelectExceptionNoBubble = + function testOnSelectTextSelection(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + selection.onSelect = function() { + throw new Error("Exception thrown in testOnSelectExceptionNoBubble"); + }; + // Now simulate the user selecting stuff + sendSelectionSetEvent(window); + test.pass("onSelect catches exceptions."); + }); + + test.waitUntilDone(5000); +}; +*/ + +// If the module doesn't support the app we're being run in, require() will +// throw. In that case, remove all tests above from exports, and add one dummy +// test that passes. +try { + require("selection"); +} +catch (err) { + // This bug should be mentioned in the error message. + let bug = "https://bugzilla.mozilla.org/show_bug.cgi?id=560716"; + if (err.message.indexOf(bug) < 0) + throw err; + for (let [prop, val] in Iterator(exports)) { + if (/^test/.test(prop) && typeof(val) === "function") + delete exports[prop]; + } + exports.testAppNotSupported = function (test) { + test.pass("The selection module does not support this application."); + }; +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-simple-prefs.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-simple-prefs.js new file mode 100644 index 0000000..581eca1 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-simple-prefs.js @@ -0,0 +1,180 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Erik Vold <erikvvold@gmail.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +const { Loader } = require("./helpers"); +const setTimeout = require("timers").setTimeout; +const notify = require("observer-service").notify; +const { jetpackID } = require("@packaging"); + +exports.testSetGetBool = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs").prefs; + + test.assertEqual(sp.test, undefined, "Value should not exist"); + sp.test = true; + test.assert(sp.test, "Value read should be the value previously set"); + + loader.unload(); + test.done(); +}; + +exports.testSetGetInt = function(test) { + test.waitUntilDone(); + + // Load the module once, set a value. + let loader = Loader(module); + let sp = loader.require("simple-prefs").prefs; + + test.assertEqual(sp["test-int"], undefined, "Value should not exist"); + sp["test-int"] = 1; + test.assertEqual(sp["test-int"], 1, "Value read should be the value previously set"); + + loader.unload(); + test.done(); +}; + +exports.testSetComplex = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs").prefs; + + try { + sp["test-complex"] = {test: true}; + test.fail("Complex values are not allowed"); + } + catch (e) { + test.pass("Complex values are not allowed"); + } + + loader.unload(); + test.done(); +}; + +exports.testSetGetString = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs").prefs; + + test.assertEqual(sp["test-string"], undefined, "Value should not exist"); + sp["test-string"] = "test"; + test.assertEqual(sp["test-string"], "test", "Value read should be the value previously set"); + + loader.unload(); + test.done(); +}; + +exports.testHasAndRemove = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs").prefs; + + sp.test = true; + test.assert(("test" in sp), "Value exists"); + delete sp.test; + test.assertEqual(sp.test, undefined, "Value should be undefined"); + + loader.unload(); + test.done(); + +}; + +exports.testPrefListener = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs"); + + let listener = function(prefName) { + test.assertEqual(prefName, "test-listen", "The prefs listener heard the right event"); + test.done(); + }; + + sp.on("test-listen", listener); + + sp.prefs["test-listen"] = true; + loader.unload(); +}; + +exports.testBtnListener = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs"); + + sp.on("test-btn-listen", function() { + test.pass("Button press event was heard"); + test.done(); + }); + notify((jetpackID + "-cmdPressed"), "", "test-btn-listen"); + + loader.unload(); +}; + +exports.testPrefRemoveListener = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs"); + let counter = 0; + + let listener = function() { + test.pass("The prefs listener was not removed yet"); + + if (++counter > 1) + test.fail("The prefs listener was not removed"); + + sp.removeListener("test-listen2", listener); + + sp.prefs["test-listen2"] = false; + + setTimeout(function() { + test.pass("The prefs listener was removed"); + loader.unload(); + test.done(); + }, 250); + }; + + sp.on("test-listen2", listener); + + sp.prefs["test-listen2"] = true; +}; diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-simple-storage.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-simple-storage.js new file mode 100644 index 0000000..95e4689 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-simple-storage.js @@ -0,0 +1,346 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const file = require("file"); +const prefs = require("preferences-service"); + +const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota"; + +let {Cc,Ci} = require("chrome"); + +const { Loader } = require("./helpers"); +const options = require("@packaging"); + +let storeFile = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); +storeFile.append("jetpack"); +storeFile.append(options.jetpackID); +storeFile.append("simple-storage"); +storeFile.append("store.json"); +let storeFilename = storeFile.path; + +exports.testSetGet = function (test) { + test.waitUntilDone(); + + // Load the module once, set a value. + let loader = newLoader(test); + let ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + test.assert(file.exists(storeFilename), "Store file should exist"); + + // Load the module again and make sure the value stuck. + loader = newLoader(test); + ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + file.remove(storeFilename); + test.done(); + }; + test.assertEqual(ss.storage.foo, val, "Value should persist"); + loader.unload(); + }; + let val = "foo"; + ss.storage.foo = val; + test.assertEqual(ss.storage.foo, val, "Value read should be value set"); + loader.unload(); +}; + +exports.testSetGetRootArray = function (test) { + setGetRoot(test, [1, 2, 3], function (arr1, arr2) { + if (arr1.length !== arr2.length) + return false; + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) + return false; + } + return true; + }); +}; + +exports.testSetGetRootBool = function (test) { + setGetRoot(test, true); +}; + +exports.testSetGetRootFunction = function (test) { + setGetRootError(test, function () {}, + "Setting storage to a function should fail"); +}; + +exports.testSetGetRootNull = function (test) { + setGetRoot(test, null); +}; + +exports.testSetGetRootNumber = function (test) { + setGetRoot(test, 3.14); +}; + +exports.testSetGetRootObject = function (test) { + setGetRoot(test, { foo: 1, bar: 2 }, function (obj1, obj2) { + for (let [prop, val] in Iterator(obj1)) { + if (!(prop in obj2) || obj2[prop] !== val) + return false; + } + for (let [prop, val] in Iterator(obj2)) { + if (!(prop in obj1) || obj1[prop] !== val) + return false; + } + return true; + }); +}; + +exports.testSetGetRootString = function (test) { + setGetRoot(test, "sho' 'nuff"); +}; + +exports.testSetGetRootUndefined = function (test) { + setGetRootError(test, undefined, "Setting storage to undefined should fail"); +}; + +exports.testEmpty = function (test) { + let loader = newLoader(test); + let ss = loader.require("simple-storage"); + loader.unload(); + test.assert(!file.exists(storeFilename), "Store file should not exist"); +}; + +exports.testMalformed = function (test) { + let stream = file.open(storeFilename, "w"); + stream.write("i'm not json"); + stream.close(); + let loader = newLoader(test); + let ss = loader.require("simple-storage"); + let empty = true; + for (let key in ss.storage) { + empty = false; + break; + } + test.assert(empty, "Malformed storage should cause root to be empty"); + loader.unload(); +}; + +// Go over quota and handle it by listener. +exports.testQuotaExceededHandle = function (test) { + test.waitUntilDone(); + prefs.set(QUOTA_PREF, 18); + + let loader = newLoader(test); + let ss = loader.require("simple-storage"); + ss.on("OverQuota", function () { + test.pass("OverQuota was emitted as expected"); + test.assertEqual(this, ss, "`this` should be simple storage"); + ss.storage = { x: 4, y: 5 }; + + manager(loader).jsonStore.onWrite = function () { + loader = newLoader(test); + ss = loader.require("simple-storage"); + let numProps = 0; + for (let prop in ss.storage) + numProps++; + test.assert(numProps, 2, + "Store should contain 2 values: " + ss.storage.toSource()); + test.assertEqual(ss.storage.x, 4, "x value should be correct"); + test.assertEqual(ss.storage.y, 5, "y value should be correct"); + manager(loader).jsonStore.onWrite = function (storage) { + prefs.reset(QUOTA_PREF); + test.done(); + }; + loader.unload(); + }; + loader.unload(); + }); + // This will be JSON.stringify()ed to: {"a":1,"b":2,"c":3} (19 bytes) + ss.storage = { a: 1, b: 2, c: 3 }; + manager(loader).jsonStore.write(); +}; + +// Go over quota but don't handle it. The last good state should still persist. +exports.testQuotaExceededNoHandle = function (test) { + test.waitUntilDone(); + prefs.set(QUOTA_PREF, 5); + + let loader = newLoader(test); + let ss = loader.require("simple-storage"); + + manager(loader).jsonStore.onWrite = function (storage) { + loader = newLoader(test); + ss = loader.require("simple-storage"); + test.assertEqual(ss.storage, val, + "Value should have persisted: " + ss.storage); + ss.storage = "some very long string that is very long"; + ss.on("OverQuota", function () { + test.pass("OverQuota emitted as expected"); + manager(loader).jsonStore.onWrite = function () { + test.fail("Over-quota value should not have been written"); + }; + loader.unload(); + + loader = newLoader(test); + ss = loader.require("simple-storage"); + test.assertEqual(ss.storage, val, + "Over-quota value should not have been written, " + + "old value should have persisted: " + ss.storage); + loader.unload(); + prefs.reset(QUOTA_PREF); + test.done(); + }); + manager(loader).jsonStore.write(); + }; + + let val = "foo"; + ss.storage = val; + loader.unload(); +}; + +exports.testQuotaUsage = function (test) { + test.waitUntilDone(); + + let quota = 21; + prefs.set(QUOTA_PREF, quota); + + let loader = newLoader(test); + let ss = loader.require("simple-storage"); + + // {"a":1} (7 bytes) + ss.storage = { a: 1 }; + test.assertEqual(ss.quotaUsage, 7 / quota, "quotaUsage should be correct"); + + // {"a":1,"bb":2} (14 bytes) + ss.storage = { a: 1, bb: 2 }; + test.assertEqual(ss.quotaUsage, 14 / quota, "quotaUsage should be correct"); + + // {"a":1,"bb":2,"cc":3} (21 bytes) + ss.storage = { a: 1, bb: 2, cc: 3 }; + test.assertEqual(ss.quotaUsage, 21 / quota, "quotaUsage should be correct"); + + manager(loader).jsonStore.onWrite = function () { + prefs.reset(QUOTA_PREF); + test.done(); + }; + loader.unload(); +}; + +exports.testUninstall = function (test) { + test.waitUntilDone(); + let loader = newLoader(test); + let ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function () { + test.assert(file.exists(storeFilename), "Store file should exist"); + + loader = newLoader(test); + ss = loader.require("simple-storage"); + loader.unload("uninstall"); + test.assert(!file.exists(storeFilename), "Store file should be removed"); + test.done(); + }; + ss.storage.foo = "foo"; + loader.unload(); +}; + +exports.testSetNoSetRead = function (test) { + test.waitUntilDone(); + + // Load the module, set a value. + let loader = newLoader(test); + let ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + test.assert(file.exists(storeFilename), "Store file should exist"); + + // Load the module again but don't access ss.storage. + loader = newLoader(test); + ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + test.fail("Nothing should be written since `storage` was not accessed."); + }; + loader.unload(); + + // Load the module a third time and make sure the value stuck. + loader = newLoader(test); + ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + file.remove(storeFilename); + test.done(); + }; + test.assertEqual(ss.storage.foo, val, "Value should persist"); + loader.unload(); + }; + let val = "foo"; + ss.storage.foo = val; + test.assertEqual(ss.storage.foo, val, "Value read should be value set"); + loader.unload(); +}; + +function manager(loader) loader.sandbox("simple-storage").manager; + +function newLoader() Loader(module); + +function setGetRoot(test, val, compare) { + test.waitUntilDone(); + + compare = compare || function (a, b) a === b; + + // Load the module once, set a value. + let loader = newLoader(test); + let ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function () { + test.assert(file.exists(storeFilename), "Store file should exist"); + + // Load the module again and make sure the value stuck. + loader = newLoader(test); + ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function () { + file.remove(storeFilename); + test.done(); + }; + test.assert(compare(ss.storage, val), "Value should persist"); + loader.unload(); + }; + ss.storage = val; + test.assert(compare(ss.storage, val), "Value read should be value set"); + loader.unload(); +} + +function setGetRootError(test, val, msg) { + let pred = "storage must be one of the following types: " + + "array, boolean, null, number, object, string"; + let loader = newLoader(test); + let ss = loader.require("simple-storage"); + test.assertRaises(function () ss.storage = val, pred, msg); + loader.unload(); +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-tabs.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-tabs.js new file mode 100644 index 0000000..ae98312 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-tabs.js @@ -0,0 +1,905 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dietrich Ayala <dietrich@mozilla.com> (Original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +var {Cc,Ci} = require("chrome"); +const { Loader } = require("./helpers"); + +// test tab.activeTab getter +exports.testActiveTab_getter = function(test) { + test.waitUntilDone(); + + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + + let url = "data:text/html,<html><head><title>foo</title></head></html>"; + require("tab-browser").addTab( + url, + { + onLoad: function(e) { + test.assert(tabs.activeTab); + test.assertEqual(tabs.activeTab.url, url); + test.assertEqual(tabs.activeTab.title, "foo"); + closeBrowserWindow(window, function() test.done()); + } + } + ); + }); +}; + +// test 'BrowserWindow' instance creation on tab 'activate' event +// See bug 648244: there was a infinite loop. +exports.testBrowserWindowCreationOnActivate = function(test) { + test.waitUntilDone(); + + let windows = require("windows").browserWindows; + let tabs = require("tabs"); + + let gotActivate = false; + + tabs.once('activate', function onActivate(eventTab) { + test.assert(windows.activeWindow, "Is able to fetch activeWindow"); + gotActivate = true; + }); + + openBrowserWindow(function(window, browser) { + test.assert(gotActivate, "Received activate event before openBrowserWindow's callback is called"); + closeBrowserWindow(window, function () test.done()); + }); +} + +// test tab.activeTab setter +exports.testActiveTab_setter = function(test) { + test.waitUntilDone(); + + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,<html><head><title>foo</title></head></html>"; + + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + test.assertEqual(tabs.activeTab.url, "about:blank", "activeTab url has not changed"); + test.assertEqual(tab.url, url, "url of new background tab matches"); + tabs.on('activate', function onActivate(eventTab) { + tabs.removeListener('activate', onActivate); + test.assertEqual(tabs.activeTab.url, url, "url after activeTab setter matches"); + test.assertEqual(eventTab, tab, "event argument is the activated tab"); + test.assertEqual(eventTab, tabs.activeTab, "the tab is the active one"); + closeBrowserWindow(window, function() test.done()); + }); + tab.activate(); + }) + + tabs.open({ + url: url, + inBackground: true + }); + }); +}; + +exports.testAutomaticDestroy = function(test) { + test.waitUntilDone(); + + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + + // Create a second tab instance that we will destroy + let called = false; + + let loader = Loader(module); + let tabs2 = loader.require("tabs"); + tabs2.on('open', function onOpen(tab) { + called = true; + }); + + loader.unload(); + + // Fire a tab event an ensure that this destroyed tab is inactive + tabs.once('open', function () { + require("timer").setTimeout(function () { + test.assert(!called, "Unloaded tab module is destroyed and inactive"); + closeBrowserWindow(window, function() test.done()); + }, 0); + }); + + tabs.open("data:text/html,foo"); + + }); +}; + +// test tab properties +exports.testTabProperties = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs= require("tabs"); + let url = "data:text/html,<html><head><title>foo</title></head><body>foo</body></html>"; + tabs.open({ + url: url, + onReady: function(tab) { + test.assertEqual(tab.title, "foo", "title of the new tab matches"); + test.assertEqual(tab.url, url, "URL of the new tab matches"); + test.assert(tab.favicon, "favicon of the new tab is not empty"); + test.assertEqual(tab.style, null, "style of the new tab matches"); + test.assertEqual(tab.index, 1, "index of the new tab matches"); + test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// test tabs iterator and length property +exports.testTabsIteratorAndLength = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let startCount = 0; + for each (let t in tabs) startCount++; + test.assertEqual(startCount, tabs.length, "length property is correct"); + let url = "data:text/html,default"; + tabs.open(url); + tabs.open(url); + tabs.open({ + url: url, + onOpen: function(tab) { + let count = 0; + for each (let t in tabs) count++; + test.assertEqual(count, startCount + 3, "iterated tab count matches"); + test.assertEqual(startCount + 3, tabs.length, "iterated tab count matches length property"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// test tab.url setter +exports.testTabLocation = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url1 = "data:text/html,foo"; + let url2 = "data:text/html,bar"; + + tabs.on('ready', function onReady(tab) { + if (tab.url != url2) + return; + tabs.removeListener('ready', onReady); + test.pass("tab.load() loaded the correct url"); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.open({ + url: url1, + onOpen: function(tab) { + tab.url = url2 + } + }); + }); +}; + +// test tab.close() +exports.testTabClose = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,foo"; + + test.assertNotEqual(tabs.activeTab.url, url, "tab is now the active tab"); + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + test.assertEqual(tabs.activeTab.url, tab.url, "tab is now the active tab"); + tab.close(function() { + closeBrowserWindow(window, function() test.done()); + }); + test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab"); + }); + + tabs.open(url); + }); +}; + +// test tab.reload() +exports.testTabReload = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,<!doctype%20html><title></title>"; + + tabs.open({ url: url, onReady: function onReady(tab) { + tab.removeListener("ready", onReady); + + browser.addEventListener( + "load", + function onLoad() { + browser.removeEventListener("load", onLoad, true); + + browser.addEventListener( + "load", + function onReload() { + browser.removeEventListener("load", onReload, true); + test.pass("the tab was loaded again"); + test.assertEqual(tab.url, url, "the tab has the same URL"); + closeBrowserWindow(window, function() test.done()); + }, + true + ); + tab.reload(); + }, + true + ); + }}); + }); +}; + +// test tab.move() +exports.testTabMove = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,foo"; + + tabs.open({ + url: url, + onOpen: function(tab) { + test.assertEqual(tab.index, 1, "tab index before move matches"); + tab.index = 0; + test.assertEqual(tab.index, 0, "tab index after move matches"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// open tab with default options +exports.testOpen = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,default"; + tabs.open({ + url: url, + onReady: function(tab) { + test.assertEqual(tab.url, url, "URL of the new tab matches"); + test.assertEqual(window.content.location, url, "URL of active tab in the current window matches"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// open pinned tab +exports.testOpenPinned = function(test) { + const xulApp = require("xul-app"); + if (xulApp.versionInRange(xulApp.platformVersion, "2.0b2", "*")) { + // test tab pinning + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,default"; + tabs.open({ + url: url, + isPinned: true, + onOpen: function(tab) { + test.assertEqual(tab.isPinned, true, "The new tab is pinned"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); + } + else { + test.pass("Pinned tabs are not supported in this application."); + } +}; + +// pin/unpin opened tab +exports.testPinUnpin = function(test) { + const xulApp = require("xul-app"); + if (xulApp.versionInRange(xulApp.platformVersion, "2.0b2", "*")) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,default"; + tabs.open({ + url: url, + onOpen: function(tab) { + tab.pin(); + test.assertEqual(tab.isPinned, true, "The tab was pinned correctly"); + tab.unpin(); + test.assertEqual(tab.isPinned, false, "The tab was unpinned correctly"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); + } + else { + test.pass("Pinned tabs are not supported in this application."); + } +}; + +// open tab in background +exports.testInBackground = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let activeUrl = tabs.activeTab.url; + let url = "data:text/html,background"; + test.assertEqual(activeWindow, window, "activeWindow matches this window"); + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + test.assertEqual(tabs.activeTab.url, activeUrl, "URL of active tab has not changed"); + test.assertEqual(tab.url, url, "URL of the new background tab matches"); + test.assertEqual(activeWindow, window, "a new window was not opened"); + test.assertNotEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL"); + closeBrowserWindow(window, function() test.done()); + }); + tabs.open({ + url: url, + inBackground: true + }); + }); +}; + +// open tab in new window +exports.testOpenInNewWindow = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + + let cache = []; + let windowUtils = require("window-utils"); + let wt = new windowUtils.WindowTracker({ + onTrack: function(win) { + cache.push(win); + }, + onUntrack: function(win) { + cache.splice(cache.indexOf(win), 1) + } + }); + let startWindowCount = cache.length; + + let url = "data:text/html,newwindow"; + tabs.open({ + url: url, + inNewWindow: true, + onReady: function(tab) { + let newWindow = cache[cache.length - 1]; + test.assertEqual(cache.length, startWindowCount + 1, "a new window was opened"); + test.assertEqual(activeWindow, newWindow, "new window is active"); + test.assertEqual(tab.url, url, "URL of the new tab matches"); + test.assertEqual(newWindow.content.location, url, "URL of new tab in new window matches"); + test.assertEqual(tabs.activeTab.url, url, "URL of activeTab matches"); + for (var i in cache) cache[i] = null; + wt.unload(); + closeBrowserWindow(newWindow, function() { + closeBrowserWindow(window, function() test.done()); + }); + } + }); + }); +}; + +// onOpen event handler +exports.testTabsEvent_onOpen = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,1"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('open', listener1); + + // add listener via collection add + tabs.on('open', function listener2(tab) { + test.assertEqual(++eventCount, 2, "both listeners notified"); + tabs.removeListener('open', listener1); + tabs.removeListener('open', listener2); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.open(url); + }); +}; + +// onClose event handler +exports.testTabsEvent_onClose = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,onclose"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + } + tabs.on('close', listener1); + + // add listener via collection add + tabs.on('close', function listener2(tab) { + test.assertEqual(++eventCount, 2, "both listeners notified"); + tabs.removeListener('close', listener1); + tabs.removeListener('close', listener2); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + tab.close(); + }); + + tabs.open(url); + }); +}; + +// onClose event handler when a window is closed +exports.testTabsEvent_onCloseWindow = function(test) { + test.waitUntilDone(); + + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + + let closeCount = 0, individualCloseCount = 0; + function listener() { + closeCount++; + } + tabs.on('close', listener); + + // One tab is already open with the window + let openTabs = 1; + function testCasePossiblyLoaded() { + if (++openTabs == 4) { + beginCloseWindow(); + } + } + + tabs.open({ + url: "data:text/html,tab2", + onOpen: function() testCasePossiblyLoaded(), + onClose: function() individualCloseCount++ + }); + + tabs.open({ + url: "data:text/html,tab3", + onOpen: function() testCasePossiblyLoaded(), + onClose: function() individualCloseCount++ + }); + + tabs.open({ + url: "data:text/html,tab4", + onOpen: function() testCasePossiblyLoaded(), + onClose: function() individualCloseCount++ + }); + + function beginCloseWindow() { + closeBrowserWindow(window, function testFinished() { + tabs.removeListener("close", listener); + + test.assertEqual(closeCount, 4, "Correct number of close events received"); + test.assertEqual(individualCloseCount, 3, + "Each tab with an attached onClose listener received a close " + + "event when the window was closed"); + + test.done(); + }); + } + + }); +} + +// onReady event handler +exports.testTabsEvent_onReady = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,onready"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('ready', listener1); + + // add listener via collection add + tabs.on('ready', function listener2(tab) { + test.assertEqual(++eventCount, 2, "both listeners notified"); + tabs.removeListener('ready', listener1); + tabs.removeListener('ready', listener2); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.open(url); + }); +}; + +// onActivate event handler +exports.testTabsEvent_onActivate = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,onactivate"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('activate', listener1); + + // add listener via collection add + tabs.on('activate', function listener2(tab) { + test.assertEqual(++eventCount, 2, "both listeners notified"); + tabs.removeListener('activate', listener1); + tabs.removeListener('activate', listener2); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.open(url); + }); +}; + +// onDeactivate event handler +exports.testTabsEvent_onDeactivate = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,ondeactivate"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('deactivate', listener1); + + // add listener via collection add + tabs.on('deactivate', function listener2(tab) { + test.assertEqual(++eventCount, 2, "both listeners notified"); + tabs.removeListener('deactivate', listener1); + tabs.removeListener('deactivate', listener2); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.on('open', function onOpen(tab) { + tabs.removeListener('open', onOpen); + tabs.open("data:text/html,foo"); + }); + + tabs.open(url); + }); +}; + +// per-tab event handlers +exports.testPerTabEvents = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let eventCount = 0; + + tabs.open({ + url: "data:text/html,foo", + onOpen: function(tab) { + // add listener via property assignment + function listener1() { + eventCount++; + }; + tab.on('ready', listener1); + + // add listener via collection add + tab.on('ready', function listener2() { + test.assertEqual(eventCount, 1, "both listeners notified"); + tab.removeListener('ready', listener1); + tab.removeListener('ready', listener2); + closeBrowserWindow(window, function() test.done()); + }); + } + }); + }); +}; + +exports.testAttachOnOpen = function (test) { + // Take care that attach has to be called on tab ready and not on tab open. + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + + tabs.open({ + url: "data:text/html,foobar", + onOpen: function (tab) { + let worker = tab.attach({ + contentScript: 'self.postMessage(document.location.href); ', + onMessage: function (msg) { + test.assertEqual(msg, "about:blank", + "Worker document url is about:blank on open"); + worker.destroy(); + closeBrowserWindow(window, function() test.done()); + } + }); + } + }); + + }); +} + +exports.testAttachOnMultipleDocuments = function (test) { + // Example of attach that process multiple tab documents + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let firstLocation = "data:text/html,foobar"; + let secondLocation = "data:text/html,bar"; + let thirdLocation = "data:text/html,fox"; + let onReadyCount = 0; + let worker1 = null; + let worker2 = null; + let detachEventCount = 0; + tabs.open({ + url: firstLocation, + onReady: function (tab) { + onReadyCount++; + if (onReadyCount == 1) { + worker1 = tab.attach({ + contentScript: 'self.on("message", ' + + ' function () self.postMessage(document.location.href)' + + ');', + onMessage: function (msg) { + test.assertEqual(msg, firstLocation, + "Worker url is equal to the 1st document"); + tab.url = secondLocation; + }, + onDetach: function () { + detachEventCount++; + test.pass("Got worker1 detach event"); + test.assertRaises(function () { + worker1.postMessage("ex-1"); + }, + /The page has been destroyed/, + "postMessage throw because worker1 is destroyed"); + checkEnd(); + } + }); + worker1.postMessage("new-doc-1"); + } + else if (onReadyCount == 2) { + + worker2 = tab.attach({ + contentScript: 'self.on("message", ' + + ' function () self.postMessage(document.location.href)' + + ');', + onMessage: function (msg) { + test.assertEqual(msg, secondLocation, + "Worker url is equal to the 2nd document"); + tab.url = thirdLocation; + }, + onDetach: function () { + detachEventCount++; + test.pass("Got worker2 detach event"); + test.assertRaises(function () { + worker2.postMessage("ex-2"); + }, + /The page has been destroyed/, + "postMessage throw because worker2 is destroyed"); + checkEnd(); + } + }); + worker2.postMessage("new-doc-2"); + } + else if (onReadyCount == 3) { + + tab.close(); + + } + + } + }); + + function checkEnd() { + if (detachEventCount != 2) + return; + + test.pass("Got all detach events"); + + closeBrowserWindow(window, function() test.done()); + } + + }); +} + + +exports.testAttachWrappers = function (test) { + // Check that content script has access to wrapped values by default + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let document = "data:text/html,<script>var globalJSVar = true; " + + " document.getElementById = 3;</script>"; + let count = 0; + + tabs.open({ + url: document, + onReady: function (tab) { + let worker = tab.attach({ + contentScript: 'try {' + + ' self.postMessage(!("globalJSVar" in window));' + + ' self.postMessage(typeof window.globalJSVar == "undefined");' + + '} catch(e) {' + + ' self.postMessage(e.message);' + + '}', + onMessage: function (msg) { + test.assertEqual(msg, true, "Worker has wrapped objects ("+count+")"); + if (count++ == 1) + closeBrowserWindow(window, function() test.done()); + } + }); + } + }); + + }); +} + +/* +// We do not offer unwrapped access to DOM since bug 601295 landed +// See 660780 to track progress of unwrap feature +exports.testAttachUnwrapped = function (test) { + // Check that content script has access to unwrapped values through unsafeWindow + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let document = "data:text/html,<script>var globalJSVar=true;</script>"; + let count = 0; + + tabs.open({ + url: document, + onReady: function (tab) { + let worker = tab.attach({ + contentScript: 'try {' + + ' self.postMessage(unsafeWindow.globalJSVar);' + + '} catch(e) {' + + ' self.postMessage(e.message);' + + '}', + onMessage: function (msg) { + test.assertEqual(msg, true, "Worker has access to javascript content globals ("+count+")"); + closeBrowserWindow(window, function() test.done()); + } + }); + } + }); + + }); +} +*/ + +exports['test window focus changes active tab'] = function(test) { + test.waitUntilDone(); + let win1 = openBrowserWindow(function() { + let win2 = openBrowserWindow(function() { + let tabs = require("tabs"); + tabs.on("activate", function onActivate() { + tabs.removeListener("activate", onActivate); + test.pass("activate was called on windows focus change."); + closeBrowserWindow(win1, function() { + closeBrowserWindow(win2, function() { test.done(); }); + }); + }); + win1.focus(); + }, "data:text/html,test window focus changes active tab</br><h1>Window #2"); + }, "data:text/html,test window focus changes active tab</br><h1>Window #1"); +}; + +exports['test ready event on new window tab'] = function(test) { + test.waitUntilDone(); + let uri = encodeURI("data:text/html,Waiting for ready event!"); + + require("tabs").on("ready", function onReady(tab) { + if (tab.url === uri) { + require("tabs").removeListener("ready", onReady); + test.pass("ready event was emitted"); + closeBrowserWindow(window, function() { + test.done(); + }); + } + }); + + let window = openBrowserWindow(function(){}, uri); +}; +/******************* helpers *********************/ + +// Helper for getting the active window +this.__defineGetter__("activeWindow", function activeWindow() { + return Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); +}); + +// Utility function to open a new browser window. +function openBrowserWindow(callback, url) { + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + let urlString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + urlString.data = url; + let window = ww.openWindow(null, "chrome://browser/content/browser.xul", + "_blank", "chrome,all,dialog=no", urlString); + + if (callback) { + window.addEventListener("load", function onLoad(event) { + if (event.target && event.target.defaultView == window) { + window.removeEventListener("load", onLoad, true); + let browsers = window.document.getElementsByTagName("tabbrowser"); + try { + require("timer").setTimeout(function () { + callback(window, browsers[0]); + }, 10); + } catch (e) { console.exception(e); } + } + }, true); + } + + return window; +} + +// Helper for calling code at window close +function closeBrowserWindow(window, callback) { + window.addEventListener("unload", function unload() { + window.removeEventListener("unload", unload, false); + callback(); + }, false); + window.close(); +} + +// If the module doesn't support the app we're being run in, require() will +// throw. In that case, remove all tests above from exports, and add one dummy +// test that passes. +try { + require("tabs"); +} +catch (err) { + // This bug should be mentioned in the error message. + let bug = "https://bugzilla.mozilla.org/show_bug.cgi?id=560716"; + if (err.message.indexOf(bug) < 0) + throw err; + for (let [prop, val] in Iterator(exports)) { + if (/^test/.test(prop) && typeof(val) === "function") + delete exports[prop]; + } + exports.testAppNotSupported = function (test) { + test.pass("the tabs module does not support this application."); + }; +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-timers.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-timers.js new file mode 100644 index 0000000..98ea613 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-timers.js @@ -0,0 +1,44 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Shane Tomlinson <stomlinson@mozilla.com> (Original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +const timers = require("timers"); + +exports.testTimeout = function (test) { + test.waitUntilDone(); + timers.setTimeout(function () { + test.pass("timers.setTimeout works"); + test.done(); + }, 0); +} diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-widget.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-widget.js new file mode 100644 index 0000000..a5ab83c --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-widget.js @@ -0,0 +1,950 @@ +const {Cc,Ci} = require("chrome"); +const { Loader } = require('./helpers'); + +exports.testConstructor = function(test) { + + const tabBrowser = require("tab-browser"); + + test.waitUntilDone(30000); + + const widgets = require("widget"); + const url = require("url"); + const windowUtils = require("window-utils"); + + let browserWindow = windowUtils.activeBrowserWindow; + let doc = browserWindow.document; + let AddonsMgrListener = browserWindow.AddonsMgrListener; + + function container() doc.getElementById("addon-bar"); + function widgetCount() container() ? container().getElementsByTagName("toolbaritem").length : 0; + let widgetStartCount = widgetCount(); + function widgetNode(index) container() ? container().getElementsByTagName("toolbaritem")[index] : null; + + // Test basic construct/destroy + AddonsMgrListener.onInstalling(); + let w = widgets.Widget({ id: "fooID", label: "foo", content: "bar" }); + AddonsMgrListener.onInstalled(); + test.assertEqual(widgetCount(), widgetStartCount + 1, "panel has correct number of child elements after widget construction"); + + // test widget height + test.assertEqual(widgetNode(0).firstChild.boxObject.height, 16, "widget has correct default height"); + + AddonsMgrListener.onUninstalling(); + w.destroy(); + AddonsMgrListener.onUninstalled(); + w.destroy(); + test.pass("Multiple destroys do not cause an error"); + test.assertEqual(widgetCount(), widgetStartCount, "panel has correct number of child elements after destroy"); + + // Test automatic widget destroy on unload + let loader = Loader(module); + let widgetsFromLoader = loader.require("widget"); + let widgetStartCount = widgetCount(); + let w = widgetsFromLoader.Widget({ id: "fooID", label: "foo", content: "bar" }); + test.assertEqual(widgetCount(), widgetStartCount + 1, "widget has been correctly added"); + loader.unload(); + test.assertEqual(widgetCount(), widgetStartCount, "widget has been destroyed on module unload"); + + // Test nothing + test.assertRaises( + function() widgets.Widget({}), + "The widget must have a non-empty label property.", + "throws on no properties"); + + // Test no label + test.assertRaises( + function() widgets.Widget({content: "foo"}), + "The widget must have a non-empty label property.", + "throws on no label"); + + // Test empty label + test.assertRaises( + function() widgets.Widget({label: "", content: "foo"}), + "The widget must have a non-empty label property.", + "throws on empty label"); + + // Test no content or image + test.assertRaises( + function() widgets.Widget({id: "fooID", label: "foo"}), + "No content or contentURL property found. Widgets must have one or the other.", + "throws on no content"); + + // Test empty content, no image + test.assertRaises( + function() widgets.Widget({id:"fooID", label: "foo", content: ""}), + "No content or contentURL property found. Widgets must have one or the other.", + "throws on empty content"); + + // Test empty image, no content + test.assertRaises( + function() widgets.Widget({id:"fooID", label: "foo", image: ""}), + "No content or contentURL property found. Widgets must have one or the other.", + "throws on empty content"); + + // Test empty content, empty image + test.assertRaises( + function() widgets.Widget({id:"fooID", label: "foo", content: "", image: ""}), + "No content or contentURL property found. Widgets must have one or the other.", + "throws on empty content"); + + // Test duplicated ID + let duplicateID = widgets.Widget({id: "foo", label: "foo", content: "bar"}); + test.assertRaises( + function() widgets.Widget({id: "foo", label: "bar", content: "bar"}), + /This widget ID is already used:/, + "throws on duplicated id"); + duplicateID.destroy(); + + // Test duplicate label, different ID + let w1 = widgets.Widget({id: "id1", label: "foo", content: "bar"}); + let w2 = widgets.Widget({id: "id2", label: "foo", content: "bar"}); + w1.destroy(); + w2.destroy(); + + // Test position restore on create/destroy/create + // Create 3 ordered widgets + let w1 = widgets.Widget({id: "first", label:"first", content: "bar"}); + let w2 = widgets.Widget({id: "second", label:"second", content: "bar"}); + let w3 = widgets.Widget({id: "third", label:"third", content: "bar"}); + // Remove the middle widget + test.assertEqual(widgetNode(1).getAttribute("label"), "second", "second widget is the second widget inserted"); + w2.destroy(); + test.assertEqual(widgetNode(1).getAttribute("label"), "third", "second widget is removed, so second widget is now the third one"); + w2 = widgets.Widget({id: "second", label:"second", content: "bar"}); + test.assertEqual(widgetNode(1).getAttribute("label"), "second", "second widget is created again, at the same location"); + // Cleanup this testcase + AddonsMgrListener.onUninstalling(); + w1.destroy(); + w2.destroy(); + w3.destroy(); + AddonsMgrListener.onUninstalled(); + + // Test concurrent widget module instances on addon-bar hiding + let loader = Loader(module); + let anotherWidgetsInstance = loader.require("widget"); + test.assert(container().collapsed, "UI is hidden when no widgets"); + AddonsMgrListener.onInstalling(); + let w1 = widgets.Widget({id: "foo", label: "foo", content: "bar"}); + // Ideally we would let AddonsMgrListener display the addon bar + // But, for now, addon bar is immediatly displayed by sdk code + // https://bugzilla.mozilla.org/show_bug.cgi?id=627484 + test.assert(!container().collapsed, "UI is already visible when we just added the widget"); + AddonsMgrListener.onInstalled(); + test.assert(!container().collapsed, "UI become visible when we notify AddonsMgrListener about end of addon installation"); + let w2 = anotherWidgetsInstance.Widget({id: "bar", label: "bar", content: "foo"}); + test.assert(!container().collapsed, "UI still visible when we add a second widget"); + AddonsMgrListener.onUninstalling(); + w1.destroy(); + AddonsMgrListener.onUninstalled(); + test.assert(!container().collapsed, "UI still visible when we remove one of two widgets"); + AddonsMgrListener.onUninstalling(); + w2.destroy(); + test.assert(!container().collapsed, "UI is still visible when we have removed all widget but still not called onUninstalled"); + AddonsMgrListener.onUninstalled(); + test.assert(container().collapsed, "UI is hidden when we have removed all widget and called onUninstalled"); + + // Helper for testing a single widget. + // Confirms proper addition and content setup. + function testSingleWidget(widgetOptions) { + // We have to display which test is being run, because here we do not + // use the regular test framework but rather a custom one that iterates + // the `tests` array. + console.info("executing: " + widgetOptions.id); + + let startCount = widgetCount(); + let widget = widgets.Widget(widgetOptions); + let node = widgetNode(startCount); + test.assert(node, "widget node at index"); + test.assertEqual(node.tagName, "toolbaritem", "widget element is correct"); + test.assertEqual(widget.width + "px", node.style.minWidth, "widget width is correct"); + test.assertEqual(widgetCount(), startCount + 1, "container has correct number of child elements"); + let content = node.firstElementChild; + test.assert(content, "found content"); + test.assertMatches(content.tagName, /iframe|image/, "content is iframe or image"); + return widget; + } + + // Array of widgets to test + // and a function to test them. + let tests = []; + function nextTest() { + test.assertEqual(widgetCount(), 0, "widget in last test property cleaned itself up"); + if (!tests.length) + test.done(); + else + require("timer").setTimeout(tests.shift(), 0); + } + function doneTest() nextTest(); + + // text widget + tests.push(function testTextWidget() testSingleWidget({ + id: "text", + label: "text widget", + content: "oh yeah", + contentScript: "self.postMessage(document.body.innerHTML);", + contentScriptWhen: "end", + onMessage: function (message) { + test.assertEqual(this.content, message, "content matches"); + this.destroy(); + doneTest(); + } + })); + + // html widget + tests.push(function testHTMLWidget() testSingleWidget({ + id: "html", + label: "html widget", + content: "<div>oh yeah</div>", + contentScript: "self.postMessage(document.body.innerHTML);", + contentScriptWhen: "end", + onMessage: function (message) { + test.assertEqual(this.content, message, "content matches"); + this.destroy(); + doneTest(); + } + })); + + // image url widget + tests.push(function testImageURLWidget() testSingleWidget({ + id: "image", + label: "image url widget", + contentURL: require("self").data.url("test.html"), + contentScript: "self.postMessage({title: document.title, " + + "tag: document.body.firstElementChild.tagName, " + + "content: document.body.firstElementChild.innerHTML});", + contentScriptWhen: "end", + onMessage: function (message) { + test.assertEqual(message.title, "foo", "title matches"); + test.assertEqual(message.tag, "P", "element matches"); + test.assertEqual(message.content, "bar", "element content matches"); + this.destroy(); + doneTest(); + } + })); + + // web uri widget + tests.push(function testWebURIWidget() testSingleWidget({ + id: "web", + label: "web uri widget", + contentURL: require("self").data.url("test.html"), + contentScript: "self.postMessage({title: document.title, " + + "tag: document.body.firstElementChild.tagName, " + + "content: document.body.firstElementChild.innerHTML});", + contentScriptWhen: "end", + onMessage: function (message) { + test.assertEqual(message.title, "foo", "title matches"); + test.assertEqual(message.tag, "P", "element matches"); + test.assertEqual(message.content, "bar", "element content matches"); + this.destroy(); + doneTest(); + } + })); + + // event: onclick + content + tests.push(function testOnclickEventContent() testSingleWidget({ + id: "click", + label: "click test widget - content", + content: "<div id='me'>foo</div>", + contentScript: "var evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('click', true, true ); " + + "document.getElementById('me').dispatchEvent(evt);", + contentScriptWhen: "end", + onClick: function() { + test.pass("onClick called"); + this.destroy(); + doneTest(); + } + })); + + // event: onmouseover + content + tests.push(function testOnmouseoverEventContent() testSingleWidget({ + id: "mouseover", + label: "mouseover test widget - content", + content: "<div id='me'>foo</div>", + contentScript: "var evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('mouseover', true, true ); " + + "document.getElementById('me').dispatchEvent(evt);", + contentScriptWhen: "end", + onMouseover: function() { + test.pass("onMouseover called"); + this.destroy(); + doneTest(); + } + })); + + // event: onmouseout + content + tests.push(function testOnmouseoutEventContent() testSingleWidget({ + id: "mouseout", + label: "mouseout test widget - content", + content: "<div id='me'>foo</div>", + contentScript: "var evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('mouseout', true, true ); " + + "document.getElementById('me').dispatchEvent(evt);", + contentScriptWhen: "end", + onMouseout: function() { + test.pass("onMouseout called"); + this.destroy(); + doneTest(); + } + })); + + // event: onclick + image + tests.push(function testOnclickEventImage() testSingleWidget({ + id: "click", + label: "click test widget - image", + contentURL: require("self").data.url("moz_favicon.ico"), + contentScript: "var evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('click', true, true ); " + + "document.body.firstElementChild.dispatchEvent(evt);", + contentScriptWhen: "end", + onClick: function() { + test.pass("onClick called"); + this.destroy(); + doneTest(); + } + })); + + // event: onmouseover + image + tests.push(function testOnmouseoverEventImage() testSingleWidget({ + id: "mouseover", + label: "mouseover test widget - image", + contentURL: require("self").data.url("moz_favicon.ico"), + contentScript: "var evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('mouseover', true, true ); " + + "document.body.firstElementChild.dispatchEvent(evt);", + contentScriptWhen: "end", + onMouseover: function() { + test.pass("onMouseover called"); + this.destroy(); + doneTest(); + } + })); + + // event: onmouseout + image + tests.push(function testOnmouseoutEventImage() testSingleWidget({ + id: "mouseout", + label: "mouseout test widget - image", + contentURL: require("self").data.url("moz_favicon.ico"), + contentScript: "var evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('mouseout', true, true ); " + + "document.body.firstElementChild.dispatchEvent(evt);", + contentScriptWhen: "end", + onMouseout: function() { + test.pass("onMouseout called"); + this.destroy(); + doneTest(); + } + })); + + // test multiple widgets + tests.push(function testMultipleWidgets() { + let w1 = widgets.Widget({id: "first", label: "first widget", content: "first content"}); + let w2 = widgets.Widget({id: "second", label: "second widget", content: "second content"}); + + w1.destroy(); + w2.destroy(); + + doneTest(); + }); + + // test updating widget content + let loads = 0; + tests.push(function testUpdatingWidgetContent() testSingleWidget({ + id: "content", + label: "content update test widget", + content: "<div id='me'>foo</div>", + contentScript: "self.postMessage(1)", + contentScriptWhen: "ready", + onMessage: function(message) { + if (!this.flag) { + this.content = "<div id='me'>bar</div>"; + this.flag = 1; + } + else { + test.assertEqual(this.content, "<div id='me'>bar</div>"); + this.destroy(); + doneTest(); + } + } + })); + + // test updating widget contentURL + let url1 = "data:text/html,<body>foodle</body>"; + let url2 = "data:text/html,<body>nistel</body>"; + + tests.push(function testUpdatingContentURL() testSingleWidget({ + id: "content", + label: "content update test widget", + contentURL: url1, + contentScript: "self.postMessage(document.location.href);", + contentScriptWhen: "end", + onMessage: function(message) { + if (!this.flag) { + test.assertEqual(this.contentURL.toString(), url1); + test.assertEqual(message, url1); + this.contentURL = url2; + this.flag = 1; + } + else { + test.assertEqual(this.contentURL.toString(), url2); + test.assertEqual(message, url2); + this.destroy(); + doneTest(); + } + } + })); + + // test tooltip + tests.push(function testTooltip() testSingleWidget({ + id: "text", + label: "text widget", + content: "oh yeah", + tooltip: "foo", + contentScript: "self.postMessage(1)", + contentScriptWhen: "ready", + onMessage: function(message) { + test.assertEqual(this.tooltip, "foo", "tooltip matches"); + this.destroy(); + doneTest(); + } + })); + + // test tooltip fallback to label + tests.push(function testTooltipFallback() testSingleWidget({ + id: "fallback", + label: "fallback", + content: "oh yeah", + contentScript: "self.postMessage(1)", + contentScriptWhen: "ready", + onMessage: function(message) { + test.assertEqual(this.tooltip, this.label, "tooltip fallbacks to label"); + this.destroy(); + doneTest(); + } + })); + + // test updating widget tooltip + let updated = false; + tests.push(function testUpdatingTooltip() testSingleWidget({ + id: "tooltip", + label: "tooltip update test widget", + tooltip: "foo", + content: "<div id='me'>foo</div>", + contentScript: "self.postMessage(1)", + contentScriptWhen: "ready", + onMessage: function(message) { + this.tooltip = "bar"; + test.assertEqual(this.tooltip, "bar", "tooltip gets updated"); + this.destroy(); + doneTest(); + } + })); + + // test allow attribute + tests.push(function testDefaultAllow() testSingleWidget({ + id: "allow", + label: "allow.script attribute", + content: "<script>document.title = 'ok';</script>", + contentScript: "self.postMessage(document.title)", + onMessage: function(message) { + test.assertEqual(message, "ok", "scripts are evaluated by default"); + this.destroy(); + doneTest(); + } + })); + + tests.push(function testExplicitAllow() testSingleWidget({ + id: "allow", + label: "allow.script attribute", + allow: {script: true}, + content: "<script>document.title = 'ok';</script>", + contentScript: "self.postMessage(document.title)", + onMessage: function(message) { + test.assertEqual(message, "ok", "scripts are evaluated when we want to"); + this.destroy(); + doneTest(); + } + })); + + tests.push(function testExplicitDisallow() testSingleWidget({ + id: "allow", + label: "allow.script attribute", + content: "<script>document.title = 'ok';</script>", + allow: {script: false}, + contentScript: "self.postMessage(document.title)", + onMessage: function(message) { + test.assertNotEqual(message, "ok", "scripts aren't evaluated when " + + "explicitly blocked it"); + this.destroy(); + doneTest(); + } + })); + + // test multiple windows + tests.push(function testMultipleWindows() { + tabBrowser.addTab("about:blank", { inNewWindow: true, onLoad: function(e) { + let browserWindow = e.target.defaultView; + let doc = browserWindow.document; + function container() doc.getElementById("addon-bar"); + function widgetCount2() container() ? container().childNodes.length : 0; + let widgetStartCount2 = widgetCount2(); + + let w1Opts = {id:"first", label: "first widget", content: "first content"}; + let w1 = testSingleWidget(w1Opts); + test.assertEqual(widgetCount2(), widgetStartCount2 + 1, "2nd window has correct number of child elements after first widget"); + + let w2Opts = {id:"second", label: "second widget", content: "second content"}; + let w2 = testSingleWidget(w2Opts); + test.assertEqual(widgetCount2(), widgetStartCount2 + 2, "2nd window has correct number of child elements after second widget"); + + w1.destroy(); + test.assertEqual(widgetCount2(), widgetStartCount2 + 1, "2nd window has correct number of child elements after first destroy"); + w2.destroy(); + test.assertEqual(widgetCount2(), widgetStartCount2, "2nd window has correct number of child elements after second destroy"); + + closeBrowserWindow(browserWindow, function() { + doneTest(); + }); + }}); + }); + + // test window closing + tests.push(function testWindowClosing() { + // 1/ Create a new widget + let w1Opts = { + id:"first", + label: "first widget", + content: "first content", + contentScript: "self.port.on('event', function () self.port.emit('event'))" + }; + let widget = testSingleWidget(w1Opts); + let windows = require("windows").browserWindows; + + // 2/ Retrieve a WidgetView for the initial browser window + let acceptDetach = false; + let mainView = widget.getView(windows.activeWindow); + test.assert(mainView, "Got first widget view"); + mainView.on("detach", function () { + // 8/ End of our test. Accept detach event only when it occurs after + // widget.destroy() + if (acceptDetach) + doneTest(); + else + test.fail("View on initial window should not be destroyed"); + }); + mainView.port.on("event", function () { + // 7/ Receive event sent during 6/ and cleanup our test + acceptDetach = true; + widget.destroy(); + }); + + // 3/ First: open a new browser window + windows.open({ + url: "about:blank", + onOpen: function(window) { + // 4/ Retrieve a WidgetView for this new window + let view = widget.getView(window); + test.assert(view, "Got second widget view"); + view.port.on("event", function () { + test.fail("We should not receive event on the detach view"); + }); + view.on("detach", function () { + // The related view is destroyed + // 6/ Send a custom event + test.assertRaises(function () { + view.port.emit("event"); + }, + /The widget has been destroyed and can no longer be used./, + "emit on a destroyed view should throw"); + widget.port.emit("event"); + }); + + // 5/ Destroy this window + window.close(); + } + }); + }); + + tests.push(function testAddonBarHide() { + // Hide the addon-bar + browserWindow.setToolbarVisibility(container(), false); + + // Then open a browser window and verify that the addon-bar remains hidden + tabBrowser.addTab("about:blank", { inNewWindow: true, onLoad: function(e) { + let browserWindow = e.target.defaultView; + let doc = browserWindow.document; + function container2() doc.getElementById("addon-bar"); + function widgetCount2() container2() ? container2().childNodes.length : 0; + let widgetStartCount2 = widgetCount2(); + + let w1Opts = {id:"first", label: "first widget", content: "first content"}; + let w1 = testSingleWidget(w1Opts); + test.assertEqual(widgetCount2(), widgetStartCount2 + 1, "2nd window has correct number of child elements after widget creation"); + + w1.destroy(); + test.assertEqual(widgetCount2(), widgetStartCount2, "2nd window has correct number of child elements after widget destroy"); + + test.assert(container().collapsed, "1st window has an hidden addon-bar"); + test.assert(container2().collapsed, "2nd window has an hidden addon-bar"); + + browserWindow.setToolbarVisibility(container(), true); + + closeBrowserWindow(browserWindow, function() { + doneTest(); + }); + }}); + }); + + // test widget.width + tests.push(function testWidgetWidth() testSingleWidget({ + id: "text", + label: "test widget.width", + content: "test width", + width: 200, + contentScript: "self.postMessage(1)", + contentScriptWhen: "ready", + onMessage: function(message) { + test.assertEqual(this.width, 200); + + let node = widgetNode(0); + test.assertEqual(this.width, node.style.minWidth.replace("px", "")); + test.assertEqual(this.width, node.firstElementChild.style.width.replace("px", "")); + this.width = 300; + test.assertEqual(this.width, node.style.minWidth.replace("px", "")); + test.assertEqual(this.width, node.firstElementChild.style.width.replace("px", "")); + + this.destroy(); + doneTest(); + } + })); + + // test click handler not respond to right-click + let clickCount = 0; + tests.push(function testNoRightClick() testSingleWidget({ + id: "click-content", + label: "click test widget - content", + content: "<div id='me'>foo</div>", + contentScript: "var evt = document.createEvent('MouseEvents'); " + + "evt.initMouseEvent('click', true, true, window, " + + " 0, 0, 0, 0, 0, false, false, false, false, 2, null); " + + "document.getElementById('me').dispatchEvent(evt); " + + "evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('click', true, true ); " + + "document.getElementById('me').dispatchEvent(evt); " + + "evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('mouseover', true, true ); " + + "document.getElementById('me').dispatchEvent(evt);", + contentScriptWhen: "end", + onClick: function() clickCount++, + onMouseover: function() { + test.assertEqual(clickCount, 1, "right click wasn't sent to click handler"); + this.destroy(); + doneTest(); + } + })); + + // kick off test execution + doneTest(); +}; + +exports.testPanelWidget1 = function testPanelWidget1(test) { + const widgets = require("widget"); + + let widget1 = widgets.Widget({ + id: "panel1", + label: "panel widget 1", + content: "<div id='me'>foo</div>", + contentScript: "var evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('click', true, true ); " + + "document.body.dispatchEvent(evt);", + contentScriptWhen: "end", + panel: require("panel").Panel({ + contentURL: "data:text/html,<body>Look ma, a panel!</body>", + onShow: function() { + widget1.destroy(); + test.pass("panel displayed on click"); + test.done(); + } + }) + }); + test.waitUntilDone(); +}; + +exports.testPanelWidget2 = function testPanelWidget2(test) { + const widgets = require("widget"); + test.assertRaises( + function() { + widgets.Widget({ + id: "panel2", + label: "panel widget 2", + panel: {} + }); + }, + "The option \"panel\" must be one of the following types: null, undefined, object", + "widget.panel must be a Panel object" + ); +}; + +exports.testPanelWidget3 = function testPanelWidget3(test) { + const widgets = require("widget"); + let onClickCalled = false; + let widget3 = widgets.Widget({ + id: "panel3", + label: "panel widget 3", + content: "<div id='me'>foo</div>", + contentScript: "var evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('click', true, true ); " + + "document.body.firstElementChild.dispatchEvent(evt);", + contentScriptWhen: "end", + onClick: function() { + onClickCalled = true; + this.panel.show(); + }, + panel: require("panel").Panel({ + contentURL: "data:text/html,<body>Look ma, a panel!</body>", + onShow: function() { + test.assert( + onClickCalled, + "onClick called on click for widget with both panel and onClick" + ); + widget3.destroy(); + test.done(); + } + }) + }); + test.waitUntilDone(); +}; + +exports.testWidgetMessaging = function testWidgetMessaging(test) { + test.waitUntilDone(); + let origMessage = "foo"; + const widgets = require("widget"); + let widget = widgets.Widget({ + id: "foo", + label: "foo", + content: "<bar>baz</bar>", + contentScriptWhen: "end", + contentScript: "self.on('message', function(data) { self.postMessage(data); }); self.postMessage('ready');", + onMessage: function(message) { + if (message == "ready") + widget.postMessage(origMessage); + else { + test.assertEqual(origMessage, message); + widget.destroy(); + test.done(); + } + } + }); +}; + +exports.testWidgetViews = function testWidgetViews(test) { + test.waitUntilDone(); + const widgets = require("widget"); + let widget = widgets.Widget({ + id: "foo", + label: "foo", + content: "<bar>baz</bar>", + contentScriptWhen: "ready", + contentScript: "self.on('message', function(data) self.postMessage(data)); self.postMessage('ready')", + onAttach: function(view) { + test.pass("WidgetView created"); + view.on("message", function () { + test.pass("Got message in WidgetView"); + widget.destroy(); + }); + view.on("detach", function () { + test.pass("WidgetView destroyed"); + test.done(); + }); + } + }); + +}; + +exports.testWidgetViewsUIEvents = function testWidgetViewsUIEvents(test) { + test.waitUntilDone(); + const widgets = require("widget"); + let view = null; + let widget = widgets.Widget({ + id: "foo", + label: "foo", + content: "<div id='me'>foo</div>", + contentScript: "var evt = document.createEvent('HTMLEvents'); " + + "evt.initEvent('click', true, true ); " + + "document.getElementById('me').dispatchEvent(evt);", + contentScriptWhen: "ready", + onAttach: function(attachView) { + view = attachView; + test.pass("Got attach event"); + }, + onClick: function (eventView) { + test.assertEqual(view, eventView, + "event first argument is equal to the WidgetView"); + let view2 = widget.getView(require("windows").browserWindows.activeWindow); + test.assertEqual(view, view2, + "widget.getView return the same WidgetView"); + widget.destroy(); + test.done(); + } + }); +}; + +exports.testWidgetViewsCustomEvents = function testWidgetViewsCustomEvents(test) { + test.waitUntilDone(); + const widgets = require("widget"); + let widget = widgets.Widget({ + id: "foo", + label: "foo", + content: "<div id='me'>foo</div>", + contentScript: "self.port.emit('event', 'ok');", + contentScriptWhen: "ready", + onAttach: function(view) { + view.port.on("event", function (data) { + test.assertEqual(data, "ok", + "event argument is valid on WidgetView"); + }); + }, + }); + widget.port.on("event", function (data) { + test.assertEqual(data, "ok", + "event argument is valid on Widget"); + widget.destroy(); + test.done(); + }); +}; + +exports.testWidgetViewsTooltip = function testWidgetViewsTooltip(test) { + test.waitUntilDone(); + const widgets = require("widget"); + + let widget = new widgets.Widget({ + id: "foo", + label: "foo", + content: "foo" + }); + let view = widget.getView(require("windows").browserWindows.activeWindow); + widget.tooltip = null; + test.assertEqual(view.tooltip, "foo", + "view tooltip defaults to base widget label"); + test.assertEqual(widget.tooltip, "foo", + "tooltip defaults to base widget label"); + widget.destroy(); + test.done(); +}; + +exports.testWidgetMove = function testWidgetMove(test) { + test.waitUntilDone(); + + let windowUtils = require("window-utils"); + let widgets = require("widget"); + + let browserWindow = windowUtils.activeBrowserWindow; + let doc = browserWindow.document; + + let label = "unique-widget-label"; + let origMessage = "message after node move"; + let gotFirstReady = false; + + let widget = widgets.Widget({ + id: "foo", + label: label, + content: "<bar>baz</bar>", + contentScriptWhen: "ready", + contentScript: "self.on('message', function(data) { self.postMessage(data); }); self.postMessage('ready');", + onMessage: function(message) { + if (message == "ready") { + if (!gotFirstReady) { + test.pass("Got first ready event"); + let widgetNode = doc.querySelector('toolbaritem[label="' + label + '"]'); + let parent = widgetNode.parentNode; + parent.insertBefore(widgetNode, parent.firstChild); + gotFirstReady = true; + } else { + test.pass("Got second ready event"); + widget.postMessage(origMessage); + } + } + else { + test.assertEqual(origMessage, message, "Got message after node move"); + widget.destroy(); + test.done(); + } + } + }); +}; + +/* +The bug is exhibited when a widget with HTML content has it's content +changed to new HTML content with a pound in it. Because the src of HTML +content is converted to a data URI, the underlying iframe doesn't +consider the content change a navigation change, so doesn't load +the new content. +*/ +exports.testWidgetWithPound = function testWidgetWithPound(test) { + test.waitUntilDone(); + + function getWidgetContent(widget) { + let windowUtils = require("window-utils"); + let browserWindow = windowUtils.activeBrowserWindow; + let doc = browserWindow.document; + let widgetNode = doc.querySelector('toolbaritem[label="' + widget.label + '"]'); + test.assert(widgetNode, 'found widget node in the front-end'); + return widgetNode.firstChild.contentDocument.body.innerHTML; + } + + let widgets = require("widget"); + let count = 0; + let widget = widgets.Widget({ + id: "1", + label: "foo", + content: "foo", + contentScript: "window.addEventListener('load', self.postMessage, false);", + onMessage: function() { + count++; + if (count == 1) { + widget.content = "foo#"; + } + else { + test.assertEqual(getWidgetContent(widget), "foo#", "content updated to pound?"); + widget.destroy(); + test.done(); + } + } + }); +}; + +/******************* helpers *********************/ + +// Helper for calling code at window close +function closeBrowserWindow(window, callback) { + require("timer").setTimeout(function() { + window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload, false); + callback(); + }, false); + window.close(); + }, 0); +} + +// ADD NO TESTS BELOW THIS LINE! /////////////////////////////////////////////// + +// If the module doesn't support the app we're being run in, require() will +// throw. In that case, remove all tests above from exports, and add one dummy +// test that passes. +try { + require("widget"); +} +catch (err) { + // This bug should be mentioned in the error message. + let bug = "https://bugzilla.mozilla.org/show_bug.cgi?id=560716"; + if (err.message.indexOf(bug) < 0) + throw err; + for (let [prop, val] in Iterator(exports)) { + if (/^test/.test(prop) && typeof(val) === "function") + delete exports[prop]; + } + exports.testAppNotSupported = function (test) { + test.pass("context-menu does not support this application."); + }; +} + diff --git a/tools/addon-sdk-1.4/packages/addon-kit/tests/test-windows.js b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-windows.js new file mode 100644 index 0000000..e2824f8 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/tests/test-windows.js @@ -0,0 +1,342 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Felipe Gomes <felipc@gmail.com> (Original author) + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const {Cc, Ci} = require("chrome"); +const { setTimeout } = require("timer"); +const { Loader } = require('./helpers'); +const wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); +let browserWindows; + +function getTestRunnerWindow() wm.getMostRecentWindow("test:runner") + +exports.testOpenAndCloseWindow = function(test) { + test.waitUntilDone(); + + test.assertEqual(browserWindows.length, 1, "Only one window open"); + + browserWindows.open({ + url: "data:text/html,<title>windows API test</title>", + onOpen: function(window) { + test.assertEqual(this, browserWindows, + "The 'this' object is the windows object."); + test.assertEqual(window.tabs.length, 1, "Only one tab open"); + test.assertEqual(browserWindows.length, 2, "Two windows open"); + window.tabs.activeTab.on('ready', function onReady(tab) { + tab.removeListener('ready', onReady); + test.assert(window.title.indexOf("windows API test") != -1, + "URL correctly loaded"); + window.close(); + }); + }, + onClose: function(window) { + test.assertEqual(window.tabs.length, 0, "Tabs were cleared"); + test.assertEqual(browserWindows.length, 1, "Only one window open"); + test.done(); + } + }); +}; + +exports.testAutomaticDestroy = function(test) { + + test.waitUntilDone(); + let windows = browserWindows; + + // Create a second windows instance that we will unload + let called = false; + let loader = Loader(module); + let windows2 = loader.require("windows").browserWindows; + windows2.on("open", function() { + called = true; + }); + + loader.unload(); + + // Fire a windows event and check that this unloaded instance is inactive + windows.open({ + url: "data:text/html,foo", + onOpen: function(window) { + setTimeout(function () { + test.assert(!called, + "Unloaded windows instance is destroyed and inactive"); + window.close(function () { + test.done(); + }); + }); + } + }); + +}; + +exports.testOnOpenOnCloseListeners = function(test) { + test.waitUntilDone(); + let windows = browserWindows; + + test.assertEqual(browserWindows.length, 1, "Only one window open"); + + let received = { + listener1: false, + listener2: false, + listener3: false, + listener4: false + } + + function listener1() { + test.assertEqual(this, windows, "The 'this' object is the windows object."); + if (received.listener1) + test.fail("Event received twice"); + received.listener1 = true; + } + + function listener2() { + if (received.listener2) + test.fail("Event received twice"); + received.listener2 = true; + } + + function listener3() { + test.assertEqual(this, windows, "The 'this' object is the windows object."); + if (received.listener3) + test.fail("Event received twice"); + received.listener3 = true; + } + + function listener4() { + if (received.listener4) + test.fail("Event received twice"); + received.listener4 = true; + } + + windows.on('open', listener1); + windows.on('open', listener2); + windows.on('close', listener3); + windows.on('close', listener4); + + function verify() { + test.assert(received.listener1, "onOpen handler called"); + test.assert(received.listener2, "onOpen handler called"); + test.assert(received.listener3, "onClose handler called"); + test.assert(received.listener4, "onClose handler called"); + + windows.removeListener('open', listener1); + windows.removeListener('open', listener2); + windows.removeListener('close', listener3); + windows.removeListener('close', listener4); + test.done(); + } + + + windows.open({ + url: "data:text/html,foo", + onOpen: function(window) { + window.close(verify); + } + }); +}; + +exports.testWindowTabsObject = function(test) { + test.waitUntilDone(); + + browserWindows.open({ + url: "data:text/html,<title>tab 1</title>", + onOpen: function onOpen(window) { + test.assertEqual(window.tabs.length, 1, "Only 1 tab open"); + + window.tabs.open({ + url: "data:text/html,<title>tab 2</title>", + inBackground: true, + onReady: function onReady(newTab) { + test.assertEqual(window.tabs.length, 2, "New tab open"); + test.assertEqual(newTab.title, "tab 2", "Correct new tab title"); + test.assertEqual(window.tabs.activeTab.title, "tab 1", "Correct active tab"); + + let i = 1; + for each (let tab in window.tabs) + test.assertEqual(tab.title, "tab " + i++, "Correct title"); + + window.close(); + } + }); + }, + onClose: function onClose(window) { + test.assertEqual(window.tabs.length, 0, "No more tabs on closed window"); + test.done(); + } + }); +}; + +exports.testActiveWindow = function(test) { + const xulApp = require("xul-app"); + if (xulApp.versionInRange(xulApp.platformVersion, "1.9.2", "1.9.2.*")) { + test.pass("This test is disabled on 3.6. For more information, see bug 598525"); + return; + } + + let windows = browserWindows; + + // API window objects + let window2, window3; + + // Raw window objects + let nonBrowserWindow = getTestRunnerWindow(), rawWindow2, rawWindow3; + + test.waitUntilDone(); + + let testSteps = [ + function() { + test.assertEqual(windows.length, 3, "Correct number of browser windows"); + let count = 0; + for (let window in windows) + count++; + test.assertEqual(count, 3, "Correct number of windows returned by iterator"); + + rawWindow2.focus(); + continueAfterFocus(rawWindow2); + }, + function() { + nonBrowserWindow.focus(); + continueAfterFocus(nonBrowserWindow); + }, + function() { + /** + * Bug 614079: This test fails intermittently on some specific linux + * environnements, without being able to reproduce it in same + * distribution with same window manager. + * Disable it until being able to reproduce it easily. + + // On linux, focus is not consistent, so we can't be sure + // what window will be on top. + // Here when we focus "non-browser" window, + // Any Browser window may be selected as "active". + test.assert(windows.activeWindow == window2 || windows.activeWindow == window3, + "Non-browser windows aren't handled by this module"); + */ + window2.activate(); + continueAfterFocus(rawWindow2); + }, + function() { + test.assertEqual(windows.activeWindow.title, window2.title, "Correct active window - 2"); + window3.activate(); + continueAfterFocus(rawWindow3); + }, + function() { + test.assertEqual(windows.activeWindow.title, window3.title, "Correct active window - 3"); + nonBrowserWindow.focus(); + finishTest(); + } + ]; + + windows.open({ + url: "data:text/html,<title>window 2</title>", + onOpen: function(window) { + window2 = window; + rawWindow2 = wm.getMostRecentWindow("navigator:browser"); + + windows.open({ + url: "data:text/html,<title>window 3</title>", + onOpen: function(window) { + window.tabs.activeTab.on('ready', function onReady() { + window3 = window; + rawWindow3 = wm.getMostRecentWindow("navigator:browser"); + nextStep() + }); + } + }); + } + }); + + function nextStep() { + if (testSteps.length > 0) + testSteps.shift()(); + } + + function continueAfterFocus(targetWindow) { + + // Based on SimpleTest.waitForFocus + var fm = Cc["@mozilla.org/focus-manager;1"]. + getService(Ci.nsIFocusManager); + + var childTargetWindow = {}; + fm.getFocusedElementForWindow(targetWindow, true, childTargetWindow); + childTargetWindow = childTargetWindow.value; + + var focusedChildWindow = {}; + if (fm.activeWindow) { + fm.getFocusedElementForWindow(fm.activeWindow, true, focusedChildWindow); + focusedChildWindow = focusedChildWindow.value; + } + + var focused = (focusedChildWindow == childTargetWindow); + if (focused) { + nextStep(); + } else { + childTargetWindow.addEventListener("focus", function focusListener() { + childTargetWindow.removeEventListener("focus", focusListener, true); + nextStep(); + }, true); + } + + } + + function finishTest() { + window3.close(function() { + window2.close(function() { + test.done(); + }); + }); + } +}; + +// If the module doesn't support the app we're being run in, require() will +// throw. In that case, remove all tests above from exports, and add one dummy +// test that passes. +try { + browserWindows = require("windows").browserWindows; +} +catch (err) { + // This bug should be mentioned in the error message. + let bug = "https://bugzilla.mozilla.org/show_bug.cgi?id=571449"; + if (err.message.indexOf(bug) < 0) + throw err; + for (let [prop, val] in Iterator(exports)) { + if (/^test/.test(prop) && typeof(val) === "function") + delete exports[prop]; + } + exports.testAppNotSupported = function (test) { + test.pass("the windows module does not support this application."); + }; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/README.md b/tools/addon-sdk-1.4/packages/api-utils/README.md new file mode 100644 index 0000000..e973e4c --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/README.md @@ -0,0 +1,31 @@ +API Utils provides a basic CommonJS infrastructure for +developing traditional XULRunner add-ons and applications. It is +the basis for the Add-on SDK. + +To address issues present in traditional add-on development, +API Utils provides mechanisms for: + +* writing and executing test cases, inspired by Python's [nose][] + package, +* tracking JS objects of interest to aid in memory profiling and leak + detection, +* registering callbacks that perform cleanup tasks when modules are + unloaded, +* easily reporting errors with full stack tracebacks. + +API Utils also has the following characteristics: + +* Beautiful, concise documentation. +* A rigorous test suite ensuring that the library doesn't break as the + Mozilla platform evolves. +* Solid developer ergonomics ensuring that developers can easily find + out why something they're doing isn't working. + +API Utils is intended to be very small and only contain the bare +minimum of functionality that all add-ons need. + +Note that the API Utils package has not fully stabilized yet, meaning that +we do still expect to make incompatible changes to its APIs in future releases +of the SDK. + + [nose]: http://code.google.com/p/python-nose/ diff --git a/tools/addon-sdk-1.4/packages/api-utils/data/content-proxy.js b/tools/addon-sdk-1.4/packages/api-utils/data/content-proxy.js new file mode 100644 index 0000000..6eadce9 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/data/content-proxy.js @@ -0,0 +1,834 @@ +"use strict"; + +let Ci = Components.interfaces; + +/** + * Access key that allows privileged code to unwrap proxy wrappers through + * valueOf: + * let xpcWrapper = proxyWrapper.valueOf(UNWRAP_ACCESS_KEY); + * This key should only be used by proxy unit test. + */ + const UNWRAP_ACCESS_KEY = {}; + + + /** + * Returns a closure that wraps arguments before calling the given function, + * which can be given to native functions that accept a function, such that when + * the closure is called, the given function is called with wrapped arguments. + * + * @param fun {Function} + * the function for which to create a closure wrapping its arguments + * @param obj {Object} + * target object from which `fun` comes from + * (optional, for debugging purpose) + * @param name {String} + * name of the attribute from which `fun` is binded on `obj` + * (optional, for debugging purpose) + * + * Example: + * function contentScriptListener(event) {} + * let wrapper = ContentScriptFunctionWrapper(contentScriptListener); + * xray.addEventListener("...", wrapper, false); + * -> Allow to `event` to be wrapped + */ +function ContentScriptFunctionWrapper(fun, obj, name) { + if ("___proxy" in fun && typeof fun.___proxy == "function") + return fun.___proxy; + + let wrappedFun = function () { + let args = []; + for (let i = 0, l = arguments.length; i < l; i++) + args.push(wrap(arguments[i])); + + //console.log("Called from native :"+obj+"."+name); + //console.log(">args "+arguments.length); + //console.log(fun); + + // Native code can execute this callback with `this` being the wrapped + // function. For example, window.mozRequestAnimationFrame. + if (this == wrappedFun) + return fun.apply(fun, args); + + return fun.apply(wrap(this), args); + }; + + Object.defineProperty(fun, "___proxy", {value : wrappedFun, + writable : false, + enumerable : false, + configurable : false}); + + return wrappedFun; +} + +/** + * Returns a closure that unwraps arguments before calling the `fun` function, + * which can be used to build a wrapper for a native function that accepts + * wrapped arguments, since native function only accept unwrapped arguments. + * + * @param fun {Function} + * the function to wrap + * @param originalObject {Object} + * target object from which `fun` comes from + * (optional, for debugging purpose) + * @param name {String} + * name of the attribute from which `fun` is binded on `originalObject` + * (optional, for debugging purpose) + * + * Example: + * wrapper.appendChild = NativeFunctionWrapper(xray.appendChild, xray); + * wrapper.appendChild(anotherWrapper); + * -> Allow to call xray.appendChild with unwrapped version of anotherWrapper + */ +function NativeFunctionWrapper(fun, originalObject, name) { + return function () { + let args = []; + let obj = this && typeof this.valueOf == "function" ? + this.valueOf(UNWRAP_ACCESS_KEY) : this; + + for (let i = 0, l = arguments.length; i < l; i++) + args.push( unwrap(arguments[i], obj, name) ); + + //if (name != "toString") + //console.log(">>calling native ["+(name?name:'#closure#')+"]: \n"+fun.apply+"\n"+obj+"\n("+args.join(', ')+")\nthis :"+obj+"from:"+originalObject+"\n"); + + // Need to use Function.prototype.apply.apply because XMLHttpRequest + // is a function (typeof return 'function') and fun.apply is null :/ + let unwrapResult = Function.prototype.apply.apply(fun, [obj, args]); + let result = wrap(unwrapResult, obj, name); + + //console.log("<< "+rr+" -> "+r); + + return result; + }; +} + +/* + * Unwrap a JS value that comes from the content script. + * Mainly converts proxy wrapper to XPCNativeWrapper. + */ +function unwrap(value, obj, name) { + //console.log("unwrap : "+value+" ("+name+")"); + if (!value) + return value; + let type = typeof value; + + // In case of proxy, unwrap them recursively + // (it should not be recursive, just in case of) + if (["object", "function"].indexOf(type) !== -1 && + "__isWrappedProxy" in value) { + while("__isWrappedProxy" in value) + value = value.valueOf(UNWRAP_ACCESS_KEY); + return value; + } + + // In case of functions we need to return a wrapper that converts native + // arguments applied to this function into proxies. + if (type == "function") + return ContentScriptFunctionWrapper(value, obj, name); + + // We must wrap objects coming from content script too, as they may have + // a function that will be called by a native method. + // For example: + // addEventListener(..., { handleEvent: function(event) {} }, ...); + if (type == "object") + return ContentScriptObjectWrapper(value); + + if (["string", "number", "boolean"].indexOf(type) !== -1) + return value; + //console.log("return non-wrapped to native : "+typeof value+" -- "+value); + return value; +} + +/** + * Returns an XrayWrapper proxy object that allow to wrap any of its function + * though `ContentScriptFunctionWrapper`. These proxies are given to + * XrayWrappers in order to automatically wrap values when they call a method + * of these proxies. So that they are only used internaly and content script, + * nor web page have ever access to them. As a conclusion, we can consider + * this code as being safe regarding web pages overload. + * + * + * @param obj {Object} + * object coming from content script context to wrap + * + * Example: + * let myListener = { handleEvent: function (event) {} }; + * node.addEventListener("click", myListener, false); + * `event` has to be wrapped, so handleEvent has to be wrapped using + * `ContentScriptFunctionWrapper` function. + * In order to do so, we build this new kind of proxies. + */ +function ContentScriptObjectWrapper(obj) { + if ("___proxy" in obj && typeof obj.___proxy == "object") + return obj.___proxy; + + function valueOf(key) { + if (key === UNWRAP_ACCESS_KEY) + return obj; + return this; + } + + let proxy = Proxy.create({ + // Fundamental traps + getPropertyDescriptor: function(name) { + return Object.getOwnPropertyDescriptor(obj, name); + }, + defineProperty: function(name, desc) { + return Object.defineProperty(obj, name, desc); + }, + getOwnPropertyNames: function () { + return Object.getOwnPropertyNames(obj); + }, + delete: function(name) { + return delete obj[name]; + }, + // derived traps + has: function(name) { + return name === "__isXrayWrapperProxy" || + name in obj; + }, + hasOwn: function(name) { + return Object.prototype.hasOwnProperty.call(obj, name); + }, + get: function(receiver, name) { + if (name == "valueOf") + return valueOf; + let value = obj[name]; + if (!value) + return value; + + return unwrap(value); + }, + set: function(receiver, name, val) { + obj[name] = val; + return true; + }, + enumerate: function() { + var result = []; + for each (let name in obj) { + result.push(name); + }; + return result; + }, + keys: function() { + return Object.keys(obj); + } + }); + + Object.defineProperty(obj, "___proxy", {value : proxy, + writable : false, + enumerable : false, + configurable : false}); + + return proxy; +} + +// List of all existing typed arrays. +// Can be found here: +// http://mxr.mozilla.org/mozilla-central/source/js/src/jsapi.cpp#1790 +const typedArraysCtor = [ + ArrayBuffer, + Int8Array, + Uint8Array, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + Uint8ClampedArray +]; + +/* + * Wrap a JS value coming from the document by building a proxy wrapper. + */ +function wrap(value, obj, name, debug) { + if (!value) + return value; + let type = typeof value; + if (type == "object") { + // Bug 671016: Typed arrays don't need to be proxified. + // We avoid checking the whole constructor list on all objects + // by doing this check only on non-extensible objects: + if (!Object.isExtensible(value) && + typedArraysCtor.indexOf(value.constructor) !== -1) + return value; + + // We may have a XrayWrapper proxy. + // For example: + // let myListener = { handleEvent: function () {} }; + // node.addEventListener("click", myListener, false); + // When native code want to call handleEvent, + // we go though ContentScriptFunctionWrapper that calls `wrap(this)` + // `this` is the XrayWrapper proxy of myListener. + // We return this object without building a CS proxy as it is already + // a value coming from the CS. + if ("__isXrayWrapperProxy" in value) + return value.valueOf(UNWRAP_ACCESS_KEY); + + // Unwrap object before wrapping it. + // It should not happen with CS proxy objects. + while("__isWrappedProxy" in value) { + value = value.valueOf(UNWRAP_ACCESS_KEY); + } + + if (XPCNativeWrapper.unwrap(value) !== value) + return getProxyForObject(value); + // In case of Event, HTMLCollection or NodeList or ??? + // XPCNativeWrapper.unwrap(value) === value + // but it's still a XrayWrapper so let's build a proxy + return getProxyForObject(value); + } + if (type == "function") { + if (XPCNativeWrapper.unwrap(value) !== value + || (typeof value.toString === "function" && + value.toString().match(/\[native code\]/))) { + return getProxyForFunction(value, NativeFunctionWrapper(value, obj, name)); + } + return value; + } + if (type == "string") + return value; + if (type == "number") + return value; + if (type == "boolean") + return value; + //console.log("return non-wrapped to wrapped : "+value); + return value; +} + +/* + * Wrap an object from the document to a proxy wrapper + */ +function getProxyForObject(obj) { + if (typeof obj != "object") { + let msg = "tried to proxify something other than an object: " + typeof obj; + console.warn(msg); + throw msg; + } + if ("__isWrappedProxy" in obj) { + return obj; + } + // Check if there is a proxy cached on this wrapper, + // but take care of prototype ___proxy attribute inheritance! + if (obj && obj.___proxy && obj.___proxy.valueOf(UNWRAP_ACCESS_KEY) === obj) { + return obj.___proxy; + } + + let proxy = Proxy.create(handlerMaker(obj)); + + Object.defineProperty(obj, "___proxy", {value : proxy, + writable : false, + enumerable : false, + configurable : false}); + return proxy; +} + +/* + * Wrap a function from the document to a proxy wrapper + */ +function getProxyForFunction(fun, callTrap) { + if (typeof fun != "function") { + let msg = "tried to proxify something other than a function: " + typeof fun; + console.warn(msg); + throw msg; + } + if ("__isWrappedProxy" in fun) + return obj; + if ("___proxy" in fun) + return fun.___proxy; + + let proxy = Proxy.createFunction(handlerMaker(fun), callTrap); + + Object.defineProperty(fun, "___proxy", {value : proxy, + writable : false, + enumerable : false, + configurable : false}); + + return proxy; +} + +/* + * Check if a DOM attribute name is an event name. + */ +function isEventName(id) { + if (id.indexOf("on") != 0 || id.length == 2) + return false; + // Taken from: + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#7616 + switch (id[2]) { + case 'a' : + return (id == "onabort" || + id == "onafterscriptexecute" || + id == "onafterprint"); + case 'b' : + return (id == "onbeforeunload" || + id == "onbeforescriptexecute" || + id == "onblur" || + id == "onbeforeprint"); + case 'c' : + return (id == "onchange" || + id == "onclick" || + id == "oncontextmenu" || + id == "oncopy" || + id == "oncut" || + id == "oncanplay" || + id == "oncanplaythrough"); + case 'd' : + return (id == "ondblclick" || + id == "ondrag" || + id == "ondragend" || + id == "ondragenter" || + id == "ondragleave" || + id == "ondragover" || + id == "ondragstart" || + id == "ondrop" || + id == "ondurationchange"); + case 'e' : + return (id == "onerror" || + id == "onemptied" || + id == "onended"); + case 'f' : + return id == "onfocus"; + case 'h' : + return id == "onhashchange"; + case 'i' : + return (id == "oninput" || + id == "oninvalid"); + case 'k' : + return (id == "onkeydown" || + id == "onkeypress" || + id == "onkeyup"); + case 'l' : + return (id == "onload" || + id == "onloadeddata" || + id == "onloadedmetadata" || + id == "onloadstart"); + case 'm' : + return (id == "onmousemove" || + id == "onmouseout" || + id == "onmouseover" || + id == "onmouseup" || + id == "onmousedown" || + id == "onmessage"); + case 'p' : + return (id == "onpaint" || + id == "onpageshow" || + id == "onpagehide" || + id == "onpaste" || + id == "onpopstate" || + id == "onpause" || + id == "onplay" || + id == "onplaying" || + id == "onprogress"); + case 'r' : + return (id == "onreadystatechange" || + id == "onreset" || + id == "onresize" || + id == "onratechange"); + case 's' : + return (id == "onscroll" || + id == "onselect" || + id == "onsubmit" || + id == "onseeked" || + id == "onseeking" || + id == "onstalled" || + id == "onsuspend"); + case 't': + return id == "ontimeupdate" + /* + // TODO: Make it work for mobile version + || + (nsDOMTouchEvent::PrefEnabled() && + (id == "ontouchstart" || + id == "ontouchend" || + id == "ontouchmove" || + id == "ontouchenter" || + id == "ontouchleave" || + id == "ontouchcancel"))*/; + + case 'u' : + return id == "onunload"; + case 'v': + return id == "onvolumechange"; + case 'w': + return id == "onwaiting"; + } + + return false; +} + +// XrayWrappers miss some attributes. +// Here is a list of functions that return a value when it detects a miss: +const NODES_INDEXED_BY_NAME = ["IMG", "FORM", "APPLET", "EMBED", "OBJECT"]; +const xRayWrappersMissFixes = [ + + // Fix bug with XPCNativeWrapper on HTMLCollection + // We can only access array item once, then it's undefined :o + function (obj, name) { + let i = parseInt(name); + if (obj.toString().match(/HTMLCollection|NodeList/) && + i >= 0 && i < obj.length) { + return wrap(XPCNativeWrapper(obj.wrappedJSObject[name]), obj, name); + } + return null; + }, + + // Trap access to document["form name"] + // that may refer to an existing form node + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#9285 + function (obj, name) { + if ("nodeType" in obj && obj.nodeType == 9) { + let node = obj.wrappedJSObject[name]; + // List of supported tag: + // http://mxr.mozilla.org/mozilla-central/source/content/html/content/src/nsGenericHTMLElement.cpp#1267 + if (node && NODES_INDEXED_BY_NAME.indexOf(node.tagName) != -1) + return wrap(XPCNativeWrapper(node)); + } + return null; + }, + + // Trap access to window["frame name"] and window.frames[i] + // that refer to an (i)frame internal window object + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#6824 + function (obj, name) { + if (typeof obj == "object" && "document" in obj) { + // Ensure that we are on a window object + try { + obj.QueryInterface(Ci.nsIDOMWindow); + } + catch(e) { + return null; + } + + // Integer case: + let i = parseInt(name); + if (i >= 0 && i in obj) { + return wrap(XPCNativeWrapper(obj[i])); + } + + // String name case: + if (name in obj.wrappedJSObject) { + let win = obj.wrappedJSObject[name]; + let nodes = obj.document.getElementsByName(name); + for (let i = 0, l = nodes.length; i < l; i++) { + let node = nodes[i]; + if ("contentWindow" in node && node.contentWindow == win) + return wrap(node.contentWindow); + } + } + } + return null; + }, + + // Trap access to form["node name"] + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#9477 + function (obj, name) { + if (typeof obj == "object" && obj.tagName == "FORM") { + let match = obj.wrappedJSObject[name]; + let nodes = obj.ownerDocument.getElementsByName(name); + for (let i = 0, l = nodes.length; i < l; i++) { + let node = nodes[i]; + if (node == match) + return wrap(node); + } + } + return null; + }, + + // Fix XPathResult's constants being undefined on XrayWrappers + // these constants are defined here: + // http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/xpath/nsIDOMXPathResult.idl + // and are only numbers. + // The platform bug 665279 was fixed in Gecko 10.0a1. + // FIXME: remove this workaround once the SDK no longer supports Firefox 9. + function (obj, name) { + if (typeof obj == "object" && name in Ci.nsIDOMXPathResult) { + let value = Ci.nsIDOMXPathResult[name]; + if (typeof value == "number" && value === obj.wrappedJSObject[name]) + return value; + } + return null; + } + +]; + +// XrayWrappers have some buggy methods. +// Here is the list of them with functions returning some replacement +// for a given object `obj`: +const xRayWrappersMethodsFixes = { + // postMessage method is checking the Javascript global + // and it expects it to be a window object. + // But in our case, the global object is our sandbox global object. + // See nsGlobalWindow::CallerInnerWindow(): + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsGlobalWindow.cpp#5893 + // nsCOMPtr<nsPIDOMWindow> win = do_QueryWrappedNative(wrapper); + // win is null + postMessage: function (obj) { + // Ensure that we are on a window object + try { + obj.QueryInterface(Ci.nsIDOMWindow); + } + catch(e) { + return null; + } + // Create a wrapper that is going to call `postMessage` through `eval` + let f = function postMessage(message, targetOrigin) { + message = message.toString().replace(/['\\]/g,"\\$&"); + targetOrigin = targetOrigin.toString().replace(/['\\]/g,"\\$&"); + + let jscode = "this.postMessage('" + message + "', '" + + targetOrigin + "')"; + return this.wrappedJSObject.eval(jscode); + }; + return getProxyForFunction(f, NativeFunctionWrapper(f)); + }, + + // Fix mozMatchesSelector uses that is broken on XrayWrappers + // when we use document.documentElement.mozMatchesSelector.call(node, expr) + // It's only working if we call mozMatchesSelector on the node itself. + // SEE BUG 658909: mozMatchesSelector returns incorrect results with XrayWrappers + mozMatchesSelector: function (obj) { + // Ensure that we are on an object to expose this buggy method + try { + // Bug 707576 removed nsIDOMNSElement. + // Can be simplified as soon as Firefox 11 become the minversion + obj.QueryInterface("nsIDOMElement" in Ci ? Ci.nsIDOMElement : + Ci.nsIDOMNSElement); + } + catch(e) { + return null; + } + // We can't use `wrap` function as `f` is not a native function, + // so wrap it manually: + let f = function mozMatchesSelector(selectors) { + return this.mozMatchesSelector(selectors); + }; + + return getProxyForFunction(f, NativeFunctionWrapper(f)); + }, + + // Bug 679054: History API doesn't work with Proxy objects. We have to pass + // regular JS objects on `pushState` and `replaceState` methods. + // In addition, the first argument has to come from the same compartment. + pushState: function (obj) { + // Ensure that we are on an object that expose History API + try { + obj.QueryInterface(Ci.nsIDOMHistory); + } + catch(e) { + return null; + } + let f = function fix() { + // Call native method with JSON objects + // (need to convert `arguments` to an array via `slice`) + return this.pushState.apply(this, JSON.parse(JSON.stringify(Array.slice(arguments)))); + }; + + return getProxyForFunction(f, NativeFunctionWrapper(f)); + }, + replaceState: function (obj) { + // Ensure that we are on an object that expose History API + try { + obj.QueryInterface(Ci.nsIDOMHistory); + } + catch(e) { + return null; + } + let f = function fix() { + // Call native method with JSON objects + // (need to convert `arguments` to an array via `slice`) + return this.replaceState.apply(this, JSON.parse(JSON.stringify(Array.slice(arguments)))); + }; + + return getProxyForFunction(f, NativeFunctionWrapper(f)); + } +}; + +/* + * Generate handler for proxy wrapper + */ +function handlerMaker(obj) { + // Overloaded attributes dictionary + let overload = {}; + // Expando attributes dictionary (i.e. onclick, onfocus, on* ...) + let expando = {}; + // Cache of methods overloaded to fix XrayWrapper bug + let methodFixes = {}; + return { + // Fundamental traps + getPropertyDescriptor: function(name) { + return Object.getOwnPropertyDescriptor(obj, name); + }, + defineProperty: function(name, desc) { + return Object.defineProperty(obj, name, desc); + }, + getOwnPropertyNames: function () { + return Object.getOwnPropertyNames(obj); + }, + delete: function(name) { + delete expando[name]; + delete overload[name]; + return delete obj[name]; + }, + + // derived traps + has: function(name) { + if (name == "___proxy") return false; + if (isEventName(name)) { + // XrayWrappers throw exception when we try to access expando attributes + // even on "name in wrapper". So avoid doing it! + return name in expando; + } + return name in obj || name in overload || name == "__isWrappedProxy" || + undefined !== this.get(null, name); + }, + hasOwn: function(name) { + return Object.prototype.hasOwnProperty.call(obj, name); + }, + get: function(receiver, name) { + if (name == "___proxy") + return undefined; + + // Overload toString in order to avoid returning "[XrayWrapper [object HTMLElement]]" + // or "[object Function]" for function's Proxy + if (name == "toString") { + if ("wrappedJSObject" in obj) { + // Bug 714778: we should not pass obj.wrappedJSObject.toString + // in order to avoid sharing its proxy over contents scripts: + return wrap(function () { + return obj.wrappedJSObject.toString.call( + this.valueOf(UNWRAP_ACCESS_KEY), arguments); + }, obj, name); + } + else { + return wrap(obj.toString, obj, name); + } + } + + // Offer a way to retrieve XrayWrapper from a proxified node through `valueOf` + if (name == "valueOf") + return function (key) { + if (key === UNWRAP_ACCESS_KEY) + return obj; + return this; + }; + + // Return overloaded value if there is one. + // It allows to overload native methods like addEventListener that + // are not saved, even on the wrapper itself. + // (And avoid some methods like toSource from being returned here! [__proto__ test]) + if (name in overload && + overload[name] != Object.getPrototypeOf(overload)[name] && + name != "__proto__") { + return overload[name]; + } + + // Catch exceptions thrown by XrayWrappers when we try to access on* + // attributes like onclick, onfocus, ... + if (isEventName(name)) { + //console.log("expando:"+obj+" - "+obj.nodeType); + return name in expando ? expando[name].original : undefined; + } + + // Overload some XrayWrappers method in order to fix its bugs + if (name in methodFixes && + methodFixes[name] != Object.getPrototypeOf(methodFixes)[name] && + name != "__proto__") + return methodFixes[name]; + if (Object.keys(xRayWrappersMethodsFixes).indexOf(name) !== -1) { + let fix = xRayWrappersMethodsFixes[name](obj); + if (fix) + return methodFixes[name] = fix; + } + + let o = obj[name]; + + // XrayWrapper miss some attributes, try to catch these and return a value + if (!o) { + for each(let atttributeFixer in xRayWrappersMissFixes) { + let fix = atttributeFixer(obj, name); + if (fix) + return fix; + } + } + + // Generic case + return wrap(o, obj, name); + + }, + + set: function(receiver, name, val) { + + if (isEventName(name)) { + //console.log("SET on* attribute : " + name + " / " + val + "/" + obj); + let shortName = name.replace(/^on/,""); + + // Unregister previously set listener + if (expando[name]) { + obj.removeEventListener(shortName, expando[name], true); + delete expando[name]; + } + + // Only accept functions + if (typeof val != "function") + return false; + + // Register a new listener + let original = val; + val = ContentScriptFunctionWrapper(val); + expando[name] = val; + val.original = original; + obj.addEventListener(name.replace(/^on/, ""), val, true); + return true; + } + + obj[name] = val; + + // Handle native method not overloaded on XrayWrappers: + // obj.addEventListener = val; -> obj.addEventlistener = native method + // And, XPCNativeWrapper bug where nested values appear to be wrapped: + // obj.customNestedAttribute = val -> obj.customNestedAttribute !== val + // obj.customNestedAttribute = "waive wrapper something" + // SEE BUG 658560: Fix identity problem with CrossOriginWrappers + // TODO: check that DOM can't be updated by the document itself and so overloaded value becomes wrong + // but I think such behavior is limited to primitive type + if ((typeof val == "function" || typeof val == "object") && name) { + overload[name] = val; + } + + return true; + }, + + enumerate: function() { + var result = []; + for each (let name in Object.keys(obj)) { + result.push(name); + }; + return result; + }, + + keys: function() { + return Object.keys(obj); + } + }; +}; + + +/* + * Wrap an object from the document to a proxy wrapper. + */ +function create(object) { + if ("wrappedJSObject" in object) + object = object.wrappedJSObject; + let xpcWrapper = XPCNativeWrapper(object); + // If we can't build an XPCNativeWrapper, it doesn't make sense to build + // a proxy. All proxy code is based on having such wrapper that store + // different JS attributes set. + // (we can't build XPCNativeWrapper when object is from the same + // principal/domain) + if (object === xpcWrapper) { + return object; + } + return getProxyForObject(xpcWrapper); +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/data/test-content-symbiont.js b/tools/addon-sdk-1.4/packages/api-utils/data/test-content-symbiont.js new file mode 100644 index 0000000..808af37 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/data/test-content-symbiont.js @@ -0,0 +1 @@ +// test-content-symbiont diff --git a/tools/addon-sdk-1.4/packages/api-utils/data/test-httpd.txt b/tools/addon-sdk-1.4/packages/api-utils/data/test-httpd.txt new file mode 100644 index 0000000..7956d3a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/data/test-httpd.txt @@ -0,0 +1 @@ +This is the HTTPD test file. diff --git a/tools/addon-sdk-1.4/packages/api-utils/data/test-trusted-document.html b/tools/addon-sdk-1.4/packages/api-utils/data/test-trusted-document.html new file mode 100644 index 0000000..166009d --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/data/test-trusted-document.html @@ -0,0 +1,13 @@ +<html>
+<head>
+ <title>Worker test</title>
+</head>
+<body>
+ <p id="paragraph">Lorem ipsum dolor sit amet.</p>
+ <script>
+ addon.port.on('addon-to-document', function (arg) {
+ addon.port.emit('document-to-addon', arg);
+ });
+ </script>
+</body>
+</html>
diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/api-utils.md b/tools/addon-sdk-1.4/packages/api-utils/docs/api-utils.md new file mode 100644 index 0000000..d87acdb --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/api-utils.md @@ -0,0 +1,153 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `api-utils` module provides some helpers useful to the SDK's high-level API +implementations. + +Introduction +------------ + +The SDK high-level API design guidelines make a number of recommendations. +This module implements some of those patterns so that your own implementations +don't need to reinvent them. + +For example, public constructors should be callable both with and without the +`new` keyword. Your module can implement this recommendation using the +`publicConstructor` function. + +Options objects or "dictionaries" are also common throughout the high-level +APIs. The guidelines recommend that public constructors should generally define +a single `options` parameter rather than defining many parameters. Since one of +the SDK's principles is to be friendly to developers, ideally all properties on +options dictionaries should be checked for correct type, and informative error +messages should be generated when clients make mistakes. With the +`validateOptions` function, your module can easily do so. + +And objects are sometimes iterable over a custom set of key/value pairs. +Such objects should have custom iterators that let consumers iterate keys, +values, or [key, value] pairs. The `addIterator` function makes it easy to do +so in a way that is consistent with the behavior of default iterators during +`for...in`, `for each...in`, and `for...in Iterator()` loops. + +<api name="publicConstructor"> +@function +Returns a function *C* that creates an instance of `privateConstructor`. *C* +may be called with or without the `new` keyword. + +The prototype of each instance returned from *C* is *C*.`prototype`, and +*C*.`prototype` is an object whose prototype is +`privateConstructor.prototype`. Instances returned from *C* are therefore +instances of both *C* and `privateConstructor`. + +Additionally, the constructor of each instance returned from *C* is *C*. + +Instances returned from *C* are automatically memory tracked using +`memory.track` under the bin name `privateConstructor.name`. + +**Example** + + function MyObject() {} + exports.MyObject = apiUtils.publicConstructor(MyObject); + +@returns {function} +A function that makes new instances of `privateConstructor`. + +@param privateConstructor {constructor} +</api> + +<api name="validateOptions"> +@function +A function to validate an options dictionary according to the specified +constraints. + +`map`, `is`, and `ok` are used in that order. + +The return value is an object whose keys are those keys in `requirements` that +are also in `options` and whose values are the corresponding return values of +`map` or the corresponding values in `options`. Note that any keys not shared +by both `requirements` and `options` are not in the returned object. + +**Examples** + +A typical use: + + var opts = { foo: 1337 }; + var requirements = { + foo: { + map: function (val) val.toString(), + is: ["string"], + ok: function (val) val.length > 0, + msg: "foo must be a non-empty string." + } + }; + var validatedOpts = apiUtils.validateOptions(opts, requirements); + // validatedOpts == { foo: "1337" } + +If the key `foo` is optional and doesn't need to be mapped: + + var opts = { foo: 1337 }; + var validatedOpts = apiUtils.validateOptions(opts, { foo: {} }); + // validatedOpts == { foo: 1337 } + + opts = {}; + validatedOpts = apiUtils.validateOptions(opts, { foo: {} }); + // validatedOpts == {} + +@returns {object} +A validated options dictionary given some requirements. If any of the +requirements are not met, an exception is thrown. + +@param options {object} +The options dictionary to validate. It's not modified. If it's null or +otherwise falsey, an empty object is assumed. + +@param requirements {object} +An object whose keys are the expected keys in `options`. Any key in +`options` that is not present in `requirements` is ignored. Each +value in `requirements` is itself an object describing the requirements +of its key. The keys of that object are the following, and each is optional: + +@prop [map] {function} +A function that's passed the value of the key in the `options`. `map`'s +return value is taken as the key's value in the final validated options, +`is`, and `ok`. If `map` throws an exception it is caught and discarded, +and the key's value is its value in `options`. + +@prop [is] {array} +An array containing the number of `typeof` type names. If the key's value is +none of these types it fails validation. Arrays and nulls are identified by +the special type names "array" and "null"; "object" will not match either. +No type coercion is done. + +@prop [ok] {function} +A function that is passed the key's value. If it returns false, the value +fails validation. + +@prop [msg] {string} +If the key's value fails validation, an exception is thrown. This string +will be used as its message. If undefined, a generic message is used, unless +`is` is defined, in which case the message will state that the value needs to +be one of the given types. +</api> + +<api name="addIterator"> +@function +Adds an iterator to the specified object that iterates keys, values, +or [key, value] pairs depending on how it is invoked, i.e.: + + for (var key in obj) { ... } // iterate keys + for each (var val in obj) { ... } // iterate values + for (var [key, val] in Iterator(obj)) { ... } // iterate pairs + +If your object only iterates either keys or values, you don't need this +function. Simply assign a generator function that iterates the keys/values +to your object's `__iterator__` property instead, f.e.: + + obj.__iterator__ = function () { for each (var i in items) yield i; } + +@param obj {object} +the object to which to add the iterator + +@param keysValsGen {function} +a generator function that yields [key, value] pairs +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/app-strings.md b/tools/addon-sdk-1.4/packages/api-utils/docs/app-strings.md new file mode 100644 index 0000000..3e4d358 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/app-strings.md @@ -0,0 +1,61 @@ +The `app-strings` module gives you access to the host application's localized +string bundles (`.properties` files). + +The module exports the `StringBundle` constructor function. To access a string +bundle, construct an instance of `StringBundle`, passing it the bundle's URL: + + var StringBundle = require("app-strings").StringBundle; + var bundle = StringBundle("chrome://browser/locale/browser.properties"); + +To get the value of a string, call the object's `get` method, passing it +the name of the string: + + var accessKey = bundle.get("contextMenuSearchText.accesskey"); + // "S" in the en-US locale + +To get the formatted value of a string that accepts arguments, call the object's +`get` method, passing it the name of the string and an array of arguments +with which to format the string: + + var searchText = bundle.get("contextMenuSearchText", + ["universe", "signs of intelligent life"]); + // 'Search universe for "signs of intelligent life"' in the en-US locale + +To get all strings in the bundle, iterate the object, which returns arrays +of the form [name, value]: + + for (var [name, value] in Iterator(bundle)) + console.log(name + " = " + value); + +Iteration +--------- + +<code>for (var name in bundle) { ... }</code> + +Iterate the names of strings in the bundle. + +<code>for each (var val in bundle) { ... }</code> + +Iterate the values of strings in the bundle. + +<code>for (var [name, value] in Iterator(bundle)) { ... }</code> + +Iterate the names and values of strings in the bundle. + + +<api name="StringBundle"> +@class +The `StringBundle` object represents a string bundle. +<api name="StringBundle"> +@constructor +Creates a StringBundle object that gives you access to a string bundle. +@param url {string} the URL of the string bundle +@returns {StringBundle} the string bundle +</api> +<api name="get"> +@method Get the value of the string with the given name. +@param [name] {string} the name of the string to get +@param [args] {array} (optional) strings that replace placeholders in the string +@returns {string} the value of the string +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/byte-streams.md b/tools/addon-sdk-1.4/packages/api-utils/docs/byte-streams.md new file mode 100644 index 0000000..f7cffe8 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/byte-streams.md @@ -0,0 +1,64 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `byte-streams` module provides streams for reading and writing bytes. + +<api name="ByteReader"> +@class +<api name="ByteReader"> +@constructor + Creates a binary input stream that reads bytes from a backing stream. +@param inputStream {stream} + The backing stream, an <a href="http://mxr.mozilla.org/mozilla-central/ +source/xpcom/io/nsIInputStream.idl"><code>nsIInputStream</code></a>. +</api> +<api name="closed"> +@property {boolean} + True if the stream is closed. +</api> + +<api name="close"> +@method + Closes both the stream and its backing stream. If the stream is already + closed, an exception is thrown. +</api> + +<api name="read"> +@method + Reads a string from the stream. If the stream is closed, an exception is + thrown. +@param [numBytes] {number} + The number of bytes to read. If not given, the remainder of the entire stream + is read. +@returns {string} + A string containing the bytes read. If the stream is at the end, returns the + empty string. +</api> +</api> + +<api name="ByteWriter"> +@class +<api name="ByteWriter"> +@constructor + Creates a binary output stream that writes bytes to a backing stream. +@param outputStream {stream} + The backing stream, an <a href="http://mxr.mozilla.org/mozilla-central/ +source/xpcom/io/nsIOutputStream.idl"><code>nsIOutputStream</code></a>. +</api> +<api name="closed"> +@property {boolean} + True if the stream is closed. +</api> +<api name="close"> +@method + Closes both the stream and its backing stream. If the stream is already + closed, an exception is thrown. +</api> +<api name="write"> +@method + Writes a string to the stream. If the stream is closed, an exception is + thrown. +@param str {string} + The string to write. +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/collection.md b/tools/addon-sdk-1.4/packages/api-utils/docs/collection.md new file mode 100644 index 0000000..796289a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/collection.md @@ -0,0 +1,73 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `collection` module provides a simple list-like class and utilities for +using it. A collection is ordered, like an array, but its items are unique, +like a set. + +<api name="Collection"> +@class +A collection object provides for...in-loop iteration. Items are yielded in the +order they were added. For example, the following code... + + var collection = require("collection"); + var c = new collection.Collection(); + c.add(1); + c.add(2); + c.add(3); + for (item in c) + console.log(item); + +... would print this to the console: + +<pre> + 1 + 2 + 3 +</pre> + +Iteration proceeds over a copy of the collection made before iteration begins, +so it is safe to mutate the collection during iteration; doing so does not +affect the results of the iteration. + +<api name="Collection"> +@constructor +Creates a new collection. The collection is backed by an array. +@param [array] {array} +If *array* is given, it will be used as the backing array. This way the caller +can fully control the collection. Otherwise a new empty array will be used, and +no one but the collection will have access to it. +</api> +<api name="length"> +@property {number} +The number of items in the collection array. +</api> +<api name="add"> +@method +Adds a single item or an array of items to the collection. Any items already +contained in the collection are ignored. +@param itemOrItems {object} An item or array of items. +@returns {Collection} The Collection. +</api> +<api name="remove"> +@method +Removes a single item or an array of items from the collection. Any items not +contained in the collection are ignored. +@param itemOrItems {object} An item or array of items. +@returns {Collection} The Collection. +</api> +</api> + +<api name="addCollectionProperty"> +@function +Adds a collection property to the given object. Setting the property to a +scalar value empties the collection and adds the value. Setting it to an array +empties the collection and adds all the items in the array. +@param object {object} +The property will be defined on this object. +@param propName {string} +The name of the property. +@param [backingArray] {array} +If given, this will be used as the collection's backing array. +</api> + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/content.md b/tools/addon-sdk-1.4/packages/api-utils/docs/content.md new file mode 100644 index 0000000..b258d02 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/content.md @@ -0,0 +1,11 @@ +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `content` module exports three different traits [Loader][], [Worker][] and +[Symbiont][]. None of this traits is intended to be used directly by programs. +Rather, they are intended to be used by other modules that provide high +level APIs to programs or libraries. + +[Loader]:packages/api-utils/docs/content/loader.html +[Worker]:packages/api-utils/docs/content/worker.html +[Symbiont]:packages/api-utils/docs/content/symbiont.html + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/content/loader.md b/tools/addon-sdk-1.4/packages/api-utils/docs/content/loader.md new file mode 100644 index 0000000..83b6276 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/content/loader.md @@ -0,0 +1,88 @@ +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +Loader is base trait and it provides set of core properties and associated +validations. Trait is useful for all the compositions providing high level +APIs for creating JavaScript contexts that can access web content. + +Loader is composed from the +[EventEmitter](packages/api-utils/docs/events.html) trait, therefore +instances of Loader and their descendants expose all the public properties +exposed by EventEmitter along with additional public properties: + +Value changes on all of the above mentioned properties emit `propertyChange` +events on an instances. + +**Example:** + +The following code creates a wrapper on hidden frame that reloads a web page +in frame every time `contentURL` property is changed: + + var hiddenFrames = require("hidden-frame"); + var { Loader } = require("content"); + var PageLoader = Loader.compose({ + constructor: function PageLoader(options) { + options = options || {}; + if (options.contentURL) + this.contentURL = options.contentURL; + this.on('propertyChange', this._onChange = this._onChange.bind(this)); + let self = this; + hiddenFrames.add(hiddenFrames.HiddenFrame({ + onReady: function onReady() { + let frame = self._frame = this.element; + self._emit('propertyChange', { contentURL: self.contentURL }); + } + })); + }, + _onChange: function _onChange(e) { + if ('contentURL' in e) + this._frame.setAttribute('src', this._contentURL); + } + }); + +<api name="Loader"> +@class +<api name="contentScriptFile"> +@property {array} +The local file URLs of content scripts to load. Content scripts specified by +this property are loaded *before* those specified by the `contentScript` +property. +</api> + +<api name="contentScript"> +@property {array} +The texts of content scripts to load. Content scripts specified by this +property are loaded *after* those specified by the `contentScriptFile` property. +</api> + +<api name="contentScriptWhen"> +@property {string} +When to load the content scripts. This may take one of the following +values: + +* "start": load content scripts immediately after the document +element for the page is inserted into the DOM, but before the DOM content +itself has been loaded +* "ready": load content scripts once DOM content has been loaded, +corresponding to the +[DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) +event +* "end": load content scripts once all the content (DOM, JS, CSS, +images) for the page has been loaded, at the time the +[window.onload event](https://developer.mozilla.org/en/DOM/window.onload) +fires + +</api> + +<api name="contentURL"> +@property {string} +The URL of the content loaded. +</api> + +<api name="allow"> +@property {object} +Permissions for the content, with the following keys: +@prop script {boolean} + Whether or not to execute script in the content. Defaults to true. +</api> +</api> + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/content/proxy.md b/tools/addon-sdk-1.4/packages/api-utils/docs/content/proxy.md new file mode 100644 index 0000000..012fbc7 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/content/proxy.md @@ -0,0 +1,237 @@ +<!-- contributed by Alexandre Poirot [apoirot@mozilla.com] --> + +Content scripts need access to the DOM of the pages they are attached to. +However, those pages should be considered to be hostile environments: we +have no control over any other scripts loaded by the web page that may be +executing in the same context. If the content scripts and scripts loaded +by the web page were to access the same DOM objects, there are two possible +security problems: + +First, a malicious page might redefine functions and properties of DOM +objects so they don't do what the add-on expects. For example, if a +content script calls `document.getElementById()` to retrieve a DOM +element, then a malicious page could redefine its behavior to return +something unexpected: + +<pre><code> +// If the web document contains the following script: +document.getElementById = function (str) { + // Overload indexOf method of all string instances + str.__proto__.indexOf = function () {return -1;}; + // Overload toString method of all object instances + str.__proto__.__proto__.toString = function () {return "evil";}; +}; +// After the following line, the content script will be compromised: +var node = document.getElementById("element"); +// Then your content script is totally out of control. +</code></pre> + +Second, changes the content script made to the DOM objects would be visible +to the page, leaking information to it. + +The general approach to fixing these problems is to wrap DOM objects in +[`XrayWrappers`](https://developer.mozilla.org/en/XPCNativeWrapper) +(also know as `XPCNativeWrapper`). This guarantees that: + +* when the content script accesses DOM properties and functions it gets the +original native version of them, ignoring any modifications made by the web +page +* changes to the DOM made by the content script are not visible to scripts +running in the page. + +However, `XrayWrapper` has some limitations and bugs, which break many +popular web frameworks. In particular, you can't: + +* define attributes like `onclick`: you have to use `addEventListener` syntax +* overload native methods on DOM objects, like this: +<pre><code> +proxy.addEventListener = function () {}; +</code></pre> +* access named elements using properties like `window[framename]` or +`document[formname]` +* use some other features that have bugs in the `XrayWrapper` +implementation, like `mozMatchesSelector` + +The `proxy` module uses `XrayWrapper` in combination with the +experimental +[Proxy API](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Proxy) +to address both the security vulnerabilities of content scripts and the +limitations of `XrayWrapper`. + +<pre> + /--------------------\ /------------------------\ + | Web document | | Content script sandbox | + | http://mozilla.org | | data/worker.js | + | | require('content-proxy'). | | + | window >-----------|- create(window) -|-> window | + \--------------------/ \------------------------/ +</pre> + + +## The Big Picture ## + +The implementation defines two different kinds of proxy: + + 1. Content script proxies that wrap DOM objects that are exposed to + content scripts as described above. + 2. XrayWrapper proxies that wrap objects from content scripts before handing + them over to XrayWrapper functions. These proxies are internal + and are not exposed to content scripts or document content. + +<pre> + /--------------------\ /------------------------\ + | Web document | | Content script sandbox | + | http://mozilla.org | | data/worker.js | + | | /-------|-> myObject = {} | + | | /----------------v--\ | | + | | | XrayWrapper Proxy | | - document | + | | \---------v---------/ \----^-------------------/ + | | v | + | | /-------------\ /----------\ | + | - document >-------|->| XrayWrapper |<-| CS proxy |-/ + \--------------------/ \-------------/ \----------/ +</pre> + +Everything begins with a single call to the `create` function exported by the +content-proxy module: + + // Retrieve the unwrapped reference to the current web page window object + var win = gBrowser.contentDocument.defaultView.wrappedJSObject; + // Or in addon sdk style + var win = require("tab-browser").activeTab.linkedBrowser.contentWindow.wrappedJSObject; + // Now create a content script proxy for the window object + var windowProxy = require("api-utils/content/content-proxy").create(win); + + // We finally use this window object as sandbox prototype, + // so that all web page globals are accessible in CS too: + var contentScriptSandbox = new Cu.Sandbox(win, { + sandboxPrototype: windowProxy + }); + +Then all other proxies are created from this one. Attempts to access DOM +attributes of this proxy are trapped, and the proxy constructs and returns +content script proxies for those attributes: + + // For example, if you simply do this: + var document = window.document; + // accessing the `document` attribute will be trapped by the `window` content script + // proxy, and that proxy will that create another content script proxy for `document` + +So the main responsibility of the content script proxy implementation is to +ensure that we always return content script proxies to the content script. + +## Internal Implementation ## + +Each content script proxy keeps a reference to the `XrayWrapper` that enables +it to be sure of calling native DOM methods. + +There are two internal functions to convert between content script proxy +values and `XrayWrapper` values. + +1. __`wrap`__ takes an XrayWrapper value and wraps it in a content script +proxy if needed. + This method is called when: + * a content script accesses an attribute of a content script proxy. + * XrayWrapper code calls a callback function defined in the content +script, so that arguments passed into the function by the XrayWrapper are +converted into content script proxies. For example, if a content script +calls `addEventListener`, then the listener function will expect any arguments +to be content script proxies. +<br/><br/> +2. __`unwrap`__ takes an object coming from the content script context and: + * if the object is a content script proxy, unwraps it back to an +XrayWrapper reference + * if the object is not a content script proxy, wraps it in an XrayWrapper +proxy. +<br/><br/> +This means we can call a XrayWrapper method either with: + + * a raw XrayWrapper object. + + // The following line doesn't work if child is a content script proxy, + // it has to be a raw XrayWrapper reference + xrayWrapper.appendChild(child) + + * an XrayWrapper proxy when you pass a custom object from the content +script context. + + var myListener = { + handleEvent: function(event) { + // `event` should be a content script proxy + } + }; + // `myListener` has to be another kind of Proxy: XrayWrapper proxy, + // that aims to catch the call to `handleEvent` in order to wrap its + // arguments in a content script proxy. + xrayWrapper.addEventListener("click", myListener, false); + + +## Stack Traces ## + +The following code: + + function listener(event) { + + } + csProxy.addEventListener("message", listener, false); + +generates the following internal calls: + + -> CS Proxy:: get("addEventListener") + -> wrap(xrayWrapper.addEventListener) + -> NativeFunctionWrapper(xrayWrapper.addEventListener) + // NativeFunctionWrapper generates: + function ("message", listener, false) { + return xraywrapper.addEventListener("message", unwrap(listener), false); + } + -> unwrap(listener) + -> ContentScriptFunctionWrapper(listener) + // ContentScriptFunctionWrapper generates: + function (event) { + return listener(wrap(event)); + } + +<br> + + // First, create an object from content script context + var myListener = { + handleEvent: function (event) { + + } + }; + // Then, pass this object as an argument to a CS proxy method + window.addEventListener("message", myListener, false); + + // Generates the following internal calls: + -> CS Proxy:: get("addEventListener") + -> wrap(xrayWrapper.addEventListener) + -> NativeFunctionWrapper(xrayWrapper.addEventListener) + // Generate the following function: + function ("message", myListener, false) { + return xraywrapper.addEventListener("message", unwrap(myListener), false); + } + -> unwrap(myListener) + -> ContentScriptObjectWrapper(myListener) + // Generate an XrayWrapper proxy and give it to xrayWrapper method. + // Then when native code fires an event, the proxy will catch it: + -> XrayWrapper Proxy:: get("handleEvent") + -> unwrap(myListener.handleEvent) + -> ContentScriptFunctionWrapper(myListener.handleEvent) + // Generate following function: + function (event) { + return myListener.handleEvent(wrap(event)); + } + + +<api name="create"> +@function + Create a content script proxy. <br/> + Doesn't create a proxy if we are not able to create a XrayWrapper for + this object: for example, if the object comes from system principal. + +@param object {Object} + The object to proxify. + +@returns {Object} + A content script proxy that wraps `object`. +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/content/symbiont.md b/tools/addon-sdk-1.4/packages/api-utils/docs/content/symbiont.md new file mode 100644 index 0000000..7519e52 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/content/symbiont.md @@ -0,0 +1,136 @@ +<!-- contributed by Myk Melez [myk@mozilla.org] --> +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + + +This module is not intended to be used directly by programs. Rather, it is +intended to be used by other modules that provide APIs to programs. + + +This module exports `Symbiont` trait that can be used for creating JavaScript +contexts that can access web content in host application frames (i.e. XUL +`<iframe>` and `<browser>` elements) and communicate with programs via +asynchronous JSON pipes. It is useful in the construction of APIs that +are compatible with the execution model codenamed "electrolysis" in which +programs run in separate processes from web content. + +Introduction +------------ + +`Symbiont` constructs a content symbiont for a given frame, it loads the +specified contentURL and scripts into it, and plumbs an asynchronous +JSON pipe between the content symbiont object and the content symbiont +context. If frame is not provided hidden frame will be created. + +Examples +-------- + + var { Symbiont } = require('content'); + var Thing = Symbiont.resolve({ constructor: '_init' }).compose({ + constructor: function Thing(options) { + // `getMyFrame` returns the host application frame in which + // the page is loaded. + this._frame = getMyFrame(); + this._init(options) + } + }); + +See the [panel][] module for a real-world example of usage of this module. + +[panel]:packages/addon-kit/docs/panel.html + +Reference +--------- + +<api name="Symbiont"> +@class +Symbiont is composed from the [Worker][] trait, therefore instances +of Symbiont and their descendants expose all the public properties +exposed by [Worker][] along with additional public properties that +are listed below: + +[Worker]:packages/api-utils/docs/content/worker.html + +<api name="Symbiont"> +@constructor +Creates a content symbiont. +@param options {object} + Options for the constructor. Includes all the keys that +the [Worker](packages/api-utils/docs/content/worker.html) +constructor accepts and a few more: + + @prop [frame] {object} + The host application frame in which the page is loaded. + If frame is not provided hidden one will be created. + @prop [contentScriptWhen="end"] {string} + When to load the content scripts. This may take one of the following + values: + + * "start": load content scripts immediately after the document + element for the page is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the page has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + + This property is optional and defaults to "end". + + @prop [allow] {object} + Permissions for the content, with the following keys: + @prop [script] {boolean} + Whether or not to execute script in the content. Defaults to true. + Optional. + Optional. +</api> + +<api name="contentScriptFile"> +@property {array} +The local file URLs of content scripts to load. Content scripts specified by +this property are loaded *before* those specified by the `contentScript` +property. +</api> + +<api name="contentScript"> +@property {array} +The texts of content scripts to load. Content scripts specified by this +property are loaded *after* those specified by the `contentScriptFile` property. +</api> + +<api name="contentScriptWhen"> +@property {string} +When to load the content scripts. This may have one of the following +values: + +* "start": load content scripts immediately after the document +element for the page is inserted into the DOM, but before the DOM content +itself has been loaded +* "ready": load content scripts once DOM content has been loaded, +corresponding to the +[DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) +event +* "end": load content scripts once all the content (DOM, JS, CSS, +images) for the page has been loaded, at the time the +[window.onload event](https://developer.mozilla.org/en/DOM/window.onload) +fires + +</api> + +<api name="contentURL"> +@property {string} +The URL of the content loaded. +</api> + +<api name="allow"> +@property {object} +Permissions for the content, with a single boolean key called `script` which +defaults to true and indicates whether or not to execute scripts in the +content. +</api> + +</api> + + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/content/worker.md b/tools/addon-sdk-1.4/packages/api-utils/docs/content/worker.md new file mode 100644 index 0000000..ec9bb3a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/content/worker.md @@ -0,0 +1,126 @@ +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +This module exports the `Worker` trait, which may be used to construct objects +implementing the [Worker][] interface defined by the W3C, with minor +differences. + +Content workers are message-passing facilities for communication between +[content scripts](dev-guide/addon-development/web-content.html) and the main +add-on code. + +It is important to note that unlike "web workers," these workers run in the +same process as web content and browser chrome, so code within workers can +block the UI. + +[Worker]:http://www.w3.org/TR/workers/#worker + +<api name="Worker"> +@class +Worker is composed from the [EventEmitter][] trait, therefore instances +of Worker and their descendants expose all the public properties +exposed by [EventEmitter][] along with additional public properties that +are listed below. + +**Example** + + var workers = require("content/worker"); + let worker = workers.Worker({ + window: require("window-utils").activeWindow, + contentScript: + "self.port.on('hello', function(name) { " + + " self.port.emit('response', window.location); " + + "});" + }); + worker.port.emit("hello", { name: "worker"}); + worker.port.on("response", function (location) { + console.log(location); + }); + +[EventEmitter]:packages/api-utils/docs/events.html + +<api name="Worker"> +@constructor +Creates a content worker. +@param options {object} +Options for the constructor, with the following keys: + @prop window {object} + The content window to create JavaScript sandbox for communication with. + @prop [contentScriptFile] {string,array} + The local file URLs of content scripts to load. Content scripts specified + by this option are loaded *before* those specified by the `contentScript` + option. Optional. + @prop [contentScript] {string,array} + The texts of content scripts to load. Content scripts specified by this + option are loaded *after* those specified by the `contentScriptFile` option. + Optional. + @prop [onMessage] {function} + Functions that will registered as a listener to a 'message' events. + @prop [onError] {function} + Functions that will registered as a listener to an 'error' events. +</api> + +<api name="port"> +@property {EventEmitter} +[EventEmitter](packages/api-utils/docs/events.html) object that allows you to: + +* send customized messages to the worker using the `port.emit` function +* receive events from the worker using the `port.on` function + +</api> + +<api name="postMessage"> +@method +Asynchronously emits `"message"` events in the enclosed worker, where content +script was loaded. +@param data {number,string,JSON} +The data to send. Must be stringifiable to JSON. +</api> + +<api name="destroy"> +@method +Destroy the worker by removing the content script from the page and removing +all registered listeners. A `detach` event is fired just before removal. +</api> + +<api name="url"> +@property {string} +The URL of the content. +</api> + +<api name="tab"> +@property {object} +If this worker is attached to a content document, returns the related +[tab](packages/addon-kit/docs/tabs.html). +</api> + +<api name="message"> +@event +This event allows the content worker to receive messages from its associated +content scripts. Calling the `self.postMessage()` function from a content +script will asynchronously emit the `message` event on the corresponding +worker. + +@argument {value} +The event listener is passed the message, which must be a +<a href = "dev-guide/addon-development/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="error"> +@event +This event allows the content worker to react to an uncaught runtime script +error that occurs in one of the content scripts. + +@argument {Error} +The event listener is passed a single argument which is an +[Error](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error) +object. +</api> + +<api name="detach"> +@event +This event is emitted when the document associated with this worker is unloaded +or the worker's `destroy()` method is called. +</api> + +</api> + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/cortex.md b/tools/addon-sdk-1.4/packages/api-utils/docs/cortex.md new file mode 100644 index 0000000..87a8eda --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/cortex.md @@ -0,0 +1,156 @@ + +## Property Encapsulation ## + +In JavaScript it is not possible to create properties that have limited or +controlled accessibility. It is possible to create non-enumerable and +non-writable properties, but still they can be discovered and accessed. +Usually so called "closure capturing" is used to encapsulate such properties +in lexical scope: + + function Foo() { + var _secret = 'secret'; + this.hello = function hello() { + return 'Hello ' + _secret; + } + } + +This provides desired result, but has side effect of degrading code readability, +especially with object-oriented programs. Another disadvantage with this pattern +is that there is no immediate solution for inheriting access to the privates +(illustrated by the following example): + + function Derived() { + this.hello = function hello() { + return _secret; + } + this.bye = function bye() { + return _secret; + } + } + Derived.prototype = Object.create(Foo.prototype); + +## Facade Objects ## + +Alternatively constructor can returned facade objects - proxies to the +instance's public properties: + + function Foo() { + var foo = Object.create(Foo.prototype); + return { + bar: foo.hello.bind(foo); + } + } + Foo.prototype._secret = 'secret'; + Foo.prototype.hello = function hello() { + return 'Hello ' + this._secret; + } + + function Derived() { + var derived = Object.create(Derived.prototype); + return { + bar: derived.hello.bind(derived); + bye: derived.bye.bind(derived); + } + } + Derived.prototype = Object.create(Foo.prototype); + Derived.prototype.bye = function bye() { + return 'Bye ' + this._secret; + }; + +While this solution solves given issue and provides proper encapsulation for +both own and inherited private properties, it does not addresses following: + + - Privates defined on the `prototype` can be compromised, since they are + accessible through the constructor (`Foo.prototype._secret`). + - Behavior of `instanceof` is broken, since `new Derived() instanceof Derived` + is going to evaluate to `false`. + +## Tamper Proofing with Property Descriptor Maps ## + +In ES5 new property descriptor maps were introduced, which can be used as a +building blocks for defining reusable peace of functionality. To some degree +they are similar to a `prototype` objects, and can be used so to define pieces +of functionality that is considered to be private (In contrast to `prototype` +they are not exposed by default). + + function Foo() { + var foo = Object.create(Foo.prototype, FooDescriptor); + var facade = Object.create(Foo.prototype); + facade.hello = foo.hello.bind(foo); + return facade; + } + Foo.prototype.hello = function hello() { + return 'Hello ' + this._secret; + } + var FooDescriptor = { + _secret: { value: 'secret' }; + } + + function Derived() { + var derived = Object.create(Derived.prototype, DerivedDescriptor); + var facade = Object.create(Derived.prototype); + facade.hello = derived.hello.bind(derived); + facade.bye = derived.bye.bind(derived); + return facade; + } + Derived.prototype = Object.create(Foo.prototype); + Derived.prototype.bye = function bye() { + return 'Bye ' + this._secret; + }; + DerivedDescriptor = {}; + + Object.keys(FooDescriptor).forEach(function(key) { + DerivedDescriptor[key] = FooDescriptor[key]; + }); + +## Cortex Objects ## + +Last approach solves all of the concerns, but adds complexity, verbosity +and decreases code readability. Combination of `Cortex`'s and `Trait`'s +will gracefully solve all these issues and keep code clean: + + var Cortex = require('cortex').Cortex; + var Trait = require('light-traits').Trait; + + var FooTrait = Trait({ + _secret: 'secret', + hello: function hello() { + return 'Hello ' + this._secret; + } + }); + function Foo() { + return Cortex(FooTrait.create(Foo.prototype)); + } + + var DerivedTrait = Trait.compose(FooTrait, Trait({ + bye: function bye() { + return 'Bye ' + this._secret; + } + })); + function Derived() { + var derived = DerivedTrait.create(Derived.prototype); + return Cortex(derived); + } + +Function `Cortex` takes any object and returns a proxy for its public +properties. By default properties are considered to be public if they don't +start with `"_"`, but default behavior can be overridden if needed, by passing +array of public property names as a second argument. + +## Gotchas ## + +`Cortex` is just a utility function to create a proxy object, and it does not +solve the `prototype`-related issues highlighted earlier, but since traits make +use of property descriptor maps instead of `prototype`s, there aren't any +issues with using `Cortex` to wrap objects created from traits. + +If you want to use `Cortex` with an object that uses a `prototype` chain, +however, you should either make sure you don't have any private properties +in the prototype chain or pass the optional third `prototype` argument. + +In the latter case, the returned proxy will inherit from the given prototype, +and the `prototype` chain of the wrapped object will be inaccessible. +However, note that the behavior of the `instanceof` operator will vary, +as `proxy instanceof Constructor` will return false even if the Constructor +function's prototype is in the wrapped object's prototype chain. + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/cuddlefish.md b/tools/addon-sdk-1.4/packages/api-utils/docs/cuddlefish.md new file mode 100644 index 0000000..6177c53 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/cuddlefish.md @@ -0,0 +1,5 @@ +`cuddlefish` is the name of the SDK's module loader. It builds on +`securable-module` to provide many SDK-specific globals such as `console` +and `memory`. + +This module still needs to be documented. diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/environment.md b/tools/addon-sdk-1.4/packages/api-utils/docs/environment.md new file mode 100644 index 0000000..5697a4f --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/environment.md @@ -0,0 +1,39 @@ +Module provides API to access, set and unset environment variables via exported +`env` object. + + var { env } = require('api-utils/environment'); + +You can get the value of an environment variable, by accessing property that +has name of desired variable: + + var PATH = env.PATH; + +You can check existence of an environment variable by checking if property with +such variable name exists: + + console.log('PATH' in env); // true + console.log('FOO' in env); // false + +You can set value of an environment variable by setting a property: + + env.FOO = 'foo'; + env.PATH += ':/my/path/' + +You can unset environment variable by deleting a property: + + delete env.FOO; + +## Limitations ## + +There is no way to enumerate existing environment variables, also `env` +won't have any enumerable properties: + + console.log(Object.keys(env)); // [] + +Environment variable will be unset, show up as non-existing if it's set +to `null`, `undefined` or `''`. + + env.FOO = null; + console.log('FOO' in env); // false + env.BAR = ''; + console.log(env.BAR); // undefined diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/errors.md b/tools/addon-sdk-1.4/packages/api-utils/docs/errors.md new file mode 100644 index 0000000..3ce5b19 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/errors.md @@ -0,0 +1,38 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> + +The `errors` module provides helpers for safely invoking user callbacks. + +<api name="catchAndLog"> +@function + Wraps a callback in a function that when invoked will catch and log any + exception thrown by the callback. +@param callback {function} + The callback to wrap. +@param [defaultResponse] {value} + This value will be returned by the wrapper if `callback` throws an exception. + If not given, `undefined` is used. +@param [logException] {function} + When `callback` throws an exception, it will be passed to this function. If + not given, the exception is logged using `console.exception()`. +@returns {function} + A function that will invoke `callback` when called. The return value of this + function is the return value of `callback` unless `callback` throws an + exception. In that case, `defaultResponse` is returned or `undefined` if + `defaultResponse` is not given. +</api> + +<api name="catchAndLogProps"> +@function + Replaces methods of an object with wrapped versions of those methods returned + by `catchAndLog()`. +@param object {object} + The object whose methods to replace. +@param props {string,array} + The names of the methods of `object` to replace, either a string for a single + method or an array of strings for multiple methods. +@param [defaultResponse] {value} + This value will be returned by any wrapper whose wrapped method throws an + exception. If not given, `undefined` is used. +@param [logException] {function} + See `catchAndLog()`. +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/es5.md b/tools/addon-sdk-1.4/packages/api-utils/docs/es5.md new file mode 100644 index 0000000..0906b0d --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/es5.md @@ -0,0 +1,52 @@ +The `es5` module provides shim layer to a versions of Firefox that do not yet +implement certain EcmaScript 5 features. + +For more information on EcmaScript 5: + +* The new APIs are described in the official [ES5 specification][]. +* A good [introduction][] to the new APIs by John Resig. +* A Google tech talk on [changes to JavaScript][]. + +**There is no need to `require` this module** since it gets preloaded into +all sandboxes automatically. + +Usage of new ES5 API's is encouraged, but since not everything can be +provided to all the versions of firefox, there are few things to be aware of: + +`Object.freeze`, `Object.seal`, `Object.preventExtensions` does not really +prevents any mutations. One thing it guarantees though, `Object.isFrozen`, +`Object.isSealed`, `Object.isExtensible` checks will behave as defined in +specification. + +`Object.defineProperty` is only partially compliant with the specification: + +- Non configurable properties will be created as configurable ones. + +- Instead of non-writable properties getters and setters will be defined, +but `Object.getOwnPropertyDescriptor` will still behave as expected +(will return property descriptor for non-writable property not a getter) + +- Defining properties using ES5 functions will break your + [custom iterators][] if you have any. Think twice before employing + custom iterators, because in most cases you can just make properties + non enumerable. If you really need to have a custom iterator, add it + after running ES5 functions and don't ignore previous iterators. + For example: + + let object = Object.create({}, { + myField: { value: 6 } + }); + object.__iterator__ = (function(original) { + return function myIterator() { + this.__iterator__ = original; + for (let key in this) { + // your logic here + } + this.__iterator__ = myIterator; + } + })(object.__iterator__); + +[custom iterators]:https://developer.mozilla.org/en/New_in_JavaScript_1.7#Iterators +[ES5 specification]:http://www.ecmascript.org/docs/tc39-2009-043.pdf +[introduction]:http://ejohn.org/blog/ecmascript-5-objects-and-properties/ +[changes to JavaScript]:http://www.youtube.com/watch?v=Kq4FpMe6cRs diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/events.md b/tools/addon-sdk-1.4/packages/api-utils/docs/events.md new file mode 100644 index 0000000..cb74c77 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/events.md @@ -0,0 +1,74 @@ +The `events` module provides base API for emitting events. + +This module is not intended to be used directly by programs. Rather, it is +intended to be used by other modules that provide APIs to programs. + +<api name="EventEmitter"> +@class +The EventEmitter is the base building block for all compositions that +would need to broadcast data to multiple consumers. + +Please note that `EventEmitter` does not expose either a method for emitting +events or a list of available event listeners as its public API. Obviously +both are accessible but from the instance itself through the private API. +<api name="EventEmitter"> +@constructor +Creates an EventEmitter object. +</api> + +<api name="on"> +@method +Registers an event `listener` that will be called when events of +specified `type` are emitted. + +If the `listener` is already registered for this `type`, a call to this +method has no effect. + +If the event listener is being registered while an event is being processed, +the event listener is not called during the current emit. + +**Example:** + + // worker is instance of EventEmitter + worker.on('message', function (data) { + console.log('data received: ' + data) + }); + +@param type {String} + The type of the event. +@param listener {Function} + The listener function that processes the event. +</api> + +<api name="once"> +@method +Registers an event `listener` that will only be called once, the next time +an event of the specified `type` is emitted. + +If the event listener is registered while an event of the specified `type` +is being emitted, the event listener will not be called during the current +emit. + +@param type {String} + The type of the event. +@param listener {Function} + The listener function that processes the event. +</api> + +<api name="removeListener"> +@method +Unregisters an event `listener` for the specified event `type`. + +If the `listener` is not registered for this `type`, a call to this +method has no effect. + +If an event listener is removed while an event is being processed, it is +still triggered by the current emit. After it is removed, the event listener +is never invoked again (unless registered again for future processing). + +@param type {String} + The type of the event. +@param listener {Function} + The listener function that processes the event. +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/file.md b/tools/addon-sdk-1.4/packages/api-utils/docs/file.md new file mode 100644 index 0000000..3dd4fb2 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/file.md @@ -0,0 +1,147 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `file` module provides access to the local filesystem. + + +Paths +----- + +Path specifications in this API are platform-specific. This means that on +Windows paths are specified using the backslash path separator (`\`), and on +Unix-like systems like Linux and OS X paths are specified using the forward +slash path separator (`/`). + +If your add-on uses literal Windows-style path specifications with this API, +your add-on likely won't work when users run it on Unix-like systems. Likewise, +if your add-on uses literal Unix-style path specifications, it won't work for +users on Windows. + +To ensure your add-on works for everyone, generate paths using the +[`join`](packages/api-utils/docs/file.html#join(...)) function. Unfortunately +this API does not currently provide a way to obtain an absolute base path which +you could then use with `join`. For now, you need to +[`require("chrome")`](dev-guide/module-development/chrome.html) and use the +XPCOM directory service as described at +[MDN](https://developer.mozilla.org/en/Code_snippets/File_I%2F%2FO#Getting_special_files). + +Note that if you do decide to hardcode Windows-style paths, be sure to escape +backslashes in strings. For example, to specify the file at `C:\Users\Myk`, you +need to use the string `"C:\\Users\\Myk"`, not `"C:\Users\Myk"`. You can read +more about escaping characters in strings at +[MDN](https://developer.mozilla.org/en/JavaScript/Guide/Values,_Variables,_and_Literals#Escaping_Characters). + + +<api name="basename"> +@function + Returns the last component of the given path. For example, + `basename("/foo/bar/baz")` returns `"baz"`. If the path has no components, + the empty string is returned. +@param path {string} + The path of a file. +@returns {string} + The last component of the given path. +</api> + +<api name="dirname"> +@function + Returns the path of the directory containing the given file. If the file is + at the top of the volume, the empty string is returned. +@param path {string} + The path of a file. +@returns {string} + The path of the directory containing the file. +</api> + +<api name="exists"> +@function + Returns true if a file exists at the given path and false otherwise. +@param path {string} + The path of a file. +@returns {boolean} + True if the file exists and false otherwise. +</api> + +<api name="join"> +@function + Takes a variable number of strings, joins them on the file system's path + separator, and returns the result. +@param ... {strings} + A variable number of strings to join. The first string must be an absolute + path. +@returns {string} + A single string formed by joining the strings on the file system's path + separator. +</api> + +<api name="list"> +@function + Returns an array of file names in the given directory. +@param path {string} + The path of the directory. +@returns {array} + An array of file names. Each is a basename, not a full path. +</api> + +<api name="mkpath"> +@function + Makes a new directory named by the given path. Any subdirectories that do not + exist are also created. `mkpath` can be called multiple times on the same + path. +@param path {string} + The path to create. +</api> + +<api name="open"> +@function + Returns a stream providing access to the contents of a file. +@param path {string} + The path of the file to open. +@param [mode] {string} + An optional string, each character of which describes a characteristic of the + returned stream. If the string contains `"r"`, the file is opened in + read-only mode. `"w"` opens the file in write-only mode. `"b"` opens the + file in binary mode. If `"b"` is not present, the file is opened in text + mode, and its contents are assumed to be UTF-8. If *`mode`* is not given, + `"r"` is assumed, and the file is opened in read-only text mode. +@returns {stream} + If the file is opened in text read-only `mode`, a `TextReader` is returned, + and if text write-only mode, a `TextWriter` is returned. See + [`text-streams`](packages/api-utils/docs/text-streams.html) for information on + these text stream objects. If the file is opened in binary read-only `mode`, + a `ByteReader` is returned, and if binary write-only mode, a `ByteWriter` is + returned. See + [`byte-streams`](packages/api-utils/docs/byte-streams.html) for more + information on these byte stream objects. Opened files should always be + closed after use by calling `close` on the returned stream. +</api> + +<api name="read"> +@function + Opens a file and returns a string containing its entire contents. +@param path {string} + The path of the file to read. +@param [mode] {string} + An optional string, each character of which describes a characteristic of the + returned stream. If the string contains `"b"`, the contents will be returned + in binary mode. If `"b"` is not present or `mode` is not given, the file + contents will be returned in text mode. +@returns {string} + A string containing the file's entire contents. +</api> + +<api name="remove"> +@function + Removes a file from the file system. To remove directories, use `rmdir`. +@param path {string} + The path of the file to remove. +</api> + +<api name="rmdir"> +@function + Removes a directory from the file system. If the directory is not empty, an + exception is thrown. +@param path {string} + The path of the directory to remove. +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/hidden-frame.md b/tools/addon-sdk-1.4/packages/api-utils/docs/hidden-frame.md new file mode 100644 index 0000000..47cd23b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/hidden-frame.md @@ -0,0 +1,79 @@ +<!-- contributed by Myk Melez [myk@mozilla.org] --> + +The `hidden-frame` module creates host application frames (i.e. XUL `<iframe>` +elements) that are not displayed to the user. It is useful in the construction +of APIs that load web content not intended to be directly seen or accessed +by users, like `page-worker`. It is also useful in the construction of APIs +that load web content for intermittent display, such as `panel`. + +This module is not intended to be used directly by programs. Rather, it is +intended to be used by other modules that provide APIs to programs. + +Introduction +------------ + +The module exports a constructor function, `HiddenFrame`, and two other +functions, `add` and `remove`. + +`HiddenFrame` constructs a new hidden frame. `add` registers a hidden frame, +preparing it to load content. `remove` unregisters a frame, unloading any +content that was loaded in it. + +Examples +-------- + +The following code creates a hidden frame, loads a web page into it, and then +logs its title: + + var hiddenFrames = require("hidden-frame"); + let hiddenFrame = hiddenFrames.add(hiddenFrames.HiddenFrame({ + onReady: function() { + this.element.contentWindow.location = "http://www.mozilla.org/"; + let self = this; + this.element.addEventListener("DOMContentLoaded", function() { + console.log(self.element.contentDocument.title); + }, true, true); + } + })); + +See the `panel` module for a real-world example of usage of this module. + +Reference +--------- +<api name="HiddenFrame"> +@class +`HiddenFrame` objects represent hidden frames. +<api name="HiddenFrame"> +@constructor +Creates a hidden frame. +@param options {object} + Options for the frame, with the following keys: + @prop onReady {function,array} + Functions to call when the frame is ready to load content. You must specify + an `onReady` callback and refrain from using the hidden frame until + the callback gets called, because hidden frames are not always ready to load + content the moment they are added. +</api> + +<api name="add"> +@function +Register a hidden frame, preparing it to load content. +@param hiddenFrame {HiddenFrame} the frame to add +</api> + +<api name="remove"> +@function +Unregister a hidden frame, unloading any content that was loaded in it. +@param hiddenFrame {HiddenFrame} the frame to remove +</api> + +<api name="element"> +@property {DOMElement} +The host application frame in which the page is loaded. +</api> + +<api name="onReady"> +@property {array} +Functions to call when the frame is ready to load content. +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/httpd.md b/tools/addon-sdk-1.4/packages/api-utils/docs/httpd.md new file mode 100644 index 0000000..53964a3 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/httpd.md @@ -0,0 +1,27 @@ +Provides an HTTP server written in JavaScript for the Mozilla platform, which +can be used in unit tests. + +The most basic usage is: + + var {startServerAsync} = require("httpd"); + var srv = startServerAsync(port, basePath); + require("unload").when(function cleanup() { + srv.stop(function() { // you should continue execution from this point. + }) + }); + +This starts a server in background (assuming you're running this code in an +application that has an event loop, such as Firefox). The server listens at +http://localhost:port/ and serves files from the specified directory. You +can serve static content or use SJS scripts, as described in documentation +on developer.mozilla.org. + +You can also use `nsHttpServer` to start the server manually: + + var {nsHttpServer} = require("httpd"); + var srv = new nsHttpServer(); + // further documentation on developer.mozilla.org + +See +[HTTP server for unit tests](https://developer.mozilla.org/En/HTTP_server_for_unit_tests) +for general information. diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/light-traits.md b/tools/addon-sdk-1.4/packages/api-utils/docs/light-traits.md new file mode 100644 index 0000000..1257c2a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/light-traits.md @@ -0,0 +1,291 @@ + +[Traits](http://en.wikipedia.org/wiki/Trait_%28computer_science%29) are a simple +mechanism for structuring object-oriented programs. They represent reusable and +composable building blocks of functionality that factor out the common +attributes and behavior of objects. + +They are a more robust alternative to +[mixins](http://en.wikipedia.org/wiki/Mixins) and +[multiple inheritance](http://en.wikipedia.org/wiki/Multiple_inheritance), +because name clashes must be explicitly resolved and composition is commutative +and associative (i.e. the order of traits in a composition is irrelevant). + +Use traits to share functionality between similar objects without duplicating +code or creating complex inheritance chains. + +## Trait Creation ## + +To create a trait, call the `Trait` constructor function exported by this +module, passing it a JavaScript object that specifies the properties of the +trait. + + let Trait = require('light-traits').Trait; + let t = Trait({ + foo: "foo", + bar: function bar() { + return "Hi!" + }, + baz: Trait.required + }); + +Traits can both provide and require properties. A *provided* property is a +property for which the trait itself provides a value. A *required* property is a +property that the trait needs in order to function correctly but for which +it doesn't provide a value. + +Required properties must be provided by another trait or by an object with a +trait. Creation of an object with a trait will fail if required properties are +not provided. Specify a required property by setting the value of the property +to `Trait.required`. + +## Object Creation ## + +Create objects with a single trait by calling the trait's `create` method. The +method takes a single argument, the object to serve as the new object's +prototype. If no prototype is specified, the new object's prototype will be +`Object.prototype`. + + let t = Trait({ + foo: 'foo', + bar: 2 + }); + let foo1 = t.create(); + let foo2 = t.create({ name: 'Super' }); + +## Trait Composition ## + +Traits are designed to be composed with other traits to create objects with the +properties of multiple traits. To compose an object with multiple traits, you +first create a composite trait and then use it to create the object. A composite +trait is a trait that contains all of the properties of the traits from which it +is composed. In the following example, MagnitudeTrait is a composite trait. + + let EqualityTrait = Trait({ + equal: Trait.required, + notEqual: function notEqual(x) { + return !this.equal(x) + } + }); + + let ComparisonTrait = Trait({ + less: Trait.required, + notEqual: Trait.required, + greater: function greater(x) { + return !this.less(x) && this.notEqual(x) + } + }); + + let MagnitudeTrait = Trait.compose(EqualityTrait, ComparisonTrait); + +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-11 121 490 190" width="490px" height="190px"> + <defs> + <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="black"> + <g> + <path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1px"/> + </g> + </marker> + </defs> + <g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"> + <g> + <rect x="9" y="165.33334" width="141" height="14"/> + <rect x="9" y="165.33334" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 165.33334)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="11" textLength="47.373047">notEqual</tspan> + </text> + <rect x="9" y="151.33334" width="141" height="14"/> + <rect x="9" y="151.33334" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 151.33334)" fill="red"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="red" x="0" y="11" textLength="29.361328">equal</tspan> + </text> + <rect x="9" y="137.33334" width="141" height="14"/> + <rect x="9" y="137.33334" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 137.33334)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="bold" x="38.49707" y="11" textLength="54.00586">EqualityTrait</tspan> + </text> + <rect x="9" y="273" width="141" height="14"/> + <rect x="9" y="273" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 273)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="11" textLength="38.021484">greater</tspan> + </text> + <rect x="9" y="259" width="141" height="14"/> + <rect x="9" y="259" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 259)" fill="red"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="red" x="0" y="11" textLength="47.373047">notEqual</tspan> + </text> + <rect x="9" y="245" width="141" height="14"/> + <rect x="9" y="245" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 245)" fill="red"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="red" x="0" y="11" textLength="21.339844">less</tspan> + </text> + <rect x="9" y="231" width="141" height="14"/> + <rect x="9" y="231" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 231)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".15332031" y="11" textLength="112.67578">ComparisonTrait</tspan> + </text> + <rect x="317.75" y="235.5" width="141" height="14"/> + <rect x="317.75" y="235.5" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(322.75 235.5)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="11" textLength="38.021484">greater</tspan> + </text> + <rect x="317.75" y="221.5" width="141" height="14"/> + <rect x="317.75" y="221.5" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(322.75 221.5)" fill="red"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="red" x="0" y="11" textLength="21.339844">less</tspan> + </text> + <rect x="317.75" y="207.5" width="141" height="14"/> + <rect x="317.75" y="207.5" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(322.75 207.5)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="11" textLength="47.373047">notEqual</tspan> + </text> + <rect x="317.75" y="193.5" width="141" height="14"/> + <rect x="317.75" y="193.5" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(322.75 193.5)" fill="red"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="red" x="0" y="11" textLength="29.361328">equal</tspan> + </text> + <rect x="317.75" y="179.5" width="141" height="14"/> + <rect x="317.75" y="179.5" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(322.75 179.5)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="bold" x="31.83789" y="11" textLength="67.32422">MagnitudeTrait</tspan> + </text> + <path d="M 150 248.83887 L 158.89999 248.83887 L 235.9 248.83887 L 235.9 224.66113 L 308.85 224.66113 L 310.85 224.66113" marker-end="url(#SharpArrow_Marker)" stroke="black" stroke-linecap="butt" stroke-linejoin="miter" stroke-width="1px"/> + <path d="M 150 171.15845 L 158.89999 171.15845 L 233.9 171.15845 L 233.9 201.6749 L 308.85 201.6749 L 310.85 201.6749" marker-end="url(#SharpArrow_Marker)" stroke="black" stroke-linecap="butt" stroke-linejoin="miter" stroke-width="1px"/> + </g> + </g> +</svg> + +## Trait Resolution ## + +Composite traits have conflicts when two of the traits in the composition +provide properties with the same name but different values (when compared using +the `===` strict equality operator). In the following example, `TC` has a +conflict because `T1` and `T2` both define a `foo` property: + + let T1 = Trait({ + foo: function () { + // do something + }, + bar: 'bar', + t1: 1 + }); + + let T2 = Trait({ + foo: function() { + // do something else + }, + bar: 'bar', + t2: 2 + }); + + let TC = Trait.compose(T1, T2); + +Attempting to create an object from a composite trait with conflicts throws a +`remaining conflicting property` exception. To create objects from such traits, +you must resolve the conflict. + +You do so by excluding or renaming the conflicting property of one of the +traits. Excluding a property removes it from the composition, so the composition +only acquires the property from the other trait. Renaming a property gives it a +new, non-conflicting name at which it can be accessed. + +In both cases, you call the `resolve` method on the trait whose property you +want to exclude or rename, passing it an object. Each key in the object is the +name of a conflicting property; each value is either `null` to exclude the +property or a string representing the new name of the property. + +For example, the conflict in the previous example could be resolved by excluding +the `foo` property of the second trait. + + let TC = Trait(T1, T2.resolve({ foo: null })); + +It could also be resolved by renaming the `foo` property of the first trait to +`foo2`: + + let TC = Trait(T1.resolve({ foo: "foo2" }), T2); + +When you resolve a conflict, the same-named property of the other trait (the one +that wasn't excluded or renamed) remains available in the composition under its +original name. + +## Constructor Functions ## + +When your code is going to create more than one object with traits, you may want +to define a constructor function to create them. To do so, create a composite +trait representing the traits the created objects should have, then define a +constructor function that creates objects with that trait and whose prototype is +the prototype of the constructor: + + let PointTrait = Trait.compose(T1, T2, T3); + function Point(options) { + let point = PointTrait.create(Point.prototype); + return point; + } + +## Property Descriptor Maps ## + +Traits are designed to work with the new object manipulation APIs defined in +[ECMAScript-262, Edition +5](http://www.ecma-international.org/publications/standards/Ecma-262.htm) (ES5). +Traits are also property descriptor maps that inherit from `Trait.prototype` to +expose methods for creating objects and resolving conflicts. + +The following trait definition: + + let FooTrait = Trait({ + foo: "foo", + bar: function bar() { + return "Hi!" + }, + baz: Trait.required + }); + +Creates the following property descriptor map: + + { + foo: { + value: 'foo', + enumerable: true, + configurable: true, + writable: true + }, + + bar: { + value: function b() { + return 'bar' + }, + enumerable: true, + configurable: true, + writable: true + }, + + baz: { + get baz() { throw new Error('Missing required property: `baz`') } + set baz() { throw new Error('Missing required property: `baz`') } + }, + + __proto__: Trait.prototype + } + +Since Traits are also property descriptor maps, they can be used with built-in +`Object.*` methods that accept such maps: + + Object.create(proto, FooTrait); + Object.defineProperties(myObject, FooTrait); + +Note that conflicting and required properties won't cause exceptions to be +thrown when traits are used with the `Object.*` methods, since those methods are +not aware of those constraints. However, such exceptions will be thrown when the +property with the conflict or the required but missing property is accessed. + +Property descriptor maps can also be used in compositions. This may be useful +for defining non-enumerable properties, for example: + + let TC = Trait.compose( + Trait({ foo: 'foo' }), + { bar: { value: 'bar', enumerable: false } } + ); + +_When using property descriptor maps in this way, make sure the map is not the +only argument to `Trait.compose`, since in that case it will be interpreted as +an object literal with properties to be defined._ + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/list.md b/tools/addon-sdk-1.4/packages/api-utils/docs/list.md new file mode 100644 index 0000000..8e1a522 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/list.md @@ -0,0 +1,94 @@ +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `"list"` module provides base building blocks for composing lists. + +<api name="Iterable"> +@class +Base trait that can be used to compose traits with non-standard +enumeration behaviors. + +This trait is supposed to be used as part of a composition, since it only +provides custom enumeration behavior to a composed object. +It defines one required `_keyValueMap` property, that is used as a hash of +"key-values" to iterate on during enumeration. + +<api name="Iterable"> +@constructor +Constructs an `Iterable` object. +</api> + +<api name="_keyValueMap"> +@property {Object} +Hash map of key-values to iterate over. _Required_ property: that is, the +property must be supplied by objects that compose this trait. +_Note: That this property can be a getter if you need dynamic behavior._ +</api> + +</api> + +<api name="List"> +@class +An ordered collection (also known as a sequence) disallowing duplicate +elements. List is composed out of `Iterable`, therefore it provides custom +enumeration behavior that is similar to array (enumerates only on the +elements of the list). + +List is a base trait and is meant to be part of a +composition, since all of its API is private except for the `length` property. + +**Examples:** + + var MyList = List.compose({ + add: function add(item1, item2, /*item3...*/) { + Array.slice(arguments).forEach(this._add.bind(this)); + }, + remove: function remove(item1, item2, /*item3...*/) { + Array.slice(arguments).forEach(this._remove.bind(this)); + } + }); + MyList('foo', 'bar', 'baz').length == 3; // true + new MyList('new', 'keyword').length == 2; // true + MyList.apply(null, [1, 2, 3]).length == 3; // true + let list = MyList(); + list.length == 0; // true + list.add(1, 2, 3) == 3; // true + +<api name="List"> +@constructor +Constructor can takes any number of elements and creates an instance of +`List` populated with the specified elements. +@param [element1] {Object|String|Number} +@param [element2] {Object|String|Number} +@param [...] {Object|String|Number} +</api> + +<api name="length"> +@property {Number} +Number of elements in this list. +</api> + +<api name="_has"> +@method +@param element {Object|Number|String} +Returns `true` if this list contains the specified `element`. +</api> +<api name="_add"> +@method +@param element {Object|Number|String} +Appends the specified `element` to the end of this list, if it doesn't +contain it. + +_Ignores the call if `element` is already contained._ +</api> +<api name="_remove"> +@method +@param element {Object|Number|String} +Removes specified `element` from this list, if it contains it. + +_Ignores the call if `element` is not contained._ +</api> +<api name="_clear"> +@method +Removes all of the elements from this list. +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/match-pattern.md b/tools/addon-sdk-1.4/packages/api-utils/docs/match-pattern.md new file mode 100644 index 0000000..125f89e --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/match-pattern.md @@ -0,0 +1,242 @@ +The `match-pattern` module can be used to test strings containing URLs +against simple patterns. + +## Specifying Patterns ## + +There are three ways you can specify patterns: + +* as an exact match string +* using a wildcard in a string +* using a regular expression + +### Exact Matches ### + +**A URL** matches only that URL. The URL must start with a scheme, end with a +slash, and contain no wildcards. + +<table> + + <colgroup> + <col width="30%"> + <col width="35%"> + <col width="35%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + <th>Example non-matching URLs</th> + </tr> + + <tr> + <td><code>"http://example.com/"</code></td> + <td><code>http://example.com/</code></td> + <td><code>http://example.com</code><br> + <code>http://example.com/foo</code><br> + <code>https://example.com/</code><br> + <code>http://foo.example.com/</code></td> + </tr> + +</table> + +### Wildcards ### + +**A single asterisk** matches any URL with an `http`, `https`, or `ftp` +scheme. For other schemes like `file`, use a scheme followed by an +asterisk, as below. + +<table> + + <colgroup> + <col width="30%"> + <col width="35%"> + <col width="35%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + <th>Example non-matching URLs</th> + </tr> + + <tr> + <td><code>"*"</code></td> + <td><code>http://example.com/</code><br> + <code>https://example.com/</code><br> + <code>ftp://example.com/</code><br> + <code>http://bar.com/foo.js</code><br> + <code>http://foo.com/</code></td> + <td><code>file://example.js</code></td> + </tr> + +</table> + +**A domain name prefixed with an asterisk and dot** matches any URL of that +domain or a subdomain, using any of `http`, `https`, `ftp`. + +<table> + + <colgroup> + <col width="30%"> + <col width="35%"> + <col width="35%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + <th>Example non-matching URLs</th> + </tr> + + <tr> + <td><code>"*.example.com"</code></td> + <td><code>http://example.com/</code><br> + <code>http://foo.example.com/</code><br> + <code>https://example.com/</code><br> + <code>http://example.com/foo</code><br> + <code>ftp://foo.example.com/</code></td> + <td><code>ldap://example.com</code><br> + <code>http://example.foo.com/</code></td> + </tr> + +</table> + +**A URL followed by an asterisk** matches that URL and any URL prefixed with +the pattern. + +<table> + + <colgroup> + <col width="30%"> + <col width="35%"> + <col width="35%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + <th>Example non-matching URLs</th> + </tr> + + <tr> + <td><code>"https://foo.com/*"</code></td> + <td><code>https://foo.com/</code><br> + <code>https://foo.com/bar</code></td> + <td><code>http://foo.com/</code><br> + <code>https://foo.com</code><br> + <code>https://bar.foo.com/</code></td> + </tr> + +</table> + +**A scheme followed by an asterisk** matches all URLs with that scheme. To +match local files, use `file://*`. + +<table> + + <colgroup> + <col width="30%"> + <col width="70%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + </tr> + + <tr> + <td><code>"file://*"</code></td> + <td><code>file://C:/file.html</code><br> + <code>file:///home/file.png</code></td> + </tr> + +</table> + +### Regular Expressions ### + +You can specify patterns using a +[regular expression](https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions): + + var { MatchPattern } = require("match-pattern"); + var pattern = new MatchPattern(/.*example.*/); + +The regular expression is subject to restrictions based on those applied to the +[HTML5 pattern attribute](http://dev.w3.org/html5/spec/common-input-element-attributes.html#attr-input-pattern). In particular: + +* The pattern must match the entire value, not just any subset. For example, the +pattern `/moz.*/` will not match the URL `http://mozilla.org`. + +* The expression is compiled with the `global`, `ignoreCase`, and `multiline` flags + disabled. The `MatchPattern` constructor will throw an exception + if you try to set any of these flags. + +<table> + + <colgroup> + <col width="30%"> + <col width="35%"> + <col width="35%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + <th>Example non-matching URLs</th> + </tr> + + <tr> + <td><code>/.*moz.*/</code></td> + <td><code>http://foo.mozilla.org/</code><br> + <code>http://mozilla.org</code><br> + <code>https://mozilla.org</code><br> + <code>http://foo.com/mozilla</code><br> + <code>http://hemozoon.org</code><br> + <code>mozscheme://foo.org</code><br></td> + <td><code>http://foo.org</code><br> + </tr> + + <tr> + <td><code>/http:\/\/moz.*/</code></td> + <td><code>http://mozilla.org</code><br> + <code>http://mozzarella.com</code></td> + <td><code>https://mozilla.org</code><br> + <code>http://foo.mozilla.org/</code><br> + <code>http://foo.com/moz</code></td> + </tr> + + <tr> + <td><code>/http.*moz.*/</code><br></td> + <td><code>http://foo.mozilla.org/</code><br> + <code>http://mozilla.org</code><br> + <code>http://hemozoon.org/</code></td> + <td><code>ftp://http/mozilla.org</code></td> + </tr> + +</table> + +## Examples ## + + var { MatchPattern } = require("match-pattern"); + var pattern = new MatchPattern("http://example.com/*"); + console.log(pattern.test("http://example.com/")); // true + console.log(pattern.test("http://example.com/foo")); // true + console.log(pattern.test("http://foo.com/")); // false! + +<api name="MatchPattern"> +@class +<api name="MatchPattern"> +@constructor + This constructor creates match pattern objects that can be used to test URLs. +@param pattern {string} + The pattern to use. See Patterns above. +</api> + +<api name="test"> +@method + Tests a URL against the match pattern. +@param url {string} + The URL to test. +@returns {boolean} + True if the URL matches the pattern and false otherwise. +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/memory.md b/tools/addon-sdk-1.4/packages/api-utils/docs/memory.md new file mode 100644 index 0000000..c90f613 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/memory.md @@ -0,0 +1,3 @@ +The `memory` module provides a concrete default implementation for the SDK's +`memory` global. For documentation on the `memory` global, see the +[Globals](dev-guide/module-development/globals.html) reference. diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/namespace.md b/tools/addon-sdk-1.4/packages/api-utils/docs/namespace.md new file mode 100644 index 0000000..4a308b8 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/namespace.md @@ -0,0 +1,66 @@ +Provides an API for creating namespaces for any given objects, which +effectively may be used for creating fields that are not part of objects +public API. + + let { Namespace } = require('api-utils/namespace'); + let ns = Namespace(); + + ns(publicAPI).secret = secret; + +One namespace may be used with multiple objects: + + let { Namespace } = require('api-utils/namespace'); + let dom = Namespace(); + + function View(element) { + let view = Object.create(View.prototype); + dom(view).element = element; + // .... + } + View.prototype.destroy = function destroy() { + let { element } = dom(this); + element.parentNode.removeChild(element); + // ... + }; + // ... + exports.View = View; + // ... + +Also, multiple namespaces can be used with one object: + + // ./widget.js + + let { Cu } = require('chrome'); + let { Namespace } = require('api-utils/namespace'); + let { View } = require('./view'); + + // Note this is completely independent from View's internal Namespace object. + let ns = Namespace(); + + function Widget(options) { + let { element, contentScript } = options; + let widget = Object.create(Widget.prototype); + View.call(widget, options.element); + ns(widget).sandbox = Cu.Sandbox(element.ownerDocument.defaultView); + // ... + } + Widget.prototype = Object.create(View.prototype); + Widget.prototype.postMessage = function postMessage(message) { + let { sandbox } = ns(this); + sandbox.postMessage(JSON.stringify(JSON.parse(message))); + ... + }; + Widget.prototype.destroy = function destroy() { + View.prototype.destroy.call(this); + // ... + delete ns(this).sandbox; + }; + exports.Widget = Widget; + +In addition access to the namespace can be shared with other code by just +handing them a namespace accessor function. + + let { dom } = require('./view'); + Widget.prototype.setInnerHTML = function setInnerHTML(html) { + dom(this).element.innerHTML = String(html); + }; diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/observer-service.md b/tools/addon-sdk-1.4/packages/api-utils/docs/observer-service.md new file mode 100644 index 0000000..a2041f3 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/observer-service.md @@ -0,0 +1,69 @@ +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `observer-service` module provides access to the +application-wide observer service singleton. + +For a list of common observer topics across a variety of Mozilla-based +applications, see the MDC page on +[Observer Notifications](https://developer.mozilla.org/en/Observer_Notifications). + +## Observer Callbacks ## + +Observer callbacks are functions of the following form: + + function callback(subject, data) { + /* Respond to the event notification here... */ + } + +In the above example, `subject` is any JavaScript object, as is +`data`. The particulars of what the two contain are specific +to the notification topic. + +<api name="add"> +@function + Adds an observer callback to be triggered whenever a notification matching the + topic is broadcast throughout the application. + +@param topic {string} + The topic to observe. + +@param callback {function,object} + Either a function or an object that implements [`nsIObserver`](http://mxr.mozilla.org/mozilla-central/source/xpcom/ds/nsIObserver.idl). + If a function, then it is called when the notification occurs. If an object, + then its `observe()` method is called when the notification occurs. + +@param [thisObject] {object} + An optional object to use as `this` when a function callback is called. +</api> + +<api name="remove"> +@function + Unsubscribes a callback from being triggered whenever a notification + matching the topic is broadcast throughout the application. + +@param topic {string} + The topic being observed by the previous call to `add()`. + +@param callback {function,object} + The callback subscribed in the previous call to `add()`, either a function or + object. + +@param [thisObject] {object} + If `thisObject` was passed to the previous call to `add()`, it should be + passed to `remove()` as well. +</api> + +<api name="notify"> +@function + Broadcasts a notification event for a topic, passing a subject and data to all + applicable observers in the application. + +@param topic {string} + The topic about which to broadcast a notification. + +@param [subject] {value} + Optional information about the topic. This can be any JS object or primitive. + If you have multiple values to pass to observers, wrap them in an object, + e.g., `{ foo: 1, bar: "some string", baz: myObject }`. +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/plain-text-console.md b/tools/addon-sdk-1.4/packages/api-utils/docs/plain-text-console.md new file mode 100644 index 0000000..3c7a5a6 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/plain-text-console.md @@ -0,0 +1,3 @@ +The `plain-text-console` module provides a minimalist implementation +of the [console](dev-guide/addon-development/console.html) global, +which simply logs all messages to standard output. diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/preferences-service.md b/tools/addon-sdk-1.4/packages/api-utils/docs/preferences-service.md new file mode 100644 index 0000000..348fb60 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/preferences-service.md @@ -0,0 +1,80 @@ +<!-- contributed by Myk Melez [myk@mozilla.org] --> +<!-- contributed by Daniel Aquino [mr.danielaquino@gmail.com] --> +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `preferences-service` module provides access to the +application-wide preferences service singleton. + + +<api name="set"> +@function +Sets the application preference `name` to `value`. +@param name {string} Preference name. +@param value {string,number,bool} Preference value. + +**Example:** + + var name = "extensions.checkCompatibility.nightly"; + require("preferences-service").set(name, false); +</api> + + +<api name="get"> +@function +Gets the application preference `name`. +@param name {string} +@param defaultValue {string,number,bool} Preference value. +@returns {string,number,bool} Preference value, returns a default value if no +preference is set. + +**Example:** + + var name = "extensions.checkCompatibility.nightly"; + var nightlyCompatChk = require("preferences-service").get(name); +</api> + + +<api name="has"> +@function +@param name {string} Preference name. +@returns {bool} Returns whether or not the application preference `name` exists. + +**Example:** + + var name = "extensions.checkCompatibility.nightly"; + if (require("preferences-service").has(name)) { + // ... + } +</api> + + +<api name="isSet"> +@function +@param name {string} Preference name. +@returns {bool} +Returns whether or not the application preference `name` both exists +and has been set to a non-default value by the user (or a program +acting on the user's behalf). + +**Example:** + + var name = "extensions.checkCompatibility.nightly"; + if (require("preferences-service").isSet(name)) { + // ... + } +</api> + + +<api name="reset"> +@function +Clears a non-default, user-set value from the application preference +`name`. If no user-set value is defined on `name`, the function +does nothing. +@param name {string} Preference name. + +**Example:** + + var name = "extensions.checkCompatibility.nightly"; + require("preferences-service").reset(name); +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/runtime.md b/tools/addon-sdk-1.4/packages/api-utils/docs/runtime.md new file mode 100644 index 0000000..e4a7ee5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/runtime.md @@ -0,0 +1,71 @@ +<!-- contributed by Wes Kocher [kwierso@gmail.com] --> + +The `runtime` module provides access to information about Firefox's +runtime environment. All properties exposed are read-only. + +For more information, see [nsIXULRuntime][nsIXULRuntime]. +[nsIXULRuntime]: https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIXULRuntime + +<api name="inSafeMode"> +@property {boolean} + This value is `true` if Firefox was started in safe mode, + otherwise `false`. +</api> + +<api name="OS"> +@property {string} + A string identifying the current operating system. For example, . + `"WINNT"`, `"Darwin"`, or `"Linux"`. See [OS_TARGET][OS_TARGET] + for a more complete list of possible values. + +[OS_TARGET]: https://developer.mozilla.org/en/OS_TARGET +</api> + +<api name="processType"> +@property {long} + The type of the caller's process, which will be one of these constants\: +<table> + <tr> + <th>Constant</th> + <th>Value</th> + <th>Description</th> + </tr> + + <tr> + <td>PROCESS_TYPE_DEFAULT</td> + <td>0</td> + <td>The default (chrome) process.</td> + </tr> + + <tr> + <td>PROCESS_TYPE_PLUGIN</td> + <td>1</td> + <td>A plugin subprocess.</td> + </tr> + + <tr> + <td>PROCESS_TYPE_CONTENT</td> + <td>2</td> + <td>A content subprocess.</td> + </tr> + + <tr> + <td>PROCESS_TYPE_IPDLUNITTEST</td> + <td>3</td> + <td>An IPDL unit testing subprocess.</td> + </tr> +</table> +</api> + +<api name="widgetToolkit"> +@property {string} + A string identifying the target widget toolkit in use. +</api> + +<api name="XPCOMABI"> +@property {string} + A string identifying the [ABI][ABI] of the current processor and compiler vtable. + This string takes the form \<`processor`\>-\<`compilerABI`\>, + for example\: "`x86-msvc`" or "`ppc-gcc3`". +[ABI]: https://developer.mozilla.org/en/XPCOM_ABI +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/securable-module.md b/tools/addon-sdk-1.4/packages/api-utils/docs/securable-module.md new file mode 100644 index 0000000..d1d392d --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/securable-module.md @@ -0,0 +1,93 @@ +The `securable-module` module allows for the recursive loading +and sandboxing of +[CommonJS Modules](http://wiki.commonjs.org/wiki/Modules/1.0) +(formerly called SecurableModules). This allows, for instance, +the creation of "mini platforms" that manage the sandboxed evaluation of code. + +## Loader Objects ## + +Loader objects encapsulate the sandboxed loading of SecurableModules +and the execution of code that relies upon them. + +<code>Loader.**runScript**(*options*)</code> + +Runs JavaScript code in the context of the Loader. *options* is an +object with the following keys: + +<table> + <tr> + <td><code>contents</code></td> + <td>A string of JavaScript code.</td> + </tr> + <tr> + <td><code>filename</code></td> + <td>An absolute URL naming the file from which the code + originates; useful for error reporting and debugging. If omitted, + this option defaults to <code>"<string>"</code>.</td> + </tr> + <tr> + <td><code>lineNo</code></td> + <td>An integer representing the line from the file which the + beginning of the code corresponds to. If ommitted, this option + defaults to <code>1</code>.</td> + </tr> + <tr> + <td><code>jsVersion</code></td> + <td>A string representing the JavaScript version that the code + should be interpreted under. If omitted, this options defaults to + the latest version of JavaScript supported by the platform.</td> + </tr> +</table> + +This method returns the most recent value evaluated by the given code. + +<code>Loader.**runScript**(*code*)</code> + +If *code* is a string of JavaScript code, this is a convenient +shorthand for `Loader.runScript({contents: code}}`. + +<code>Loader.**require**(*module*)</code> + +This loads the given module name using the standard `require()` +semantics and returns the loaded module. + +## Functions ## + +<code>securable-module.**Loader**(*options*)</code> + +Creates a new SecurableModule Loader. *options* is an object with +the following keys: + +<table> + <tr> + <td><code>rootPaths</code></td> + <td>A list of absolute URLs that will be searched, in order, for + SecurableModules when <code>require()</code> is called by any code + executing within the context of the Loader.</td> + </tr> + <tr> + <td><code>rootPath</code></td> + <td>A single absolute URL; this is a convenience option, + synonymous with setting <code>rootPaths</code> to an array containing + a single URL.</td> + </tr> + <tr> + <td><code>defaultPrincipal</code></td> + <td>A string representing the default principal given to any code + that is executed by the Loader. This can be <code>"system"</code>, in + which case code executed has full chrome access (including access + to the <code>Components</code> object which allows it to access the + Mozilla platform unrestricted). + Alternatively, it can be a URL, such as <code>"http://www.foo.com"</code>, + in which case it is treated like web content. If left unspecified, + the default value of this option is <code>"http://www.mozilla.org"</code>. + </td> + </tr> + <tr> + <td><code>globals</code></td> + <td>An object containing the names and values of all variables + that will be injected into the global scope of all code executed + by the Loader.</td> + </tr> +</table> + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/tab-browser.md b/tools/addon-sdk-1.4/packages/api-utils/docs/tab-browser.md new file mode 100644 index 0000000..b844206 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/tab-browser.md @@ -0,0 +1,136 @@ +<!-- contributed by Dietrich Ayala [dietrich@mozilla.com] --> + +The `tab-browser` module is a low-level API that provides privileged +access to browser tab events and actions. + +Introduction +------------ + +The `tab-browser` module contains helpers for tracking tabbrowser elements +and tabs, as well as a few utilities for actions such as opening a new +tab, and catching all tab content loads. + +This is a low-level API that has full privileges, and is intended to be used +by SDK internal modules. If you just need easy access to tab events for your +add-on, use the Tabs module (JEP 110). + +<api name="activeTab"> +@property {element} +The XUL tab element of the currently active tab. +</api> + +<api name="addTab"> +@function +Adds a new tab. + +**Example** + + var tabBrowser = require("tab-browser"); + tabBrowser.addTab("http://google.com"); + + var tabBrowser = require("tab-browser"); + tabBrowser.addTab("http://google.com", { + inBackground: true + }); + + var tabBrowser = require("tab-browser"); + tabBrowser.addTab("http://google.com", { + inNewWindow: true, + onLoad: function(tab) { + console.log("tab is open."); + } + }); + +@returns {element} +The XUL tab element of the newly created tab. + +@param URL {string} +The URL to be opened in the new tab. + +@param options {object} +Options for how and where to open the new tab. + +@prop [inNewWindow] {boolean} +An optional parameter whose key can be set in `options`. +If true, the tab is opened in a new window. Default is false. + +@prop [inBackground] {boolean} +An optional parameter whose key can be set in `options`. +If true, the tab is opened adjacent to the active tab, but not +switched to. Default is false. + +@prop [onLoad] {function} +An optional parameter whose key can be set in `options`. +A callback function that is called once the tab has loaded. +The XUL element for the tab is passed as a parameter to +this function. +</api> + +<api name="Tracker"> +@function +Register a delegate object to be notified when tabbrowsers are created +and destroyed. + +The onTrack method will be called once per pre-existing tabbrowser, upon +tracker registration. + +**Example** + + var tabBrowser = require("tab-browser"); + let tracker = { + onTrack: function(tabbrowser) { + console.log("A new tabbrowser is being tracked."); + }, + onUntrack: function(tabbrowser) { + console.log("A tabbrowser is no longer being tracked."); + } + }; + tabBrowser.Tracker(tracker); + +@param delegate {object} +Delegate object to be notified each time a tabbrowser is created or destroyed. +The object should contain the following methods: + +@prop [onTrack] {function} +Method of delegate that is called when a new tabbrowser starts to be tracked. +The tabbrowser element is passed as a parameter to this method. + +@prop [onUntrack] {function} +Method of delegate that is called when a tabbrowser stops being tracked. +The tabbrowser element is passed as a parameter to this method. +</api> + +<api name="TabTracker"> +@function +Register a delegate object to be notified when tabs are opened and closed. + + +The onTrack method will be called once per pre-existing tab, upon +tracker registration. + +**Example** + + var tabBrowser = require("tab-browser"); + let tracker = { + onTrack: function(tab) { + console.log("A new tab is being tracked."); + }, + onUntrack: function(tab) { + console.log("A tab is no longer being tracked."); + } + }; + tabBrowser.TabTracker(tracker); + +@param delegate {object} +Delegate object to be notified each time a tab is opened or closed. +The object should contain the following methods: + +@prop [onTrack] {function} +Method of delegate that is called when a new tab starts to be tracked. +The tab element is passed as a parameter to this method. + +@prop [onUntrack] {function} +Method of delegate that is called when a tab stops being tracked. +The tab element is passed as a parameter to this method. +</api> + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/text-streams.md b/tools/addon-sdk-1.4/packages/api-utils/docs/text-streams.md new file mode 100644 index 0000000..c057e06 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/text-streams.md @@ -0,0 +1,98 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `text-streams` module provides streams for reading and writing text using +particular character encodings. + +<api name="TextReader"> +@class +<api name="TextReader"> +@constructor + Creates a buffered input stream that reads text from a backing stream using a + given text encoding. +@param inputStream {stream} + The backing stream, an [`nsIInputStream`](http://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsIInputStream.idl). + It must already be opened. +@param [charset] {string} + `inputStream` is expected to be in the character encoding named by this value. + If not specified, "UTF-8" is assumed. See [`nsICharsetConverterManager.idl`](http://mxr.mozilla.org/mozilla-central/source/intl/uconv/idl/nsICharsetConverterManager.idl) + for documentation on how to determine other valid values for this. +</api> + +<api name="closed"> +@property {boolean} + True if the stream is closed. +</api> + +<api name="close"> +@method + Closes both the stream and its backing stream. +</api> + +<api name="read"> +@method + Reads and returns a string from the stream. If the stream is closed, an + exception is thrown. +@param [numChars] {number} + The number of characters to read. If not given, the remainder of the stream + is read. +@returns {string} + The string read. If the stream is at the end, the empty string is returned. +</api> + +</api> + + +<api name="TextWriter"> +@class +<api name="TextWriter"> +@constructor + Creates a buffered output stream that writes text to a backing stream using a + given text encoding. +@param outputStream {stream} + The backing stream, an [`nsIOutputStream`](http://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsIOutputStream.idl). + It must already be opened. +@param [charset] {string} + Text will be written to `outputStream` using the character encoding named by + this value. If not specified, "UTF-8" is assumed. See [`nsICharsetConverterManager.idl`](http://mxr.mozilla.org/mozilla-central/source/intl/uconv/idl/nsICharsetConverterManager.idl) + for documentation on how to determine other valid values for this. +</api> + +<api name="closed"> +@property {boolean} + True if the stream is closed. +</api> + +<api name="close"> +@method + Flushes the backing stream's buffer and closes both the stream and the backing + stream. If the stream is already closed, an exception is thrown. +</api> + +<api name="flush"> +@method + Flushes the backing stream's buffer. +</api> + +<api name="write"> +@method + Writes a string to the stream. If the stream is closed, an exception is + thrown. +@param str {string} + The string to write. +</api> + +<api name="writeAsync"> +@method + Writes a string on a background thread. After the write completes, the + backing stream's buffer is flushed, and both the stream and the backing stream + are closed, also on the background thread. If the stream is already closed, + an exception is thrown immediately. +@param str {string} + The string to write. +@param [callback] {callback} + *`callback`*, if given, must be a function. It's called as `callback(error)` + when the write completes. `error` is an `Error` object or undefined if there + was no error. Inside *`callback`*, `this` is the `TextWriter` object. +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/traceback.md b/tools/addon-sdk-1.4/packages/api-utils/docs/traceback.md new file mode 100644 index 0000000..fd7ddb5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/traceback.md @@ -0,0 +1,62 @@ +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + + +The `traceback` module contains functionality similar to +Python's [traceback](http://docs.python.org/library/traceback.html) module. + +## JSON Traceback Objects ## + +Tracebacks are stored in JSON format. The stack is represented as an +array in which the most recent stack frame is the last element; each +element thus represents a stack frame and has the following keys: + +<table> + <tr> + <td><code>filename</code></td> + <td>The name of the file that the stack frame takes place in.</td> + </tr> + <tr> + <td><code>lineNo</code></td> + <td>The line number is being executed at the stack frame.</td> + </tr> + <tr> + <td><code>funcName</code></td> + <td>The name of the function being executed at the stack frame, or + <code>null</code> if the function is anonymous or the stack frame is + being executed in a top-level script or module.</td> + </tr> +</table> + +<api name="fromException"> +@function + Attempts to extract the traceback from *`exception`*. + +@returns {traceback} + JSON representation of the traceback or `null` if not found. + +@param exception {exception} + exception where exception is an `nsIException`. +</api> + +See [nsIException](https://developer.mozilla.org/en/NsIException) for more +information. + +<api name="get"> +@function + +@returns {JSON} + Returns the JSON representation of the stack at the point that this + function is called. +</api> + +<api name="format"> +@function +Given a JSON representation of the stack or an exception instance, +returns a formatted plain text representation of it, similar to +Python's formatted stack tracebacks. If no argument is provided, the +stack at the point this function is called is used. + +@param [tbOrException] {object} +</api> + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/traits.md b/tools/addon-sdk-1.4/packages/api-utils/docs/traits.md new file mode 100644 index 0000000..66e8f02 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/traits.md @@ -0,0 +1,240 @@ +<!-- contributed by Irakli Gozalishvil [gozala@mozilla.com] --> + +The `traits` module provides base building blocks for secure object +composition. It exports base trait / constructor function that +constructs an instance of `Trait`. + +[Traits](http://en.wikipedia.org/wiki/Trait_%28computer_science%29) are a +simple composition mechanism for structuring object-oriented programs. Traits +are similar to +[interfaces](http://en.wikipedia.org/wiki/Interface_%28object-oriented_programming%29), +except that they often define only a part of an object's data and behavior and +are intended to be used in conjunction with other traits to completely define +the object. + +Traits are also considered to be a more robust alternative to +[mixins](http://en.wikipedia.org/wiki/Mixins) because, name conflicts have to +be resolved explicitly by composer & because trait composition is +order-independent (hence more declarative). + + +There are some other implementations of traits in JavaScript & some ideas / +APIs are borrowed from them: + +- [traitsjs](http://www.traitsjs.org/) +- [joose](http://code.google.com/p/joose-js/) + +Object-capability security model +-------------------------------- + +Implementation uses an +[object-capability security model](http://en.wikipedia.org/wiki/Object-capability_model) +to allow protection of private APIs. At the same private APIs can be shared +between among trait composition parties. To put it simply: All the properties +whose names start with `"_"` are considered to be **private**, and are +unaccessible from anywhere except other **public** methods / accessors of the +instance that had been defined during composition. + +<api name="Trait"> +@class +<api name="Trait"> +@constructor +Creates an instance of Trait and returns it if it has no `constructor` method +defined. If instance has `constructor` method, then it is called with all the +arguments passed to this function and returned value is returned instead, +unless it's `undefined`. In that case instance is returned. + +`Trait` function represents a base trait. As with any other trait it represents +a constructor function for creating instances of its own & a placeholder +for a trait compositions functions. +</api> + +<api name="compose"> +@method +Composes new trait out of itself and traits / property maps passed as an +arguments. If two or more traits / property maps have properties with the same +name, the new trait will contain a "conflict" property for that name (see +examples in Examples section to find out more about "conflict" properties). +This is a commutative and associative operation, and the order of its +arguments is not significant. + +**Examples:** + +Let's say we want to define a reusable piece of code for a lists of elements. + + var { Trait } = require('traits'); + var List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = []; + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.splice(index, 1); + } + }); + +Instances of `List` can be created by calling `List` function with or without +`new` keyword. + + let l1 = List(); + l1 instanceof List; // true + let l2 = new List(); + l2 instanceof List; // true + +As you can see `add` and `remove` functions are capable of accessing private +`_list` property, but thats about it, there's nothing else that will be able +to access this property: + + '_list' in l1; // false + '_list' in l2; // false + '_list' in List.protoype; // false + l1.has = function(name) name in this + l1.has('_list'); // false + l1.length; // 0 + l1.add('test') + l1.length // 1 + +@param trait1 {Object|Function} + Trait or property map to compose new trait from. +@param trait2 {Object|Function} + Trait or property map to compose new trait from. +@param ... {Object|Function} + Traits or property maps to compose new trait from. + +@returns {Function} + New trait containing the combined properties of all the traits. +</api> + +<api name="required"> +@property {Object} +Singleton, used during trait composition to define "required" properties. + +**Example:** + + var Enumerable = Trait.compose({ + list: Trait.required, + forEach: function forEach(consumer) { + return this.list.forEach(consumer); + } + }); + + let c1 = Enumerable(); // Error: Missing required property: list + + var EnumerableList = List.compose({ + get list() this._list.slice(0) + }, Enumerable); + + let c2 = EnumerableList(); + c2.add('test') + c2.length // 1 + c2.list[0] // 'test' + c2.forEach(console.log) // > info: 'test 0 test' + +</api> + + +<api name="resolve"> +@method +Composes a new trait that has all the same properties +as the trait on which it is called, except that each property listed +in the `resolutions` argument will be renamed from the name +of the property in the `resolutions` argument to its value. +And if its value is `null`, the property will become required. + +**Example:** + + var Range = List.resolve({ + constructor: null, + add: '_add', + }).compose({ + min: null, + max: null, + get list() this._list.slice(0), + constructor: function Range(min, max) { + this.min = min; + this.max = max; + this._list = []; + }, + add: function(item) { + if (item <= this.max && item >= this.min) + this._add(item) + } + }); + + + let r = Range(0, 10); + r.min; // 0 + r.max; // 10 + r.length; // 0; + r.add(5); + r.length; // 1 + r.add(12); + r.length; // 1 (12 was not in a range) + +@param resolutions {Object} +@returns {Function} + New resolved trait. +</api> + +<api name="override"> +@method +Composes a new trait with all of the combined properties of `this` and the +argument traits. In contrast to `compose`, `override` immediately resolves +all conflicts resulting from this composition by overriding the properties of +later traits. Trait priority is from left to right. I.e. the properties of +the leftmost trait are never overridden. + +**Example:** + + // will compose trait with conflict property 'constructor' + var ConstructableList = List.compose({ + constructor: function List() this._list = Array.slice(arguments) + }); + // throws error with message 'Remaining conflicting property: constructor' + ConstructableList(1, 2, 3); + + var ConstructableList = List.override({ + constructor: function List() this._list = Array.slice(arguments) + }); + ConstructableList(1, 2, 3).length // 3 + +@param trait1 {Object|Function} + Trait or property map to compose new trait from. +@param trait2 {Object|Function} + Trait or property map to compose new trait from. +@param ... {Object|Function} + Traits or property maps to compose new trait from. + +@returns {Function} + New trait containing the combined properties of all the traits. +</api> + +<api name="_public"> +@property {Object} +Internal property of instance representing public API that is exposed to the +consumers of an instance. +</api> + +<api name='toString'> +@method +Textual representation of an object. All the traits will return: +`'[object Trait]'` string, unless they have `constructor` property, in that +case string `'Trait'` is replaced with the name of `constructor` property. + +**Example:** + + var MyTrait = Trait.compose({ + constructor: function MyTrait() { + // do your initialization here + } + }); + MyTrait().toString(); // [object MyTrait] + +</api> +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/unit-test.md b/tools/addon-sdk-1.4/packages/api-utils/docs/unit-test.md new file mode 100644 index 0000000..401b551 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/unit-test.md @@ -0,0 +1,389 @@ +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> +<!-- edited by Shane Tomlinson[stomlinson@mozilla.com] --> + +The `unit-test` module makes it easy to find and run unit tests. + +<api name="test"> +@class +Each function which represents a test case is passed a single argument +`test`, which represents the test runner. + +<api name="pass"> +@method + Marks a test as passing, with the given optional message. + +@param [message] {string} + Optional passing message. +</api> + + +<api name="fail"> +@method + Marks a test as failing, with the given optional message. + +@param [message] {string} + Optional failure message. +</api> + +<api name="expectFail"> +@method + *experimental* Expect the test enclosed within `func` to fail. + +@param func {function} + A function that should contain a test that is expected to fail. +</api> + +<api name="exception"> +@method + Marks a test as failing due to the given exception having been thrown. + This can be put in a `catch` clause. + +@param e {exception} + An exception. +</api> + +<api name="assert"> +@method + Ensures that `a` has a truthy value. + +@param a {value} + Value to verify. +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + +<api name="assertEqual"> +@method + Ensures that `a == b` without recursing into `a` or `b`. + +@param a {value} + A value. + +@param b {value} + Another value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + +<api name="assertNotEqual"> +@method + Ensures that `a != b` without recursing into `a` or `b`. + +@param a {value} + A value. + +@param b {value} + Another value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + +<api name="assertStrictEqual"> +@method + Ensures that `a === b` without recursing into `a` or `b`. + +@param a {value} + A value. + +@param b {value} + Another value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + +<api name="assertNotStrictEqual"> +@method + Ensures that `a !== b` without recursing into `a` or `b`. + +@param a {value} + A value. + +@param b {value} + Another value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + +<api name="assertMatches"> +@method + Ensures that the given string matches the given regular expression. + If it does, marks a test as passing, otherwise marks a test as + failing. + +@param string {string} + The string to test. + +@param regexp {regexp} + The string should match this regular expression. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + +<api name="assertRaises"> +@method + Calls the function `func` with no arguments, expecting an exception + to be raised. If nothing is raised, marks the test as failing. If an + exception is raised, the exception's `message` property is + compared with `predicate`: if `predicate` is a string, then a + simple equality comparison is done with `message`. Otherwise, + if `predicate` is a regular expression, `message` is tested + against it. + +@param func {function} + A function that should raise an exception when called. + +@param predicate {string,regexp} + A string or regular expression to compare to the exception's message. + +@param [message] {string} + Depending on the outcome, a test is marked as passing or failing, and + *message* is logged. +</api> + + +<api name="assertFunction"> +@method + Ensures that `a` is a function. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertUndefined"> +@method + Ensures that `a` is `undefined`. `null`, `0`, and `false` will all fail. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertNotUndefined"> +@method + Ensures that `a` is not `undefined`. `null`, `0`, and `false` will all pass. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertNull"> +@method + Ensures that `a` is `null`. `undefined`, `0`, and `false` will all fail. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertNotNull"> +@method + Ensures that `a` is not `null`. `undefined`, `0`, and `false` will all pass. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertObject"> +@method + Ensures that `a` is an object. A function, string, or number will fail. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertString"> +@method + Ensures that `a` is a string. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertArray"> +@method + Ensures that `a` is an array. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertNumber"> +@method + Ensures that `a` is a number. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="waitUntilDone"> +@method + Puts the test runner into asynchronous testing mode, waiting up to + *timeout* milliseconds for `test.done()` to be called. This + is intended for use in situations where a test suite schedules a + callback, calls `test.waitUntilDone()`, and then calls + `test.done()` in the callback. + +@param [timeout] {integer} + If this number of milliseconds elapses and `test.done()` has not yet been + called, the test is marked as failing. +</api> + + +<api name="done"> +@method + Marks a test as being complete. Assumes a previous call to + `test.waitUntilDone()`. +</api> + +</api> + + +<api name="waitUntil"> +@method + Ensures that `a` returns a truthy value within a reasonable amount of time. + +@param a {function} + Function that returns the value to verify. +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + +<api name="waitUntilEqual"> +@method + Ensures that `a == b` returned values or values without without recursing + into `a` or `b`. + +@param a {Function} + A value, or a function that returns a value. + +@param b {value} + Another value, or a function that returns value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + +<api name="waitUntilNotEqual"> +@method + Ensures that `a != b` without recursing into `a` or `b`. + +@param a {Function} + A value, or a function that returns a value. + +@param b {value} + Another value, or a function that returns another value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + +<api name="waitUntilMatches"> +@method + Ensures that the given string matches the given regular expression. + If it does, marks the test as passing, otherwise marks the test as + failing. + +@param string {Function} + A function that returns the string to test. + +@param regexp {regexp} + The string should match this regular expression. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + + +<api name="findAndRunTests"> +@function + The list of directories is searched for SecurableModules that start + with the prefix `test-`. Each module matching this criteria is + expected to export functions that are test cases or a suite of test + cases; each is called with a single argument, which is a Test Runner + Object. + +@param options {object} + An object with the following properties: + @prop dirs {string} + A list of absolute paths representing directories to search + for tests in. It's assumed that all of these directories are also + in the module search path, i.e. any JS files found in them are + SecurableModules that can be loaded via a call to + `require()`. + @prop onDone {function} + A function to call when testing is complete. +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/unload.md b/tools/addon-sdk-1.4/packages/api-utils/docs/unload.md new file mode 100644 index 0000000..a7e3f3f --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/unload.md @@ -0,0 +1,57 @@ +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `unload` module allows modules to register callbacks that are called +when they are unloaded. It is similar to the CommonJS module of the same +name in the [Narwhal][] platform. + +[Narwhal]: http://narwhaljs.org/ + +<api name="ensure"> +@function + Calling `ensure()` on an object does two things: + + 1. It replaces a destructor method with a wrapper method that will never call + the destructor more than once. + 2. It ensures that this wrapper method is called when `send()` is + called. + + Therefore, when you register an object with `ensure()`, you can call its + destructor method yourself, you can let it happen for you, or you can do both. + + The destructor will be called with a single argument describing the reason + for the unload; see `when()`. If `object` does not have the expected + destructor method, then an exception is thrown when `ensure()` is called. + +@param object {object} + An object that defines a destructor method. +@param [name] {string} + Optional name of the destructor method. Default is `unload`. +</api> + +<api name="when"> +@function + Registers a function to be called when `send()` is called. + +@param callback {function} + A function that will be called when `send()` is called. It is called with a + single argument, one of the following strings describing the reason for + unload: `"uninstall"`, `"disable"`, `"shutdown"`, `"upgrade"`, or + `"downgrade"`. (On Gecko 1.9.2-based applications such as Firefox 3.6, + `"upgrade"` and `"downgrade"` are not available, and `"shutdown"` will be sent + in their place.) If a reason could not be determined, `undefined` will be + passed instead. Note that if an add-on is unloaded with reason `"disable"`, + it will not be notified about `"uninstall"` while it is disabled. A solution + to this issue is being investigated; see bug 571049. +</api> + +<api name="send"> +@function + Sends an "unload signal", thereby triggering all callbacks registered via + `when()`. In general, this function need not be manually called; it is + automatically triggered by the embedder. + +@param [reason] {string} + An optional string describing the reason for unload; see `unload.when()`. +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/url.md b/tools/addon-sdk-1.4/packages/api-utils/docs/url.md new file mode 100644 index 0000000..dee0580 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/url.md @@ -0,0 +1,81 @@ +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + + +The `url` module provides functionality for the parsing and retrieving of URLs. + +<api name="URL"> +@class +<api name="URL"> +@constructor + The URL constructor creates an object that represents a URL, verifying that + the provided string is a valid URL in the process. Any API in the SDK which + has a URL parameter will accept `URL` objects, not raw strings, unless + otherwise noted. + +@param source {string} + A string to be converted into a URL. If `source` is not a valid URI, this + constructor will throw an exception. + +@param [base] {string} + An optional string used to resolve relative `source` URLs into absolute ones. +</api> + +<api name="scheme"> +@property {string} + The name of the protocol in the URL. +</api> + +<api name="userPass"> +@property {string} + The username:password part of the URL, `null` if not present. +</api> + +<api name="host"> +@property {string} + The host of the URL, `null` if not present. +</api> + +<api name="port"> +@property {integer} + The port number of the URL, `null` if none was specified. +</api> + +<api name="path"> +@property {string} + The path component of the URL. +</api> + +<api name="toString"> +@method + Returns a string representation of the URL. +@returns {string} + The URL as a string. +</api> +</api> + +<api name="toFilename"> +@function + Attempts to convert the given URL to a native file path. This function will + automatically attempt to resolve non-file protocols, such as the `resource:` + protocol, to their place on the file system. An exception is raised if the URL + can't be converted; otherwise, the native file path is returned as a string. + +@param url {string} + The URL, as a string, to be converted. + +@returns {string} + The converted native file path as a string. +</api> + +<api name="fromFilename"> +@function + Converts the given native file path to a `file:` URL. + +@param path {string} + The native file path, as a string, to be converted. + +@returns {string} + The converted URL as a string. +</api> + diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/window-utils.md b/tools/addon-sdk-1.4/packages/api-utils/docs/window-utils.md new file mode 100644 index 0000000..3de84b5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/window-utils.md @@ -0,0 +1,84 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Erik Vold [erikvvold@gmail.com] --> + +The `window-utils` module provides helpers for accessing and tracking +application windows. These windows implement the [`nsIDOMWindow`][nsIDOMWindow] +interface. + +[nsIDOMWindow]: http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIDOMWindow.idl + +<api name="WindowTracker"> +@class +`WindowTracker` objects make it easy to "monkeypatch" windows when a program is +loaded and "un-monkeypatch" those windows when the program is unloaded. For +example, if a Firefox add-on needs to add a status bar icon to all browser +windows, it can use a single `WindowTracker` object to gain access to windows +when they are opened and closed and also when the add-on is loaded and unloaded. + +When a window is opened or closed, a `WindowTracker` notifies its delegate +object, which is passed to the constructor. The delegate is also notified of +all windows that are open at the time that the `WindowTracker` is created and +all windows that are open at the time that the `WindowTracker` is unloaded. The +caller can therefore use the same code to act on all windows, regardless of +whether they are currently open or are opened in the future, or whether they are +closed while the parent program is loaded or remain open when the program is +unloaded. + +When a window is opened or when a window is open at the time that the +`WindowTracker` is created, the delegate's `onTrack()` method is called and +passed the window. + +When a window is closed or when a window is open at the time that the +`WindowTracker` is unloaded, the delegate's `onUntrack()` method is called and +passed the window. (The `WindowTracker` is unloaded when its its `unload()` +method is called, or when its parent program is unloaded, disabled, or +uninstalled, whichever comes first.) + +**Example** + + var delegate = { + onTrack: function (window) { + console.log("Tracking a window: " + window.location); + // Modify the window! + }, + onUntrack: function (window) { + console.log("Untracking a window: " + window.location); + // Undo your modifications! + } + }; + var winUtils = require("window-utils"); + var tracker = new winUtils.WindowTracker(delegate); + +<api name="WindowTracker"> +@constructor + A `WindowTracker` object listens for openings and closings of application + windows. +@param delegate {object} + An object that implements `onTrack()` and `onUntrack()` methods. +@prop onTrack {function} + A function to be called when a window is open or loads, with the window as the + first and only argument. +@prop [onUntrack] {function} + A function to be called when a window unloads, with the window as the first + and only argument. +</api> +</api> + +<api name="windowIterator"> +@function + An iterator for windows currently open in the application. + +**Example** + + var winUtils = require("window-utils"); + for (window in winUtils.windowIterator()) + console.log("An open window! " + window.location); + +</api> + +<api name="closeOnUnload"> +@function + Marks an application window to be closed when the program is unloaded. +@param window {window} + The window to close. +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/xhr.md b/tools/addon-sdk-1.4/packages/api-utils/docs/xhr.md new file mode 100644 index 0000000..f2d3ee3 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/xhr.md @@ -0,0 +1,91 @@ +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `xhr` module provides access to `XMLHttpRequest` functionality, also known +as AJAX. + +## Limitations ## + +The `XMLHttpRequest` object is currently fairly limited, and does not +yet implement the `addEventListener()` or `removeEventListener()` +methods. It also doesn't yet implement the `upload` property. + +Furthermore, the `XMLHttpRequest` object does not currently support +the `mozBackgroundRequest` property. All security UI, such as +username/password prompts, are automatically suppressed, so if +required authentication information isn't passed to the `open()` +method, the request will fail. + +## Resource Use ## + +Whenever this module is unloaded, all in-progress requests are immediately +aborted. + +## Security Concerns ## + +By default, the `XMLHttpRequest` object grants full access to any +protocol scheme, which means that it can be used to read from (but not +write to) the host system's entire filesystem. It also has unfettered +access to any local area networks, VPNs, and the internet. + +### Threat Model ### + +The `XMLHttpRequest` object can be used by an add-on to "phone +home" and transmit potentially sensitive user data to third +parties. + +If access to the filesystem isn't prevented, it could easily be used +to access sensitive user data, though this may be inconsequential if +the client can't access the network. + +If access to local area networks isn't prevented, malicious code could access +sensitive data. + +If transmission of cookies isn't prevented, malicious code could access +sensitive data. + +Attenuating access based on a regular expression may be ineffective if +it's easy to write a regular expression that *looks* safe but contains +a special character or two that makes it far less secure than it seems +at first glance. + +### Possible Attenuations ### + +<span class="aside"> +We may also want to consider attenuating further based on domain name +and possibly even restricting the protocol to `https:` only, to reduce +risk. +</span> + +Before being exposed to unprivileged code, this object needs +to be attenuated in such a way that, at the very least, it can't +access the user's filesystem. This can probably be done most securely +by white-listing the protocols that can be used in the URL passed to +the `open()` method, and limiting them to `http:`, `https:`, and +possibly a special scheme that can be used to access the add-on's +packaged, read-only resources. + +Finally, we need to also consider attenuating http/https requests such +that they're "sandboxed" and don't communicate potentially sensitive +cookie information. + +<api name="XMLHttpRequest"> +@class + +<api name="XMLHttpRequest"> +@constructor + Creates an `XMLHttpRequest`. This is a constructor, so its use should always + be preceded by the `new` operator. For more information about + `XMLHttpRequest` objects, see the MDC page on + [Using XMLHttpRequest](https://developer.mozilla.org/En/Using_XMLHttpRequest) + and the Limitations section in this page. +</api> +</api> + +<api name="getRequestCount"> +@function + Returns the number of `XMLHttpRequest` objects that are alive (i.e., currently + active or about to be). +@returns {integer} + The number of live `XMLHttpRequest` objects. +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/xpcom.md b/tools/addon-sdk-1.4/packages/api-utils/docs/xpcom.md new file mode 100644 index 0000000..6cb5428 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/xpcom.md @@ -0,0 +1,223 @@ +Using this module you can: + +* register a component with +[XPCOM](https://developer.mozilla.org/en/Creating_XPCOM_Components), +making it available to all XPCOM clients +* retrieve a factory for a given XPCOM component +* generate a UUID + +The module also exposes the +[XPCOMUtils](https://developer.mozilla.org/en/JavaScript_code_modules/XPCOMUtils.jsm) +module. + +<api name="register"> +@function + +Makes a component available through XPCOM. + +This function creates and registers a factory for a component given a +constructor for it and some metadata: a +[class ID](https://developer.mozilla.org/en/Creating_XPCOM_Components/An_Overview_of_XPCOM#CID), a [contract ID](https://developer.mozilla.org/en/Creating_XPCOM_Components/An_Overview_of_XPCOM#Contract_ID), +and a name. + +<span class="aside">In this example the HelloWorld component is available to JavaScript only, so we use the technique documented under the "Using wrappedJSObject" section of [How to Build an XPCOM Component in JavaScript](https://developer.mozilla.org/en/How_to_Build_an_XPCOM_Component_in_Javascript).</span> + + var xpcom = require("xpcom"); + + function HelloWorld() { + this.wrappedJSObject = this; + } + + HelloWorld.prototype = { + QueryInterface: xpcom.utils.generateQI(), + hello: function() { + return "Hello World!"; + } + }; + + xpcom.register({name: "Hello World Component", + contractID: "@me.org/myComponent", + create: HelloWorld}); + +XPCOM clients can subsequently access this factory and use it to create +instances of the component. + + var {Ci} = require("chrome"); + + var factory = xpcom.getClass("@me.org/myComponent", Ci.nsIFactory); + var helloWorld = factory.createInstance(null, Ci.nsISupports).wrappedJSObject; + console.log(helloWorld.hello()); + +`register()` returns a Factory object for the component which implements +the `createInstance()` and `QueryInterface()` functions of the +[`nsIFactory`](https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIFactory) and +[`nsISupports`](https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsISupports) +interfaces, as well as defining an `unregister()` function to remove the +component from XPCOM. + +When the module is unloaded, all components registered via the `register()` +function are automatically unregistered. + +@param options {object} + +@prop [uuid] {nsIDPtr} +A [UUID](https://developer.mozilla.org/en/Generating_GUIDs) which will be +used as the +[class ID](https://developer.mozilla.org/en/Creating_XPCOM_Components/An_Overview_of_XPCOM#CID) +for this component. If you don't include this option, the `register()` +function will generate a new UUID. + +@prop create {function} +The constructor for the component. + +@prop name {string} +A human-readable name for the component. + +@prop contractID {string} +A human-readable string which will be used as the +[contract ID](https://developer.mozilla.org/en/Creating_XPCOM_Components/An_Overview_of_XPCOM#Contract_ID) +for the component. An XPCOM client will be able to use this value to access +the component. + +@returns {Factory} +See the documentation for the `Factory` class in this page. +</api> + +<api name="getClass"> +@function +Returns the factory object for the class specified by `contractID`. + +For example, given a registered XPCOM component which is identified with +the contract ID "@me.org/myComponent", we can access a factory and then +use it to instantiate the component in the following way: + + var xpcom = require("xpcom"); + var {Ci} = require("chrome"); + + var factory = xpcom.getClass("@me.org/myComponent", Ci.nsIFactory); + var helloWorld = factory.createInstance(null, Ci.nsISupports).wrappedJSObject; + console.log(helloWorld.hello()); + +@param contractID {string} +The +[contract ID](https://developer.mozilla.org/en/Creating_XPCOM_Components/An_Overview_of_XPCOM#Contract_ID) +for the component whose factory will be returned. + +@param [iid] {iid} +The interface type to be returned. These objects are usually accessed through +the `Components.interfaces`, or `Ci`, object. + +The methods of this interface will be callable on the returned factory object. +Usually you want this to be +[`Ci.nsIFactory`](https://developer.mozilla.org/En/nsIFactory), but if you know +a component has a factory that implements a more specific type of factory +interface, you can pass that interface here. If you don't include this option +only the methods of +[`nsISupports`](https://developer.mozilla.org/En/NsISupports) +will be callable, which is probably not what you want. + +@returns {object} +The factory object. The type of this object will depend on the value of the +`iid` argument. If no `iid` argument is specified it will be of type +[`nsISupports`](https://developer.mozilla.org/En/NsISupports). + +Note that this object is not a `Factory` object as defined by this module. +If you previously registered the component by calling the `register()` +function and you need to access the `Factory` object for the component, for +example to call the `Factory`'s `unregister()` method, you can do so by +getting the +[`wrappedJSObject`](https://developer.mozilla.org/en/wrappedJSObject) +property of the returned object: + + var factory = xpcom.getClass("@me.org/myComp", Ci.nsIFactory).wrappedJSObject; + factory.unregister(); + +</api> + +<api name="utils"> +@property {object} +The +[XPCOMUtils](https://developer.mozilla.org/en/JavaScript_code_modules/XPCOMUtils.jsm) +module. +</api> + +<api name="makeUuid"> +@function +Generates and returns a new +[UUID](https://developer.mozilla.org/en/Generating_GUIDs). + +Calling `toString()` on this object will yield the UUID in string form. +@returns {nsIDPtr} +</api> + +<api name="Factory"> +@class + +When a component is made available through XPCOM using the `register()` +function, `register()` returns a `Factory` object that can be used to +instantiate the component using its `createInstance()` function: + + var factory = require("xpcom").register({ + name: "My Component", + contractID: "@me.org/myComponent", + create: MyComponent + }); + + var {Ci} = require("chrome"); + var component = factory.createInstance(null, Ci.nsISupports).wrappedJSObject; + +In this example we haven't defined a custom interface ID for the component. +Instead we pass `Ci.nsISupports` as the interface ID, and use `wrappedJSObject` +to retrieve the component. For more details on this technique see the +[guide to building XPCOM components in JavaScript](https://developer.mozilla.org/en/How_to_Build_an_XPCOM_Component_in_Javascript). + +`Factory` also implements its own `unregister()` function, +which unregisters the component from XPCOM. + +<api name="createInstance"> +@method +Creates an instance of the component associated with this factory. + +@param outer {nsISupports} +This argument must be `null`, or the function throws +`Cr.NS_ERROR_NO_AGGREGATION`. + +@param iid {iid} +Interface identifier. These objects are usually accessed through +the `Components.interfaces`, or `Ci`, object. The methods of this +interface will be callable on the returned object. + +If the object implements an interface that's already defined in XPCOM, you +can pass that in here: + + var about = aboutFactory.createInstance(null, Ci.nsIAboutModule); + // You can now access the nsIAboutModule interface of the 'about' object + +If you will be getting the `wrappedJSObject` property from the returned +object to access its JavaScript implementation, pass `Ci.nsISupports` here: + + var custom = factory.createInstance(null, Ci.nsISupports).wrappedJSObject; + // You can now access the interface defined for the 'custom' object + +</api> + +<api name="QueryInterface"> +@method +This method is called automatically by XPCOM, so usually you don't need +to call it yourself. It returns the `Factory` object itself such that the +methods of the given interface are callable on it. + +@param interfaces {iid} +There are only two legal values for this parameter: `Ci.nsIFactory` and +`Ci.nsISupports`. Any other value will cause this method to throw +`Cr.NS_ERROR_NO_INTERFACE`. + +@returns {Factory} +</api> + +<api name="unregister"> +@method +Unregisters the factory's component. +</api> + +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/docs/xul-app.md b/tools/addon-sdk-1.4/packages/api-utils/docs/xul-app.md new file mode 100644 index 0000000..00379db --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/docs/xul-app.md @@ -0,0 +1,72 @@ +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> + +The `xul-app` module provides facilities for introspecting the application on +which your program is running. + +With the exception of `ids`, each of these properties exposes the attribute of +the same name on the [`nsIXULAppInfo`][nsIXULAppInfo] interface. For more +information, see the [MDC documentation][]. + +[nsIXULAppInfo]: http://mxr.mozilla.org/mozilla-central/source/xpcom/system/nsIXULAppInfo.idl +[MDC documentation]: https://developer.mozilla.org/en/nsIXULAppInfo + +<api name="ID"> +@property {string} + The GUID of the host application. For example, for Firefox this value is + `"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"`. +</api> + +<api name="name"> +@property {string} + The host application name. For example, `"Firefox"`. +</api> + +<api name="version"> +@property {string} + The host application version. +</api> + +<api name="platformVersion"> +@property {string} + The Gecko/XULRunner platform version. +</api> + +<api name="ids"> +@property {object} + A mapping of application names to their IDs. For example, + `ids["Firefox"] == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"`. +</api> + +<api name="is"> +@function + Checks whether the host application is the given application. +@param name {string} + A host application name. +@returns {boolean} + True if the host application is `name` and false otherwise. +</api> + +<api name="isOneOf"> +@function + Checks whether the host application is one of the given applications. +@param names {array} + An array of host application names. +@returns {boolean} + True if the host application is one of the `names` and false otherwise. +</api> + +<api name="versionInRange"> +@function + Compares a given version to a version range. See the [MDC documentation](https://developer.mozilla.org/en/Toolkit_version_format#Comparing_versions) + for details on version comparisons. +@param version {string} + The version to compare. +@param lowInclusive {string} + The lower bound of the version range to compare. The range includes this + bound. +@param highExclusive {string} + The upper bound of the version range to compare. The range does not include + this bound. +@returns {boolean} + True if `version` falls in the given range and false otherwise. +</api> diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/api-utils.js b/tools/addon-sdk-1.4/packages/api-utils/lib/api-utils.js new file mode 100644 index 0000000..17d18cc --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/api-utils.js @@ -0,0 +1,186 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * Edward Lee <edilee@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +// The possible return values of getTypeOf. +const VALID_TYPES = [ + "array", + "boolean", + "function", + "null", + "number", + "object", + "string", + "undefined", +]; + +/** + * Returns a function C that creates instances of privateCtor. C may be called + * with or without the new keyword. The prototype of each instance returned + * from C is C.prototype, and C.prototype is an object whose prototype is + * privateCtor.prototype. Instances returned from C will therefore be instances + * of both C and privateCtor. Additionally, the constructor of each instance + * returned from C is C. + * + * @param privateCtor + * A constructor. + * @return A function that makes new instances of privateCtor. + */ +exports.publicConstructor = function publicConstructor(privateCtor) { + function PublicCtor() { + let obj = { constructor: PublicCtor, __proto__: PublicCtor.prototype }; + memory.track(obj, privateCtor.name); + privateCtor.apply(obj, arguments); + return obj; + } + PublicCtor.prototype = { __proto__: privateCtor.prototype }; + return PublicCtor; +}; + +/** + * Returns a validated options dictionary given some requirements. If any of + * the requirements are not met, an exception is thrown. + * + * @param options + * An object, the options dictionary to validate. It's not modified. + * If it's null or otherwise falsey, an empty object is assumed. + * @param requirements + * An object whose keys are the expected keys in options. Any key in + * options that is not present in requirements is ignored. Each value + * in requirements is itself an object describing the requirements of + * its key. There are four optional keys in this object: + * map: A function that's passed the value of the key in options. + * map's return value is taken as the key's value in the final + * validated options, is, and ok. If map throws an exception + * it's caught and discarded, and the key's value is its value in + * options. + * is: An array containing any number of the typeof type names. If + * the key's value is none of these types, it fails validation. + * Arrays and null are identified by the special type names + * "array" and "null"; "object" will not match either. No type + * coercion is done. + * ok: A function that's passed the key's value. If it returns + * false, the value fails validation. + * msg: If the key's value fails validation, an exception is thrown. + * This string will be used as its message. If undefined, a + * generic message is used, unless is is defined, in which case + * the message will state that the value needs to be one of the + * given types. + * @return An object whose keys are those keys in requirements that are also in + * options and whose values are the corresponding return values of map + * or the corresponding values in options. Note that any keys not + * shared by both requirements and options are not in the returned + * object. + */ +exports.validateOptions = function validateOptions(options, requirements) { + options = options || {}; + let validatedOptions = {}; + let mapThrew = false; + + for (let [key, req] in Iterator(requirements)) { + let [optsVal, keyInOpts] = (key in options) ? + [options[key], true] : + [undefined, false]; + if (req.map) { + try { + optsVal = req.map(optsVal); + } + catch (err) { + mapThrew = true; + } + } + if (req.is) { + // Sanity check the caller's type names. + req.is.forEach(function (typ) { + if (VALID_TYPES.indexOf(typ) < 0) { + let msg = 'Internal error: invalid requirement type "' + typ + '".'; + throw new Error(msg); + } + }); + if (req.is.indexOf(getTypeOf(optsVal)) < 0) + throw requirementError(key, req); + } + if (req.ok && !req.ok(optsVal)) + throw requirementError(key, req); + + if (keyInOpts || (req.map && !mapThrew)) + validatedOptions[key] = optsVal; + } + + return validatedOptions; +}; + +exports.addIterator = function addIterator(obj, keysValsGenerator) { + obj.__iterator__ = function(keysOnly, keysVals) { + let keysValsIterator = keysValsGenerator.call(this); + + // "for (.. in ..)" gets only keys, "for each (.. in ..)" gets values, + // and "for (.. in Iterator(..))" gets [key, value] pairs. + let index = keysOnly ? 0 : 1; + while (true) + yield keysVals ? keysValsIterator.next() : keysValsIterator.next()[index]; + }; +}; + +// Similar to typeof, except arrays and null are identified by "array" and +// "null", not "object". +let getTypeOf = exports.getTypeOf = function getTypeOf(val) { + let typ = typeof(val); + if (typ === "object") { + if (!val) + return "null"; + if (Array.isArray(val)) + return "array"; + } + return typ; +} + +// Returns a new Error with a nice message. +function requirementError(key, requirement) { + let msg = requirement.msg; + if (!msg) { + msg = 'The option "' + key + '" '; + msg += requirement.is ? + "must be one of the following types: " + requirement.is.join(", ") : + "is invalid."; + } + return new Error(msg); +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/app-strings.js b/tools/addon-sdk-1.4/packages/api-utils/lib/app-strings.js new file mode 100644 index 0000000..120c26e --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/app-strings.js @@ -0,0 +1,95 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is String Bundle. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez <myk@mozilla.org> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci} = require("chrome"); +const apiUtils = require("./api-utils"); + +/** + * A bundle of strings. + * + * @param url {String} + * the URL of the string bundle + */ +exports.StringBundle = apiUtils.publicConstructor(function StringBundle(url) { + + let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(url); + + this.__defineGetter__("url", function () url); + + /** + * Get a string from the bundle. + * + * @param name {String} + * the name of the string to get + * @param args {array} [optional] + * an array of arguments that replace occurrences of %S in the string + * + * @returns {String} the value of the string + */ + this.get = function strings_get(name, args) { + try { + if (args) + return stringBundle.formatStringFromName(name, args, args.length); + else + return stringBundle.GetStringFromName(name); + } + catch(ex) { + // f.e. "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) + // [nsIStringBundle.GetStringFromName]" + throw new Error("String '" + name + "' could not be retrieved from the " + + "bundle due to an unknown error (it doesn't exist?)."); + } + }, + + /** + * Iterate the strings in the bundle. + * + */ + apiUtils.addIterator( + this, + function keysValsGen() { + let enumerator = stringBundle.getSimpleEnumeration(); + while (enumerator.hasMoreElements()) { + let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + yield [elem.key, elem.value]; + } + } + ); +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/array.js b/tools/addon-sdk-1.4/packages/api-utils/lib/array.js new file mode 100644 index 0000000..a41acdd --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/array.js @@ -0,0 +1,102 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +/** + * Returns `true` if given `array` contain given `element` or `false` + * otherwise. + * @param {Array} array + * Target array. + * @param {Object|String|Number|Boolean} element + * Element being looked up. + * @returns {Boolean} + */ +var has = exports.has = function has(array, element) { + // shorter and faster equivalent of `array.indexOf(element) >= 0` + return !!~array.indexOf(element); +}; + +/** + * Adds given `element` to the given `array` if it does not contain it yet. + * `true` is returned if element was added otherwise `false` is returned. + * @param {Array} array + * Target array. + * @param {Object|String|Number|Boolean} element + * Element to be added. + * @returns {Boolean} + */ +var add = exports.add = function add(array, element) { + var result; + if ((result = !has(array, element))) + array.push(element); + + return result; +}; + +/** + * Removes first occurrence of the given `element` from the given `array`. If + * `array` does not contain given `element` `false` is returned otherwise + * `true` is returned. + * @param {Array} array + * Target array. + * @param {Object|String|Number|Boolean} element + * Element to be removed. + * @returns {Boolean} + */ +exports.remove = function remove(array, element) { + var result; + if ((result = has(array, element))) + array.splice(array.indexOf(element), 1); + + return result; +}; + +/** + * Produces a duplicate-free version of the given `array`. + * @param {Array} array + * Source array. + * @returns {Array} + */ +exports.unique = function unique(array) { + var value = []; + return array.forEach(function(element) { + add(value, element); + }); + return value; +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/byte-streams.js b/tools/addon-sdk-1.4/packages/api-utils/lib/byte-streams.js new file mode 100644 index 0000000..b44357e --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/byte-streams.js @@ -0,0 +1,135 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +exports.ByteReader = ByteReader; +exports.ByteWriter = ByteWriter; + +const {Cc, Ci} = require("chrome"); + +// This just controls the maximum number of bytes we read in at one time. +const BUFFER_BYTE_LEN = 0x8000; + +function ByteReader(inputStream) { + const self = this; + + let stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(inputStream); + + let manager = new StreamManager(this, stream); + + this.read = function ByteReader_read(numBytes) { + manager.ensureOpened(); + if (typeof(numBytes) !== "number") + numBytes = Infinity; + + let data = ""; + let read = 0; + try { + while (true) { + let avail = stream.available(); + let toRead = Math.min(numBytes - read, avail, BUFFER_BYTE_LEN); + if (toRead <= 0) + break; + data += stream.readBytes(toRead); + read += toRead; + } + } + catch (err) { + throw new Error("Error reading from stream: " + err); + } + + return data; + }; +} + +function ByteWriter(outputStream) { + const self = this; + + let stream = Cc["@mozilla.org/binaryoutputstream;1"]. + createInstance(Ci.nsIBinaryOutputStream); + stream.setOutputStream(outputStream); + + let manager = new StreamManager(this, stream); + + this.write = function ByteWriter_write(str) { + manager.ensureOpened(); + try { + stream.writeBytes(str, str.length); + } + catch (err) { + throw new Error("Error writing to stream: " + err); + } + }; +} + + +// This manages the lifetime of stream, a ByteReader or ByteWriter. It defines +// closed and close() on stream and registers an unload listener that closes +// rawStream if it's still opened. It also provides ensureOpened(), which +// throws an exception if the stream is closed. +function StreamManager(stream, rawStream) { + const self = this; + this.rawStream = rawStream; + this.opened = true; + + stream.__defineGetter__("closed", function stream_closed() { + return !self.opened; + }); + + stream.close = function stream_close() { + self.ensureOpened(); + self.unload(); + }; + + require("./unload").ensure(this); +} + +StreamManager.prototype = { + ensureOpened: function StreamManager_ensureOpened() { + if (!this.opened) + throw new Error("The stream is closed and cannot be used."); + }, + unload: function StreamManager_unload() { + this.rawStream.close(); + this.opened = false; + } +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/channel.js b/tools/addon-sdk-1.4/packages/api-utils/lib/channel.js new file mode 100644 index 0000000..b7f1e61 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/channel.js @@ -0,0 +1,67 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const { jetpackID } = require('@packaging'); + +// TODO: Create a bug report and remove this workaround once it's fixed. +// Only function needs defined in the context of the message manager window +// can be registered via `addMessageListener`. +function listener(callee) { + return function listener() { return callee.apply(this, arguments); }; +} +function messageListener(scope, callee) { + return scope ? scope.eval('(' + listener + ')')(callee) : callee +} + +exports.channel = function channel(scope, messageManager, address, raw) { + address = jetpackID + ':' + address + return { + input: function input(next, stop) { + let listener = messageListener(scope, function onMessage(message) { + if (false === next(raw ? message : message.json)) + messageManager.removeMessageListener(address, listener); + }); + messageManager.addMessageListener(address, listener); + }, + output: function output(data) { + messageManager.sendAsyncMessage(address, data); + }, + sync: !messageManager.sendSyncMessage ? null : function sync(data) { + messageManager.sendSyncMessage(address, data); + } + }; +}; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/collection.js b/tools/addon-sdk-1.4/packages/api-utils/lib/collection.js new file mode 100644 index 0000000..5525a5a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/collection.js @@ -0,0 +1,141 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +exports.Collection = Collection; + +/** + * Adds a collection property to the given object. Setting the property to a + * scalar value empties the collection and adds the value. Setting it to an + * array empties the collection and adds all the items in the array. + * + * @param obj + * The property will be defined on this object. + * @param propName + * The name of the property. + * @param array + * If given, this will be used as the collection's backing array. + */ +exports.addCollectionProperty = function addCollProperty(obj, propName, array) { + array = array || []; + let publicIface = new Collection(array); + + obj.__defineSetter__(propName, function (itemOrItems) { + array.splice(0, array.length); + publicIface.add(itemOrItems); + }); + + obj.__defineGetter__(propName, function () { + return publicIface; + }); +}; + +/** + * A collection is ordered, like an array, but its items are unique, like a set. + * + * @param array + * The collection is backed by an array. If this is given, it will be + * used as the backing array. This way the caller can fully control the + * collection. Otherwise a new empty array will be used, and no one but + * the collection will have access to it. + */ +function Collection(array) { + array = array || []; + + /** + * Provides iteration over the collection. Items are yielded in the order + * they were added. + */ + this.__iterator__ = function Collection___iterator__() { + let items = array.slice(); + for (let i = 0; i < items.length; i++) + yield items[i]; + }; + + /** + * The number of items in the collection. + */ + this.__defineGetter__("length", function Collection_get_length() { + return array.length; + }); + + /** + * Adds a single item or an array of items to the collection. Any items + * already contained in the collection are ignored. + * + * @param itemOrItems + * An item or array of items. + * @return The collection. + */ + this.add = function Collection_add(itemOrItems) { + let items = toArray(itemOrItems); + for (let i = 0; i < items.length; i++) { + let item = items[i]; + if (array.indexOf(item) < 0) + array.push(item); + } + return this; + }; + + /** + * Removes a single item or an array of items from the collection. Any items + * not contained in the collection are ignored. + * + * @param itemOrItems + * An item or array of items. + * @return The collection. + */ + this.remove = function Collection_remove(itemOrItems) { + let items = toArray(itemOrItems); + for (let i = 0; i < items.length; i++) { + let idx = array.indexOf(items[i]); + if (idx >= 0) + array.splice(idx, 1); + } + return this; + }; +}; + +function toArray(itemOrItems) { + let isArr = itemOrItems && + itemOrItems.constructor && + itemOrItems.constructor.name === "Array"; + return isArr ? itemOrItems : [itemOrItems]; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/content.js b/tools/addon-sdk-1.4/packages/api-utils/lib/content.js new file mode 100644 index 0000000..a46a5ff --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/content.js @@ -0,0 +1,44 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +exports.Loader = require('./content/loader').Loader; +exports.Symbiont = require('./content/symbiont').Symbiont; +exports.Worker = require('./content/worker').Worker; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/content/loader.js b/tools/addon-sdk-1.4/packages/api-utils/lib/content/loader.js new file mode 100644 index 0000000..25bc651 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/content/loader.js @@ -0,0 +1,203 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { EventEmitter } = require('../events'); +const { validateOptions, getTypeOf } = require('../api-utils'); +const { URL, toFilename } = require('../url'); +const file = require('../file'); + +// map of property validations +const valid = { + contentURL: { + ok: function (value) { + try { + URL(value); + } + catch(e) { + return false; + } + return true; + }, + msg: 'The `contentURL` option must be a valid URL.' + }, + contentScriptFile: { + is: ['undefined', 'null', 'string', 'array'], + map: function(value) 'undefined' === getTypeOf(value) ? null : value, + ok: function(value) { + if (getTypeOf(value) === 'array') { + // Make sure every item is a local file URL. + return value.every(function (item) { + try { + toFilename(item); + return true; + } + catch(e) { + return false; + } + }); + } + return true; + }, + msg: 'The `contentScriptFile` option must be a local file URL or an array of' + + 'URLs.' + }, + contentScript: { + is: ['undefined', 'null', 'string', 'array'], + map: function(value) 'undefined' === getTypeOf(value) ? null : value, + ok: function(value) 'array' !== getTypeOf(value) ? true : + value.every(function(item) 'string' === getTypeOf(item)) + , + msg: 'The script option must be a string or an array of strings.' + }, + contentScriptWhen: { + is: ['string'], + ok: function(value) ['start', 'ready', 'end'].indexOf(value) >= 0, + map: function(value) { + return value || 'end'; + }, + msg: 'The `contentScriptWhen` option must be either "start", "ready" or "end".' + } +}; +exports.validationAttributes = valid; + +/** + * Shortcut function to validate property with validation. + * @param {Object|Number|String} suspect + * value to validate + * @param {Object} validation + * validation rule passed to `api-utils` + */ +function validate(suspect, validation) validateOptions( + { $: suspect }, + { $: validation } +).$ + +function Allow(script) ({ + get script() script, + set script(value) script = !!value +}) + +/** + * Trait is intended to be used in some composition. It provides set of core + * properties and bounded validations to them. Trait is useful for all the + * compositions providing high level APIs for interaction with content. + * Property changes emit `"propertyChange"` events on instances. + */ +const Loader = EventEmitter.compose({ + /** + * Permissions for the content, with the following keys: + * @property {Object} [allow = { script: true }] + * @property {Boolean} [allow.script = true] + * Whether or not to execute script in the content. Defaults to true. + */ + get allow() this._allow || (this._allow = Allow(true)), + set allow(value) this.allow.script = value && value.script, + _allow: null, + /** + * The content to load. Either a string of HTML or a URL. + * @type {String} + */ + get contentURL() this._contentURL, + set contentURL(value) { + value = validate(value, valid.contentURL); + if (this._contentURL != value) { + this._emit('propertyChange', { + contentURL: this._contentURL = value + }); + } + }, + _contentURL: null, + /** + * When to load the content scripts. + * Possible values are "end" (default), which loads them once all page + * contents have been loaded, "ready", which loads them once DOM nodes are + * ready (ie like DOMContentLoaded event), and "start", which loads them once + * the `window` object for the page has been created, but before any scripts + * specified by the page have been loaded. + * Property change emits `propertyChange` event on instance with this key + * and new value. + * @type {'start'|'ready'|'end'} + */ + get contentScriptWhen() this._contentScriptWhen, + set contentScriptWhen(value) { + value = validate(value, valid.contentScriptWhen); + if (value !== this._contentScriptWhen) { + this._emit('propertyChange', { + contentScriptWhen: this._contentScriptWhen = value + }); + } + }, + _contentScriptWhen: 'end', + /** + * The URLs of content scripts. + * Property change emits `propertyChange` event on instance with this key + * and new value. + * @type {String[]} + */ + get contentScriptFile() this._contentScriptFile, + set contentScriptFile(value) { + value = validate(value, valid.contentScriptFile); + if (value != this._contentScriptFile) { + this._emit('propertyChange', { + contentScriptFile: this._contentScriptFile = value + }); + } + }, + _contentScriptFile: null, + /** + * The texts of content script. + * Property change emits `propertyChange` event on instance with this key + * and new value. + * @type {String|undefined} + */ + get contentScript() this._contentScript, + set contentScript(value) { + value = validate(value, valid.contentScript); + if (value != this._contentScript) { + this._emit('propertyChange', { + contentScript: this._contentScript = value + }); + } + }, + _contentScript: null +}); +exports.Loader = Loader; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/content/symbiont.js b/tools/addon-sdk-1.4/packages/api-utils/lib/content/symbiont.js new file mode 100644 index 0000000..d6dbf38 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/content/symbiont.js @@ -0,0 +1,217 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez <myk@mozilla.org> (Original Author) + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const { Worker } = require('./worker'); +const { Loader } = require('./loader'); +const hiddenFrames = require('../hidden-frame'); +const observers = require('../observer-service'); +const unload = require('../unload'); + +/** + * This trait is layered on top of `Worker` and in contrast to symbiont + * Worker constructor requires `content` option that represents content + * that will be loaded in the provided frame, if frame is not provided + * Worker will create hidden one. + */ +const Symbiont = Worker.resolve({ + constructor: '_initWorker', + destroy: '_workerDestroy' + }).compose(Loader, { + + /** + * The constructor requires all the options that are required by + * `require('content').Worker` with the difference that the `frame` option + * is optional. If `frame` is not provided, `contentURL` is expected. + * @param {Object} options + * @param {String} options.contentURL + * URL of a content to load into `this._frame` and create worker for. + * @param {Element} [options.frame] + * iframe element that is used to load `options.contentURL` into. + * if frame is not provided hidden iframe will be created. + */ + constructor: function Symbiont(options) { + options = options || {}; + + if ('contentURL' in options) + this.contentURL = options.contentURL; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('allow' in options) + this.allow = options.allow; + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + if ('frame' in options) { + this._initFrame(options.frame); + } + else { + let self = this; + this._hiddenFrame = hiddenFrames.HiddenFrame({ + onReady: function onFrame() { + self._initFrame(this.element); + } + }); + hiddenFrames.add(this._hiddenFrame); + } + + unload.ensure(this._public, "destroy"); + }, + + destroy: function destroy() { + this._workerDestroy(); + this._unregisterListener(); + this._frame = null; + if (this._hiddenFrame) { + hiddenFrames.remove(this._hiddenFrame); + this._hiddenFrame = null; + } + }, + + /** + * XUL iframe or browser elements with attribute `type` being `content`. + * Used to create `ContentSymbiont` from. + * @type {nsIFrame|nsIBrowser} + */ + _frame: null, + + /** + * Listener to the `'frameReady"` event (emitted when `iframe` is ready). + * Removes listener, sets right permissions to the frame and loads content. + */ + _initFrame: function _initFrame(frame) { + if (this._loadListener) + this._unregisterListener(); + + this._frame = frame; + frame.docShell.allowJavascript = this.allow.script; + frame.setAttribute("src", this._contentURL); + + // Inject `addon` object in document if we load a document from + // one of our addon folder and if no content script are defined. bug 612726 + let isDataResource = + typeof this._contentURL == "string" && + this._contentURL.indexOf(require("@packaging").uriPrefix) == 0; + let hasContentScript = + (Array.isArray(this.contentScript) ? this.contentScript.length > 0 + : !!this.contentScript) || + (Array.isArray(this.contentScriptFile) ? this.contentScriptFile.length > 0 + : !!this.contentScriptFile); + // If we have to inject `addon` we have to do it before document + // script execution, so during `start`: + this._injectInDocument = isDataResource && !hasContentScript; + if (this._injectInDocument) + this.contentScriptWhen = "start"; + + if ((frame.contentDocument.readyState == "complete" || + (frame.contentDocument.readyState == "interactive" && + this.contentScriptWhen != 'end' )) && + frame.contentDocument.location == this._contentURL) { + // In some cases src doesn't change and document is already ready + // (for ex: when the user moves a widget while customizing toolbars.) + this._onInit(); + return; + } + + let self = this; + + if ('start' == this.contentScriptWhen) { + this._loadEvent = 'start'; + observers.add('document-element-inserted', + this._loadListener = function onStart(doc) { + + let window = doc.defaultView; + if (window && window == frame.contentWindow) { + self._unregisterListener(); + self._onInit(); + } + + }); + return; + } + + let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded'; + let self = this; + this._loadEvent = eventName; + frame.addEventListener(eventName, + this._loadListener = function _onReady(event) { + + if (event.target != frame.contentDocument) + return; + self._unregisterListener(); + + self._onInit(); + + }, true); + + }, + + /** + * Unregister listener that watchs for document being ready to be injected. + * This listener is registered in `Symbiont._initFrame`. + */ + _unregisterListener: function _unregisterListener() { + if (!this._loadListener) + return; + if (this._loadEvent == "start") { + observers.remove('document-element-inserted', this._loadListener); + } + else { + this._frame.removeEventListener(this._loadEvent, this._loadListener, + true); + } + this._loadListener = null; + }, + + /** + * Called by Symbiont itself when the frame is ready to load + * content scripts according to contentScriptWhen. Overloaded by Panel. + */ + _onInit: function () { + this._initWorker({ window: this._frame.contentWindow }); + } + +}); +exports.Symbiont = Symbiont; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/content/worker.js b/tools/addon-sdk-1.4/packages/api-utils/lib/content/worker.js new file mode 100644 index 0000000..6ded506 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/content/worker.js @@ -0,0 +1,662 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const { Trait } = require('../traits'); +const { EventEmitter, EventEmitterTrait } = require('../events'); +const { Ci, Cu, Cc } = require('chrome'); +const timer = require('../timer'); +const { toFilename } = require('../url'); +const file = require('../file'); +const unload = require('../unload'); +const observers = require('../observer-service'); +const { Cortex } = require('../cortex'); +const { Enqueued } = require('../utils/function'); +const self = require("self"); +const scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + +const CONTENT_PROXY_URL = self.data.url("content-proxy.js"); + +const JS_VERSION = '1.8'; + +const ERR_DESTROYED = + "The page has been destroyed and can no longer be used."; + +/** + * This key is not exported and should only be used for proxy tests. + * The following `PRIVATE_KEY` is used in addon module scope in order to tell + * Worker API to expose `UNWRAP_ACCESS_KEY` in content script. + * This key allows test-content-proxy.js to unwrap proxy with valueOf: + * let xpcWrapper = proxyWrapper.valueOf(UNWRAP_ACCESS_KEY); + */ +const PRIVATE_KEY = {}; + +function ensureArgumentsAreJSON(array, window) { + // JSON.stringify is buggy with cross-sandbox values, + // it may return "{}" on functions. Use a replacer to match them correctly. + function replacer(k, v) { + return typeof v === "function" ? undefined : v; + } + // If a window is given, we use its `JSON.parse` object in order to + // create JS objects for its compartments (See bug 714891) + let parse = JSON.parse; + if (window) { + // As we can't directly rely on `window.wrappedJSObject.JSON`, we create + // a temporary sandbox in order to get access to a safe `JSON` object: + parse = Cu.Sandbox(window).JSON.parse; + } + return parse(JSON.stringify(array, replacer)); +} + +/** + * Extended `EventEmitter` allowing us to emit events asynchronously. + */ +const AsyncEventEmitter = EventEmitter.compose({ + /** + * Emits event in the next turn of event loop. + */ + _asyncEmit: function _asyncEmit() { + timer.setTimeout(function emitter(emit, scope, params) { + emit.apply(scope, params); + }, 0, this._emit, this, arguments) + } +}); + +/** + * Local trait providing implementation of the workers global scope. + * Used to configure global object in the sandbox. + * @see http://www.w3.org/TR/workers/#workerglobalscope + */ +const WorkerGlobalScope = AsyncEventEmitter.compose({ + on: Trait.required, + _removeAllListeners: Trait.required, + + // wrapped functions from `'timer'` module. + // Wrapper adds `try catch` blocks to the callbacks in order to + // emit `error` event on a symbiont if exception is thrown in + // the Worker global scope. + // @see http://www.w3.org/TR/workers/#workerutils + + // List of all living timeouts/intervals + _timers: null, + + setTimeout: function setTimeout(callback, delay) { + let params = Array.slice(arguments, 2); + let id = timer.setTimeout(function(self) { + try { + delete self._timers[id]; + callback.apply(null, params); + } catch(e) { + self._addonWorker._asyncEmit('error', e); + } + }, delay, this); + this._timers[id] = true; + return id; + }, + clearTimeout: function clearTimeout(id){ + delete this._timers[id]; + return timer.clearTimeout(id); + }, + + setInterval: function setInterval(callback, delay) { + let params = Array.slice(arguments, 2); + let id = timer.setInterval(function(self) { + try { + callback.apply(null, params); + } catch(e) { + self._addonWorker._asyncEmit('error', e); + } + }, delay, this); + this._timers[id] = true; + return id; + }, + clearInterval: function clearInterval(id) { + delete this._timers[id]; + return timer.clearInterval(id); + }, + + /** + * `onMessage` function defined in the global scope of the worker context. + */ + get _onMessage() this.__onMessage, + set _onMessage(value) { + let listener = this.__onMessage; + if (listener && value !== listener) { + this.removeListener('message', listener); + this.__onMessage = undefined; + } + if (value) + this.on('message', this.__onMessage = value); + }, + __onMessage: undefined, + + /** + * Function for sending data to the addon side. + * Validates that data is a `JSON` or primitive value and emits + * 'message' event on the worker in the next turn of the event loop. + * _Later this will be sending data across process boundaries._ + * @param {JSON|String|Number|Boolean} data + */ + postMessage: function postMessage(data) { + if (!this._addonWorker) + throw new Error(ERR_DESTROYED); + this._addonWorker._asyncEmit('message', ensureArgumentsAreJSON(data)); + }, + + /** + * EventEmitter, that behaves (calls listeners) asynchronously. + * A way to send customized messages to / from the worker. + * Events from in the worker can be observed / emitted via self.on / self.emit + */ + get port() this._port._public, + + /** + * Same object than this.port but private API. + * Allow access to _asyncEmit, in order to send event to port. + */ + _port: null, + + /** + * Alias to the global scope in the context of worker. Similar to + * `window` concept. + */ + get self() this._public, + + /** + * Configures sandbox and loads content scripts into it. + * @param {Worker} worker + * content worker + */ + constructor: function WorkerGlobalScope(worker) { + this._addonWorker = worker; + + // Hack in order to allow addon worker to access _asyncEmit + // as this is the private object of WorkerGlobalScope + worker._contentWorker = this; + + // create an event emitter that receive and send events from/to the addon + let contentWorker = this; + this._port = EventEmitterTrait.create({ + emit: function () { + let addonWorker = contentWorker._addonWorker; + if (!addonWorker) + throw new Error(ERR_DESTROYED); + addonWorker._onContentScriptEvent.apply(addonWorker, arguments); + } + }); + // create emit that executes in next turn of event loop. + this._port._asyncEmit = Enqueued(this._port._emit); + // expose wrapped port, that exposes only public properties. + this._port._public = Cortex(this._port); + + // We receive an unwrapped window, with raw js access + let window = worker._window; + + let proto = window; + let proxySandbox = null; + // Build content proxies only if the document has a non-system principal + if (window.wrappedJSObject) { + // Instantiate the proxy code in another Sandbox in order to prevent + // content script from polluting globals used by proxy code + proxySandbox = Cu.Sandbox(window, { + wantXrays: true + }); + proxySandbox.console = console; + // Execute the proxy code + scriptLoader.loadSubScript(CONTENT_PROXY_URL, proxySandbox); + // Get a reference of the window's proxy + proto = proxySandbox.create(window); + } + + // Create the sandbox and bind it to window in order for content scripts to + // have access to all standard globals (window, document, ...) + let sandbox = this._sandbox = new Cu.Sandbox(window, { + sandboxPrototype: proto, + wantXrays: true + }); + Object.defineProperties(sandbox, { + // We need "this === window === top" to be true in toplevel scope: + window: { get: function() sandbox }, + top: { get: function() sandbox }, + // Use the Greasemonkey naming convention to provide access to the + // unwrapped window object so the content script can access document + // JavaScript values. + // NOTE: this functionality is experimental and may change or go away + // at any time! + unsafeWindow: { get: function () window.wrappedJSObject } + }); + + // Internal feature that is only used by SDK tests: + // Expose unlock key to content script context. + // See `PRIVATE_KEY` definition for more information. + if (proxySandbox && worker._expose_key) + sandbox.UNWRAP_ACCESS_KEY = proxySandbox.UNWRAP_ACCESS_KEY; + // Initialize timer lists + this._timers = {}; + + let publicAPI = this._public; + + // List of content script globals: + let keys = ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', + 'self']; + for each (let key in keys) { + Object.defineProperty( + sandbox, key, Object.getOwnPropertyDescriptor(publicAPI, key) + ); + } + let self = this; + Object.defineProperties(sandbox, { + onMessage: { + get: function() self._onMessage, + set: function(value) { + console.warn("The global `onMessage` function in content scripts " + + "is deprecated in favor of the `self.on()` function. " + + "Replace `onMessage = function (data){}` definitions " + + "with calls to `self.on('message', function (data){})`. " + + "For more info on `self.on`, see " + + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); + self._onMessage = value; + }, + configurable: true + }, + console: { value: console, configurable: true }, + + // Deprecated use of on/postMessage from globals + on: { + value: function () { + console.warn("The global `on()` function in content scripts is " + + "deprecated in favor of the `self.on()` function, " + + "which works the same. Replace calls to `on()` with " + + "calls to `self.on()`" + + "For more info on `self.on`, see " + + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); + publicAPI.on.apply(publicAPI, arguments); + }, + configurable: true + }, + postMessage: { + value: function () { + console.warn("The global `postMessage()` function in content " + + "scripts is deprecated in favor of the " + + "`self.postMessage()` function, which works the same. " + + "Replace calls to `postMessage()` with calls to " + + "`self.postMessage()`." + + "For more info on `self.on`, see " + + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); + publicAPI.postMessage.apply(publicAPI, arguments); + }, + configurable: true + } + }); + + // Temporary fix for test-widget, that pass self.postMessage to proxy code + // that first try to access to `___proxy` and then call it through `apply`. + // We need to move function given to content script to a sandbox + // with same principal than the content script. + // In the meantime, we need to allow such access explicitly + // by using `__exposedProps__` property, documented here: + // https://developer.mozilla.org/en/XPConnect_wrappers + sandbox.self.postMessage.__exposedProps__ = { + ___proxy: 'rw', + apply: 'rw' + } + + // Inject `addon` global into target document if document is trusted, + // `addon` in document is equivalent to `self` in content script. + if (worker._injectInDocument) { + let win = window.wrappedJSObject ? window.wrappedJSObject : window; + Object.defineProperty(win, "addon", { + get: function () publicAPI + } + ); + } + + // The order of `contentScriptFile` and `contentScript` evaluation is + // intentional, so programs can load libraries like jQuery from script URLs + // and use them in scripts. + let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile + : null, + contentScript = ('contentScript' in worker) ? worker.contentScript : null; + + if (contentScriptFile) { + if (Array.isArray(contentScriptFile)) + this._importScripts.apply(this, contentScriptFile); + else + this._importScripts(contentScriptFile); + } + if (contentScript) { + this._evaluate( + Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript + ); + } + }, + _destructor: function _destructor() { + this._removeAllListeners(); + // Unregister all setTimeout/setInterval + // We can use `clearTimeout` for both setTimeout/setInterval + // as internal implementation of timer module use same method for both. + for (let id in this._timers) + timer.clearTimeout(id); + this._sandbox = null; + this._addonWorker = null; + this.__onMessage = undefined; + }, + + /** + * JavaScript sandbox where all the content scripts are evaluated. + * {Sandbox} + */ + _sandbox: null, + + /** + * Reference to the addon side of the worker. + * @type {Worker} + */ + _addonWorker: null, + + /** + * Evaluates code in the sandbox. + * @param {String} code + * JavaScript source to evaluate. + * @param {String} [filename='javascript:' + code] + * Name of the file + */ + _evaluate: function(code, filename) { + filename = filename || 'javascript:' + code; + try { + Cu.evalInSandbox(code, this._sandbox, JS_VERSION, filename, 1); + } + catch(e) { + this._addonWorker._asyncEmit('error', e); + } + }, + /** + * Imports scripts to the sandbox by reading files under urls and + * evaluating its source. If exception occurs during evaluation + * `"error"` event is emitted on the worker. + * This is actually an analog to the `importScript` method in web + * workers but in our case it's not exposed even though content + * scripts may be able to do it synchronously since IO operation + * takes place in the UI process. + */ + _importScripts: function _importScripts(url) { + let urls = Array.slice(arguments, 0); + for each (let contentScriptFile in urls) { + try { + let filename = toFilename(contentScriptFile); + this._evaluate(file.read(filename), filename); + } + catch(e) { + this._addonWorker._asyncEmit('error', e) + } + } + } +}); + +/** + * Message-passing facility for communication between code running + * in the content and add-on process. + * @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/content/worker + */ +const Worker = AsyncEventEmitter.compose({ + on: Trait.required, + _asyncEmit: Trait.required, + _removeAllListeners: Trait.required, + + /** + * Sends a message to the worker's global scope. Method takes single + * argument, which represents data to be sent to the worker. The data may + * be any primitive type value or `JSON`. Call of this method asynchronously + * emits `message` event with data value in the global scope of this + * symbiont. + * + * `message` event listeners can be set either by calling + * `self.on` with a first argument string `"message"` or by + * implementing `onMessage` function in the global scope of this worker. + * @param {Number|String|JSON} data + */ + postMessage: function postMessage(data) { + if (!this._contentWorker) + throw new Error(ERR_DESTROYED); + this._contentWorker._asyncEmit('message', + ensureArgumentsAreJSON(data, this._window)); + }, + + /** + * EventEmitter, that behaves (calls listeners) asynchronously. + * A way to send customized messages to / from the worker. + * Events from in the worker can be observed / emitted via + * worker.on / worker.emit. + */ + get port() { + // We generate dynamically this attribute as it needs to be accessible + // before Worker.constructor gets called. (For ex: Panel) + + // create an event emitter that receive and send events from/to the worker + let self = this; + this._port = EventEmitterTrait.create({ + emit: function () self._emitEventToContent(arguments) + }); + // create emit that executes in next turn of event loop. + this._port._asyncEmit = Enqueued(this._port._emit); + // expose wrapped port, that exposes only public properties: + // We need to destroy this getter in order to be able to set the + // final value. We need to update only public port attribute as we never + // try to access port attribute from private API. + delete this._public.port; + this._public.port = Cortex(this._port); + // Replicate public port to the private object + delete this.port; + this.port = this._public.port; + + return this._port; + }, + + /** + * Same object than this.port but private API. + * Allow access to _asyncEmit, in order to send event to port. + */ + _port: null, + + /** + * Emit a custom event to the content script, + * i.e. emit this event on `self.port` + */ + _emitEventToContent: function _emitEventToContent(args) { + // We need to save events that are emitted before the worker is + // initialized + if (!this._inited) { + this._earlyEvents.push(args); + return; + } + + // We throw exception when the worker has been destroyed + if (!this._contentWorker) { + throw new Error(ERR_DESTROYED); + } + + let scope = this._contentWorker._port; + // Ensure that we pass only JSON values + let array = Array.prototype.slice.call(args); + scope._asyncEmit.apply(scope, ensureArgumentsAreJSON(array, this._window)); + }, + + // Is worker connected to the content worker (i.e. WorkerGlobalScope) ? + _inited: false, + + // List of custom events fired before worker is initialized + get _earlyEvents() { + delete this._earlyEvents; + this._earlyEvents = []; + return this._earlyEvents; + }, + + constructor: function Worker(options) { + options = options || {}; + + if ('window' in options) + this._window = options.window; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + if ('onDetach' in options) + this.on('detach', options.onDetach); + + // Internal feature that is only used by SDK unit tests. + // See `PRIVATE_KEY` definition for more information. + if ('exposeUnlockKey' in options && options.exposeUnlockKey === PRIVATE_KEY) + this._expose_key = true; + + // Track document unload to destroy this worker. + // We can't watch for unload event on page's window object as it + // prevents bfcache from working: + // https://developer.mozilla.org/En/Working_with_BFCache + this._windowID = this._window. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils). + currentInnerWindowID; + observers.add("inner-window-destroyed", + this._documentUnload = this._documentUnload.bind(this)); + + unload.ensure(this._public, "destroy"); + + // Ensure that worker._port is initialized for contentWorker to be able + // to send use event during WorkerGlobalScope(this) + this.port; + + // will set this._contentWorker pointing to the private API: + WorkerGlobalScope(this); + + // Mainly enable worker.port.emit to send event to the content worker + this._inited = true; + + // Flush all events that have been fired before the worker is initialized. + this._earlyEvents.forEach((function (args) this._emitEventToContent(args)). + bind(this)); + }, + + _documentUnload: function _documentUnload(subject, topic, data) { + let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (innerWinID != this._windowID) return false; + this._workerCleanup(); + return true; + }, + + get url() { + // this._window will be null after detach + return this._window ? this._window.document.location.href : null; + }, + + get tab() { + if (this._window) { + let tab = require("../tabs/tab"); + // this._window will be null after detach + return tab.getTabForWindow(this._window); + } + return null; + }, + + /** + * Tells content worker to unload itself and + * removes all the references from itself. + */ + destroy: function destroy() { + this._workerCleanup(); + this._removeAllListeners('message'); + this._removeAllListeners('error'); + this._removeAllListeners('detach'); + }, + + /** + * Remove all internal references to the attached document + * Tells _port to unload itself and removes all the references from itself. + */ + _workerCleanup: function _workerCleanup() { + // maybe unloaded before content side is created + // As Symbiont call worker.constructor on document load + if (this._contentWorker) + this._contentWorker._destructor(); + this._contentWorker = null; + this._window = null; + // This method may be called multiple times, + // avoid dispatching `detach` event more than once + if (this._windowID) { + this._windowID = null; + observers.remove("inner-window-destroyed", this._documentUnload); + this._earlyEvents.slice(0, this._earlyEvents.length); + this._emit("detach"); + } + }, + + /** + * Receive an event from the content script that need to be sent to + * worker.port. Provide a way for composed object to catch all events. + */ + _onContentScriptEvent: function _onContentScriptEvent() { + // Ensure that we pass only JSON values + let array = Array.prototype.slice.call(arguments); + this._port._asyncEmit.apply(this._port, ensureArgumentsAreJSON(array)); + }, + + /** + * Reference to the content side of the worker. + * @type {WorkerGlobalScope} + */ + _contentWorker: null, + + /** + * Reference to the window that is accessible from + * the content scripts. + * @type {Object} + */ + _window: null, + + /** + * Flag to enable `addon` object injection in document. (bug 612726) + * @type {Boolean} + */ + _injectInDocument: false +}); +exports.Worker = Worker; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/cortex.js b/tools/addon-sdk-1.4/packages/api-utils/lib/cortex.js new file mode 100644 index 0000000..059d9de --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/cortex.js @@ -0,0 +1,139 @@ +/* vim:set ts=2 sw=2 sts=2 + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +// `var` is being used in the module in order to make it reusable in +// environments in which `let` and `const` is not yet supported. + +// Returns `object`'s property value, where `name` is a name of the property. +function get(object, name) { + return object[name]; +} + +// Assigns `value` to the `object`'s property, where `name` is the name of the +// property. +function set(object, name, value) { + return object[name] = value; +} + +/** + * Given an `object` containing a property with the given `name`, create + * a property descriptor that can be used to define alias/proxy properties + * on other objects. A change in the value of an alias will propagate + * to the aliased property and vice versa. + */ +function createAliasProperty(object, name) { + // Getting own property descriptor of an `object` for the given `name` as + // we are going to create proxy analog. + var property = Object.getOwnPropertyDescriptor(object, name); + var descriptor = { + configurable: property.configurable, + enumerable: property.enumerable, + alias: true + }; + + // If the original property has a getter and/or setter, bind a + // corresponding getter/setter in the alias descriptor to the original + // object, so the `this` object in the getter/setter is the original object + // rather than the alias. + if ("get" in property && property.get) + descriptor.get = property.get.bind(object); + if ("set" in property && property.set) + descriptor.set = property.set.bind(object); + + // If original property was a value property. + if ("value" in property) { + // If original property is a method using it's `object` bounded copy. + if (typeof property.value === "function") { + descriptor.value = property.value.bind(object); + // Also preserving writability of the original property. + descriptor.writable = property.writable; + } + + // If the original property was just a data property, we create proxy + // accessors using our custom get/set functions to propagate changes to the + // original `object` and vice versa. + else { + descriptor.get = get.bind(null, object, name); + descriptor.set = set.bind(null, object, name); + } + } + return descriptor; +} + +// Defines property on `object` object with a name `alias` if given if not +// defaults to `name` that represents an alias of `source[name]`. If aliased +// property was an assessor or a method `this` pseudo-variable will be `source` +// when invoked. If aliased property was a data property changes on any of the +// aliases will propagate to the `source[name]` and also other way round. +function defineAlias(source, target, name, alias) { + return Object.defineProperty(target, alias || name, + createAliasProperty(source, name)); +} + +/** + * Function takes any `object` and returns a proxy for its own public + * properties. By default properties are considered to be public if they don't + * start with `"_"`, but default behavior can be overridden if needed, by + * passing array of public property `names` as a second argument. By default + * returned object will be direct decedent of the given `object`'s prototype, + * but this can be overridden by passing third optional argument, that will be + * used as `prototype` instead. + * @param {Object} object + * Object to create cortex for. + * @param {String[]} [names] + * Optional array of public property names. + * @param {Object} [prototype] + * Optional argument that will be used as `prototype` of the returned object, + * if not provided `Object.getPrototypeOf(object)` is used instead. + */ +exports.Cortex = function Cortex(object, names, prototype) { + // Creating a cortex object from the given `prototype`, if one was not + // provided then `prototype` of a given `object` is used. This allows + // consumer to define expected behavior `instanceof`. In common case + // `prototype` argument can be omitted to preserve same behavior of + // `instanceof` as on original `object`. + var cortex = Object.create(prototype || Object.getPrototypeOf(object)); + // Creating alias properties on the `cortex` object for all the own + // properties of the original `object` that are contained in `names` array. + // If `names` array is not provided then all the properties that don't + // start with `"_"` are aliased. + Object.getOwnPropertyNames(object).forEach(function (name) { + if ((!names && "_" !== name.charAt(0)) || (names && ~names.indexOf(name))) + defineAlias(object, cortex, name); + }); + return cortex; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/cuddlefish.js b/tools/addon-sdk-1.4/packages/api-utils/lib/cuddlefish.js new file mode 100644 index 0000000..c0c9935 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/cuddlefish.js @@ -0,0 +1,320 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +var EXPORTED_SYMBOLS = [ 'Loader' ]; + +!function(exports) { + +"use strict"; + +const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu, + results: Cr, manager: Cm } = Components; +const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); +const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1']. + getService(Ci.mozIJSSubScriptLoader); + +const Sandbox = { + new: function (prototype, principal) { + let sandbox = Object.create(Sandbox, { + sandbox: { + value: Cu.Sandbox(principal || Sandbox.principal, { + sandboxPrototype: prototype || Sandbox.prototype, + wantXrays: Sandbox.wantXrays + }) + } + }); + // There are few properties (dump, Iterator) that by default appear in + // sandboxes shadowing properties provided by a prototype. To workaround + // this we override all such properties by copying them directly to the + // sandbox. + Object.keys(prototype).forEach(function onEach(key) { + if (sandbox.sandbox[key] !== prototype[key]) + sandbox.sandbox[key] = prototype[key]; + }); + return sandbox; + }, + evaluate: function evaluate(source, uri, lineNumber) { + return Cu.evalInSandbox( + source, + this.sandbox, + this.version, + uri, + lineNumber || this.lineNumber + ); + }, + load: function load(uri) { + scriptLoader.loadSubScript(uri, this.sandbox); + }, + merge: function merge(properties) { + Object.getOwnPropertyNames(properties).forEach(function(name) { + Object.defineProperty(this.sandbox, name, + Object.getOwnPropertyDescriptor(properties, name)); + }, this); + }, + principal: systemPrincipal, + version: '1.8', + lineNumber: 1, + wantXrays: false, + prototype: {} +}; + +// the Module object made available to CommonJS modules when they are +// evaluated, along with 'exports' and 'uri' +const Module = { + new: function(id, path, uri) { + let module = Object.create(this); + + module.id = id; + module.path = path; + module.uri = uri; + module.exports = {}; + + return module; + }, + // TODO: I'd like to remove this, it's not used adds complexity and does + // not has much adoption in commonjs either. + setExports: function setExports(exports) { + this.exports = exports; + } +}; + +const Loader = { + new: function (options) { + let loader = Object.create(Loader, { + // Manifest generated by a linker, containing map of module url's mapped + // to it's requirements, comes from harness-options.json + manifest: { value: options.manifest || {} }, + + // Following property may be passed in (usually for mocking purposes) in + // order to override default modules cache. + modules: { value: options.modules || Object.create(Loader.modules) }, + globals: { value: options.globals || {} }, + + uriPrefix: { value: options.uriPrefix }, + + sandboxes: { value: {} } + }); + loader.require = this.require.bind(loader, options.loader); + + // some 'magic' modules, that have no corresponding .js file + loader.modules['@packaging'] = Object.freeze({ + id: '@packaging', + exports: JSON.parse(JSON.stringify(options)) + }); + loader.modules['@loader'] = Object.freeze({ + exports: Object.freeze({ Loader: Loader }), + id: '@loader' + }); + + // This special module defines globals which will be added to every + // module this loader creates + let globals = loader.require('api-utils/globals!'); + Object.getOwnPropertyNames(globals).forEach(function(name) { + Object.defineProperty(loader.globals, name, + Object.getOwnPropertyDescriptor(globals, name)); + }); + // Freeze globals so that modules won't have a chance to mutate scope of + // other modules. + Object.freeze(globals); + + // Override global `dump` so that it behaves same as in any other module ( + // currently we override dump to write to a file instead of `stdout` so that + // python can read it on windows). + dump = globals.dump; + + return Object.freeze(loader); + }, + modules: { + 'chrome': Object.freeze({ + exports: Object.freeze({ + Cc: Cc, + CC: CC, + Ci: Ci, + Cu: Cu, + Cr: Cr, + Cm: Cm, + components: Components, + messageManager: 'addMessageListener' in exports ? exports : null + }), + id: 'chrome' + }), + 'self': function self(loader, requirer) { + return loader.require('api-utils/self!').create(requirer.path); + }, + }, + + // populate a Module by evaluating the CommonJS module code in the sandbox + load: function load(module) { + let require = Loader.require.bind(this, module.path); + require.main = this.main; + let sandbox = this.sandboxes[module.path] = Sandbox.new(this.globals); + sandbox.merge({ + require: require, + module: module, + exports: module.exports + }); + + sandbox.load(module.uri); + + // Workaround for bug 674195. Freezing objects from other sandboxes fail, + // so we create descendant and freeze it instead. + if (typeof(module.exports) === 'object') { + module.exports = Object.prototype.isPrototypeOf(module.exports) ? + Object.freeze(module.exports) : + Object.freeze(Object.create(module.exports)); + } + }, + + // this require() is the main entry point for regular CommonJS modules. The + // bind() in load (above) causes those modules to get a very restricted + // form of this require(): one which can only ever reference this one + // loader, and which always uses their URI as a "base" (so they're limited + // to their own manifest entries, and can't import anything off the + // manifest). + require: function require(base, id) { + let module, manifest = this.manifest[base], requirer = this.modules[base]; + + if (!id) + throw Error("you must provide a module name when calling require() from " + + (requirer && requirer.id), base, id); + + // If we have a manifest for requirer, then all it's requirements have been + // registered by linker and we should have a `path` to the required module. + // Even pseudo-modules like 'chrome', 'self', '@packaging', and '@loader' + // have pseudo-paths: exactly those same names. + // details see: Bug-697422. + let requirement = manifest && manifest.requirements[id]; + if (!requirement) + throw Error("Module: " + (requirer && requirer.id) + ' located at ' + + base + " has no authority to load: " + id); + let path = requirement.path; + + if (path in this.modules) { + module = this.modules[path]; + } + else { + let uri = this.uriPrefix + path; + module = this.modules[path] = Module.new(id, path, uri); + this.load(module); + Object.freeze(module); + } + + // "magic" modules which have contents that depend upon who imports them + // (like "self") are expressed in the Loader's pre-populated 'modules' + // table as callable functions, which are given the reference to this + // Loader and a copy of the importer's URI + // + // TODO: Find a better way to implement `self`. + // Maybe something like require('self!path/to/data') + if (typeof(module) === 'function') + module = module(this, requirer); + + return module.exports; + }, + + // process.process() will eventually cause a call to main() to be evaluated + // in the addon's context. This function loads and executes the addon's + // entry point module. + main: function main(id, path) { + try { + let uri = this.uriPrefix + path; + let module = this.modules[path] = Module.new(id, path, uri); + this.load(module); // this is where the addon's main.js finally runs + let program = Object.freeze(module).exports; + + if (typeof(program.onUnload) === 'function') + this.require('api-utils/unload').when(program.onUnload); + + if (program.main) { + let { exit, staticArgs } = this.require('api-utils/system'); + let { loadReason } = this.require('@packaging'); + program.main({ loadReason: loadReason, staticArgs: staticArgs }, + { print: function($) dump($ + '\n'), quit: exit }); + } + } catch (error) { + Cu.reportError(error); + if (this.globals.console) this.globals.console.exception(error); + throw error; + } + }, + + // This is the main entry-point: bootstrap.js calls this when the add-on is + // installed. The order of calls is a bit confusing, but here's what + // happens (in temporal order): + // * process.spawn creates a new XUL 'browser' element which will house the + // main addon code. When e10s is active, this uses a real separate OS + // process. When e10s is disabled, this element lives in the one original + // process. Either way, its API is the same. + // * Grab the channel named "require!" and attach a handler which will load + // modules (in the chrome process) when requested to by the addon + // process. This handler uses Loader.require to import the module, then + // calls the module's .initialize() function to connect a new channel. + // The remote caller winds up with a channel reference, which they can + // use to send messages to the newly loaded module. This is for e10s. + // * After the channel handler is attached, process.process() (invoked by + // process.spawn()) will use loadScript() to evaluate code in the + // 'browser' element (which is where the main addon code starts running), + // to do the following: + // * create a Loader, initialized with the same manifest and + // harness-options.json that we've got + // * invoke it's main() method, with the name and path of the addon's + // entry module (which comes from linker via harness-options.js, and is + // usually main.js). That executes main(), above. + // * main() loads the addon's main.js, which executes all top-level + // forms. If the module defines an "exports.main=" function, we invoke + // that too. This is where the addon finally gets to run. + spawn: function spawn(id, path) { + let loader = this; + let process = this.require('api-utils/process'); + process.spawn(id, path)(function(addon) { + // Listen to `require!` channel's input messages from the add-on process + // and load modules being required. + addon.channel('require!').input(function({ requirer: { path }, id }) { + try { + Loader.require.call(loader, path, id).initialize(addon.channel(id)); + } catch (error) { + this.globals.console.exception(error); + } + }); + }); + }, + unload: function unload(reason, callback) { + this.require('api-utils/unload').send(reason, callback); + } +}; +exports.Loader = Loader; + +}(this); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/dom/events.js b/tools/addon-sdk-1.4/packages/api-utils/lib/dom/events.js new file mode 100644 index 0000000..da9c93d --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/dom/events.js @@ -0,0 +1,169 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +// Utility function that returns copy of the given `text` with last character +// removed if it is `"s"`. +function singularify(text) { + return text[text.length - 1] === "s" ? text.substr(0, text.length - 1) : text; +} + +// Utility function that takes event type, argument is passed to +// `document.createEvent` and returns name of the initializer method of the +// given event. Please note that there are some event types whose initializer +// methods can't be guessed by this function. For more details see following +// link: https://developer.mozilla.org/En/DOM/Document.createEvent +function getInitializerName(category) { + return "init" + singularify(category); +} + +/** + * Registers an event `listener` on a given `element`, that will be called + * when events of specified `type` is dispatched on the `element`. + * @param {Element} element + * Dom element to register listener on. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type) to + * listen for. + * @param {Function} listener + * Function that is called whenever an event of the specified `type` + * occurs. + * @param {Boolean} capture + * If true, indicates that the user wishes to initiate capture. After + * initiating capture, all events of the specified type will be dispatched + * to the registered listener before being dispatched to any `EventTarget`s + * beneath it in the DOM tree. Events which are bubbling upward through + * the tree will not trigger a listener designated to use capture. + * See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow) + * for a detailed explanation. + */ +function on(element, type, listener, capture) { + // `capture` defaults to `false`. + capture = capture || false; + element.addEventListener(type, listener, capture); +} +exports.on = on; + +/** + * Registers an event `listener` on a given `element`, that will be called + * only once, next time event of specified `type` is dispatched on the + * `element`. + * @param {Element} element + * Dom element to register listener on. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type) to + * listen for. + * @param {Function} listener + * Function that is called whenever an event of the specified `type` + * occurs. + * @param {Boolean} capture + * If true, indicates that the user wishes to initiate capture. After + * initiating capture, all events of the specified type will be dispatched + * to the registered listener before being dispatched to any `EventTarget`s + * beneath it in the DOM tree. Events which are bubbling upward through + * the tree will not trigger a listener designated to use capture. + * See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow) + * for a detailed explanation. + */ +function once(element, type, listener, capture) { + on(element, type, function selfRemovableListener(event) { + removeListener(element, type, selfRemovableListener, capture); + listener.apply(this, arguments); + }, capture); +} +exports.once = once; + +/** + * Unregisters an event `listener` on a given `element` for the events of the + * specified `type`. + * + * @param {Element} element + * Dom element to unregister listener from. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type) to + * listen for. + * @param {Function} listener + * Function that is called whenever an event of the specified `type` + * occurs. + * @param {Boolean} capture + * If true, indicates that the user wishes to initiate capture. After + * initiating capture, all events of the specified type will be dispatched + * to the registered listener before being dispatched to any `EventTarget`s + * beneath it in the DOM tree. Events which are bubbling upward through + * the tree will not trigger a listener designated to use capture. + * See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow) + * for a detailed explanation. + */ +function removeListener(element, type, listener, capture) { + element.removeEventListener(type, listener, capture); +} +exports.removeListener = removeListener; + +/** + * Emits event of the specified `type` and `category` on the given `element`. + * Specified `settings` are used to initialize event before dispatching it. + * @param {Element} element + * Dom element to dispatch event on. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type). + * @param {Object} options + * Options object containing following properties: + * - `category`: String passed to the `document.createEvent`. Option is + * optional and defaults to "UIEvents". + * - `initializer`: If passed it will be used as name of the method used + * to initialize event. If omitted name will be generated from the + * `category` field by prefixing it with `"init"` and removing last + * character if it matches `"s"`. + * - `settings`: Array of settings that are forwarded to the event + * initializer after firs `type` argument. + * @see https://developer.mozilla.org/En/DOM/Document.createEvent + */ +function emit(element, type, { category, initializer, settings }) { + category = category || "UIEvents"; + initializer = initializer || getInitializerName(category); + let document = element.ownerDocument; + let event = document.createEvent(category); + event[initializer].apply(event, [type].concat(settings)); + element.dispatchEvent(event); +}; +exports.emit = emit; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/dom/events/keys.js b/tools/addon-sdk-1.4/packages/api-utils/lib/dom/events/keys.js new file mode 100644 index 0000000..155625a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/dom/events/keys.js @@ -0,0 +1,93 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { emit } = require("../events"); +const { getCodeForKey, toJSON } = require("../../keyboard/utils"); +const { has } = require("../../array"); +const { isString } = require("../../type"); + +const INITIALIZER = "initKeyEvent"; +const CATEGORY = "KeyboardEvent"; + +function Options(options) { + if (!isString(options)) + return options; + + var { key, modifiers } = toJSON(options); + return { + key: key, + control: has(modifiers, "control"), + alt: has(modifiers, "alt"), + shift: has(modifiers, "shift"), + meta: has(modifiers, "meta") + }; +} + +var keyEvent = exports.keyEvent = function keyEvent(element, type, options) { + + emit(element, type, { + initializer: INITIALIZER, + category: CATEGORY, + settings: [ + !("bubbles" in options) || options.bubbles !== false, + !("cancelable" in options) || options.cancelable !== false, + "window" in options && options.window ? options.window : null, + "control" in options && !!options.control, + "alt" in options && !!options.alt, + "shift" in options && !!options.shift, + "meta" in options && !!options.meta, + getCodeForKey(options.key) || 0, + options.key.length === 1 ? options.key.charCodeAt(0) : 0 + ] + }); +} + +exports.keyDown = function keyDown(element, options) { + keyEvent(element, "keydown", Options(options)); +}; + +exports.keyUp = function keyUp(element, options) { + keyEvent(element, "keyup", Options(options)); +}; + +exports.keyPress = function keyPress(element, options) { + keyEvent(element, "keypress", Options(options)); +}; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/env!.js b/tools/addon-sdk-1.4/packages/api-utils/lib/env!.js new file mode 100644 index 0000000..642d599 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/env!.js @@ -0,0 +1,52 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const { messageManager } = require("chrome"); +const { channel } = require("./channel"); + +module.exports = function load(module) { + return { + require: function require(id) { + // Load required module on the chrome process. + channel(messageManager, messageManager, 'require!').sync({ + requirer: module, + id: id + }); + return channel(messageManager, messageManager, id); + } + }; +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/environment.js b/tools/addon-sdk-1.4/packages/api-utils/lib/environment.js new file mode 100644 index 0000000..5753801 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/environment.js @@ -0,0 +1,86 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +'use strict'; + +const { Cc, Ci } = require('chrome'); +const { get, set, exists } = Cc['@mozilla.org/process/environment;1']. + getService(Ci.nsIEnvironment); + +exports.env = Proxy.create({ + // XPCOM does not provides a way to enumerate environment variables, so we + // just don't support enumeration. + getPropertyNames: function() [], + getOwnPropertyNames: function() [], + enumerate: function() [], + keys: function() [], + // We do not support freezing, cause it would make it impossible to set new + // environment variables. + fix: function() undefined, + // We present all environment variables as own properties of this object, + // so we just delegate this call to `getOwnPropertyDescriptor`. + getPropertyDescriptor: function(name) this.getOwnPropertyDescriptor(name), + // If environment variable with this name is defined, we generate proprety + // descriptor for it, otherwise fall back to `undefined` so that for consumer + // this property does not exists. + getOwnPropertyDescriptor: function(name) { + return !exists(name) ? undefined : { + value: get(name), + enumerable: false, // Non-enumerable as we don't support enumeration. + configurable: true, // Configurable as it may be deleted. + writable: true // Writable as we do support set. + } + }, + + // New environment variables can be defined just by defining properties + // on this object. + defineProperty: function(name, { value }) set(name, value), + delete: function(name) set(name, null), + + // We present all properties as own, there for we just delegate to `hasOwn`. + has: function(name) this.hasOwn(name), + // We do support checks for existence of an environment variable, via `in` + // operator on this object. + hasOwn: function(name) exists(name), + + // On property get / set we do read / write appropriate environment variables, + // please note though, that variables with names of standard object properties + // intentionally (so that this behaves as normal object) can not be + // read / set. + get: function(proxy, name) Object.prototype[name] || get(name) || undefined, + set: function(proxy, name, value) Object.prototype[name] || set(name, value) +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/errors.js b/tools/addon-sdk-1.4/packages/api-utils/lib/errors.js new file mode 100644 index 0000000..56be526 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/errors.js @@ -0,0 +1,92 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +function logToConsole(e) { + console.exception(e); +} + +var catchAndLog = exports.catchAndLog = function(callback, + defaultResponse, + logException) { + if (!logException) + logException = logToConsole; + + return function() { + try { + return callback.apply(this, arguments); + } catch (e) { + logException(e); + return defaultResponse; + } + }; +}; + +exports.catchAndLogProps = function catchAndLogProps(object, + props, + defaultResponse, + logException) { + if (typeof(props) == "string") + props = [props]; + props.forEach( + function(property) { + object[property] = catchAndLog(object[property], + defaultResponse, + logException); + }); +}; + +/** + * Catch and return an exception while calling the callback. If the callback + * doesn't throw, return the return value of the callback in a way that makes it + * possible to distinguish between a return value and an exception. + * + * This function is useful when you need to pass the result of a call across + * a process boundary (across which exceptions don't propagate). It probably + * doesn't need to be factored out into this module, since it is only used by + * a single caller, but putting it here works around bug 625560. + */ +exports.catchAndReturn = function(callback) { + return function() { + try { + return { returnValue: callback.apply(this, arguments) }; + } + catch (exception) { + return { exception: exception }; + } + }; +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/events.js b/tools/addon-sdk-1.4/packages/api-utils/lib/events.js new file mode 100644 index 0000000..4e36c00 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/events.js @@ -0,0 +1,202 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * Drew Willcoxon <adw@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const ERROR_TYPE = 'error', + UNCAUGHT_ERROR = 'An error event was dispatched for which there was' + + ' no listener.', + BAD_LISTENER = 'The event listener must be a function.'; +/** + * This object is used to create an `EventEmitter` that, useful for composing + * objects that emit events. It implements an interface like `EventTarget` from + * DOM Level 2, which is implemented by Node objects in implementations that + * support the DOM Event Model. + * @see http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget + * @see http://nodejs.org/api.html#EventEmitter + * @see http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/events/EventDispatcher.html + */ +const eventEmitter = { + /** + * Registers an event `listener` that is called every time events of + * specified `type` are emitted. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + * @example + * worker.on('message', function (data) { + * console.log('data received: ' + data) + * }) + */ + on: function on(type, listener) { + if ('function' !== typeof listener) + throw new Error(BAD_LISTENER); + let listeners = this._listeners(type); + if (0 > listeners.indexOf(listener)) + listeners.push(listener); + // Use of `_public` is required by the legacy traits code that will go away + // once bug-637633 is fixed. + return this._public || this; + }, + + /** + * Registers an event `listener` that is called once the next time an event + * of the specified `type` is emitted. + * @param {String} type + * The type of the event. + * @param {Function} listener + * The listener function that processes the event. + */ + once: function once(type, listener) { + this.on(type, function selfRemovableListener() { + this.removeListener(type, selfRemovableListener); + listener.apply(this, arguments); + }); + }, + + /** + * Unregister `listener` for the specified event type. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ + removeListener: function removeListener(type, listener) { + if ('function' !== typeof listener) + throw new Error(BAD_LISTENER); + let listeners = this._listeners(type), + index = listeners.indexOf(listener); + if (0 <= index) + listeners.splice(index, 1); + // Use of `_public` is required by the legacy traits code, that will go away + // once bug-637633 is fixed. + return this._public || this; + }, + + /** + * Hash of listeners on this EventEmitter. + */ + _events: null, + + /** + * Returns an array of listeners for the specified event `type`. This array + * can be manipulated, e.g. to remove listeners. + * @param {String} type + * The type of event. + */ + _listeners: function listeners(type) { + let events = this._events || (this._events = {}); + return events[type] || (events[type] = []); + }, + + /** + * Execute each of the listeners in order with the supplied arguments. + * Returns `true` if listener for this event was called, `false` if there are + * no listeners for this event `type`. + * + * All the exceptions that are thrown by listeners during the emit + * are caught and can be handled by listeners of 'error' event. Thrown + * exceptions are passed as an argument to an 'error' event listener. + * If no 'error' listener is registered exception will propagate to a + * caller of this method. + * + * **It's recommended to have a default 'error' listener in all the complete + * composition that in worst case may dump errors to the console.** + * + * @param {String} type + * The type of event. + * @params {Object|Number|String|Boolean} + * Arguments that will be passed to listeners. + * @returns {Boolean} + */ + _emit: function _emit(type, event) { + let args = Array.slice(arguments); + // Use of `_public` is required by the legacy traits code that will go away + // once bug-637633 is fixed. + args.unshift(this._public || this); + return this._emitOnObject.apply(this, args); + }, + + /** + * A version of _emit that lets you specify the object on which listeners are + * called. This is a hack that is sometimes necessary when such an object + * (exports, for example) cannot be an EventEmitter for some reason, but other + * object(s) managing events for the object are EventEmitters. Once bug + * 577782 is fixed, this method shouldn't be necessary. + * + * @param {object} targetObj + * The object on which listeners will be called. + * @param {string} type + * The event name. + * @param {value} event + * The first argument to pass to listeners. + * @param {value} ... + * More arguments to pass to listeners. + * @returns {boolean} + */ + _emitOnObject: function _emitOnObject(targetObj, type, event /* , ... */) { + let listeners = this._listeners(type).slice(0); + // If there is no 'error' event listener then throw. + if (type === ERROR_TYPE && !listeners.length) + console.exception(event); + if (!listeners.length) + return false; + let params = Array.slice(arguments, 2); + for each (let listener in listeners) { + try { + listener.apply(targetObj, params); + } catch(e) { + this._emit('error', e); + } + } + return true; + }, + + /** + * Removes all the event listeners for the specified event `type`. + * @param {String} type + * The type of event. + */ + _removeAllListeners: function _removeAllListeners(type) { + this._listeners(type).splice(0); + return this; + } +}; +exports.EventEmitter = require("./traits").Trait.compose(eventEmitter); +exports.EventEmitterTrait = require('./light-traits').Trait(eventEmitter); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/events/assembler.js b/tools/addon-sdk-1.4/packages/api-utils/lib/events/assembler.js new file mode 100644 index 0000000..26860d6 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/events/assembler.js @@ -0,0 +1,86 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Trait } = require("../light-traits"); +const { removeListener, on } = require("../dom/events"); + +/** + * Trait may be used for building objects / composing traits that wish to handle + * multiple dom events from multiple event targets in one place. Event targets + * can be added / removed by calling `observe / ignore` methods. Composer should + * provide array of event types it wishes to handle as property + * `supportedEventsTypes` and function for handling all those events as + * `handleEvent` property. + */ +exports.DOMEventAssembler = Trait({ + /** + * Function that is supposed to handle all the supported events (that are + * present in the `supportedEventsTypes`) from all the observed + * `eventTargets`. + * @param {Event} event + * Event being dispatched. + */ + handleEvent: Trait.required, + /** + * Array of supported event names. + * @type {String[]} + */ + supportedEventsTypes: Trait.required, + /** + * Adds `eventTarget` to the list of observed `eventTarget`s. Listeners for + * supported events will be registered on the given `eventTarget`. + * @param {EventTarget} eventTarget + */ + observe: function observe(eventTarget) { + this.supportedEventsTypes.forEach(function(eventType) { + on(eventTarget, eventType, this); + }, this); + }, + /** + * Removes `eventTarget` from the list of observed `eventTarget`s. Listeners + * for all supported events will be unregistered from the given `eventTarget`. + * @param {EventTarget} eventTarget + */ + ignore: function ignore(eventTarget) { + this.supportedEventsTypes.forEach(function(eventType) { + removeListener(eventTarget, eventType, this); + }, this); + } +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/file.js b/tools/addon-sdk-1.4/packages/api-utils/lib/file.js new file mode 100644 index 0000000..30cb356 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/file.js @@ -0,0 +1,227 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is nsINarwhal. + * + * The Initial Developer of the Original Code is + * Irakli Gozalishvili <rfobic@gmail.com>. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <rfobic@gmail.com> + * Atul Varma <atul@mozilla.com> + * Drew Willcoxon <adw@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci,Cr} = require("chrome"); +const byteStreams = require("./byte-streams"); +const textStreams = require("./text-streams"); + +// Flags passed when opening a file. See nsprpub/pr/include/prio.h. +const OPEN_FLAGS = { + RDONLY: parseInt("0x01"), + WRONLY: parseInt("0x02"), + CREATE_FILE: parseInt("0x08"), + APPEND: parseInt("0x10"), + TRUNCATE: parseInt("0x20"), + EXCL: parseInt("0x80") +}; + +var dirsvc = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + +function MozFile(path) { + var file = Cc['@mozilla.org/file/local;1'] + .createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return file; +} + +function ensureReadable(file) { + if (!file.isReadable()) + throw new Error("path is not readable: " + file.path); +} + +function ensureDir(file) { + ensureExists(file); + if (!file.isDirectory()) + throw new Error("path is not a directory: " + file.path); +} + +function ensureFile(file) { + ensureExists(file); + if (!file.isFile()) + throw new Error("path is not a file: " + file.path); +} + +function ensureExists(file) { + if (!file.exists()) + throw friendlyError(Cr.NS_ERROR_FILE_NOT_FOUND, file.path); +} + +function friendlyError(errOrResult, filename) { + var isResult = typeof(errOrResult) === "number"; + var result = isResult ? errOrResult : errOrResult.result; + switch (result) { + case Cr.NS_ERROR_FILE_NOT_FOUND: + return new Error("path does not exist: " + filename); + } + return isResult ? new Error("XPCOM error code: " + errOrResult) : errOrResult; +} + +exports.exists = function exists(filename) { + return MozFile(filename).exists(); +}; + +exports.isFile = function isFile(filename) { + return MozFile(filename).isFile(); +}; + +exports.read = function read(filename, mode) { + if (typeof(mode) !== "string") + mode = ""; + + // Ensure mode is read-only. + mode = /b/.test(mode) ? "b" : ""; + + var stream = exports.open(filename, mode); + try { + var str = stream.read(); + } + finally { + stream.close(); + } + + return str; +}; + +exports.join = function join(base) { + if (arguments.length < 2) + throw new Error("need at least 2 args"); + base = MozFile(base); + for (var i = 1; i < arguments.length; i++) + base.append(arguments[i]); + return base.path; +}; + +exports.dirname = function dirname(path) { + var parent = MozFile(path).parent; + return parent ? parent.path : ""; +}; + +exports.basename = function basename(path) { + var leafName = MozFile(path).leafName; + + // On Windows, leafName when the path is a volume letter and colon ("c:") is + // the path itself. But such a path has no basename, so we want the empty + // string. + return leafName == path ? "" : leafName; +}; + +exports.list = function list(path) { + var file = MozFile(path); + ensureDir(file); + ensureReadable(file); + + var entries = file.directoryEntries; + var entryNames = []; + while(entries.hasMoreElements()) { + var entry = entries.getNext(); + entry.QueryInterface(Ci.nsIFile); + entryNames.push(entry.leafName); + } + return entryNames; +}; + +exports.open = function open(filename, mode) { + var file = MozFile(filename); + if (typeof(mode) !== "string") + mode = ""; + + // File opened for write only. + if (/w/.test(mode)) { + if (file.exists()) + ensureFile(file); + var stream = Cc['@mozilla.org/network/file-output-stream;1']. + createInstance(Ci.nsIFileOutputStream); + var openFlags = OPEN_FLAGS.WRONLY | + OPEN_FLAGS.CREATE_FILE | + OPEN_FLAGS.TRUNCATE; + var permFlags = parseInt("0644"); // u+rw go+r + try { + stream.init(file, openFlags, permFlags, 0); + } + catch (err) { + throw friendlyError(err, filename); + } + return /b/.test(mode) ? + new byteStreams.ByteWriter(stream) : + new textStreams.TextWriter(stream); + } + + // File opened for read only, the default. + ensureFile(file); + stream = Cc['@mozilla.org/network/file-input-stream;1']. + createInstance(Ci.nsIFileInputStream); + try { + stream.init(file, OPEN_FLAGS.RDONLY, 0, 0); + } + catch (err) { + throw friendlyError(err, filename); + } + return /b/.test(mode) ? + new byteStreams.ByteReader(stream) : + new textStreams.TextReader(stream); +}; + +exports.remove = function remove(path) { + var file = MozFile(path); + ensureFile(file); + file.remove(false); +}; + +exports.mkpath = function mkpath(path) { + var file = MozFile(path); + if (!file.exists()) + file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755")); // u+rwx go+rx + else if (!file.isDirectory()) + throw new Error("The path already exists and is not a directory: " + path); +}; + +exports.rmdir = function rmdir(path) { + var file = MozFile(path); + ensureDir(file); + try { + file.remove(false); + } + catch (err) { + // Bug 566950 explains why we're not catching a specific exception here. + throw new Error("The directory is not empty: " + path); + } +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/find-tests.js b/tools/addon-sdk-1.4/packages/api-utils/lib/find-tests.js new file mode 100644 index 0000000..be29d19 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/find-tests.js @@ -0,0 +1 @@ +// this file left intentionally blank diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/globals!.js b/tools/addon-sdk-1.4/packages/api-utils/lib/globals!.js new file mode 100644 index 0000000..27fd8ba --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/globals!.js @@ -0,0 +1,113 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +let { Cc, Ci } = require('chrome'); +let { PlainTextConsole } = require('./plain-text-console'); +let options = require('@packaging'); + +// On windows dump does not writes into stdout so cfx can't read thous dumps. +// To workaround this issue we write to a special file from which cfx will +// read and print to the console. +// For more details see: bug-673383 +exports.dump = (function define(global) { + const PR_WRONLY = 0x02; + const PR_CREATE_FILE = 0x08; + const PR_APPEND = 0x10; + let print = Object.getPrototypeOf(global).dump + if (print) return print; + if ('logFile' in options) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(options.logFile); + let stream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(file, PR_WRONLY|PR_CREATE_FILE|PR_APPEND, -1, 0); + + return function print(message) { + message = String(message); + stream.write(message, message.length); + stream.flush(); + }; + } + return dump; +})(this); + +// Override the default Iterator function with one that passes +// a second argument to custom iterator methods that identifies +// the call as originating from an Iterator function so the custom +// iterator method can return [key, value] pairs just like default +// iterators called via the default Iterator function. +exports.Iterator = (function(DefaultIterator) { + return function Iterator(obj, keysOnly) { + if ("__iterator__" in obj && !keysOnly) + return obj.__iterator__.call(obj, false, true); + return DefaultIterator(obj, keysOnly); + }; +})(Iterator); + +// TODO: Remove memory from the globals, as it raises security concerns and +// there is no real reason to favor global memory over +// `require('api-utils/memory')`. For details see: Bug-620559 +exports.memory = require('./memory'); +exports.console = new PlainTextConsole(exports.dump); + +// Provide CommonJS `define` to allow authoring modules in a format that can be +// loaded both into jetpack and into browser via AMD loaders. +Object.defineProperty(exports, 'define', { + // `define` is provided as a lazy getter that binds below defined `define` + // function to the module scope, so that require, exports and module + // variables remain accessible. + configurable: true, + get: (function() { + function define(factory) { + factory = Array.slice(arguments).pop(); + factory.call(this, this.require, this.exports, this.module); + } + + return function getter() { + // Redefine `define` as a static property to make sure that module + // gets access to the same function so that `define === define` is + // `true`. + Object.defineProperty(this, 'define', { + configurable: false, + value: define.bind(this) + }); + return this.define; + } + })() +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/hidden-frame.js b/tools/addon-sdk-1.4/packages/api-utils/lib/hidden-frame.js new file mode 100644 index 0000000..241a4bc --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/hidden-frame.js @@ -0,0 +1,200 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Felipe Gomes <felipc@gmail.com> (Original Author) + * Myk Melez <myk@mozilla.org> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc, Ci} = require("chrome"); +const errors = require("./errors"); +const apiUtils = require("./api-utils"); +const timer = require("./timer"); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +let hostFrame, hostDocument, hiddenWindow, isHostFrameReady = false; + +if (!require("./xul-app").isOneOf(["Firefox", "Thunderbird"])) { + throw new Error([ + "The hidden-frame module currently supports only Firefox and Thunderbird. ", + "In the future, we would like it to support other applications, however. ", + "Please see https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more ", + "information." + ].join("")); +} + +let appShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); +hiddenWindow = appShellService.hiddenDOMWindow; + +if (!hiddenWindow) { + throw new Error([ + "The hidden-frame module needs an app that supports a hidden window. ", + "We would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more information." + ].join("")); +} + +// Check if we can use the hidden window itself to host our iframes. +// If it's not a suitable host, the hostFrame will be lazily created +// by the first HiddenFrame instance. +if (hiddenWindow.location.protocol == "chrome:" && + (hiddenWindow.document.contentType == "application/vnd.mozilla.xul+xml" || + hiddenWindow.document.contentType == "application/xhtml+xml")) { + hostFrame = hiddenWindow; + hostDocument = hiddenWindow.document; + isHostFrameReady = true; +} + +function setHostFrameReady() { + hostDocument = hostFrame.contentDocument; + hostFrame.removeEventListener("DOMContentLoaded", setHostFrameReady, false); + isHostFrameReady = true; +} + +// This cache is used to access friend properties between functions +// without exposing them on the public API. +let cache = []; + +exports.HiddenFrame = apiUtils.publicConstructor(HiddenFrame); + +function HiddenFrame(options) { + options = options || {}; + let self = this; + + for each (let [key, val] in Iterator(apiUtils.validateOptions(options, { + onReady: { + is: ["undefined", "function", "array"], + ok: function(v) { + if (apiUtils.getTypeOf(v) === "array") { + // make sure every item is a function + return v.every(function (item) typeof(item) === "function") + } + return true; + } + } + }))) { + if (typeof(val) != "undefined") + options[key] = val; + } + + require("./collection").addCollectionProperty(this, "onReady"); + if (options.onReady) + this.onReady.add(options.onReady); + + if (!hostFrame) { + hostFrame = hiddenWindow.document.createElement("iframe"); + + // ugly ugly hack. This is the most lightweight chrome:// file I could find on the tree + // This hack should be removed by proper platform support on bug 565388 + hostFrame.setAttribute("src", "chrome://global/content/mozilla.xhtml"); + hostFrame.addEventListener("DOMContentLoaded", setHostFrameReady, false); + + hiddenWindow.document.body.appendChild(hostFrame); + } + + this.toString = function toString() "[object Frame]"; +} + +exports.add = function JP_SDK_Frame_add(frame) { + if (!(frame instanceof HiddenFrame)) + throw new Error("The object to be added must be a HiddenFrame."); + + // This instance was already added. + if (cache.filter(function (v) v.frame === frame)[0]) + return frame; + + function createElement() { + hostFrame.removeEventListener("DOMContentLoaded", createElement, false); + + let element = hostDocument.createElementNS(XUL_NS, "iframe"); + + element.setAttribute("type", "content"); + hostDocument.documentElement.appendChild(element); + + /* Public API: hiddenFrame.element */ + frame.__defineGetter__("element", function () element); + + // Notify consumers that the frame is ready. + function onReadyListener(event) { + element.removeEventListener("DOMContentLoaded", onReadyListener, false); + if (event.target == element.contentDocument) { + for (let handler in frame.onReady) + errors.catchAndLog(function () handler.call(frame))(); + } + } + element.addEventListener("DOMContentLoaded", onReadyListener, false); + + cache.push({ + frame: frame, + element: element, + unload: function unload() { + hostDocument.documentElement.removeChild(element); + } + }); + } + + /* Begin element construction or schedule it for later */ + if (isHostFrameReady) { + createElement(); + } else { + hostFrame.addEventListener("DOMContentLoaded", createElement, false); + } + + return frame; +} + +exports.remove = function remove(frame) { + if (!(frame instanceof HiddenFrame)) + throw new Error("The object to be removed must be a HiddenFrame."); + + let entry = cache.filter(function (v) v.frame === frame)[0]; + if (!entry) + return; + + entry.unload(); + cache.splice(cache.indexOf(entry), 1); +} + +require("./unload").when(function () { + for each (let entry in cache.slice()) + exports.remove(entry.frame); + + if (hostFrame && hostFrame !== hiddenWindow) + hiddenWindow.document.body.removeChild(hostFrame); +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/httpd.js b/tools/addon-sdk-1.4/packages/api-utils/lib/httpd.js new file mode 100644 index 0000000..ebf3643 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/httpd.js @@ -0,0 +1,5202 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** +* Version: MPL 1.1/GPL 2.0/LGPL 2.1 +* +* The contents of this file are subject to the Mozilla Public License Version +* 1.1 (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* http://www.mozilla.org/MPL/ +* +* Software distributed under the License is distributed on an "AS IS" basis, +* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +* for the specific language governing rights and limitations under the +* License. +* +* The Original Code is the httpd.js server. +* +* The Initial Developer of the Original Code is +* Mozilla Corporation. +* Portions created by the Initial Developer are Copyright (C) 2006 +* the Initial Developer. All Rights Reserved. +* +* Contributor(s): +* Darin Fisher (v1, netwerk/test/TestServ.js) +* Christian Biesinger (v2, netwerk/test/unit/head_http_server.js) +* Jeff Walden <jwalden+code@mit.edu> (v3, netwerk/test/httpserver/httpd.js) +* Robert Sayre <sayrer@gmail.com> +* +* Alternatively, the contents of this file may be used under the terms of +* either the GNU General Public License Version 2 or later (the "GPL"), or +* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +* in which case the provisions of the GPL or the LGPL are applicable instead +* of those above. If you wish to allow use of your version of this file only +* under the terms of either the GPL or the LGPL, and not to allow others to +* use your version of this file under the terms of the MPL, indicate your +* decision by deleting the provisions above and replace them with the notice +* and other provisions required by the GPL or the LGPL. If you do not delete +* the provisions above, a recipient may use your version of this file under +* the terms of any one of the MPL, the GPL or the LGPL. +* +* ***** END LICENSE BLOCK ***** */ + +/* +* An implementation of an HTTP server both as a loadable script and as an XPCOM +* component. See the accompanying README file for user documentation on +* httpd.js. +*/ + + +var {components,Cc,Ci,Cr,Cu} = require("chrome"); +components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +const CC = components.Constructor; + +const PR_UINT32_MAX = Math.pow(2, 32) - 1; + +/** True if debugging output is enabled, false otherwise. */ +var DEBUG = false; // non-const *only* so tweakable in server tests + +/** True if debugging output should be timestamped. */ +var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests + +var gGlobalObject = Cc["@mozilla.org/systemprincipal;1"].createInstance(); + +/** +* Asserts that the given condition holds. If it doesn't, the given message is +* dumped, a stack trace is printed, and an exception is thrown to attempt to +* stop execution (which unfortunately must rely upon the exception not being +* accidentally swallowed by the code that uses it). +*/ +function NS_ASSERT(cond, msg) +{ + if (DEBUG && !cond) + { + dumpn("###!!!"); + dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!")); + dumpn("###!!! Stack follows:"); + + var stack = new Error().stack.split(/\n/); + dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n")); + + throw Cr.NS_ERROR_ABORT; + } +} + +/** Constructs an HTTP error object. */ +function HttpError(code, description) +{ + this.code = code; + this.description = description; +} +HttpError.prototype = +{ + toString: function() + { + return this.code + " " + this.description; + } +}; + +/** +* Errors thrown to trigger specific HTTP server responses. +*/ +const HTTP_400 = new HttpError(400, "Bad Request"); +const HTTP_401 = new HttpError(401, "Unauthorized"); +const HTTP_402 = new HttpError(402, "Payment Required"); +const HTTP_403 = new HttpError(403, "Forbidden"); +const HTTP_404 = new HttpError(404, "Not Found"); +const HTTP_405 = new HttpError(405, "Method Not Allowed"); +const HTTP_406 = new HttpError(406, "Not Acceptable"); +const HTTP_407 = new HttpError(407, "Proxy Authentication Required"); +const HTTP_408 = new HttpError(408, "Request Timeout"); +const HTTP_409 = new HttpError(409, "Conflict"); +const HTTP_410 = new HttpError(410, "Gone"); +const HTTP_411 = new HttpError(411, "Length Required"); +const HTTP_412 = new HttpError(412, "Precondition Failed"); +const HTTP_413 = new HttpError(413, "Request Entity Too Large"); +const HTTP_414 = new HttpError(414, "Request-URI Too Long"); +const HTTP_415 = new HttpError(415, "Unsupported Media Type"); +const HTTP_417 = new HttpError(417, "Expectation Failed"); + +const HTTP_500 = new HttpError(500, "Internal Server Error"); +const HTTP_501 = new HttpError(501, "Not Implemented"); +const HTTP_502 = new HttpError(502, "Bad Gateway"); +const HTTP_503 = new HttpError(503, "Service Unavailable"); +const HTTP_504 = new HttpError(504, "Gateway Timeout"); +const HTTP_505 = new HttpError(505, "HTTP Version Not Supported"); + +/** Creates a hash with fields corresponding to the values in arr. */ +function array2obj(arr) +{ + var obj = {}; + for (var i = 0; i < arr.length; i++) + obj[arr[i]] = arr[i]; + return obj; +} + +/** Returns an array of the integers x through y, inclusive. */ +function range(x, y) +{ + var arr = []; + for (var i = x; i <= y; i++) + arr.push(i); + return arr; +} + +/** An object (hash) whose fields are the numbers of all HTTP error codes. */ +const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505))); + + +/** +* The character used to distinguish hidden files from non-hidden files, a la +* the leading dot in Apache. Since that mechanism also hides files from +* easy display in LXR, ls output, etc. however, we choose instead to use a +* suffix character. If a requested file ends with it, we append another +* when getting the file on the server. If it doesn't, we just look up that +* file. Therefore, any file whose name ends with exactly one of the character +* is "hidden" and available for use by the server. +*/ +const HIDDEN_CHAR = "^"; + +/** +* The file name suffix indicating the file containing overridden headers for +* a requested file. +*/ +const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR; + +/** Type used to denote SJS scripts for CGI-like functionality. */ +const SJS_TYPE = "sjs"; + +/** Base for relative timestamps produced by dumpn(). */ +var firstStamp = 0; + +/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */ +function dumpn(str) +{ + if (DEBUG) + { + var prefix = "HTTPD-INFO | "; + if (DEBUG_TIMESTAMP) + { + if (firstStamp === 0) + firstStamp = Date.now(); + + var elapsed = Date.now() - firstStamp; // milliseconds + var min = Math.floor(elapsed / 60000); + var sec = (elapsed % 60000) / 1000; + + if (sec < 10) + prefix += min + ":0" + sec.toFixed(3) + " | "; + else + prefix += min + ":" + sec.toFixed(3) + " | "; + } + + dump(prefix + str + "\n"); + } +} + +/** Dumps the current JS stack if DEBUG. */ +function dumpStack() +{ + // peel off the frames for dumpStack() and Error() + var stack = new Error().stack.split(/\n/).slice(2); + stack.forEach(dumpn); +} + + +/** The XPCOM thread manager. */ +var gThreadManager = null; + +/** The XPCOM prefs service. */ +var gRootPrefBranch = null; +function getRootPrefBranch() +{ + if (!gRootPrefBranch) + { + gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + } + return gRootPrefBranch; +} + +/** +* JavaScript constructors for commonly-used classes; precreating these is a +* speedup over doing the same from base principles. See the docs at +* http://developer.mozilla.org/en/docs/components.Constructor for details. +*/ +const ServerSocket = CC("@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init"); +const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init"); +const Pipe = CC("@mozilla.org/pipe;1", + "nsIPipe", + "init"); +const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", + "init"); +const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1", + "nsIConverterInputStream", + "init"); +const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1", + "nsIWritablePropertyBag2"); +const SupportsString = CC("@mozilla.org/supports-string;1", + "nsISupportsString"); + +/* These two are non-const only so a test can overwrite them. */ +var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); +var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream"); + +/** +* Returns the RFC 822/1123 representation of a date. +* +* @param date : Number +* the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT +* @returns string +* the representation of the given date +*/ +function toDateString(date) +{ + // + // rfc1123-date = wkday "," SP date1 SP time SP "GMT" + // date1 = 2DIGIT SP month SP 4DIGIT + // ; day month year (e.g., 02 Jun 1982) + // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT + // ; 00:00:00 - 23:59:59 + // wkday = "Mon" | "Tue" | "Wed" + // | "Thu" | "Fri" | "Sat" | "Sun" + // month = "Jan" | "Feb" | "Mar" | "Apr" + // | "May" | "Jun" | "Jul" | "Aug" + // | "Sep" | "Oct" | "Nov" | "Dec" + // + + const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + /** +* Processes a date and returns the encoded UTC time as a string according to +* the format specified in RFC 2616. +* +* @param date : Date +* the date to process +* @returns string +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" +*/ + function toTime(date) + { + var hrs = date.getUTCHours(); + var rv = (hrs < 10) ? "0" + hrs : hrs; + + var mins = date.getUTCMinutes(); + rv += ":"; + rv += (mins < 10) ? "0" + mins : mins; + + var secs = date.getUTCSeconds(); + rv += ":"; + rv += (secs < 10) ? "0" + secs : secs; + + return rv; + } + + /** +* Processes a date and returns the encoded UTC date as a string according to +* the date1 format specified in RFC 2616. +* +* @param date : Date +* the date to process +* @returns string +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" +*/ + function toDate1(date) + { + var day = date.getUTCDate(); + var month = date.getUTCMonth(); + var year = date.getUTCFullYear(); + + var rv = (day < 10) ? "0" + day : day; + rv += " " + monthStrings[month]; + rv += " " + year; + + return rv; + } + + date = new Date(date); + + const fmtString = "%wkday%, %date1% %time% GMT"; + var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]); + rv = rv.replace("%time%", toTime(date)); + return rv.replace("%date1%", toDate1(date)); +} + +/** +* Prints out a human-readable representation of the object o and its fields, +* omitting those whose names begin with "_" if showMembers != true (to ignore +* "private" properties exposed via getters/setters). +*/ +function printObj(o, showMembers) +{ + var s = "******************************\n"; + s += "o = {\n"; + for (var i in o) + { + if (typeof(i) != "string" || + (showMembers || (i.length > 0 && i[0] != "_"))) + s+= " " + i + ": " + o[i] + ",\n"; + } + s += " };\n"; + s += "******************************"; + dumpn(s); +} + +/** +* Instantiates a new HTTP server. +*/ +function nsHttpServer() +{ + if (!gThreadManager) + gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + + /** The port on which this server listens. */ + this._port = undefined; + + /** The socket associated with this. */ + this._socket = null; + + /** The handler used to process requests to this server. */ + this._handler = new ServerHandler(this); + + /** Naming information for this server. */ + this._identity = new ServerIdentity(); + + /** +* Indicates when the server is to be shut down at the end of the request. +*/ + this._doQuit = false; + + /** +* True if the socket in this is closed (and closure notifications have been +* sent and processed if the socket was ever opened), false otherwise. +*/ + this._socketClosed = true; + + /** +* Used for tracking existing connections and ensuring that all connections +* are properly cleaned up before server shutdown; increases by 1 for every +* new incoming connection. +*/ + this._connectionGen = 0; + + /** +* Hash of all open connections, indexed by connection number at time of +* creation. +*/ + this._connections = {}; +} +nsHttpServer.prototype = +{ + classID: components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"), + + // NSISERVERSOCKETLISTENER + + /** +* Processes an incoming request coming in on the given socket and contained +* in the given transport. +* +* @param socket : nsIServerSocket +* the socket through which the request was served +* @param trans : nsISocketTransport +* the transport for the request/response +* @see nsIServerSocketListener.onSocketAccepted +*/ + onSocketAccepted: function(socket, trans) + { + dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")"); + + dumpn(">>> new connection on " + trans.host + ":" + trans.port); + + const SEGMENT_SIZE = 8192; + const SEGMENT_COUNT = 1024; + try + { + var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT) + .QueryInterface(Ci.nsIAsyncInputStream); + var output = trans.openOutputStream(0, 0, 0); + } + catch (e) + { + dumpn("*** error opening transport streams: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + var connectionNumber = ++this._connectionGen; + + try + { + var conn = new Connection(input, output, this, socket.port, trans.port, + connectionNumber); + var reader = new RequestReader(conn); + + // XXX add request timeout functionality here! + + // Note: must use main thread here, or we might get a GC that will cause + // threadsafety assertions. We really need to fix XPConnect so that + // you can actually do things in multi-threaded JS. :-( + input.asyncWait(reader, 0, 0, gThreadManager.mainThread); + } + catch (e) + { + // Assume this connection can't be salvaged and bail on it completely; + // don't attempt to close it so that we can assert that any connection + // being closed is in this._connections. + dumpn("*** error in initial request-processing stages: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + this._connections[connectionNumber] = conn; + dumpn("*** starting connection " + connectionNumber); + }, + + /** +* Called when the socket associated with this is closed. +* +* @param socket : nsIServerSocket +* the socket being closed +* @param status : nsresult +* the reason the socket stopped listening (NS_BINDING_ABORTED if the server +* was stopped using nsIHttpServer.stop) +* @see nsIServerSocketListener.onStopListening +*/ + onStopListening: function(socket, status) + { + dumpn(">>> shutting down server on port " + socket.port); + this._socketClosed = true; + if (!this._hasOpenConnections()) + { + dumpn("*** no open connections, notifying async from onStopListening"); + + // Notify asynchronously so that any pending teardown in stop() has a + // chance to run first. + var self = this; + var stopEvent = + { + run: function() + { + dumpn("*** _notifyStopped async callback"); + self._notifyStopped(); + } + }; + gThreadManager.currentThread + .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + + // NSIHTTPSERVER + + // + // see nsIHttpServer.start + // + start: function(port) + { + this._start(port, "localhost") + }, + + _start: function(port, host) + { + if (this._socket) + throw Cr.NS_ERROR_ALREADY_INITIALIZED; + + this._port = port; + this._doQuit = this._socketClosed = false; + + this._host = host; + + // The listen queue needs to be long enough to handle + // network.http.max-connections-per-server concurrent connections, + // plus a safety margin in case some other process is talking to + // the server as well. + var prefs = getRootPrefBranch(); + var maxConnections = + prefs.getIntPref("network.http.max-connections-per-server") + 5; + + try + { + var loopback = true; + if (this._host != "127.0.0.1" && this._host != "localhost") { + var loopback = false; + } + + var socket = new ServerSocket(this._port, + loopback, // true = localhost, false = everybody + maxConnections); + dumpn(">>> listening on port " + socket.port + ", " + maxConnections + + " pending connections"); + socket.asyncListen(this); + this._identity._initialize(port, host, true); + this._socket = socket; + } + catch (e) + { + dumpn("!!! could not start server on port " + port + ": " + e); + throw Cr.NS_ERROR_NOT_AVAILABLE; + } + }, + + // + // see nsIHttpServer.stop + // + stop: function(callback) + { + if (!callback) + throw Cr.NS_ERROR_NULL_POINTER; + if (!this._socket) + throw Cr.NS_ERROR_UNEXPECTED; + + this._stopCallback = typeof callback === "function" + ? callback + : function() { callback.onStopped(); }; + + dumpn(">>> stopping listening on port " + this._socket.port); + this._socket.close(); + this._socket = null; + + // We can't have this identity any more, and the port on which we're running + // this server now could be meaningless the next time around. + this._identity._teardown(); + + this._doQuit = false; + + // socket-close notification and pending request completion happen async + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (file && (!file.exists() || file.isDirectory())) + throw Cr.NS_ERROR_INVALID_ARG; + + this._handler.registerFile(path, file); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // XXX true path validation! + if (path.charAt(0) != "/" || + path.charAt(path.length - 1) != "/" || + (directory && + (!directory.exists() || !directory.isDirectory()))) + throw Cr.NS_ERROR_INVALID_ARG; + + // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping + // exists! + + this._handler.registerDirectory(path, directory); + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + this._handler.registerPathHandler(path, handler); + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(code, handler) + { + this._handler.registerErrorHandler(code, handler); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler: function(handler) + { + this._handler.setIndexHandler(handler); + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType: function(ext, type) + { + this._handler.registerContentType(ext, type); + }, + + // + // see nsIHttpServer.serverIdentity + // + get identity() + { + return this._identity; + }, + + // + // see nsIHttpServer.getState + // + getState: function(path, k) + { + return this._handler._getState(path, k); + }, + + // + // see nsIHttpServer.setState + // + setState: function(path, k, v) + { + return this._handler._setState(path, k, v); + }, + + // + // see nsIHttpServer.getSharedState + // + getSharedState: function(k) + { + return this._handler._getSharedState(k); + }, + + // + // see nsIHttpServer.setSharedState + // + setSharedState: function(k, v) + { + return this._handler._setSharedState(k, v); + }, + + // + // see nsIHttpServer.getObjectState + // + getObjectState: function(k) + { + return this._handler._getObjectState(k); + }, + + // + // see nsIHttpServer.setObjectState + // + setObjectState: function(k, v) + { + return this._handler._setObjectState(k, v); + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIServerSocketListener) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // NON-XPCOM PUBLIC API + + /** +* Returns true iff this server is not running (and is not in the process of +* serving any requests still to be processed when the server was last +* stopped after being run). +*/ + isStopped: function() + { + return this._socketClosed && !this._hasOpenConnections(); + }, + + // PRIVATE IMPLEMENTATION + + /** True if this server has any open connections to it, false otherwise. */ + _hasOpenConnections: function() + { + // + // If we have any open connections, they're tracked as numeric properties on + // |this._connections|. The non-standard __count__ property could be used + // to check whether there are any properties, but standard-wise, even + // looking forward to ES5, there's no less ugly yet still O(1) way to do + // this. + // + for (var n in this._connections) + return true; + return false; + }, + + /** Calls the server-stopped callback provided when stop() was called. */ + _notifyStopped: function() + { + NS_ASSERT(this._stopCallback !== null, "double-notifying?"); + NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now"); + + // + // NB: We have to grab this now, null out the member, *then* call the + // callback here, or otherwise the callback could (indirectly) futz with + // this._stopCallback by starting and immediately stopping this, at + // which point we'd be nulling out a field we no longer have a right to + // modify. + // + var callback = this._stopCallback; + this._stopCallback = null; + try + { + callback(); + } + catch (e) + { + // not throwing because this is specified as being usually (but not + // always) asynchronous + dump("!!! error running onStopped callback: " + e + "\n"); + } + }, + + /** +* Notifies this server that the given connection has been closed. +* +* @param connection : Connection +* the connection that was closed +*/ + _connectionClosed: function(connection) + { + NS_ASSERT(connection.number in this._connections, + "closing a connection " + this + " that we never added to the " + + "set of open connections?"); + NS_ASSERT(this._connections[connection.number] === connection, + "connection number mismatch? " + + this._connections[connection.number]); + delete this._connections[connection.number]; + + // Fire a pending server-stopped notification if it's our responsibility. + if (!this._hasOpenConnections() && this._socketClosed) + this._notifyStopped(); + }, + + /** +* Requests that the server be shut down when possible. +*/ + _requestQuit: function() + { + dumpn(">>> requesting a quit"); + dumpStack(); + this._doQuit = true; + } +}; + + +// +// RFC 2396 section 3.2.2: +// +// host = hostname | IPv4address +// hostname = *( domainlabel "." ) toplabel [ "." ] +// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum +// toplabel = alpha | alpha *( alphanum | "-" ) alphanum +// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit +// + +const HOST_REGEX = + new RegExp("^(?:" + + // *( domainlabel "." ) + "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" + + // toplabel + "[a-z](?:[a-z0-9-]*[a-z0-9])?" + + "|" + + // IPv4 address + "\\d+\\.\\d+\\.\\d+\\.\\d+" + + ")$", + "i"); + + +/** +* Represents the identity of a server. An identity consists of a set of +* (scheme, host, port) tuples denoted as locations (allowing a single server to +* serve multiple sites or to be used behind both HTTP and HTTPS proxies for any +* host/port). Any incoming request must be to one of these locations, or it +* will be rejected with an HTTP 400 error. One location, denoted as the +* primary location, is the location assigned in contexts where a location +* cannot otherwise be endogenously derived, such as for HTTP/1.0 requests. +* +* A single identity may contain at most one location per unique host/port pair; +* other than that, no restrictions are placed upon what locations may +* constitute an identity. +*/ +function ServerIdentity() +{ + /** The scheme of the primary location. */ + this._primaryScheme = "http"; + + /** The hostname of the primary location. */ + this._primaryHost = "127.0.0.1" + + /** The port number of the primary location. */ + this._primaryPort = -1; + + /** +* The current port number for the corresponding server, stored so that a new +* primary location can always be set if the current one is removed. +*/ + this._defaultPort = -1; + + /** +* Maps hosts to maps of ports to schemes, e.g. the following would represent +* https://example.com:789/ and http://example.org/: +* +* { +* "xexample.com": { 789: "https" }, +* "xexample.org": { 80: "http" } +* } +* +* Note the "x" prefix on hostnames, which prevents collisions with special +* JS names like "prototype". +*/ + this._locations = { "xlocalhost": {} }; +} +ServerIdentity.prototype = +{ + // NSIHTTPSERVERIDENTITY + + // + // see nsIHttpServerIdentity.primaryScheme + // + get primaryScheme() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryScheme; + }, + + // + // see nsIHttpServerIdentity.primaryHost + // + get primaryHost() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryHost; + }, + + // + // see nsIHttpServerIdentity.primaryPort + // + get primaryPort() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryPort; + }, + + // + // see nsIHttpServerIdentity.add + // + add: function(scheme, host, port) + { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) + this._locations["x" + host] = entry = {}; + + entry[port] = scheme; + }, + + // + // see nsIHttpServerIdentity.remove + // + remove: function(scheme, host, port) + { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) + return false; + + var present = port in entry; + delete entry[port]; + + if (this._primaryScheme == scheme && + this._primaryHost == host && + this._primaryPort == port && + this._defaultPort !== -1) + { + // Always keep at least one identity in existence at any time, unless + // we're in the process of shutting down (the last condition above). + this._primaryPort = -1; + this._initialize(this._defaultPort, host, false); + } + + return present; + }, + + // + // see nsIHttpServerIdentity.has + // + has: function(scheme, host, port) + { + this._validate(scheme, host, port); + + return "x" + host in this._locations && + scheme === this._locations["x" + host][port]; + }, + + // + // see nsIHttpServerIdentity.has + // + getScheme: function(host, port) + { + this._validate("http", host, port); + + var entry = this._locations["x" + host]; + if (!entry) + return ""; + + return entry[port] || ""; + }, + + // + // see nsIHttpServerIdentity.setPrimary + // + setPrimary: function(scheme, host, port) + { + this._validate(scheme, host, port); + + this.add(scheme, host, port); + + this._primaryScheme = scheme; + this._primaryHost = host; + this._primaryPort = port; + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE IMPLEMENTATION + + /** +* Initializes the primary name for the corresponding server, based on the +* provided port number. +*/ + _initialize: function(port, host, addSecondaryDefault) + { + this._host = host; + if (this._primaryPort !== -1) + this.add("http", host, port); + else + this.setPrimary("http", "localhost", port); + this._defaultPort = port; + + // Only add this if we're being called at server startup + if (addSecondaryDefault && host != "127.0.0.1") + this.add("http", "127.0.0.1", port); + }, + + /** +* Called at server shutdown time, unsets the primary location only if it was +* the default-assigned location and removes the default location from the +* set of locations used. +*/ + _teardown: function() + { + if (this._host != "127.0.0.1") { + // Not the default primary location, nothing special to do here + this.remove("http", "127.0.0.1", this._defaultPort); + } + + // This is a *very* tricky bit of reasoning here; make absolutely sure the + // tests for this code pass before you commit changes to it. + if (this._primaryScheme == "http" && + this._primaryHost == this._host && + this._primaryPort == this._defaultPort) + { + // Make sure we don't trigger the readding logic in .remove(), then remove + // the default location. + var port = this._defaultPort; + this._defaultPort = -1; + this.remove("http", this._host, port); + + // Ensure a server start triggers the setPrimary() path in ._initialize() + this._primaryPort = -1; + } + else + { + // No reason not to remove directly as it's not our primary location + this.remove("http", this._host, this._defaultPort); + } + }, + + /** +* Ensures scheme, host, and port are all valid with respect to RFC 2396. +* +* @throws NS_ERROR_ILLEGAL_VALUE +* if any argument doesn't match the corresponding production +*/ + _validate: function(scheme, host, port) + { + if (scheme !== "http" && scheme !== "https") + { + dumpn("*** server only supports http/https schemes: '" + scheme + "'"); + dumpStack(); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + if (!HOST_REGEX.test(host)) + { + dumpn("*** unexpected host: '" + host + "'"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + if (port < 0 || port > 65535) + { + dumpn("*** unexpected port: '" + port + "'"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + } +}; + + +/** +* Represents a connection to the server (and possibly in the future the thread +* on which the connection is processed). +* +* @param input : nsIInputStream +* stream from which incoming data on the connection is read +* @param output : nsIOutputStream +* stream to write data out the connection +* @param server : nsHttpServer +* the server handling the connection +* @param port : int +* the port on which the server is running +* @param outgoingPort : int +* the outgoing port used by this connection +* @param number : uint +* a serial number used to uniquely identify this connection +*/ +function Connection(input, output, server, port, outgoingPort, number) +{ + dumpn("*** opening new connection " + number + " on port " + outgoingPort); + + /** Stream of incoming data. */ + this.input = input; + + /** Stream for outgoing data. */ + this.output = output; + + /** The server associated with this request. */ + this.server = server; + + /** The port on which the server is running. */ + this.port = port; + + /** The outgoing poort used by this connection. */ + this._outgoingPort = outgoingPort; + + /** The serial number of this connection. */ + this.number = number; + + /** +* The request for which a response is being generated, null if the +* incoming request has not been fully received or if it had errors. +*/ + this.request = null; + + /** State variables for debugging. */ + this._closed = this._processed = false; +} +Connection.prototype = +{ + /** Closes this connection's input/output streams. */ + close: function() + { + dumpn("*** closing connection " + this.number + + " on port " + this._outgoingPort); + + this.input.close(); + this.output.close(); + this._closed = true; + + var server = this.server; + server._connectionClosed(this); + + // If an error triggered a server shutdown, act on it now + if (server._doQuit) + server.stop(function() { /* not like we can do anything better */ }); + }, + + /** +* Initiates processing of this connection, using the data in the given +* request. +* +* @param request : Request +* the request which should be processed +*/ + process: function(request) + { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + + this.request = request; + this.server._handler.handleResponse(this); + }, + + /** +* Initiates processing of this connection, generating a response with the +* given HTTP error code. +* +* @param code : uint +* an HTTP code, so in the range [0, 1000) +* @param request : Request +* incomplete data about the incoming request (since there were errors +* during its processing +*/ + processError: function(code, request) + { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + this.request = request; + this.server._handler.handleError(code, this); + }, + + /** Converts this to a string for debugging purposes. */ + toString: function() + { + return "<Connection(" + this.number + + (this.request ? ", " + this.request.path : "") +"): " + + (this._closed ? "closed" : "open") + ">"; + } +}; + + + +/** Returns an array of count bytes from the given input stream. */ +function readBytes(inputStream, count) +{ + return new BinaryInputStream(inputStream).readByteArray(count); +} + + + +/** Request reader processing states; see RequestReader for details. */ +const READER_IN_REQUEST_LINE = 0; +const READER_IN_HEADERS = 1; +const READER_IN_BODY = 2; +const READER_FINISHED = 3; + + +/** +* Reads incoming request data asynchronously, does any necessary preprocessing, +* and forwards it to the request handler. Processing occurs in three states: +* +* READER_IN_REQUEST_LINE Reading the request's status line +* READER_IN_HEADERS Reading headers in the request +* READER_IN_BODY Reading the body of the request +* READER_FINISHED Entire request has been read and processed +* +* During the first two stages, initial metadata about the request is gathered +* into a Request object. Once the status line and headers have been processed, +* we start processing the body of the request into the Request. Finally, when +* the entire body has been read, we create a Response and hand it off to the +* ServerHandler to be given to the appropriate request handler. +* +* @param connection : Connection +* the connection for the request being read +*/ +function RequestReader(connection) +{ + /** Connection metadata for this request. */ + this._connection = connection; + + /** +* A container providing line-by-line access to the raw bytes that make up the +* data which has been read from the connection but has not yet been acted +* upon (by passing it to the request handler or by extracting request +* metadata from it). +*/ + this._data = new LineData(); + + /** +* The amount of data remaining to be read from the body of this request. +* After all headers in the request have been read this is the value in the +* Content-Length header, but as the body is read its value decreases to zero. +*/ + this._contentLength = 0; + + /** The current state of parsing the incoming request. */ + this._state = READER_IN_REQUEST_LINE; + + /** Metadata constructed from the incoming request for the request handler. */ + this._metadata = new Request(connection.port); + + /** +* Used to preserve state if we run out of line data midway through a +* multi-line header. _lastHeaderName stores the name of the header, while +* _lastHeaderValue stores the value we've seen so far for the header. +* +* These fields are always either both undefined or both strings. +*/ + this._lastHeaderName = this._lastHeaderValue = undefined; +} +RequestReader.prototype = +{ + // NSIINPUTSTREAMCALLBACK + + /** +* Called when more data from the incoming request is available. This method +* then reads the available data from input and deals with that data as +* necessary, depending upon the syntax of already-downloaded data. +* +* @param input : nsIAsyncInputStream +* the stream of incoming data from the connection +*/ + onInputStreamReady: function(input) + { + dumpn("*** onInputStreamReady(input=" + input + ") on thread " + + gThreadManager.currentThread + " (main is " + + gThreadManager.mainThread + ")"); + dumpn("*** this._state == " + this._state); + + // Handle cases where we get more data after a request error has been + // discovered but *before* we can close the connection. + var data = this._data; + if (!data) + return; + + try + { + data.appendBytes(readBytes(input, input.available())); + } + catch (e) + { + if (streamClosed(e)) + { + dumpn("*** WARNING: unexpected error when reading from socket; will " + + "be treated as if the input stream had been closed"); + dumpn("*** WARNING: actual error was: " + e); + } + + // We've lost a race -- input has been closed, but we're still expecting + // to read more data. available() will throw in this case, and since + // we're dead in the water now, destroy the connection. + dumpn("*** onInputStreamReady called on a closed input, destroying " + + "connection"); + this._connection.close(); + return; + } + + switch (this._state) + { + default: + NS_ASSERT(false, "invalid state: " + this._state); + break; + + case READER_IN_REQUEST_LINE: + if (!this._processRequestLine()) + break; + /* fall through */ + + case READER_IN_HEADERS: + if (!this._processHeaders()) + break; + /* fall through */ + + case READER_IN_BODY: + this._processBody(); + } + + if (this._state != READER_FINISHED) + input.asyncWait(this, 0, 0, gThreadManager.currentThread); + }, + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIInputStreamCallback) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE API + + /** +* Processes unprocessed, downloaded data as a request line. +* +* @returns boolean +* true iff the request line has been fully processed +*/ + _processRequestLine: function() + { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + // Servers SHOULD ignore any empty line(s) received where a Request-Line + // is expected (section 4.1). + var data = this._data; + var line = {}; + var readSuccess; + while ((readSuccess = data.readLine(line)) && line.value == "") + dumpn("*** ignoring beginning blank line..."); + + // if we don't have a full line, wait until we do + if (!readSuccess) + return false; + + // we have the first non-blank line + try + { + this._parseRequestLine(line.value); + this._state = READER_IN_HEADERS; + return true; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Processes stored data, assuming it is either at the beginning or in +* the middle of processing request headers. +* +* @returns boolean +* true iff header data in the request has been fully processed +*/ + _processHeaders: function() + { + NS_ASSERT(this._state == READER_IN_HEADERS); + + // XXX things to fix here: + // + // - need to support RFC 2047-encoded non-US-ASCII characters + + try + { + var done = this._parseHeaders(); + if (done) + { + var request = this._metadata; + + // XXX this is wrong for requests with transfer-encodings applied to + // them, particularly chunked (which by its nature can have no + // meaningful Content-Length header)! + this._contentLength = request.hasHeader("Content-Length") + ? parseInt(request.getHeader("Content-Length"), 10) + : 0; + dumpn("_processHeaders, Content-length=" + this._contentLength); + + this._state = READER_IN_BODY; + } + return done; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Processes stored data, assuming it is either at the beginning or in +* the middle of processing the request body. +* +* @returns boolean +* true iff the request body has been fully processed +*/ + _processBody: function() + { + NS_ASSERT(this._state == READER_IN_BODY); + + // XXX handle chunked transfer-coding request bodies! + + try + { + if (this._contentLength > 0) + { + var data = this._data.purge(); + var count = Math.min(data.length, this._contentLength); + dumpn("*** loading data=" + data + " len=" + data.length + + " excess=" + (data.length - count)); + + var bos = new BinaryOutputStream(this._metadata._bodyOutputStream); + bos.writeByteArray(data, count); + this._contentLength -= count; + } + + dumpn("*** remaining body data len=" + this._contentLength); + if (this._contentLength == 0) + { + this._validateRequest(); + this._state = READER_FINISHED; + this._handleResponse(); + return true; + } + + return false; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Does various post-header checks on the data in this request. +* +* @throws : HttpError +* if the request was malformed in some way +*/ + _validateRequest: function() + { + NS_ASSERT(this._state == READER_IN_BODY); + + dumpn("*** _validateRequest"); + + var metadata = this._metadata; + var headers = metadata._headers; + + // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header + var identity = this._connection.server.identity; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) + { + if (!headers.hasHeader("Host")) + { + dumpn("*** malformed HTTP/1.1 or greater request with no Host header!"); + throw HTTP_400; + } + + // If the Request-URI wasn't absolute, then we need to determine our host. + // We have to determine what scheme was used to access us based on the + // server identity data at this point, because the request just doesn't + // contain enough data on its own to do this, sadly. + if (!metadata._host) + { + var host, port; + var hostPort = headers.getHeader("Host"); + var colon = hostPort.indexOf(":"); + if (colon < 0) + { + host = hostPort; + port = ""; + } + else + { + host = hostPort.substring(0, colon); + port = hostPort.substring(colon + 1); + } + + // NB: We allow an empty port here because, oddly, a colon may be + // present even without a port number, e.g. "example.com:"; in this + // case the default port applies. + if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) + { + dumpn("*** malformed hostname (" + hostPort + ") in Host " + + "header, 400 time"); + throw HTTP_400; + } + + // If we're not given a port, we're stuck, because we don't know what + // scheme to use to look up the correct port here, in general. Since + // the HTTPS case requires a tunnel/proxy and thus requires that the + // requested URI be absolute (and thus contain the necessary + // information), let's assume HTTP will prevail and use that. + port = +port || 80; + + var scheme = identity.getScheme(host, port); + if (!scheme) + { + dumpn("*** unrecognized hostname (" + hostPort + ") in Host " + + "header, 400 time"); + throw HTTP_400; + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + } + } + else + { + NS_ASSERT(metadata._host === undefined, + "HTTP/1.0 doesn't allow absolute paths in the request line!"); + + metadata._scheme = identity.primaryScheme; + metadata._host = identity.primaryHost; + metadata._port = identity.primaryPort; + } + + NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port), + "must have a location we recognize by now!"); + }, + + /** +* Handles responses in case of error, either in the server or in the request. +* +* @param e +* the specific error encountered, which is an HttpError in the case where +* the request is in some way invalid or cannot be fulfilled; if this isn't +* an HttpError we're going to be paranoid and shut down, because that +* shouldn't happen, ever +*/ + _handleError: function(e) + { + // Don't fall back into normal processing! + this._state = READER_FINISHED; + + var server = this._connection.server; + if (e instanceof HttpError) + { + var code = e.code; + } + else + { + dumpn("!!! UNEXPECTED ERROR: " + e + + (e.lineNumber ? ", line " + e.lineNumber : "")); + + // no idea what happened -- be paranoid and shut down + code = 500; + server._requestQuit(); + } + + // make attempted reuse of data an error + this._data = null; + + this._connection.processError(code, this._metadata); + }, + + /** +* Now that we've read the request line and headers, we can actually hand off +* the request to be handled. +* +* This method is called once per request, after the request line and all +* headers and the body, if any, have been received. +*/ + _handleResponse: function() + { + NS_ASSERT(this._state == READER_FINISHED); + + // We don't need the line-based data any more, so make attempted reuse an + // error. + this._data = null; + + this._connection.process(this._metadata); + }, + + + // PARSING + + /** +* Parses the request line for the HTTP request associated with this. +* +* @param line : string +* the request line +*/ + _parseRequestLine: function(line) + { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + dumpn("*** _parseRequestLine('" + line + "')"); + + var metadata = this._metadata; + + // clients and servers SHOULD accept any amount of SP or HT characters + // between fields, even though only a single SP is required (section 19.3) + var request = line.split(/[ \t]+/); + if (!request || request.length != 3) + throw HTTP_400; + + metadata._method = request[0]; + + // get the HTTP version + var ver = request[2]; + var match = ver.match(/^HTTP\/(\d+\.\d+)$/); + if (!match) + throw HTTP_400; + + // determine HTTP version + try + { + metadata._httpVersion = new nsHttpVersion(match[1]); + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) + throw "unsupported HTTP version"; + } + catch (e) + { + // we support HTTP/1.0 and HTTP/1.1 only + throw HTTP_501; + } + + + var fullPath = request[1]; + var serverIdentity = this._connection.server.identity; + + var scheme, host, port; + + if (fullPath.charAt(0) != "/") + { + // No absolute paths in the request line in HTTP prior to 1.1 + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) + throw HTTP_400; + + try + { + var uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(fullPath, null, null); + fullPath = uri.path; + scheme = uri.scheme; + host = metadata._host = uri.asciiHost; + port = uri.port; + if (port === -1) + { + if (scheme === "http") + port = 80; + else if (scheme === "https") + port = 443; + else + throw HTTP_400; + } + } + catch (e) + { + // If the host is not a valid host on the server, the response MUST be a + // 400 (Bad Request) error message (section 5.2). Alternately, the URI + // is malformed. + throw HTTP_400; + } + + if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/") + throw HTTP_400; + } + + var splitter = fullPath.indexOf("?"); + if (splitter < 0) + { + // _queryString already set in ctor + metadata._path = fullPath; + } + else + { + metadata._path = fullPath.substring(0, splitter); + metadata._queryString = fullPath.substring(splitter + 1); + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + }, + + /** +* Parses all available HTTP headers in this until the header-ending CRLFCRLF, +* adding them to the store of headers in the request. +* +* @throws +* HTTP_400 if the headers are malformed +* @returns boolean +* true if all headers have now been processed, false otherwise +*/ + _parseHeaders: function() + { + NS_ASSERT(this._state == READER_IN_HEADERS); + + dumpn("*** _parseHeaders"); + + var data = this._data; + + var headers = this._metadata._headers; + var lastName = this._lastHeaderName; + var lastVal = this._lastHeaderValue; + + var line = {}; + while (true) + { + NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)), + lastName === undefined ? + "lastVal without lastName? lastVal: '" + lastVal + "'" : + "lastName without lastVal? lastName: '" + lastName + "'"); + + if (!data.readLine(line)) + { + // save any data we have from the header we might still be processing + this._lastHeaderName = lastName; + this._lastHeaderValue = lastVal; + return false; + } + + var lineText = line.value; + var firstChar = lineText.charAt(0); + + // blank line means end of headers + if (lineText == "") + { + // we're finished with the previous header + if (lastName) + { + try + { + headers.setHeader(lastName, lastVal, true); + } + catch (e) + { + dumpn("*** e == " + e); + throw HTTP_400; + } + } + else + { + // no headers in request -- valid for HTTP/1.0 requests + } + + // either way, we're done processing headers + this._state = READER_IN_BODY; + return true; + } + else if (firstChar == " " || firstChar == "\t") + { + // multi-line header if we've already seen a header line + if (!lastName) + { + // we don't have a header to continue! + throw HTTP_400; + } + + // append this line's text to the value; starts with SP/HT, so no need + // for separating whitespace + lastVal += lineText; + } + else + { + // we have a new header, so set the old one (if one existed) + if (lastName) + { + try + { + headers.setHeader(lastName, lastVal, true); + } + catch (e) + { + dumpn("*** e == " + e); + throw HTTP_400; + } + } + + var colon = lineText.indexOf(":"); // first colon must be splitter + if (colon < 1) + { + // no colon or missing header field-name + throw HTTP_400; + } + + // set header name, value (to be set in the next loop, usually) + lastName = lineText.substring(0, colon); + lastVal = lineText.substring(colon + 1); + } // empty, continuation, start of header + } // while (true) + } +}; + + +/** The character codes for CR and LF. */ +const CR = 0x0D, LF = 0x0A; + +/** +* Calculates the number of characters before the first CRLF pair in array, or +* -1 if the array contains no CRLF pair. +* +* @param array : Array +* an array of numbers in the range [0, 256), each representing a single +* character; the first CRLF is the lowest index i where +* |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|, +* if such an |i| exists, and -1 otherwise +* @returns int +* the index of the first CRLF if any were present, -1 otherwise +*/ +function findCRLF(array) +{ + for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1)) + { + if (array[i + 1] == LF) + return i; + } + return -1; +} + + +/** +* A container which provides line-by-line access to the arrays of bytes with +* which it is seeded. +*/ +function LineData() +{ + /** An array of queued bytes from which to get line-based characters. */ + this._data = []; +} +LineData.prototype = +{ + /** +* Appends the bytes in the given array to the internal data cache maintained +* by this. +*/ + appendBytes: function(bytes) + { + Array.prototype.push.apply(this._data, bytes); + }, + + /** +* Removes and returns a line of data, delimited by CRLF, from this. +* +* @param out +* an object whose "value" property will be set to the first line of text +* present in this, sans CRLF, if this contains a full CRLF-delimited line +* of text; if this doesn't contain enough data, the value of the property +* is undefined +* @returns boolean +* true if a full line of data could be read from the data in this, false +* otherwise +*/ + readLine: function(out) + { + var data = this._data; + var length = findCRLF(data); + if (length < 0) + return false; + + // + // We have the index of the CR, so remove all the characters, including + // CRLF, from the array with splice, and convert the removed array into the + // corresponding string, from which we then strip the trailing CRLF. + // + // Getting the line in this matter acknowledges that substring is an O(1) + // operation in SpiderMonkey because strings are immutable, whereas two + // splices, both from the beginning of the data, are less likely to be as + // cheap as a single splice plus two extra character conversions. + // + var line = String.fromCharCode.apply(null, data.splice(0, length + 2)); + out.value = line.substring(0, length); + + return true; + }, + + /** +* Removes the bytes currently within this and returns them in an array. +* +* @returns Array +* the bytes within this when this method is called +*/ + purge: function() + { + var data = this._data; + this._data = []; + return data; + } +}; + + + +/** +* Creates a request-handling function for an nsIHttpRequestHandler object. +*/ +function createHandlerFunc(handler) +{ + return function(metadata, response) { handler.handle(metadata, response); }; +} + + +/** +* The default handler for directories; writes an HTML response containing a +* slightly-formatted directory listing. +*/ +function defaultIndexHandler(metadata, response) +{ + response.setHeader("Content-Type", "text/html", false); + + var path = htmlEscape(decodeURI(metadata.path)); + + // + // Just do a very basic bit of directory listings -- no need for too much + // fanciness, especially since we don't have a style sheet in which we can + // stick rules (don't want to pollute the default path-space). + // + + var body = '<html>\ +<head>\ +<title>' + path + '</title>\ +</head>\ +<body>\ +<h1>' + path + '</h1>\ +<ol style="list-style-type: none">'; + + var directory = metadata.getProperty("directory").QueryInterface(Ci.nsILocalFile); + NS_ASSERT(directory && directory.isDirectory()); + + var fileList = []; + var files = directory.directoryEntries; + while (files.hasMoreElements()) + { + var f = files.getNext().QueryInterface(Ci.nsIFile); + var name = f.leafName; + if (!f.isHidden() && + (name.charAt(name.length - 1) != HIDDEN_CHAR || + name.charAt(name.length - 2) == HIDDEN_CHAR)) + fileList.push(f); + } + + fileList.sort(fileSort); + + for (var i = 0; i < fileList.length; i++) + { + var file = fileList[i]; + try + { + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) + name = name.substring(0, name.length - 1); + var sep = file.isDirectory() ? "/" : ""; + + // Note: using " to delimit the attribute here because encodeURIComponent + // passes through '. + var item = '<li><a href="' + encodeURIComponent(name) + sep + '">' + + htmlEscape(name) + sep + + '</a></li>'; + + body += item; + } + catch (e) { /* some file system error, ignore the file */ } + } + + body += ' </ol>\ +</body>\ +</html>'; + + response.bodyOutputStream.write(body, body.length); +} + +/** +* Sorts a and b (nsIFile objects) into an aesthetically pleasing order. +*/ +function fileSort(a, b) +{ + var dira = a.isDirectory(), dirb = b.isDirectory(); + + if (dira && !dirb) + return -1; + if (dirb && !dira) + return 1; + + var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase(); + return nameb > namea ? -1 : 1; +} + + +/** +* Converts an externally-provided path into an internal path for use in +* determining file mappings. +* +* @param path +* the path to convert +* @param encoded +* true if the given path should be passed through decodeURI prior to +* conversion +* @throws URIError +* if path is incorrectly encoded +*/ +function toInternalPath(path, encoded) +{ + if (encoded) + path = decodeURI(path); + + var comps = path.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) + { + var comp = comps[i]; + if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) + comps[i] = comp + HIDDEN_CHAR; + } + return comps.join("/"); +} + + +/** +* Adds custom-specified headers for the given file to the given response, if +* any such headers are specified. +* +* @param file +* the file on the disk which is to be written +* @param metadata +* metadata about the incoming request +* @param response +* the Response to which any specified headers/data should be written +* @throws HTTP_500 +* if an error occurred while processing custom-specified headers +*/ +function maybeAddHeaders(file, metadata, response) +{ + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) + name = name.substring(0, name.length - 1); + + var headerFile = file.parent; + headerFile.append(name + HEADERS_SUFFIX); + + if (!headerFile.exists()) + return; + + const PR_RDONLY = 0x01; + var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + try + { + var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + + var line = {value: ""}; + var more = lis.readLine(line); + + if (!more && line.value == "") + return; + + + // request line + + var status = line.value; + if (status.indexOf("HTTP ") == 0) + { + status = status.substring(5); + var space = status.indexOf(" "); + var code, description; + if (space < 0) + { + code = status; + description = ""; + } + else + { + code = status.substring(0, space); + description = status.substring(space + 1, status.length); + } + + response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description); + + line.value = ""; + more = lis.readLine(line); + } + + // headers + while (more || line.value != "") + { + var header = line.value; + var colon = header.indexOf(":"); + + response.setHeader(header.substring(0, colon), + header.substring(colon + 1, header.length), + false); // allow overriding server-set headers + + line.value = ""; + more = lis.readLine(line); + } + } + catch (e) + { + dumpn("WARNING: error in headers for " + metadata.path + ": " + e); + throw HTTP_500; + } + finally + { + fis.close(); + } +} + + +/** +* An object which handles requests for a server, executing default and +* overridden behaviors as instructed by the code which uses and manipulates it. +* Default behavior includes the paths / and /trace (diagnostics), with some +* support for HTTP error pages for various codes and fallback to HTTP 500 if +* those codes fail for any reason. +* +* @param server : nsHttpServer +* the server in which this handler is being used +*/ +function ServerHandler(server) +{ + // FIELDS + + /** +* The nsHttpServer instance associated with this handler. +*/ + this._server = server; + + /** +* A FileMap object containing the set of path->nsILocalFile mappings for +* all directory mappings set in the server (e.g., "/" for /var/www/html/, +* "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2). +* +* Note carefully: the leading and trailing "/" in each path (not file) are +* removed before insertion to simplify the code which uses this. You have +* been warned! +*/ + this._pathDirectoryMap = new FileMap(); + + /** +* Custom request handlers for the server in which this resides. Path-handler +* pairs are stored as property-value pairs in this property. +* +* @see ServerHandler.prototype._defaultPaths +*/ + this._overridePaths = {}; + + /** +* Custom request handlers for the error handlers in the server in which this +* resides. Path-handler pairs are stored as property-value pairs in this +* property. +* +* @see ServerHandler.prototype._defaultErrors +*/ + this._overrideErrors = {}; + + /** +* Maps file extensions to their MIME types in the server, overriding any +* mapping that might or might not exist in the MIME service. +*/ + this._mimeMappings = {}; + + /** +* The default handler for requests for directories, used to serve directories +* when no index file is present. +*/ + this._indexHandler = defaultIndexHandler; + + /** Per-path state storage for the server. */ + this._state = {}; + + /** Entire-server state storage. */ + this._sharedState = {}; + + /** Entire-server state storage for nsISupports values. */ + this._objectState = {}; +} +ServerHandler.prototype = +{ + // PUBLIC API + + /** +* Handles a request to this server, responding to the request appropriately +* and initiating server shutdown if necessary. +* +* This method never throws an exception. +* +* @param connection : Connection +* the connection for this request +*/ + handleResponse: function(connection) + { + var request = connection.request; + var response = new Response(connection); + + var path = request.path; + dumpn("*** path == " + path); + + try + { + try + { + if (path in this._overridePaths) + { + // explicit paths first, then files based on existing directory mappings, + // then (if the file doesn't exist) built-in server default paths + dumpn("calling override for " + path); + this._overridePaths[path](request, response); + } + else + { + this._handleDefault(request, response); + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + if (!(e instanceof HttpError)) + { + dumpn("*** unexpected error: e == " + e); + throw HTTP_500; + } + if (e.code !== 404) + throw e; + + dumpn("*** default: " + (path in this._defaultPaths)); + + response = new Response(connection); + if (path in this._defaultPaths) + this._defaultPaths[path](request, response); + else + throw HTTP_404; + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + var errorCode = "internal"; + + try + { + if (!(e instanceof HttpError)) + throw e; + + errorCode = e.code; + dumpn("*** errorCode == " + errorCode); + + response = new Response(connection); + if (e.customErrorHandling) + e.customErrorHandling(response); + this._handleError(errorCode, request, response); + return; + } + catch (e2) + { + dumpn("*** error handling " + errorCode + " error: " + + "e2 == " + e2 + ", shutting down server"); + + connection.server._requestQuit(); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (!file) + { + dumpn("*** unregistering '" + path + "' mapping"); + delete this._overridePaths[path]; + return; + } + + dumpn("*** registering '" + path + "' as mapping to " + file.path); + file = file.clone(); + + var self = this; + this._overridePaths[path] = + function(request, response) + { + if (!file.exists()) + throw HTTP_404; + + response.setStatusLine(request.httpVersion, 200, "OK"); + self._writeFileResponse(request, file, response, 0, file.fileSize); + }; + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + // XXX true path validation! + if (path.charAt(0) != "/") + throw Cr.NS_ERROR_INVALID_ARG; + + this._handlerToField(handler, this._overridePaths, path); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // strip off leading and trailing '/' so that we can use lastIndexOf when + // determining exactly how a path maps onto a mapped directory -- + // conditional is required here to deal with "/".substring(1, 0) being + // converted to "/".substring(0, 1) per the JS specification + var key = path.length == 1 ? "" : path.substring(1, path.length - 1); + + // the path-to-directory mapping code requires that the first character not + // be "/", or it will go into an infinite loop + if (key.charAt(0) == "/") + throw Cr.NS_ERROR_INVALID_ARG; + + key = toInternalPath(key, false); + + if (directory) + { + dumpn("*** mapping '" + path + "' to the location " + directory.path); + this._pathDirectoryMap.put(key, directory); + } + else + { + dumpn("*** removing mapping for '" + path + "'"); + this._pathDirectoryMap.put(key, null); + } + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(err, handler) + { + if (!(err in HTTP_ERROR_CODES)) + dumpn("*** WARNING: registering non-HTTP/1.1 error code " + + "(" + err + ") handler -- was this intentional?"); + + this._handlerToField(handler, this._overrideErrors, err); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler: function(handler) + { + if (!handler) + handler = defaultIndexHandler; + else if (typeof(handler) != "function") + handler = createHandlerFunc(handler); + + this._indexHandler = handler; + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType: function(ext, type) + { + if (!type) + delete this._mimeMappings[ext]; + else + this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type); + }, + + // PRIVATE API + + /** +* Sets or remove (if handler is null) a handler in an object with a key. +* +* @param handler +* a handler, either function or an nsIHttpRequestHandler +* @param dict +* The object to attach the handler to. +* @param key +* The field name of the handler. +*/ + _handlerToField: function(handler, dict, key) + { + // for convenience, handler can be a function if this is run from xpcshell + if (typeof(handler) == "function") + dict[key] = handler; + else if (handler) + dict[key] = createHandlerFunc(handler); + else + delete dict[key]; + }, + + /** +* Handles a request which maps to a file in the local filesystem (if a base +* path has already been set; otherwise the 404 error is thrown). +* +* @param metadata : Request +* metadata for the incoming request +* @param response : Response +* an uninitialized Response to the given request, to be initialized by a +* request handler +* @throws HTTP_### +* if an HTTP error occurred (usually HTTP_404); note that in this case the +* calling code must handle post-processing of the response +*/ + _handleDefault: function(metadata, response) + { + dumpn("*** _handleDefault()"); + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + + var path = metadata.path; + NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">"); + + // determine the actual on-disk file; this requires finding the deepest + // path-to-directory mapping in the requested URL + var file = this._getFileForPath(path); + + // the "file" might be a directory, in which case we either serve the + // contained index.html or make the index handler write the response + if (file.exists() && file.isDirectory()) + { + file.append("index.html"); // make configurable? + if (!file.exists() || file.isDirectory()) + { + metadata._ensurePropertyBag(); + metadata._bag.setPropertyAsInterface("directory", file.parent); + this._indexHandler(metadata, response); + return; + } + } + + // alternately, the file might not exist + if (!file.exists()) + throw HTTP_404; + + var start, end; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) && + metadata.hasHeader("Range") && + this._getTypeFromFile(file) !== SJS_TYPE) + { + var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/); + if (!rangeMatch) + throw HTTP_400; + + if (rangeMatch[1] !== undefined) + start = parseInt(rangeMatch[1], 10); + + if (rangeMatch[2] !== undefined) + end = parseInt(rangeMatch[2], 10); + + if (start === undefined && end === undefined) + throw HTTP_400; + + // No start given, so the end is really the count of bytes from the + // end of the file. + if (start === undefined) + { + start = Math.max(0, file.fileSize - end); + end = file.fileSize - 1; + } + + // start and end are inclusive + if (end === undefined || end >= file.fileSize) + end = file.fileSize - 1; + + if (start !== undefined && start >= file.fileSize) { + var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable"); + HTTP_416.customErrorHandling = function(errorResponse) + { + maybeAddHeaders(file, metadata, errorResponse); + }; + throw HTTP_416; + } + + if (end < start) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + start = 0; + end = file.fileSize - 1; + } + else + { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize; + response.setHeader("Content-Range", contentRange); + } + } + else + { + start = 0; + end = file.fileSize - 1; + } + + // finally... + dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " + + start + " to " + end + " inclusive"); + this._writeFileResponse(metadata, file, response, start, end - start + 1); + }, + + /** +* Writes an HTTP response for the given file, including setting headers for +* file metadata. +* +* @param metadata : Request +* the Request for which a response is being generated +* @param file : nsILocalFile +* the file which is to be sent in the response +* @param response : Response +* the response to which the file should be written +* @param offset: uint +* the byte offset to skip to when writing +* @param count: uint +* the number of bytes to write +*/ + _writeFileResponse: function(metadata, file, response, offset, count) + { + const PR_RDONLY = 0x01; + + var type = this._getTypeFromFile(file); + if (type === SJS_TYPE) + { + var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + try + { + var sis = new ScriptableInputStream(fis); + var s = Cu.Sandbox(gGlobalObject); + s.importFunction(dump, "dump"); + + // Define a basic key-value state-preservation API across requests, with + // keys initially corresponding to the empty string. + var self = this; + var path = metadata.path; + s.importFunction(function getState(k) + { + return self._getState(path, k); + }); + s.importFunction(function setState(k, v) + { + self._setState(path, k, v); + }); + s.importFunction(function getSharedState(k) + { + return self._getSharedState(k); + }); + s.importFunction(function setSharedState(k, v) + { + self._setSharedState(k, v); + }); + s.importFunction(function getObjectState(k, callback) + { + callback(self._getObjectState(k)); + }); + s.importFunction(function setObjectState(k, v) + { + self._setObjectState(k, v); + }); + s.importFunction(function registerPathHandler(p, h) + { + self.registerPathHandler(p, h); + }); + + // Make it possible for sjs files to access their location + this._setState(path, "__LOCATION__", file.path); + + try + { + // Alas, the line number in errors dumped to console when calling the + // request handler is simply an offset from where we load the SJS file. + // Work around this in a reasonably non-fragile way by dynamically + // getting the line number where we evaluate the SJS file. Don't + // separate these two lines! + var line = new Error().lineNumber; + Cu.evalInSandbox(sis.read(file.fileSize), s); + } + catch (e) + { + dumpn("*** syntax error in SJS at " + file.path + ": " + e); + throw HTTP_500; + } + + try + { + s.handleRequest(metadata, response); + } + catch (e) + { + dump("*** error running SJS at " + file.path + ": " + + e + " on line " + + (e instanceof Error + ? e.lineNumber + " in httpd.js" + : (e.lineNumber - line)) + "\n"); + throw HTTP_500; + } + } + finally + { + fis.close(); + } + } + else + { + try + { + response.setHeader("Last-Modified", + toDateString(file.lastModifiedTime), + false); + } + catch (e) { /* lastModifiedTime threw, ignore */ } + + response.setHeader("Content-Type", type, false); + maybeAddHeaders(file, metadata, response); + response.setHeader("Content-Length", "" + count, false); + + var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + offset = offset || 0; + count = count || file.fileSize; + NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset"); + NS_ASSERT(count >= 0, "bad count"); + NS_ASSERT(offset + count <= file.fileSize, "bad total data size"); + + try + { + if (offset !== 0) + { + // Seek (or read, if seeking isn't supported) to the correct offset so + // the data sent to the client matches the requested range. + if (fis instanceof Ci.nsISeekableStream) + fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset); + else + new ScriptableInputStream(fis).read(offset); + } + } + catch (e) + { + fis.close(); + throw e; + } + + function writeMore() + { + gThreadManager.currentThread + .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL); + } + + var input = new BinaryInputStream(fis); + var output = new BinaryOutputStream(response.bodyOutputStream); + var writeData = + { + run: function() + { + var chunkSize = Math.min(65536, count); + count -= chunkSize; + NS_ASSERT(count >= 0, "underflow"); + + try + { + var data = input.readByteArray(chunkSize); + NS_ASSERT(data.length === chunkSize, + "incorrect data returned? got " + data.length + + ", expected " + chunkSize); + output.writeByteArray(data, data.length); + if (count === 0) + { + fis.close(); + response.finish(); + } + else + { + writeMore(); + } + } + catch (e) + { + try + { + fis.close(); + } + finally + { + response.finish(); + } + throw e; + } + } + }; + + writeMore(); + + // Now that we know copying will start, flag the response as async. + response.processAsync(); + } + }, + + /** +* Get the value corresponding to a given key for the given path for SJS state +* preservation across requests. +* +* @param path : string +* the path from which the given state is to be retrieved +* @param k : string +* the key whose corresponding value is to be returned +* @returns string +* the corresponding value, which is initially the empty string +*/ + _getState: function(path, k) + { + var state = this._state; + if (path in state && k in state[path]) + return state[path][k]; + return ""; + }, + + /** +* Set the value corresponding to a given key for the given path for SJS state +* preservation across requests. +* +* @param path : string +* the path from which the given state is to be retrieved +* @param k : string +* the key whose corresponding value is to be set +* @param v : string +* the value to be set +*/ + _setState: function(path, k, v) + { + if (typeof v !== "string") + throw new Error("non-string value passed"); + var state = this._state; + if (!(path in state)) + state[path] = {}; + state[path][k] = v; + }, + + /** +* Get the value corresponding to a given key for SJS state preservation +* across requests. +* +* @param k : string +* the key whose corresponding value is to be returned +* @returns string +* the corresponding value, which is initially the empty string +*/ + _getSharedState: function(k) + { + var state = this._sharedState; + if (k in state) + return state[k]; + return ""; + }, + + /** +* Set the value corresponding to a given key for SJS state preservation +* across requests. +* +* @param k : string +* the key whose corresponding value is to be set +* @param v : string +* the value to be set +*/ + _setSharedState: function(k, v) + { + if (typeof v !== "string") + throw new Error("non-string value passed"); + this._sharedState[k] = v; + }, + + /** +* Returns the object associated with the given key in the server for SJS +* state preservation across requests. +* +* @param k : string +* the key whose corresponding object is to be returned +* @returns nsISupports +* the corresponding object, or null if none was present +*/ + _getObjectState: function(k) + { + if (typeof k !== "string") + throw new Error("non-string key passed"); + return this._objectState[k] || null; + }, + + /** +* Sets the object associated with the given key in the server for SJS +* state preservation across requests. +* +* @param k : string +* the key whose corresponding object is to be set +* @param v : nsISupports +* the object to be associated with the given key; may be null +*/ + _setObjectState: function(k, v) + { + if (typeof k !== "string") + throw new Error("non-string key passed"); + if (typeof v !== "object") + throw new Error("non-object value passed"); + if (v && !("QueryInterface" in v)) + { + throw new Error("must pass an nsISupports; use wrappedJSObject to ease " + + "pain when using the server from JS"); + } + + this._objectState[k] = v; + }, + + /** +* Gets a content-type for the given file, first by checking for any custom +* MIME-types registered with this handler for the file's extension, second by +* asking the global MIME service for a content-type, and finally by failing +* over to application/octet-stream. +* +* @param file : nsIFile +* the nsIFile for which to get a file type +* @returns string +* the best content-type which can be determined for the file +*/ + _getTypeFromFile: function(file) + { + try + { + var name = file.leafName; + var dot = name.lastIndexOf("."); + if (dot > 0) + { + var ext = name.slice(dot + 1); + if (ext in this._mimeMappings) + return this._mimeMappings[ext]; + } + return Cc["@mozilla.org/uriloader/external-helper-app-service;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(file); + } + catch (e) + { + return "application/octet-stream"; + } + }, + + /** +* Returns the nsILocalFile which corresponds to the path, as determined using +* all registered path->directory mappings and any paths which are explicitly +* overridden. +* +* @param path : string +* the server path for which a file should be retrieved, e.g. "/foo/bar" +* @throws HttpError +* when the correct action is the corresponding HTTP error (i.e., because no +* mapping was found for a directory in path, the referenced file doesn't +* exist, etc.) +* @returns nsILocalFile +* the file to be sent as the response to a request for the path +*/ + _getFileForPath: function(path) + { + // decode and add underscores as necessary + try + { + path = toInternalPath(path, true); + } + catch (e) + { + throw HTTP_400; // malformed path + } + + // next, get the directory which contains this path + var pathMap = this._pathDirectoryMap; + + // An example progression of tmp for a path "/foo/bar/baz/" might be: + // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", "" + var tmp = path.substring(1); + while (true) + { + // do we have a match for current head of the path? + var file = pathMap.get(tmp); + if (file) + { + // XXX hack; basically disable showing mapping for /foo/bar/ when the + // requested path was /foo/bar, because relative links on the page + // will all be incorrect -- we really need the ability to easily + // redirect here instead + if (tmp == path.substring(1) && + tmp.length != 0 && + tmp.charAt(tmp.length - 1) != "/") + file = null; + else + break; + } + + // if we've finished trying all prefixes, exit + if (tmp == "") + break; + + tmp = tmp.substring(0, tmp.lastIndexOf("/")); + } + + // no mapping applies, so 404 + if (!file) + throw HTTP_404; + + + // last, get the file for the path within the determined directory + var parentFolder = file.parent; + var dirIsRoot = (parentFolder == null); + + // Strategy here is to append components individually, making sure we + // never move above the given directory; this allows paths such as + // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling"; + // this component-wise approach also means the code works even on platforms + // which don't use "/" as the directory separator, such as Windows + var leafPath = path.substring(tmp.length + 1); + var comps = leafPath.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) + { + var comp = comps[i]; + + if (comp == "..") + file = file.parent; + else if (comp == "." || comp == "") + continue; + else + file.append(comp); + + if (!dirIsRoot && file.equals(parentFolder)) + throw HTTP_403; + } + + return file; + }, + + /** +* Writes the error page for the given HTTP error code over the given +* connection. +* +* @param errorCode : uint +* the HTTP error code to be used +* @param connection : Connection +* the connection on which the error occurred +*/ + handleError: function(errorCode, connection) + { + var response = new Response(connection); + + dumpn("*** error in request: " + errorCode); + + this._handleError(errorCode, new Request(connection.port), response); + }, + + /** +* Handles a request which generates the given error code, using the +* user-defined error handler if one has been set, gracefully falling back to +* the x00 status code if the code has no handler, and failing to status code +* 500 if all else fails. +* +* @param errorCode : uint +* the HTTP error which is to be returned +* @param metadata : Request +* metadata for the request, which will often be incomplete since this is an +* error +* @param response : Response +* an uninitialized Response should be initialized when this method +* completes with information which represents the desired error code in the +* ideal case or a fallback code in abnormal circumstances (i.e., 500 is a +* fallback for 505, per HTTP specs) +*/ + _handleError: function(errorCode, metadata, response) + { + if (!metadata) + throw Cr.NS_ERROR_NULL_POINTER; + + var errorX00 = errorCode - (errorCode % 100); + + try + { + if (!(errorCode in HTTP_ERROR_CODES)) + dumpn("*** WARNING: requested invalid error: " + errorCode); + + // RFC 2616 says that we should try to handle an error by its class if we + // can't otherwise handle it -- if that fails, we revert to handling it as + // a 500 internal server error, and if that fails we throw and shut down + // the server + + // actually handle the error + try + { + if (errorCode in this._overrideErrors) + this._overrideErrors[errorCode](metadata, response); + else + this._defaultErrors[errorCode](metadata, response); + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + // don't retry the handler that threw + if (errorX00 == errorCode) + throw HTTP_500; + + dumpn("*** error in handling for error code " + errorCode + ", " + + "falling back to " + errorX00 + "..."); + response = new Response(response._connection); + if (errorX00 in this._overrideErrors) + this._overrideErrors[errorX00](metadata, response); + else if (errorX00 in this._defaultErrors) + this._defaultErrors[errorX00](metadata, response); + else + throw HTTP_500; + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(); + return; + } + + // we've tried everything possible for a meaningful error -- now try 500 + dumpn("*** error in handling for error code " + errorX00 + ", falling " + + "back to 500..."); + + try + { + response = new Response(response._connection); + if (500 in this._overrideErrors) + this._overrideErrors[500](metadata, response); + else + this._defaultErrors[500](metadata, response); + } + catch (e2) + { + dumpn("*** multiple errors in default error handlers!"); + dumpn("*** e == " + e + ", e2 == " + e2); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // FIELDS + + /** +* This object contains the default handlers for the various HTTP error codes. +*/ + _defaultErrors: + { + 400: function(metadata, response) + { + // none of the data in metadata is reliable, so hard-code everything here + response.setStatusLine("1.1", 400, "Bad Request"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Bad request\n"; + response.bodyOutputStream.write(body, body.length); + }, + 403: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 403, "Forbidden"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>403 Forbidden</title></head>\ +<body>\ +<h1>403 Forbidden</h1>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 404: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>404 Not Found</title></head>\ +<body>\ +<h1>404 Not Found</h1>\ +<p>\ +<span style='font-family: monospace;'>" + + htmlEscape(metadata.path) + + "</span> was not found.\ +</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 416: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 416, + "Requested Range Not Satisfiable"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head>\ +<title>416 Requested Range Not Satisfiable</title></head>\ +<body>\ +<h1>416 Requested Range Not Satisfiable</h1>\ +<p>The byte range was not valid for the\ +requested resource.\ +</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 500: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 500, + "Internal Server Error"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>500 Internal Server Error</title></head>\ +<body>\ +<h1>500 Internal Server Error</h1>\ +<p>Something's broken in this server and\ +needs to be fixed.</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 501: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 501, "Not Implemented"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>501 Not Implemented</title></head>\ +<body>\ +<h1>501 Not Implemented</h1>\ +<p>This server is not (yet) Apache.</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 505: function(metadata, response) + { + response.setStatusLine("1.1", 505, "HTTP Version Not Supported"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>505 HTTP Version Not Supported</title></head>\ +<body>\ +<h1>505 HTTP Version Not Supported</h1>\ +<p>This server only supports HTTP/1.0 and HTTP/1.1\ +connections.</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + } + }, + + /** +* Contains handlers for the default set of URIs contained in this server. +*/ + _defaultPaths: + { + "/": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>httpd.js</title></head>\ +<body>\ +<h1>httpd.js</h1>\ +<p>If you're seeing this page, httpd.js is up and\ +serving requests! Now set a base path and serve some\ +files!</p>\ +</body>\ +</html>"; + + response.bodyOutputStream.write(body, body.length); + }, + + "/trace": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Request-URI: " + + metadata.scheme + "://" + metadata.host + ":" + metadata.port + + metadata.path + "\n\n"; + body += "Request (semantically equivalent, slightly reformatted):\n\n"; + body += metadata.method + " " + metadata.path; + + if (metadata.queryString) + body += "?" + metadata.queryString; + + body += " HTTP/" + metadata.httpVersion + "\r\n"; + + var headEnum = metadata.headers; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n"; + } + + response.bodyOutputStream.write(body, body.length); + } + } +}; + + +/** +* Maps absolute paths to files on the local file system (as nsILocalFiles). +*/ +function FileMap() +{ + /** Hash which will map paths to nsILocalFiles. */ + this._map = {}; +} +FileMap.prototype = +{ + // PUBLIC API + + /** +* Maps key to a clone of the nsILocalFile value if value is non-null; +* otherwise, removes any extant mapping for key. +* +* @param key : string +* string to which a clone of value is mapped +* @param value : nsILocalFile +* the file to map to key, or null to remove a mapping +*/ + put: function(key, value) + { + if (value) + this._map[key] = value.clone(); + else + delete this._map[key]; + }, + + /** +* Returns a clone of the nsILocalFile mapped to key, or null if no such +* mapping exists. +* +* @param key : string +* key to which the returned file maps +* @returns nsILocalFile +* a clone of the mapped file, or null if no mapping exists +*/ + get: function(key) + { + var val = this._map[key]; + return val ? val.clone() : null; + } +}; + + +// Response CONSTANTS + +// token = *<any CHAR except CTLs or separators> +// CHAR = <any US-ASCII character (0-127)> +// CTL = <any US-ASCII control character (0-31) and DEL (127)> +// separators = "(" | ")" | "<" | ">" | "@" +// | "," | ";" | ":" | "\" | <"> +// | "/" | "[" | "]" | "?" | "=" +// | "{" | "}" | SP | HT +const IS_TOKEN_ARRAY = + [0, 0, 0, 0, 0, 0, 0, 0, // 0 + 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 0, 0, 0, 0, 0, 0, 0, 0, // 16 + 0, 0, 0, 0, 0, 0, 0, 0, // 24 + + 0, 1, 0, 1, 1, 1, 1, 1, // 32 + 0, 0, 1, 1, 0, 1, 1, 0, // 40 + 1, 1, 1, 1, 1, 1, 1, 1, // 48 + 1, 1, 0, 0, 0, 0, 0, 0, // 56 + + 0, 1, 1, 1, 1, 1, 1, 1, // 64 + 1, 1, 1, 1, 1, 1, 1, 1, // 72 + 1, 1, 1, 1, 1, 1, 1, 1, // 80 + 1, 1, 1, 0, 0, 0, 1, 1, // 88 + + 1, 1, 1, 1, 1, 1, 1, 1, // 96 + 1, 1, 1, 1, 1, 1, 1, 1, // 104 + 1, 1, 1, 1, 1, 1, 1, 1, // 112 + 1, 1, 1, 0, 1, 0, 1]; // 120 + + +/** +* Determines whether the given character code is a CTL. +* +* @param code : uint +* the character code +* @returns boolean +* true if code is a CTL, false otherwise +*/ +function isCTL(code) +{ + return (code >= 0 && code <= 31) || (code == 127); +} + +/** +* Represents a response to an HTTP request, encapsulating all details of that +* response. This includes all headers, the HTTP version, status code and +* explanation, and the entity itself. +* +* @param connection : Connection +* the connection over which this response is to be written +*/ +function Response(connection) +{ + /** The connection over which this response will be written. */ + this._connection = connection; + + /** +* The HTTP version of this response; defaults to 1.1 if not set by the +* handler. +*/ + this._httpVersion = nsHttpVersion.HTTP_1_1; + + /** +* The HTTP code of this response; defaults to 200. +*/ + this._httpCode = 200; + + /** +* The description of the HTTP code in this response; defaults to "OK". +*/ + this._httpDescription = "OK"; + + /** +* An nsIHttpHeaders object in which the headers in this response should be +* stored. This property is null after the status line and headers have been +* written to the network, and it may be modified up until it is cleared, +* except if this._finished is set first (in which case headers are written +* asynchronously in response to a finish() call not preceded by +* flushHeaders()). +*/ + this._headers = new nsHttpHeaders(); + + /** +* Set to true when this response is ended (completely constructed if possible +* and the connection closed); further actions on this will then fail. +*/ + this._ended = false; + + /** +* A stream used to hold data written to the body of this response. +*/ + this._bodyOutputStream = null; + + /** +* A stream containing all data that has been written to the body of this +* response so far. (Async handlers make the data contained in this +* unreliable as a way of determining content length in general, but auxiliary +* saved information can sometimes be used to guarantee reliability.) +*/ + this._bodyInputStream = null; + + /** +* A stream copier which copies data to the network. It is initially null +* until replaced with a copier for response headers; when headers have been +* fully sent it is replaced with a copier for the response body, remaining +* so for the duration of response processing. +*/ + this._asyncCopier = null; + + /** +* True if this response has been designated as being processed +* asynchronously rather than for the duration of a single call to +* nsIHttpRequestHandler.handle. +*/ + this._processAsync = false; + + /** +* True iff finish() has been called on this, signaling that no more changes +* to this may be made. +*/ + this._finished = false; + + /** +* True iff powerSeized() has been called on this, signaling that this +* response is to be handled manually by the response handler (which may then +* send arbitrary data in response, even non-HTTP responses). +*/ + this._powerSeized = false; +} +Response.prototype = +{ + // PUBLIC CONSTRUCTION API + + // + // see nsIHttpResponse.bodyOutputStream + // + get bodyOutputStream() + { + if (this._finished) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + if (!this._bodyOutputStream) + { + var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX, + null); + this._bodyOutputStream = pipe.outputStream; + this._bodyInputStream = pipe.inputStream; + if (this._processAsync || this._powerSeized) + this._startAsyncProcessor(); + } + + return this._bodyOutputStream; + }, + + // + // see nsIHttpResponse.write + // + write: function(data) + { + if (this._finished) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + var dataAsString = String(data); + this.bodyOutputStream.write(dataAsString, dataAsString.length); + }, + + // + // see nsIHttpResponse.setStatusLine + // + setStatusLine: function(httpVersion, code, description) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + if (!(code >= 0 && code < 1000)) + throw Cr.NS_ERROR_INVALID_ARG; + + try + { + var httpVer; + // avoid version construction for the most common cases + if (!httpVersion || httpVersion == "1.1") + httpVer = nsHttpVersion.HTTP_1_1; + else if (httpVersion == "1.0") + httpVer = nsHttpVersion.HTTP_1_0; + else + httpVer = new nsHttpVersion(httpVersion); + } + catch (e) + { + throw Cr.NS_ERROR_INVALID_ARG; + } + + // Reason-Phrase = *<TEXT, excluding CR, LF> + // TEXT = <any OCTET except CTLs, but including LWS> + // + // XXX this ends up disallowing octets which aren't Unicode, I think -- not + // much to do if description is IDL'd as string + if (!description) + description = ""; + for (var i = 0; i < description.length; i++) + if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t") + throw Cr.NS_ERROR_INVALID_ARG; + + // set the values only after validation to preserve atomicity + this._httpDescription = description; + this._httpCode = code; + this._httpVersion = httpVer; + }, + + // + // see nsIHttpResponse.setHeader + // + setHeader: function(name, value, merge) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + this._headers.setHeader(name, value, merge); + }, + + // + // see nsIHttpResponse.processAsync + // + processAsync: function() + { + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._processAsync) + return; + this._ensureAlive(); + + dumpn("*** processing connection " + this._connection.number + " async"); + this._processAsync = true; + + /* +* Either the bodyOutputStream getter or this method is responsible for +* starting the asynchronous processor and catching writes of data to the +* response body of async responses as they happen, for the purpose of +* forwarding those writes to the actual connection's output stream. +* If bodyOutputStream is accessed first, calling this method will create +* the processor (when it first is clear that body data is to be written +* immediately, not buffered). If this method is called first, accessing +* bodyOutputStream will create the processor. If only this method is +* called, we'll write nothing, neither headers nor the nonexistent body, +* until finish() is called. Since that delay is easily avoided by simply +* getting bodyOutputStream or calling write(""), we don't worry about it. +*/ + if (this._bodyOutputStream && !this._asyncCopier) + this._startAsyncProcessor(); + }, + + // + // see nsIHttpResponse.seizePower + // + seizePower: function() + { + if (this._processAsync) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + return; + this._ensureAlive(); + + dumpn("*** forcefully seizing power over connection " + + this._connection.number + "..."); + + // Purge any already-written data without sending it. We could as easily + // swap out the streams entirely, but that makes it possible to acquire and + // unknowingly use a stale reference, so we require there only be one of + // each stream ever for any response to avoid this complication. + if (this._asyncCopier) + this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED); + this._asyncCopier = null; + if (this._bodyOutputStream) + { + var input = new BinaryInputStream(this._bodyInputStream); + var avail; + while ((avail = input.available()) > 0) + input.readByteArray(avail); + } + + this._powerSeized = true; + if (this._bodyOutputStream) + this._startAsyncProcessor(); + }, + + // + // see nsIHttpResponse.finish + // + finish: function() + { + if (!this._processAsync && !this._powerSeized) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._finished) + return; + + dumpn("*** finishing connection " + this._connection.number); + this._startAsyncProcessor(); // in case bodyOutputStream was never accessed + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + this._finished = true; + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // POST-CONSTRUCTION API (not exposed externally) + + /** +* The HTTP version number of this, as a string (e.g. "1.1"). +*/ + get httpVersion() + { + this._ensureAlive(); + return this._httpVersion.toString(); + }, + + /** +* The HTTP status code of this response, as a string of three characters per +* RFC 2616. +*/ + get httpCode() + { + this._ensureAlive(); + + var codeString = (this._httpCode < 10 ? "0" : "") + + (this._httpCode < 100 ? "0" : "") + + this._httpCode; + return codeString; + }, + + /** +* The description of the HTTP status code of this response, or "" if none is +* set. +*/ + get httpDescription() + { + this._ensureAlive(); + + return this._httpDescription; + }, + + /** +* The headers in this response, as an nsHttpHeaders object. +*/ + get headers() + { + this._ensureAlive(); + + return this._headers; + }, + + // + // see nsHttpHeaders.getHeader + // + getHeader: function(name) + { + this._ensureAlive(); + + return this._headers.getHeader(name); + }, + + /** +* Determines whether this response may be abandoned in favor of a newly +* constructed response. A response may be abandoned only if it is not being +* sent asynchronously and if raw control over it has not been taken from the +* server. +* +* @returns boolean +* true iff no data has been written to the network +*/ + partiallySent: function() + { + dumpn("*** partiallySent()"); + return this._processAsync || this._powerSeized; + }, + + /** +* If necessary, kicks off the remaining request processing needed to be done +* after a request handler performs its initial work upon this response. +*/ + complete: function() + { + dumpn("*** complete()"); + if (this._processAsync || this._powerSeized) + { + NS_ASSERT(this._processAsync ^ this._powerSeized, + "can't both send async and relinquish power"); + return; + } + + NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?"); + + this._startAsyncProcessor(); + + // Now make sure we finish processing this request! + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + }, + + /** +* Abruptly ends processing of this response, usually due to an error in an +* incoming request but potentially due to a bad error handler. Since we +* cannot handle the error in the usual way (giving an HTTP error page in +* response) because data may already have been sent (or because the response +* might be expected to have been generated asynchronously or completely from +* scratch by the handler), we stop processing this response and abruptly +* close the connection. +* +* @param e : Error +* the exception which precipitated this abort, or null if no such exception +* was generated +*/ + abort: function(e) + { + dumpn("*** abort(<" + e + ">)"); + + // This response will be ended by the processor if one was created. + var copier = this._asyncCopier; + if (copier) + { + // We dispatch asynchronously here so that any pending writes of data to + // the connection will be deterministically written. This makes it easier + // to specify exact behavior, and it makes observable behavior more + // predictable for clients. Note that the correctness of this depends on + // callbacks in response to _waitToReadData in WriteThroughCopier + // happening asynchronously with respect to the actual writing of data to + // bodyOutputStream, as they currently do; if they happened synchronously, + // an event which ran before this one could write more data to the + // response body before we get around to canceling the copier. We have + // tests for this in test_seizepower.js, however, and I can't think of a + // way to handle both cases without removing bodyOutputStream access and + // moving its effective write(data, length) method onto Response, which + // would be slower and require more code than this anyway. + gThreadManager.currentThread.dispatch({ + run: function() + { + dumpn("*** canceling copy asynchronously..."); + copier.cancel(Cr.NS_ERROR_UNEXPECTED); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + } + else + { + this.end(); + } + }, + + /** +* Closes this response's network connection, marks the response as finished, +* and notifies the server handler that the request is done being processed. +*/ + end: function() + { + NS_ASSERT(!this._ended, "ending this response twice?!?!"); + + this._connection.close(); + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + + this._finished = true; + this._ended = true; + }, + + // PRIVATE IMPLEMENTATION + + /** +* Sends the status line and headers of this response if they haven't been +* sent and initiates the process of copying data written to this response's +* body to the network. +*/ + _startAsyncProcessor: function() + { + dumpn("*** _startAsyncProcessor()"); + + // Handle cases where we're being called a second time. The former case + // happens when this is triggered both by complete() and by processAsync(), + // while the latter happens when processAsync() in conjunction with sent + // data causes abort() to be called. + if (this._asyncCopier || this._ended) + { + dumpn("*** ignoring second call to _startAsyncProcessor"); + return; + } + + // Send headers if they haven't been sent already and should be sent, then + // asynchronously continue to send the body. + if (this._headers && !this._powerSeized) + { + this._sendHeaders(); + return; + } + + this._headers = null; + this._sendBody(); + }, + + /** +* Signals that all modifications to the response status line and headers are +* complete and then sends that data over the network to the client. Once +* this method completes, a different response to the request that resulted +* in this response cannot be sent -- the only possible action in case of +* error is to abort the response and close the connection. +*/ + _sendHeaders: function() + { + dumpn("*** _sendHeaders()"); + + NS_ASSERT(this._headers); + NS_ASSERT(!this._powerSeized); + + // request-line + var statusLine = "HTTP/" + this.httpVersion + " " + + this.httpCode + " " + + this.httpDescription + "\r\n"; + + // header post-processing + + var headers = this._headers; + headers.setHeader("Connection", "close", false); + headers.setHeader("Server", "httpd.js", false); + if (!headers.hasHeader("Date")) + headers.setHeader("Date", toDateString(Date.now()), false); + + // Any response not being processed asynchronously must have an associated + // Content-Length header for reasons of backwards compatibility with the + // initial server, which fully buffered every response before sending it. + // Beyond that, however, it's good to do this anyway because otherwise it's + // impossible to test behaviors that depend on the presence or absence of a + // Content-Length header. + if (!this._processAsync) + { + dumpn("*** non-async response, set Content-Length"); + + var bodyStream = this._bodyInputStream; + var avail = bodyStream ? bodyStream.available() : 0; + + // XXX assumes stream will always report the full amount of data available + headers.setHeader("Content-Length", "" + avail, false); + } + + + // construct and send response + dumpn("*** header post-processing completed, sending response head..."); + + // request-line + var preambleData = [statusLine]; + + // headers + var headEnum = headers.enumerator; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + var values = headers.getHeaderValues(fieldName); + for (var i = 0, sz = values.length; i < sz; i++) + preambleData.push(fieldName + ": " + values[i] + "\r\n"); + } + + // end request-line/headers + preambleData.push("\r\n"); + + var preamble = preambleData.join(""); + + var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null); + responseHeadPipe.outputStream.write(preamble, preamble.length); + + var response = this; + var copyObserver = + { + onStartRequest: function(request, cx) + { + dumpn("*** preamble copying started"); + }, + + onStopRequest: function(request, cx, statusCode) + { + dumpn("*** preamble copying complete " + + "[status=0x" + statusCode.toString(16) + "]"); + + if (!components.isSuccessCode(statusCode)) + { + dumpn("!!! header copying problems: non-success statusCode, " + + "ending response"); + + response.end(); + } + else + { + response._sendBody(); + } + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + var headerCopier = this._asyncCopier = + new WriteThroughCopier(responseHeadPipe.inputStream, + this._connection.output, + copyObserver, null); + + responseHeadPipe.outputStream.close(); + + // Forbid setting any more headers or modifying the request line. + this._headers = null; + }, + + /** +* Asynchronously writes the body of the response (or the entire response, if +* seizePower() has been called) to the network. +*/ + _sendBody: function() + { + dumpn("*** _sendBody"); + + NS_ASSERT(!this._headers, "still have headers around but sending body?"); + + // If no body data was written, we're done + if (!this._bodyInputStream) + { + dumpn("*** empty body, response finished"); + this.end(); + return; + } + + var response = this; + var copyObserver = + { + onStartRequest: function(request, context) + { + dumpn("*** onStartRequest"); + }, + + onStopRequest: function(request, cx, statusCode) + { + dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]"); + + if (statusCode === Cr.NS_BINDING_ABORTED) + { + dumpn("*** terminating copy observer without ending the response"); + } + else + { + if (!components.isSuccessCode(statusCode)) + dumpn("*** WARNING: non-success statusCode in onStopRequest"); + + response.end(); + } + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + dumpn("*** starting async copier of body data..."); + this._asyncCopier = + new WriteThroughCopier(this._bodyInputStream, this._connection.output, + copyObserver, null); + }, + + /** Ensures that this hasn't been ended. */ + _ensureAlive: function() + { + NS_ASSERT(!this._ended, "not handling response lifetime correctly"); + } +}; + +/** +* Size of the segments in the buffer used in storing response data and writing +* it to the socket. +*/ +Response.SEGMENT_SIZE = 8192; + +/** Serves double duty in WriteThroughCopier implementation. */ +function notImplemented() +{ + throw Cr.NS_ERROR_NOT_IMPLEMENTED; +} + +/** Returns true iff the given exception represents stream closure. */ +function streamClosed(e) +{ + return e === Cr.NS_BASE_STREAM_CLOSED || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED); +} + +/** Returns true iff the given exception represents a blocked stream. */ +function wouldBlock(e) +{ + return e === Cr.NS_BASE_STREAM_WOULD_BLOCK || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK); +} + +/** +* Copies data from source to sink as it becomes available, when that data can +* be written to sink without blocking. +* +* @param source : nsIAsyncInputStream +* the stream from which data is to be read +* @param sink : nsIAsyncOutputStream +* the stream to which data is to be copied +* @param observer : nsIRequestObserver +* an observer which will be notified when the copy starts and finishes +* @param context : nsISupports +* context passed to observer when notified of start/stop +* @throws NS_ERROR_NULL_POINTER +* if source, sink, or observer are null +*/ +function WriteThroughCopier(source, sink, observer, context) +{ + if (!source || !sink || !observer) + throw Cr.NS_ERROR_NULL_POINTER; + + /** Stream from which data is being read. */ + this._source = source; + + /** Stream to which data is being written. */ + this._sink = sink; + + /** Observer watching this copy. */ + this._observer = observer; + + /** Context for the observer watching this. */ + this._context = context; + + /** +* True iff this is currently being canceled (cancel has been called, the +* callback may not yet have been made). +*/ + this._canceled = false; + + /** +* False until all data has been read from input and written to output, at +* which point this copy is completed and cancel() is asynchronously called. +*/ + this._completed = false; + + /** Required by nsIRequest, meaningless. */ + this.loadFlags = 0; + /** Required by nsIRequest, meaningless. */ + this.loadGroup = null; + /** Required by nsIRequest, meaningless. */ + this.name = "response-body-copy"; + + /** Status of this request. */ + this.status = Cr.NS_OK; + + /** Arrays of byte strings waiting to be written to output. */ + this._pendingData = []; + + // start copying + try + { + observer.onStartRequest(this, context); + this._waitToReadData(); + this._waitForSinkClosure(); + } + catch (e) + { + dumpn("!!! error starting copy: " + e + + ("lineNumber" in e ? ", line " + e.lineNumber : "")); + dumpn(e.stack); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + } +} +WriteThroughCopier.prototype = +{ + /* nsISupports implementation */ + + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIInputStreamCallback) || + iid.equals(Ci.nsIOutputStreamCallback) || + iid.equals(Ci.nsIRequest) || + iid.equals(Ci.nsISupports)) + { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // NSIINPUTSTREAMCALLBACK + + /** +* Receives a more-data-in-input notification and writes the corresponding +* data to the output. +* +* @param input : nsIAsyncInputStream +* the input stream on whose data we have been waiting +*/ + onInputStreamReady: function(input) + { + if (this._source === null) + return; + + dumpn("*** onInputStreamReady"); + + // + // Ordinarily we'll read a non-zero amount of data from input, queue it up + // to be written and then wait for further callbacks. The complications in + // this method are the cases where we deviate from that behavior when errors + // occur or when copying is drawing to a finish. + // + // The edge cases when reading data are: + // + // Zero data is read + // If zero data was read, we're at the end of available data, so we can + // should stop reading and move on to writing out what we have (or, if + // we've already done that, onto notifying of completion). + // A stream-closed exception is thrown + // This is effectively a less kind version of zero data being read; the + // only difference is that we notify of completion with that result + // rather than with NS_OK. + // Some other exception is thrown + // This is the least kind result. We don't know what happened, so we + // act as though the stream closed except that we notify of completion + // with the result NS_ERROR_UNEXPECTED. + // + + var bytesWanted = 0, bytesConsumed = -1; + try + { + input = new BinaryInputStream(input); + + bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE); + dumpn("*** input wanted: " + bytesWanted); + + if (bytesWanted > 0) + { + var data = input.readByteArray(bytesWanted); + bytesConsumed = data.length; + this._pendingData.push(String.fromCharCode.apply(String, data)); + } + + dumpn("*** " + bytesConsumed + " bytes read"); + + // Handle the zero-data edge case in the same place as all other edge + // cases are handled. + if (bytesWanted === 0) + throw Cr.NS_BASE_STREAM_CLOSED; + } + catch (e) + { + if (streamClosed(e)) + { + dumpn("*** input stream closed"); + e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED; + } + else + { + dumpn("!!! unexpected error reading from input, canceling: " + e); + e = Cr.NS_ERROR_UNEXPECTED; + } + + this._doneReadingSource(e); + return; + } + + var pendingData = this._pendingData; + + NS_ASSERT(bytesConsumed > 0); + NS_ASSERT(pendingData.length > 0, "no pending data somehow?"); + NS_ASSERT(pendingData[pendingData.length - 1].length > 0, + "buffered zero bytes of data?"); + + NS_ASSERT(this._source !== null); + + // Reading has gone great, and we've gotten data to write now. What if we + // don't have a place to write that data, because output went away just + // before this read? Drop everything on the floor, including new data, and + // cancel at this point. + if (this._sink === null) + { + pendingData.length = 0; + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we've read the data, and we know we have a place to write it. We + // need to queue up the data to be written, but *only* if none is queued + // already -- if data's already queued, the code that actually writes the + // data will make sure to wait on unconsumed pending data. + try + { + if (pendingData.length === 1) + this._waitToWriteData(); + } + catch (e) + { + dumpn("!!! error waiting to write data just read, swallowing and " + + "writing only what we already have: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Whee! We successfully read some data, and it's successfully queued up to + // be written. All that remains now is to wait for more data to read. + try + { + this._waitToReadData(); + } + catch (e) + { + dumpn("!!! error waiting to read more data: " + e); + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + } + }, + + + // NSIOUTPUTSTREAMCALLBACK + + /** +* Callback when data may be written to the output stream without blocking, or +* when the output stream has been closed. +* +* @param output : nsIAsyncOutputStream +* the output stream on whose writability we've been waiting, also known as +* this._sink +*/ + onOutputStreamReady: function(output) + { + if (this._sink === null) + return; + + dumpn("*** onOutputStreamReady"); + + var pendingData = this._pendingData; + if (pendingData.length === 0) + { + // There's no pending data to write. The only way this can happen is if + // we're waiting on the output stream's closure, so we can respond to a + // copying failure as quickly as possible (rather than waiting for data to + // be available to read and then fail to be copied). Therefore, we must + // be done now -- don't bother to attempt to write anything and wrap + // things up. + dumpn("!!! output stream closed prematurely, ending copy"); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + + NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?"); + + // + // Write out the first pending quantum of data. The possible errors here + // are: + // + // The write might fail because we can't write that much data + // Okay, we've written what we can now, so re-queue what's left and + // finish writing it out later. + // The write failed because the stream was closed + // Discard pending data that we can no longer write, stop reading, and + // signal that copying finished. + // Some other error occurred. + // Same as if the stream were closed, but notify with the status + // NS_ERROR_UNEXPECTED so the observer knows something was wonky. + // + + try + { + var quantum = pendingData[0]; + + // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on + // undefined behavior! We're only using this because writeByteArray + // is unusably broken for asynchronous output streams; see bug 532834 + // for details. + var bytesWritten = output.write(quantum, quantum.length); + if (bytesWritten === quantum.length) + pendingData.shift(); + else + pendingData[0] = quantum.substring(bytesWritten); + + dumpn("*** wrote " + bytesWritten + " bytes of data"); + } + catch (e) + { + if (wouldBlock(e)) + { + NS_ASSERT(pendingData.length > 0, + "stream-blocking exception with no data to write?"); + NS_ASSERT(pendingData[0].length > 0, + "stream-blocking exception with empty quantum?"); + this._waitToWriteData(); + return; + } + + if (streamClosed(e)) + dumpn("!!! output stream prematurely closed, signaling error..."); + else + dumpn("!!! unknown error: " + e + ", quantum=" + quantum); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // The day is ours! Quantum written, now let's see if we have more data + // still to write. + try + { + if (pendingData.length > 0) + { + this._waitToWriteData(); + return; + } + } + catch (e) + { + dumpn("!!! unexpected error waiting to write pending data: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we have no more pending data to write -- but might we get more in + // the future? + if (this._source !== null) + { + /* +* If we might, then wait for the output stream to be closed. (We wait +* only for closure because we have no data to write -- and if we waited +* for a specific amount of data, we would get repeatedly notified for no +* reason if over time the output stream permitted more and more data to +* be written to it without blocking.) +*/ + this._waitForSinkClosure(); + } + else + { + /* +* On the other hand, if we can't have more data because the input +* stream's gone away, then it's time to notify of copy completion. +* Victory! +*/ + this._sink = null; + this._cancelOrDispatchCancelCallback(Cr.NS_OK); + } + }, + + + // NSIREQUEST + + /** Returns true if the cancel observer hasn't been notified yet. */ + isPending: function() + { + return !this._completed; + }, + + /** Not implemented, don't use! */ + suspend: notImplemented, + /** Not implemented, don't use! */ + resume: notImplemented, + + /** +* Cancels data reading from input, asynchronously writes out any pending +* data, and causes the observer to be notified with the given error code when +* all writing has finished. +* +* @param status : nsresult +* the status to pass to the observer when data copying has been canceled +*/ + cancel: function(status) + { + dumpn("*** cancel(" + status.toString(16) + ")"); + + if (this._canceled) + { + dumpn("*** suppressing a late cancel"); + return; + } + + this._canceled = true; + this.status = status; + + // We could be in the middle of absolutely anything at this point. Both + // input and output might still be around, we might have pending data to + // write, and in general we know nothing about the state of the world. We + // therefore must assume everything's in progress and take everything to its + // final steady state (or so far as it can go before we need to finish + // writing out remaining data). + + this._doneReadingSource(status); + }, + + + // PRIVATE IMPLEMENTATION + + /** +* Stop reading input if we haven't already done so, passing e as the status +* when closing the stream, and kick off a copy-completion notice if no more +* data remains to be written. +* +* @param e : nsresult +* the status to be used when closing the input stream +*/ + _doneReadingSource: function(e) + { + dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")"); + + this._finishSource(e); + if (this._pendingData.length === 0) + this._sink = null; + else + NS_ASSERT(this._sink !== null, "null output?"); + + // If we've written out all data read up to this point, then it's time to + // signal completion. + if (this._sink === null) + { + NS_ASSERT(this._pendingData.length === 0, "pending data still?"); + this._cancelOrDispatchCancelCallback(e); + } + }, + + /** +* Stop writing output if we haven't already done so, discard any data that +* remained to be sent, close off input if it wasn't already closed, and kick +* off a copy-completion notice. +* +* @param e : nsresult +* the status to be used when closing input if it wasn't already closed +*/ + _doneWritingToSink: function(e) + { + dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")"); + + this._pendingData.length = 0; + this._sink = null; + this._doneReadingSource(e); + }, + + /** +* Completes processing of this copy: either by canceling the copy if it +* hasn't already been canceled using the provided status, or by dispatching +* the cancel callback event (with the originally provided status, of course) +* if it already has been canceled. +* +* @param status : nsresult +* the status code to use to cancel this, if this hasn't already been +* canceled +*/ + _cancelOrDispatchCancelCallback: function(status) + { + dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")"); + + NS_ASSERT(this._source === null, "should have finished input"); + NS_ASSERT(this._sink === null, "should have finished output"); + NS_ASSERT(this._pendingData.length === 0, "should have no pending data"); + + if (!this._canceled) + { + this.cancel(status); + return; + } + + var self = this; + var event = + { + run: function() + { + dumpn("*** onStopRequest async callback"); + + self._completed = true; + try + { + self._observer.onStopRequest(self, self._context, self.status); + } + catch (e) + { + NS_ASSERT(false, + "how are we throwing an exception here? we control " + + "all the callers! " + e); + } + } + }; + + gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** +* Kicks off another wait for more data to be available from the input stream. +*/ + _waitToReadData: function() + { + dumpn("*** _waitToReadData"); + this._source.asyncWait(this, 0, Response.SEGMENT_SIZE, + gThreadManager.mainThread); + }, + + /** +* Kicks off another wait until data can be written to the output stream. +*/ + _waitToWriteData: function() + { + dumpn("*** _waitToWriteData"); + + var pendingData = this._pendingData; + NS_ASSERT(pendingData.length > 0, "no pending data to write?"); + NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?"); + + this._sink.asyncWait(this, 0, pendingData[0].length, + gThreadManager.mainThread); + }, + + /** +* Kicks off a wait for the sink to which data is being copied to be closed. +* We wait for stream closure when we don't have any data to be copied, rather +* than waiting to write a specific amount of data. We can't wait to write +* data because the sink might be infinitely writable, and if no data appears +* in the source for a long time we might have to spin quite a bit waiting to +* write, waiting to write again, &c. Waiting on stream closure instead means +* we'll get just one notification if the sink dies. Note that when data +* starts arriving from the sink we'll resume waiting for data to be written, +* dropping this closure-only callback entirely. +*/ + _waitForSinkClosure: function() + { + dumpn("*** _waitForSinkClosure"); + + this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0, + gThreadManager.mainThread); + }, + + /** +* Closes input with the given status, if it hasn't already been closed; +* otherwise a no-op. +* +* @param status : nsresult +* status code use to close the source stream if necessary +*/ + _finishSource: function(status) + { + dumpn("*** _finishSource(" + status.toString(16) + ")"); + + if (this._source !== null) + { + this._source.closeWithStatus(status); + this._source = null; + } + } +}; + + +/** +* A container for utility functions used with HTTP headers. +*/ +const headerUtils = +{ + /** +* Normalizes fieldName (by converting it to lowercase) and ensures it is a +* valid header field name (although not necessarily one specified in RFC +* 2616). +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not match the field-name production in RFC 2616 +* @returns string +* fieldName converted to lowercase if it is a valid header, for characters +* where case conversion is possible +*/ + normalizeFieldName: function(fieldName) + { + if (fieldName == "") + throw Cr.NS_ERROR_INVALID_ARG; + + for (var i = 0, sz = fieldName.length; i < sz; i++) + { + if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)]) + { + dumpn(fieldName + " is not a valid header field name!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + } + + return fieldName.toLowerCase(); + }, + + /** +* Ensures that fieldValue is a valid header field value (although not +* necessarily as specified in RFC 2616 if the corresponding field name is +* part of the HTTP protocol), normalizes the value if it is, and +* returns the normalized value. +* +* @param fieldValue : string +* a value to be normalized as an HTTP header field value +* @throws NS_ERROR_INVALID_ARG +* if fieldValue does not match the field-value production in RFC 2616 +* @returns string +* fieldValue as a normalized HTTP header field value +*/ + normalizeFieldValue: function(fieldValue) + { + // field-value = *( field-content | LWS ) + // field-content = <the OCTETs making up the field-value + // and consisting of either *TEXT or combinations + // of token, separators, and quoted-string> + // TEXT = <any OCTET except CTLs, + // but including LWS> + // LWS = [CRLF] 1*( SP | HT ) + // + // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + // qdtext = <any TEXT except <">> + // quoted-pair = "\" CHAR + // CHAR = <any US-ASCII character (octets 0 - 127)> + + // Any LWS that occurs between field-content MAY be replaced with a single + // SP before interpreting the field value or forwarding the message + // downstream (section 4.2); we replace 1*LWS with a single SP + var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " "); + + // remove leading/trailing LWS (which has been converted to SP) + val = val.replace(/^ +/, "").replace(/ +$/, ""); + + // that should have taken care of all CTLs, so val should contain no CTLs + for (var i = 0, len = val.length; i < len; i++) + if (isCTL(val.charCodeAt(i))) + throw Cr.NS_ERROR_INVALID_ARG; + + // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly + // normalize, however, so this can be construed as a tightening of the + // spec and not entirely as a bug + return val; + } +}; + + + +/** +* Converts the given string into a string which is safe for use in an HTML +* context. +* +* @param str : string +* the string to make HTML-safe +* @returns string +* an HTML-safe version of str +*/ +function htmlEscape(str) +{ + // this is naive, but it'll work + var s = ""; + for (var i = 0; i < str.length; i++) + s += "&#" + str.charCodeAt(i) + ";"; + return s; +} + + +/** +* Constructs an object representing an HTTP version (see section 3.1). +* +* @param versionString +* a string of the form "#.#", where # is an non-negative decimal integer with +* or without leading zeros +* @throws +* if versionString does not specify a valid HTTP version number +*/ +function nsHttpVersion(versionString) +{ + var matches = /^(\d+)\.(\d+)$/.exec(versionString); + if (!matches) + throw "Not a valid HTTP version!"; + + /** The major version number of this, as a number. */ + this.major = parseInt(matches[1], 10); + + /** The minor version number of this, as a number. */ + this.minor = parseInt(matches[2], 10); + + if (isNaN(this.major) || isNaN(this.minor) || + this.major < 0 || this.minor < 0) + throw "Not a valid HTTP version!"; +} +nsHttpVersion.prototype = +{ + /** +* Returns the standard string representation of the HTTP version represented +* by this (e.g., "1.1"). +*/ + toString: function () + { + return this.major + "." + this.minor; + }, + + /** +* Returns true if this represents the same HTTP version as otherVersion, +* false otherwise. +* +* @param otherVersion : nsHttpVersion +* the version to compare against this +*/ + equals: function (otherVersion) + { + return this.major == otherVersion.major && + this.minor == otherVersion.minor; + }, + + /** True if this >= otherVersion, false otherwise. */ + atLeast: function(otherVersion) + { + return this.major > otherVersion.major || + (this.major == otherVersion.major && + this.minor >= otherVersion.minor); + } +}; + +nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0"); +nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1"); + + +/** +* An object which stores HTTP headers for a request or response. +* +* Note that since headers are case-insensitive, this object converts headers to +* lowercase before storing them. This allows the getHeader and hasHeader +* methods to work correctly for any case of a header, but it means that the +* values returned by .enumerator may not be equal case-sensitively to the +* values passed to setHeader when adding headers to this. +*/ +function nsHttpHeaders() +{ + /** +* A hash of headers, with header field names as the keys and header field +* values as the values. Header field names are case-insensitive, but upon +* insertion here they are converted to lowercase. Header field values are +* normalized upon insertion to contain no leading or trailing whitespace. +* +* Note also that per RFC 2616, section 4.2, two headers with the same name in +* a message may be treated as one header with the same field name and a field +* value consisting of the separate field values joined together with a "," in +* their original order. This hash stores multiple headers with the same name +* in this manner. +*/ + this._headers = {}; +} +nsHttpHeaders.prototype = +{ + /** +* Sets the header represented by name and value in this. +* +* @param name : string +* the header name +* @param value : string +* the header value +* @throws NS_ERROR_INVALID_ARG +* if name or value is not a valid header component +*/ + setHeader: function(fieldName, fieldValue, merge) + { + var name = headerUtils.normalizeFieldName(fieldName); + var value = headerUtils.normalizeFieldValue(fieldValue); + + // The following three headers are stored as arrays because their real-world + // syntax prevents joining individual headers into a single header using + // ",". See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77> + if (merge && name in this._headers) + { + if (name === "www-authenticate" || + name === "proxy-authenticate" || + name === "set-cookie") + { + this._headers[name].push(value); + } + else + { + this._headers[name][0] += "," + value; + NS_ASSERT(this._headers[name].length === 1, + "how'd a non-special header have multiple values?") + } + } + else + { + this._headers[name] = [value]; + } + }, + + /** +* Returns the value for the header specified by this. +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @throws NS_ERROR_NOT_AVAILABLE +* if the given header does not exist in this +* @returns string +* the field value for the given header, possibly with non-semantic changes +* (i.e., leading/trailing whitespace stripped, whitespace runs replaced +* with spaces, etc.) at the option of the implementation; multiple +* instances of the header will be combined with a comma, except for +* the three headers noted in the description of getHeaderValues +*/ + getHeader: function(fieldName) + { + return this.getHeaderValues(fieldName).join("\n"); + }, + + /** +* Returns the value for the header specified by fieldName as an array. +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @throws NS_ERROR_NOT_AVAILABLE +* if the given header does not exist in this +* @returns [string] +* an array of all the header values in this for the given +* header name. Header values will generally be collapsed +* into a single header by joining all header values together +* with commas, but certain headers (Proxy-Authenticate, +* WWW-Authenticate, and Set-Cookie) violate the HTTP spec +* and cannot be collapsed in this manner. For these headers +* only, the returned array may contain multiple elements if +* that header has been added more than once. +*/ + getHeaderValues: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + + if (name in this._headers) + return this._headers[name]; + else + throw Cr.NS_ERROR_NOT_AVAILABLE; + }, + + /** +* Returns true if a header with the given field name exists in this, false +* otherwise. +* +* @param fieldName : string +* the field name whose existence is to be determined in this +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @returns boolean +* true if the header's present, false otherwise +*/ + hasHeader: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + return (name in this._headers); + }, + + /** +* Returns a new enumerator over the field names of the headers in this, as +* nsISupportsStrings. The names returned will be in lowercase, regardless of +* how they were input using setHeader (header names are case-insensitive per +* RFC 2616). +*/ + get enumerator() + { + var headers = []; + for (var i in this._headers) + { + var supports = new SupportsString(); + supports.data = i; + headers.push(supports); + } + + return new nsSimpleEnumerator(headers); + } +}; + + +/** +* Constructs an nsISimpleEnumerator for the given array of items. +* +* @param items : Array +* the items, which must all implement nsISupports +*/ +function nsSimpleEnumerator(items) +{ + this._items = items; + this._nextIndex = 0; +} +nsSimpleEnumerator.prototype = +{ + hasMoreElements: function() + { + return this._nextIndex < this._items.length; + }, + getNext: function() + { + if (!this.hasMoreElements()) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + return this._items[this._nextIndex++]; + }, + QueryInterface: function(aIID) + { + if (Ci.nsISimpleEnumerator.equals(aIID) || + Ci.nsISupports.equals(aIID)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + + +/** +* A representation of the data in an HTTP request. +* +* @param port : uint +* the port on which the server receiving this request runs +*/ +function Request(port) +{ + /** Method of this request, e.g. GET or POST. */ + this._method = ""; + + /** Path of the requested resource; empty paths are converted to '/'. */ + this._path = ""; + + /** Query string, if any, associated with this request (not including '?'). */ + this._queryString = ""; + + /** Scheme of requested resource, usually http, always lowercase. */ + this._scheme = "http"; + + /** Hostname on which the requested resource resides. */ + this._host = undefined; + + /** Port number over which the request was received. */ + this._port = port; + + var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null); + + /** Stream from which data in this request's body may be read. */ + this._bodyInputStream = bodyPipe.inputStream; + + /** Stream to which data in this request's body is written. */ + this._bodyOutputStream = bodyPipe.outputStream; + + /** +* The headers in this request. +*/ + this._headers = new nsHttpHeaders(); + + /** +* For the addition of ad-hoc properties and new functionality without having +* to change nsIHttpRequest every time; currently lazily created, as its only +* use is in directory listings. +*/ + this._bag = null; +} +Request.prototype = +{ + // SERVER METADATA + + // + // see nsIHttpRequest.scheme + // + get scheme() + { + return this._scheme; + }, + + // + // see nsIHttpRequest.host + // + get host() + { + return this._host; + }, + + // + // see nsIHttpRequest.port + // + get port() + { + return this._port; + }, + + // REQUEST LINE + + // + // see nsIHttpRequest.method + // + get method() + { + return this._method; + }, + + // + // see nsIHttpRequest.httpVersion + // + get httpVersion() + { + return this._httpVersion.toString(); + }, + + // + // see nsIHttpRequest.path + // + get path() + { + return this._path; + }, + + // + // see nsIHttpRequest.queryString + // + get queryString() + { + return this._queryString; + }, + + // HEADERS + + // + // see nsIHttpRequest.getHeader + // + getHeader: function(name) + { + return this._headers.getHeader(name); + }, + + // + // see nsIHttpRequest.hasHeader + // + hasHeader: function(name) + { + return this._headers.hasHeader(name); + }, + + // + // see nsIHttpRequest.headers + // + get headers() + { + return this._headers.enumerator; + }, + + // + // see nsIPropertyBag.enumerator + // + get enumerator() + { + this._ensurePropertyBag(); + return this._bag.enumerator; + }, + + // + // see nsIHttpRequest.headers + // + get bodyInputStream() + { + return this._bodyInputStream; + }, + + // + // see nsIPropertyBag.getProperty + // + getProperty: function(name) + { + this._ensurePropertyBag(); + return this._bag.getProperty(name); + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE IMPLEMENTATION + + /** Ensures a property bag has been created for ad-hoc behaviors. */ + _ensurePropertyBag: function() + { + if (!this._bag) + this._bag = new WritablePropertyBag(); + } +}; + + +// XPCOM trappings +if ("XPCOMUtils" in this && // Firefox 3.6 doesn't load XPCOMUtils in this scope for some reason... + "generateNSGetFactory" in XPCOMUtils) { + var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]); +} + + + +/** +* Creates a new HTTP server listening for loopback traffic on the given port, +* starts it, and runs the server until the server processes a shutdown request, +* spinning an event loop so that events posted by the server's socket are +* processed. +* +* This method is primarily intended for use in running this script from within +* xpcshell and running a functional HTTP server without having to deal with +* non-essential details. +* +* Note that running multiple servers using variants of this method probably +* doesn't work, simply due to how the internal event loop is spun and stopped. +* +* @note +* This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code); +* you should use this server as a component in Mozilla 1.8. +* @param port +* the port on which the server will run, or -1 if there exists no preference +* for a specific port; note that attempting to use some values for this +* parameter (particularly those below 1024) may cause this method to throw or +* may result in the server being prematurely shut down +* @param basePath +* a local directory from which requests will be served (i.e., if this is +* "/home/jwalden/" then a request to /index.html will load +* /home/jwalden/index.html); if this is omitted, only the default URLs in +* this server implementation will be functional +*/ +function server(port, basePath) +{ + if (basePath) + { + var lp = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + lp.initWithPath(basePath); + } + + // if you're running this, you probably want to see debugging info + DEBUG = true; + + var srv = new nsHttpServer(); + if (lp) + srv.registerDirectory("/", lp); + srv.registerContentType("sjs", SJS_TYPE); + srv.identity.setPrimary("http", "localhost", port); + srv.start(port); + + var thread = gThreadManager.currentThread; + while (!srv.isStopped()) + thread.processNextEvent(true); + + // get rid of any pending requests + while (thread.hasPendingEvents()) + thread.processNextEvent(true); + + DEBUG = false; +} + +function startServerAsync(port, basePath) +{ + if (basePath) + { + var lp = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + lp.initWithPath(basePath); + } + + var srv = new nsHttpServer(); + if (lp) + srv.registerDirectory("/", lp); + srv.registerContentType("sjs", "sjs"); + srv.identity.setPrimary("http", "localhost", port); + srv.start(port); + return srv; +} + +exports.nsHttpServer = nsHttpServer; +exports.ScriptableInputStream = ScriptableInputStream; +exports.server = server; +exports.startServerAsync = startServerAsync; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/keyboard/hotkeys.js b/tools/addon-sdk-1.4/packages/api-utils/lib/keyboard/hotkeys.js new file mode 100644 index 0000000..6851671 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/keyboard/hotkeys.js @@ -0,0 +1,141 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * Paul Vet <original.roju@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { observer: keyboardObserver } = require("./observer"); +const { getKeyForCode, normalize, isFunctionKey, + MODIFIERS } = require("./utils"); + +/** + * Register a global `hotkey` that executes `listener` when the key combination + * in `hotkey` is pressed. If more then one `listener` is registered on the same + * key combination only last one will be executed. + * + * @param {string} hotkey + * Key combination in the format of 'modifier key'. + * + * Examples: + * + * "accel s" + * "meta shift i" + * "control alt d" + * + * Modifier keynames: + * + * - **shift**: The Shift key. + * - **alt**: The Alt key. On the Macintosh, this is the Option key. On + * Macintosh this can only be used in conjunction with another modifier, + * since `Alt+Letter` combinations are reserved for entering special + * characters in text. + * - **meta**: The Meta key. On the Macintosh, this is the Command key. + * - **control**: The Control key. + * - **accel**: The key used for keyboard shortcuts on the user's platform, + * which is Control on Windows and Linux, and Command on Mac. Usually, this + * would be the value you would use. + * + * @param {function} listener + * Function to execute when the `hotkey` is executed. + */ +exports.register = function register(hotkey, listener) { + hotkey = normalize(hotkey); + hotkeys[hotkey] = listener; +}; + +/** + * Unregister a global `hotkey`. If passed `listener` is not the one registered + * for the given `hotkey`, the call to this function will be ignored. + * + * @param {string} hotkey + * Key combination in the format of 'modifier key'. + * @param {function} listener + * Function that will be invoked when the `hotkey` is pressed. + */ +exports.unregister = function unregister(hotkey, listener) { + hotkey = normalize(hotkey); + if (hotkeys[hotkey] === listener) + delete hotkeys[hotkey]; +}; + +/** + * Map of hotkeys and associated functions. + */ +const hotkeys = exports.hotkeys = {}; + +keyboardObserver.on("keydown", function onKeypress(event, window) { + let key, modifiers = []; + let isChar = "isChar" in event && event.isChar; + let which = "which" in event ? event.which : null; + let keyCode = "keyCode" in event ? event.keyCode : null; + + if ("shiftKey" in event && event.shiftKey) + modifiers.push("shift"); + if ("altKey" in event && event.altKey) + modifiers.push("alt"); + if ("ctrlKey" in event && event.ctrlKey) + modifiers.push("control"); + if ("metaKey" in event && event.metaKey) + modifiers.push("meta"); + + // If it's not a printable character then we fall back to a human readable + // equivalent of one of the following constants. + // http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/events/nsIDOMKeyEvent.idl + key = getKeyForCode(keyCode); + + // If only non-function (f1 - f24) key or only modifiers are pressed we don't + // have a valid combination so we return immediately (Also, sometimes + // `keyCode` may be one for the modifier which means we do not have a + // modifier). + if (!key || (!isFunctionKey(key) && !modifiers.length) || key in MODIFIERS) + return; + + let combination = normalize({ key: key, modifiers: modifiers }); + let hotkey = hotkeys[combination]; + + if (hotkey) { + try { + hotkey(); + } catch (exception) { + console.exception(exception); + } finally { + // Work around bug 582052 by preventing the (nonexistent) default action. + event.preventDefault(); + } + } +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/keyboard/observer.js b/tools/addon-sdk-1.4/packages/api-utils/lib/keyboard/observer.js new file mode 100644 index 0000000..6968551 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/keyboard/observer.js @@ -0,0 +1,86 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Trait } = require("../light-traits"); +const { EventEmitterTrait: EventEmitter } = require("../events"); +const { DOMEventAssembler } = require("../events/assembler"); +const { browserWindowIterator, isBrowser } = require('../window-utils'); +const { observer: windowObserver } = require("../windows/observer"); + +// Event emitter objects used to register listeners and emit events on them +// when they occur. +const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ + /** + * Method is implemented by `EventEmitter` and is used just for emitting + * events on registered listeners. + */ + _emit: Trait.required, + /** + * Events that are supported and emitted by the module. + */ + supportedEventsTypes: [ "keydown", "keyup", "keypress" ], + /** + * Function handles all the supported events on all the windows that are + * observed. Method is used to proxy events to the listeners registered on + * this event emitter. + * @param {Event} event + * Keyboard event being emitted. + */ + handleEvent: function handleEvent(event) { + this._emit(event.type, event, event.target.ownerDocument.defaultView); + } +}); + +// Adding each opened window to a list of observed windows. +windowObserver.on("open", function onOpen(window) { + if (isBrowser(window)) + observer.observe(window); +}); +// Removing each closed window form the list of observed windows. +windowObserver.on("close", function onClose(window) { + if (isBrowser(window)) + observer.ignore(window); +}); + +// Making observer aware of already opened windows. +for each (let window in browserWindowIterator()) + observer.observe(window); + +exports.observer = observer; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/keyboard/utils.js b/tools/addon-sdk-1.4/packages/api-utils/lib/keyboard/utils.js new file mode 100644 index 0000000..0c9a7fb --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/keyboard/utils.js @@ -0,0 +1,220 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * Henri Wiechers <hwiechers@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Cc, Ci } = require("chrome"); +const runtime = require("../runtime"); +const { isString } = require("../type"); +const array = require("../array"); + + +const SWP = "{{SEPARATOR}}"; +const SEPARATOR = "-" +const INVALID_COMBINATION = "Hotkey key combination must contain one or more " + + "modifiers and only one key"; + +// Map of modifier key mappings. +const MODIFIERS = exports.MODIFIERS = { + 'accel': runtime.OS === "Darwin" ? 'meta' : 'control', + 'meta': 'meta', + 'control': 'control', + 'ctrl': 'control', + 'option': 'alt', + 'command': 'meta', + 'alt': 'alt', + 'shift': 'shift' +}; + +// Hash of key:code pairs for all the chars supported by `nsIDOMKeyEvent`. +// This is just a copy of the `nsIDOMKeyEvent` hash with normalized names. +// @See: http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/events/nsIDOMKeyEvent.idl +const CODES = exports.CODES = new function Codes() { + let nsIDOMKeyEvent = Ci.nsIDOMKeyEvent; + // Names that will be substituted with a shorter analogs. + let aliases = { + 'subtract': '-', + 'add': '+', + 'equals': '=', + 'slash': '/', + 'backslash': '\\', + 'openbracket': '[', + 'closebracket': ']', + 'quote': '\'', + 'backquote': '`', + 'period': '.', + 'semicolon': ';', + 'comma': ',' + }; + + // Normalizing keys and copying values to `this` object. + Object.keys(nsIDOMKeyEvent).filter(function(key) { + // Filter out only key codes. + return key.indexOf('DOM_VK') === 0; + }).map(function(key) { + // Map to key:values + return [ key, nsIDOMKeyEvent[key] ]; + }).map(function([key, value]) { + return [ key.replace('DOM_VK_', '').replace('_', '').toLowerCase(), value ]; + }).forEach(function ([ key, value ]) { + this[aliases[key] || key] = value; + }, this); +}; + +// Inverted `CODES` hash of `code:key`. +const KEYS = exports.KEYS = new function Keys() { + Object.keys(CODES).forEach(function(key) { + this[CODES[key]] = key; + }, this) +} + +exports.getKeyForCode = function getKeyForCode(code) { + return (code in KEYS) && KEYS[code]; +}; +exports.getCodeForKey = function getCodeForKey(key) { + return (key in CODES) && CODES[key]; +}; + +/** + * Utility function that takes string or JSON that defines a `hotkey` and + * returns normalized string version of it. + * @param {JSON|String} hotkey + * @param {String} [separator=" "] + * Optional string that represents separator used to concatenate keys in the + * given `hotkey`. + * @returns {String} + * @examples + * + * require("keyboard/hotkeys").normalize("b Shift accel"); + * // 'control shift b' -> on windows & linux + * // 'meta shift b' -> on mac + * require("keyboard/hotkeys").normalize("alt-d-shift", "-"); + * // 'alt shift d' + */ +var normalize = exports.normalize = function normalize(hotkey, separator) { + if (!isString(hotkey)) + hotkey = toString(hotkey, separator); + return toString(toJSON(hotkey, separator), separator); +}; + +/* + * Utility function that splits a string of characters that defines a `hotkey` + * into modifier keys and the defining key. + * @param {String} hotkey + * @param {String} [separator=" "] + * Optional string that represents separator used to concatenate keys in the + * given `hotkey`. + * @returns {JSON} + * @examples + * + * require("keyboard/hotkeys").toJSON("accel shift b"); + * // { key: 'b', modifiers: [ 'control', 'shift' ] } -> on windows & linux + * // { key: 'b', modifiers: [ 'meta', 'shift' ] } -> on mac + * + * require("keyboard/hotkeys").normalize("alt-d-shift", "-"); + * // { key: 'd', modifiers: [ 'alt', 'shift' ] } + */ +var toJSON = exports.toJSON = function toJSON(hotkey, separator) { + separator = separator || SEPARATOR; + // Since default separator is `-`, combination may take form of `alt--`. To + // avoid misbehavior we replace `--` with `-{{SEPARATOR}}` where + // `{{SEPARATOR}}` can be swapped later. + hotkey = hotkey.toLowerCase().replace(separator + separator, separator + SWP); + + let value = {}; + let modifiers = []; + let keys = hotkey.split(separator); + keys.forEach(function(name) { + // If name is `SEPARATOR` than we swap it back. + if (name === SWP) + name = separator; + if (name in MODIFIERS) { + array.add(modifiers, MODIFIERS[name]); + } else { + if (!value.key) + value.key = name; + else + throw new TypeError(INVALID_COMBINATION); + } + }); + + if (!value.key) + throw new TypeError(INVALID_COMBINATION); + + value.modifiers = modifiers.sort(); + return value; +}; + +/** + * Utility function that takes object that defines a `hotkey` and returns + * string representation of it. + * + * _Please note that this function does not validates data neither it normalizes + * it, if you are unsure that data is well formed use `normalize` function + * instead. + * + * @param {JSON} hotkey + * @param {String} [separator=" "] + * Optional string that represents separator used to concatenate keys in the + * given `hotkey`. + * @returns {String} + * @examples + * + * require("keyboard/hotkeys").toString({ + * key: 'b', + * modifiers: [ 'control', 'shift' ] + * }, '+'); + * // 'control+shift+b + * + */ +var toString = exports.toString = function toString(hotkey, separator) { + let keys = hotkey.modifiers.slice(); + keys.push(hotkey.key); + return keys.join(separator || SEPARATOR); +}; + +/** + * Utility function takes `key` name and returns `true` if it's function key + * (F1, ..., F24) and `false` if it's not. + */ +var isFunctionKey = exports.isFunctionKey = function isFunctionKey(key) { + var $ + return key[0].toLowerCase() === 'f' && + ($ = parseInt(key.substr(1)), 0 < $ && $ < 25); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/light-traits.js b/tools/addon-sdk-1.4/packages/api-utils/lib/light-traits.js new file mode 100644 index 0000000..1afaad5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/light-traits.js @@ -0,0 +1,626 @@ +/* vim:ts=2:sts=2:sw=2: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <rfobic@gmail.com> (Original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +// `var` is being used in the module in order to make it reusable in +// environments in which `let` is not yet supported. + +// Shortcut to `Object.prototype.hasOwnProperty.call`. +// owns(object, name) would be the same as +// Object.prototype.hasOwnProperty.call(object, name); +var owns = Function.prototype.call.bind(Object.prototype.hasOwnProperty); + +/** + * Whether or not given property descriptors are equivalent. They are + * equivalent either if both are marked as 'conflict' or 'required' property + * or if all the properties of descriptors are equal. + * @param {Object} actual + * @param {Object} expected + */ +function equivalentDescriptors(actual, expected) { + return (actual.conflict && expected.conflict) || + (actual.required && expected.required) || + equalDescriptors(actual, expected); +} +/** + * Whether or not given property descriptors define equal properties. + */ +function equalDescriptors(actual, expected) { + return actual.get === expected.get && + actual.set === expected.set && + actual.value === expected.value && + !!actual.enumerable === !!expected.enumerable && + !!actual.configurable === !!expected.configurable && + !!actual.writable === !!expected.writable; +} + +// Utilities that throwing exceptions for a properties that are marked +// as "required" or "conflict" properties. +function throwConflictPropertyError(name) { + throw new Error("Remaining conflicting property: `" + name + "`"); +} +function throwRequiredPropertyError(name) { + throw new Error("Missing required property: `" + name + "`"); +} + +/** + * Generates custom **required** property descriptor. Descriptor contains + * non-standard property `required` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function RequiredPropertyDescriptor(name) { + // Creating function by binding first argument to a property `name` on the + // `throwConflictPropertyError` function. Created function is used as a + // getter & setter of the created property descriptor. This way we ensure + // that we throw exception late (on property access) if object with + // `required` property was instantiated using built-in `Object.create`. + var accessor = throwRequiredPropertyError.bind(null, name); + return { get: accessor, set: accessor, required: true }; +} + +/** + * Generates custom **conflicting** property descriptor. Descriptor contains + * non-standard property `conflict` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function ConflictPropertyDescriptor(name) { + // For details see `RequiredPropertyDescriptor` since idea is same. + var accessor = throwConflictPropertyError.bind(null, name); + return { get: accessor, set: accessor, conflict: true }; +} + +/** + * Tests if property is marked as `required` property. + */ +function isRequiredProperty(object, name) { + return !!object[name].required; +} + +/** + * Tests if property is marked as `conflict` property. + */ +function isConflictProperty(object, name) { + return !!object[name].conflict; +} + +/** + * Function tests whether or not method of the `source` object with a given + * `name` is inherited from `Object.prototype`. + */ +function isBuiltInMethod(name, source) { + var target = Object.prototype[name]; + + // If methods are equal then we know it's `true`. + return target == source || + // If `source` object comes form a different sandbox `==` will evaluate + // to `false`, in that case we check if functions names and sources match. + (String(target) === String(source) && target.name === source.name); +} + +/** + * Function overrides `toString` and `constructor` methods of a given `target` + * object with a same-named methods of a given `source` if methods of `target` + * object are inherited / copied from `Object.prototype`. + * @see create + */ +function overrideBuiltInMethods(target, source) { + if (isBuiltInMethod("toString", target.toString)) { + Object.defineProperty(target, "toString", { + value: source.toString, + configurable: true, + enumerable: false + }); + } + + if (isBuiltInMethod("constructor", target.constructor)) { + Object.defineProperty(target, "constructor", { + value: source.constructor, + configurable: true, + enumerable: false + }); + } +} + +/** + * Composes new trait with the same own properties as the original trait, + * except that all property names appearing in the first argument are replaced + * by "required" property descriptors. + * @param {String[]} keys + * Array of strings property names. + * @param {Object} trait + * A trait some properties of which should be excluded. + * @returns {Object} + * @example + * var newTrait = exclude(["name", ...], trait) + */ +function exclude(names, trait) { + var map = {}; + + Object.keys(trait).forEach(function(name) { + + // If property is not excluded (the array of names does not contain it), + // or it is a "required" property, copy it to the property descriptor `map` + // that will be used for creation of resulting trait. + if (!~names.indexOf(name) || isRequiredProperty(trait, name)) + map[name] = { value: trait[name], enumerable: true }; + + // For all the `names` in the exclude name array we create required + // property descriptors and copy them to the `map`. + else + map[name] = { value: RequiredPropertyDescriptor(name), enumerable: true }; + }); + + return Object.create(Trait.prototype, map); +} + +/** + * Composes new instance of `Trait` with a properties of a given `trait`, + * except that all properties whose name is an own property of `renames` will + * be renamed to `renames[name]` and a `"required"` property for name will be + * added instead. + * + * For each renamed property, a required property is generated. If + * the `renames` map two properties to the same name, a conflict is generated. + * If the `renames` map a property to an existing unrenamed property, a + * conflict is generated. + * + * @param {Object} renames + * An object whose own properties serve as a mapping from old names to new + * names. + * @param {Object} trait + * A new trait with renamed properties. + * @returns {Object} + * @example + * + * // Return trait with `bar` property equal to `trait.foo` and with + * // `foo` and `baz` "required" properties. + * var renamedTrait = rename({ foo: "bar", baz: null }), trait); + * + * // t1 and t2 are equivalent traits + * var t1 = rename({a: "b"}, t); + * var t2 = compose(exclude(["a"], t), { a: { required: true }, b: t[a] }); + */ +function rename(renames, trait) { + var map = {}; + + // Loop over all the properties of the given `trait` and copy them to a + // property descriptor `map` that will be used for the creation of the + // resulting trait. Also, rename properties in the `map` as specified by + // `renames`. + Object.keys(trait).forEach(function(name) { + var alias; + + // If the property is in the `renames` map, and it isn't a "required" + // property (which should never need to be aliased because "required" + // properties never conflict), then we must try to rename it. + if (owns(renames, name) && !isRequiredProperty(trait, name)) { + alias = renames[name]; + + // If the `map` already has the `alias`, and it isn't a "required" + // property, that means the `alias` conflicts with an existing name for a + // provided trait (that can happen if >=2 properties are aliased to the + // same name). In this case we mark it as a conflicting property. + // Otherwise, everything is fine, and we copy property with an `alias` + // name. + if (owns(map, alias) && !map[alias].value.required) { + map[alias] = { + value: ConflictPropertyDescriptor(alias), + enumerable: true + }; + } + else { + map[alias] = { + value: trait[name], + enumerable: true + }; + } + + // Regardless of whether or not the rename was successful, we check to + // see if the original `name` exists in the map (such a property + // could exist if previous another property was aliased to this `name`). + // If it isn't, we mark it as "required", to make sure the caller + // provides another value for the old name, which methods of the trait + // might continue to reference. + if (!owns(map, name)) { + map[name] = { + value: RequiredPropertyDescriptor(name), + enumerable: true + }; + } + } + + // Otherwise, either the property isn't in the `renames` map (thus the + // caller is not trying to rename it) or it is a "required" property. + // Either way, we don't have to alias the property, we just have to copy it + // to the map. + else { + // The property isn't in the map yet, so we copy it over. + if (!owns(map, name)) { + map[name] = { value: trait[name], enumerable: true }; + } + + // The property is already in the map (that means another property was + // aliased with this `name`, which creates a conflict if the property is + // not marked as "required"), so we have to mark it as a "conflict" + // property. + else if (!isRequiredProperty(trait, name)) { + map[name] = { + value: ConflictPropertyDescriptor(name), + enumerable: true + }; + } + } + }); + return Object.create(Trait.prototype, map); +} + +/** + * Composes new resolved trait, with all the same properties as the original + * `trait`, except that all properties whose name is an own property of + * `resolutions` will be renamed to `resolutions[name]`. + * + * If `resolutions[name]` is `null`, the value is mapped to a property + * descriptor that is marked as a "required" property. + */ +function resolve(resolutions, trait) { + var renames = {}; + var exclusions = []; + + // Go through each mapping in `resolutions` object and distribute it either + // to `renames` or `exclusions`. + Object.keys(resolutions).forEach(function(name) { + + // If `resolutions[name]` is a truthy value then it's a mapping old -> new + // so we copy it to `renames` map. + if (resolutions[name]) + renames[name] = resolutions[name]; + + // Otherwise it's not a mapping but an exclusion instead in which case we + // add it to the `exclusions` array. + else + exclusions.push(name); + }); + + // First `exclude` **then** `rename` and order is important since + // `exclude` and `rename` are not associative. + return rename(renames, exclude(exclusions, trait)); +} + +/** + * Create a Trait (a custom property descriptor map) that represents the given + * `object`'s own properties. Property descriptor map is a "custom", because it + * inherits from `Trait.prototype` and it's property descriptors may contain + * two attributes that is not part of the ES5 specification: + * + * - "required" (this property must be provided by another trait + * before an instance of this trait can be created) + * - "conflict" (when the trait is composed with another trait, + * a unique value for this property is provided by two or more traits) + * + * Data properties bound to the `Trait.required` singleton exported by + * this module will be marked as "required" properties. + * + * @param {Object} object + * Map of properties to compose trait from. + * @returns {Trait} + * Trait / Property descriptor map containing all the own properties of the + * given argument. + */ +function trait(object) { + var map; + var trait = object; + + if (!(object instanceof Trait)) { + // If the passed `object` is not already an instance of `Trait`, we create + // a property descriptor `map` containing descriptors for the own properties + // of the given `object`. `map` is then used to create a `Trait` instance + // after all properties are mapped. Note that we can't create a trait and + // then just copy properties into it since that will fail for inherited + // read-only properties. + map = {}; + + // Each own property of the given `object` is mapped to a data property + // whose value is a property descriptor. + Object.keys(object).forEach(function (name) { + + // If property of an `object` is equal to a `Trait.required`, it means + // that it was marked as "required" property, in which case we map it + // to "required" property. + if (Trait.required == + Object.getOwnPropertyDescriptor(object, name).value) { + map[name] = { + value: RequiredPropertyDescriptor(name), + enumerable: true + }; + } + // Otherwise property is mapped to it's property descriptor. + else { + map[name] = { + value: Object.getOwnPropertyDescriptor(object, name), + enumerable: true + }; + } + }); + + trait = Object.create(Trait.prototype, map); + } + return trait; +} + +/** + * Compose a property descriptor map that inherits from `Trait.prototype` and + * contains property descriptors for all the own properties of the passed + * traits. + * + * If two or more traits have own properties with the same name, the returned + * trait will contain a "conflict" property for that name. Composition is a + * commutative and associative operation, and the order of its arguments is + * irrelevant. + */ +function compose(trait1, trait2/*, ...*/) { + // Create a new property descriptor `map` to which all the own properties + // of the passed traits are copied. This map will be used to create a `Trait` + // instance that will be the result of this composition. + var map = {}; + + // Properties of each passed trait are copied to the composition. + Array.prototype.forEach.call(arguments, function(trait) { + // Copying each property of the given trait. + Object.keys(trait).forEach(function(name) { + + // If `map` already owns a property with the `name` and it is not + // marked "required". + if (owns(map, name) && !map[name].value.required) { + + // If the source trait's property with the `name` is marked as + // "required", we do nothing, as the requirement was already resolved + // by a property in the `map` (because it already contains a + // non-required property with that `name`). But if properties are just + // different, we have a name clash and we substitute it with a property + // that is marked "conflict". + if (!isRequiredProperty(trait, name) && + !equivalentDescriptors(map[name].value, trait[name]) + ) { + map[name] = { + value: ConflictPropertyDescriptor(name), + enumerable: true + }; + } + } + + // Otherwise, the `map` does not have an own property with the `name`, or + // it is marked "required". Either way, the trait's property is copied to + // the map (if the property of the `map` is marked "required", it is going + // to be resolved by the property that is being copied). + else { + map[name] = { value: trait[name], enumerable: true }; + } + }); + }); + + return Object.create(Trait.prototype, map); +} + +/** + * `defineProperties` is like `Object.defineProperties`, except that it + * ensures that: + * - An exception is thrown if any property in a given `properties` map + * is marked as "required" property and same named property is not + * found in a given `prototype`. + * - An exception is thrown if any property in a given `properties` map + * is marked as "conflict" property. + * @param {Object} object + * Object to define properties on. + * @param {Object} properties + * Properties descriptor map. + * @returns {Object} + * `object` that was passed as a first argument. + */ +function defineProperties(object, properties) { + + // Create a map into which we will copy each verified property from the given + // `properties` description map. We use it to verify that none of the + // provided properties is marked as a "conflict" property and that all + // "required" properties are resolved by a property of an `object`, so we + // can throw an exception before mutating object if that isn't the case. + var verifiedProperties = {}; + + // Coping each property from a given `properties` descriptor map to a + // verified map of property descriptors. + Object.keys(properties).forEach(function(name) { + + // If property is marked as "required" property and we don't have a same + // named property in a given `object` we throw an exception. If `object` + // has same named property just skip this property since required property + // is was inherited and there for requirement was satisfied. + if (isRequiredProperty(properties, name)) { + if (!(name in object)) + throwRequiredPropertyError(name); + } + + // If property is marked as "conflict" property we throw an exception. + else if (isConflictProperty(properties, name)) { + throwConflictPropertyError(name); + } + + // If property is not marked neither as "required" nor "conflict" property + // we copy it to verified properties map. + else { + verifiedProperties[name] = properties[name]; + } + }); + + // If no exceptions were thrown yet, we know that our verified property + // descriptor map has no properties marked as "conflict" or "required", + // so we just delegate to the built-in `Object.defineProperties`. + return Object.defineProperties(object, verifiedProperties); +} + +/** + * `create` is like `Object.create`, except that it ensures that: + * - An exception is thrown if any property in a given `properties` map + * is marked as "required" property and same named property is not + * found in a given `prototype`. + * - An exception is thrown if any property in a given `properties` map + * is marked as "conflict" property. + * @param {Object} prototype + * prototype of the composed object + * @param {Object} properties + * Properties descriptor map. + * @returns {Object} + * An object that inherits form a given `prototype` and implements all the + * properties defined by a given `properties` descriptor map. + */ +function create(prototype, properties) { + + // Creating an instance of the given `prototype`. + var object = Object.create(prototype); + + // Overriding `toString`, `constructor` methods if they are just inherited + // from `Object.prototype` with a same named methods of the `Trait.prototype` + // that will have more relevant behavior. + overrideBuiltInMethods(object, Trait.prototype); + + // Trying to define given `properties` on the `object`. We use our custom + // `defineProperties` function instead of build-in `Object.defineProperties` + // that behaves exactly the same, except that it will throw if any + // property in the given `properties` descriptor is marked as "required" or + // "conflict" property. + return defineProperties(object, properties); +} + +/** + * Composes new trait. If two or more traits have own properties with the + * same name, the new trait will contain a "conflict" property for that name. + * "compose" is a commutative and associative operation, and the order of its + * arguments is not significant. + * + * **Note:** Use `Trait.compose` instead of calling this function with more + * than one argument. The multiple-argument functionality is strictly for + * backward compatibility. + * + * @params {Object} trait + * Takes traits as an arguments + * @returns {Object} + * New trait containing the combined own properties of all the traits. + * @example + * var newTrait = compose(trait_1, trait_2, ..., trait_N) + */ +function Trait(trait1, trait2) { + + // If the function was called with one argument, the argument should be + // an object whose properties are mapped to property descriptors on a new + // instance of Trait, so we delegate to the trait function. + // If the function was called with more than one argument, those arguments + // should be instances of Trait or plain property descriptor maps + // whose properties should be mixed into a new instance of Trait, + // so we delegate to the compose function. + + return trait2 === undefined ? trait(trait1) : compose.apply(null, arguments); +} + +Object.freeze(Object.defineProperties(Trait.prototype, { + toString: { + value: function toString() { + return "[object " + this.constructor.name + "]"; + } + }, + + /** + * `create` is like `Object.create`, except that it ensures that: + * - An exception is thrown if this trait defines a property that is + * marked as required property and same named property is not + * found in a given `prototype`. + * - An exception is thrown if this trait contains property that is + * marked as "conflict" property. + * @param {Object} + * prototype of the compared object + * @returns {Object} + * An object with all of the properties described by the trait. + */ + create: { + value: function createTrait(prototype) { + return create(undefined === prototype ? Object.prototype : prototype, + this); + }, + enumerable: true + }, + + /** + * Composes a new resolved trait, with all the same properties as the original + * trait, except that all properties whose name is an own property of + * `resolutions` will be renamed to the value of `resolutions[name]`. If + * `resolutions[name]` is `null`, the property is marked as "required". + * @param {Object} resolutions + * An object whose own properties serve as a mapping from old names to new + * names, or to `null` if the property should be excluded. + * @returns {Object} + * New trait with the same own properties as the original trait but renamed. + */ + resolve: { + value: function resolveTrait(resolutions) { + return resolve(resolutions, this); + }, + enumerable: true + } +})); + +/** + * @see compose + */ +Trait.compose = Object.freeze(compose); +Object.freeze(compose.prototype); + +/** + * Constant singleton, representing placeholder for required properties. + * @type {Object} + */ +Trait.required = Object.freeze(Object.create(Object.prototype, { + toString: { + value: Object.freeze(function toString() { + return "<Trait.required>"; + }) + } +})); +Object.freeze(Trait.required.toString.prototype); + +exports.Trait = Object.freeze(Trait); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/list.js b/tools/addon-sdk-1.4/packages/api-utils/lib/list.js new file mode 100644 index 0000000..8643a0a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/list.js @@ -0,0 +1,147 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const { Trait } = require('./traits'); + +/** + * @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/list + */ +const Iterable = Trait.compose({ + /** + * Hash map of key-values to iterate over. + * Note: That this property can be a getter if you need dynamic behavior. + * @type {Object} + */ + _keyValueMap: Trait.required, + /** + * Custom iterator providing `Iterable`s enumeration behavior. + * @param {Boolean} onKeys + */ + __iterator__: function __iterator__(onKeys, onKeyValue) { + let map = this._keyValueMap; + for (let key in map) + yield onKeyValue ? [key, map[key]] : onKeys ? key : map[key]; + } +}); +exports.Iterable = Iterable; + +/** + * An ordered collection (also known as a sequence) disallowing duplicate + * elements. List is composed out of `Iterable` there for it provides custom + * enumeration behavior that is similar to array (enumerates only on the + * elements of the list). List is a base trait and is meant to be a part of + * composition, since all of it's API is private except length property. + */ +const List = Trait.resolve({ toString: null }).compose({ + _keyValueMap: null, + /** + * List constructor can take any number of element to populate itself. + * @params {Object|String|Number} element + * @example + * List(1,2,3).length == 3 // true + */ + constructor: function List() { + this._keyValueMap = []; + for (let i = 0, ii = arguments.length; i < ii; i++) + this._add(arguments[i]); + }, + /** + * Number of elements in this list. + * @type {Number} + */ + get length() this._keyValueMap.length, + /** + * Returns a string representing this list. + * @returns {String} + */ + toString: function toString() 'List(' + this._keyValueMap + ')', + /** + * Returns `true` if this list contains the specified `element`. + * @param {Object|Number|String} element + * @returns {Boolean} + */ + _has: function _has(element) 0 <= this._keyValueMap.indexOf(element), + /** + * Appends the specified `element` to the end of this list, if it doesn't + * contains it. Ignores the call if `element` is already contained. + * @param {Object|Number|String} element + */ + _add: function _add(element) { + let list = this._keyValueMap, + index = list.indexOf(element); + if (0 > index) + list.push(this._public[list.length] = element); + }, + /** + * Removes specified `element` from this list, if it contains it. + * Ignores the call if `element` is not contained. + * @param {Object|Number|String} element + */ + _remove: function _remove(element) { + let list = this._keyValueMap, + index = list.indexOf(element); + if (0 <= index) { + delete this._public[list.length - 1]; + list.splice(index, 1); + for (let length = list.length; index < length; index++) + this._public[index] = list[index]; + } + }, + /** + * Removes all of the elements from this list. + */ + _clear: function _clear() { + for (let i = 0, ii = this._keyValueMap.length; i < ii; i ++) + delete this._public[i]; + this._keyValueMap.splice(0); + }, + /** + * Custom iterator providing `List`s enumeration behavior. + * We cant reuse `_iterator` that is defined by `Iterable` since it provides + * iteration in an arbitrary order. + * @see https://developer.mozilla.org/en/JavaScript/Reference/Statements/for...in + * @param {Boolean} onKeys + */ + __iterator__: function __iterator__(onKeys, onKeyValue) { + let array = this._keyValueMap.slice(0), + i = -1; + for each(let element in array) + yield onKeyValue ? [++i, element] : onKeys ? ++i : element; + } +}); +exports.List = List; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/match-pattern.js b/tools/addon-sdk-1.4/packages/api-utils/lib/match-pattern.js new file mode 100644 index 0000000..802fe3a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/match-pattern.js @@ -0,0 +1,137 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Nickolay Ponomarev. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Nickolay Ponomarev <asqueella@gmail.com> (Original Author) + * Irakli Gozalishvili <gozala@mozilla.com> + * Drew Willcoxon <adw@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; +const { URL } = require("./url"); + +exports.MatchPattern = MatchPattern; + +function MatchPattern(pattern) { + if (typeof pattern.test == "function") { + + // For compatibility with -moz-document rules, we require the RegExp's + // global, ignoreCase, and multiline flags to be set to false. + if (pattern.global) { + throw new Error("A RegExp match pattern cannot be set to `global` " + + "(i.e. //g)."); + } + if (pattern.ignoreCase) { + throw new Error("A RegExp match pattern cannot be set to `ignoreCase` " + + "(i.e. //i)."); + } + if (pattern.multiline) { + throw new Error("A RegExp match pattern cannot be set to `multiline` " + + "(i.e. //m)."); + } + + this.regexp = pattern; + } + else { + let firstWildcardPosition = pattern.indexOf("*"); + let lastWildcardPosition = pattern.lastIndexOf("*"); + if (firstWildcardPosition != lastWildcardPosition) + throw new Error("There can be at most one '*' character in a wildcard."); + + if (firstWildcardPosition == 0) { + if (pattern.length == 1) + this.anyWebPage = true; + else if (pattern[1] != ".") + throw new Error("Expected a *.<domain name> string, got: " + pattern); + else + this.domain = pattern.substr(2); + } + else { + if (pattern.indexOf(":") == -1) { + throw new Error("When not using *.example.org wildcard, the string " + + "supplied is expected to be either an exact URL to " + + "match or a URL prefix. The provided string ('" + + pattern + "') is unlikely to match any pages."); + } + + if (firstWildcardPosition == -1) + this.exactURL = pattern; + else if (firstWildcardPosition == pattern.length - 1) + this.urlPrefix = pattern.substr(0, pattern.length - 1); + else { + throw new Error("The provided wildcard ('" + pattern + "') has a '*' " + + "in an unexpected position. It is expected to be the " + + "first or the last character in the wildcard."); + } + } + } +} + +MatchPattern.prototype = { + + test: function MatchPattern_test(urlStr) { + try { + var url = URL(urlStr); + } + catch (err) { + return false; + } + + // Test the URL against a RegExp pattern. For compatibility with + // -moz-document rules, we require the RegExp to match the entire URL, + // so we not only test for a match, we also make sure the matched string + // is the entire URL string. + // + // Assuming most URLs don't match most match patterns, we call `test` for + // speed when determining whether or not the URL matches, then call `exec` + // for the small subset that match to make sure the entire URL matches. + // + if (this.regexp && this.regexp.test(urlStr) && + this.regexp.exec(urlStr)[0] == urlStr) + return true; + + if (this.anyWebPage && /^(https?|ftp)$/.test(url.scheme)) + return true; + if (this.exactURL && this.exactURL == urlStr) + return true; + if (this.domain && url.host && + url.host.slice(-this.domain.length) == this.domain) + return true; + if (this.urlPrefix && 0 == urlStr.indexOf(this.urlPrefix)) + return true; + + return false; + } + +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/memory.js b/tools/addon-sdk-1.4/packages/api-utils/lib/memory.js new file mode 100644 index 0000000..2ff0486 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/memory.js @@ -0,0 +1,146 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci,Cu,components} = require("chrome"); +var trackedObjects = {}; + +var Compacter = { + INTERVAL: 5000, + notify: function(timer) { + var newTrackedObjects = {}; + for (let name in trackedObjects) { + var oldBin = trackedObjects[name]; + var newBin = []; + var strongRefs = []; + for (var i = 0; i < oldBin.length; i++) { + var strongRef = oldBin[i].weakref.get(); + if (strongRef && strongRefs.indexOf(strongRef) == -1) { + strongRefs.push(strongRef); + newBin.push(oldBin[i]); + } + } + if (newBin.length) + newTrackedObjects[name] = newBin; + } + trackedObjects = newTrackedObjects; + } +}; + +var timer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + +timer.initWithCallback(Compacter, + Compacter.INTERVAL, + Ci.nsITimer.TYPE_REPEATING_SLACK); + +var track = exports.track = function track(object, bin, stackFrameNumber) { + var frame = components.stack.caller; + var weakref = Cu.getWeakReference(object); + if (!bin) + bin = object.constructor.name; + if (bin == "Object") + bin = frame.name; + if (!bin) + bin = "generic"; + if (!(bin in trackedObjects)) + trackedObjects[bin] = []; + + if (stackFrameNumber > 0) + for (var i = 0; i < stackFrameNumber; i++) + frame = frame.caller; + + trackedObjects[bin].push({weakref: weakref, + created: new Date(), + filename: frame.filename, + lineNo: frame.lineNumber, + bin: bin}); +}; + +var getBins = exports.getBins = function getBins() { + var names = []; + for (let name in trackedObjects) + names.push(name); + return names; +}; + +var getObjects = exports.getObjects = function getObjects(bin) { + function getLiveObjectsInBin(bin, array) { + for (var i = 0; i < bin.length; i++) { + var object = bin[i].weakref.get(); + if (object) + array.push(bin[i]); + } + } + + var results = []; + if (bin) { + if (bin in trackedObjects) + getLiveObjectsInBin(trackedObjects[bin], results); + } else + for (let name in trackedObjects) + getLiveObjectsInBin(trackedObjects[name], results); + return results; +}; + +var gc = exports.gc = function gc() { + // Components.utils.forceGC() doesn't currently perform + // cycle collection, which means that e.g. DOM elements + // won't be collected by it. Fortunately, there are + // other ways... + + var window = Cc["@mozilla.org/appshell/appShellService;1"] + .getService(Ci.nsIAppShellService) + .hiddenDOMWindow; + var test_utils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + test_utils.garbageCollect(); + Compacter.notify(); + + // Not sure why, but sometimes it appears that we don't get + // them all with just one CC, so let's do it again. + test_utils.garbageCollect(); +}; + +require("./unload").when( + function() { + trackedObjects = {}; + if (timer) { + timer.cancel(); + timer = null; + } + }); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/namespace.js b/tools/addon-sdk-1.4/packages/api-utils/lib/namespace.js new file mode 100644 index 0000000..ae50f36 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/namespace.js @@ -0,0 +1,55 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +/** + * Function creates a new namespace. Optionally `prototype` object may be + * passed, in which case namespace objects will inherit from it. Returned value + * is a function that can be used to get access to the namespaced properties + * for the passed object. + * @examples + * const ns = Namespace(); + * ns(myObject).secret = secret; + */ +exports.Namespace = function Namespace(prototype) { + prototype = prototype || Object.prototype; + const map = new WeakMap(); + return function namespace(target) { + return map.get(target) || + map.set(target, Object.create(prototype)), map.get(target); + }; +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/observer-service.js b/tools/addon-sdk-1.4/packages/api-utils/lib/observer-service.js new file mode 100644 index 0000000..f408dd5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/observer-service.js @@ -0,0 +1,212 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Observers. + * + * The Initial Developer of the Original Code is Daniel Aquino. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Daniel Aquino <mr.danielaquino@gmail.com> + * Myk Melez <myk@mozilla.org> + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci} = require("chrome"); +var xpcom = require("./xpcom"); + +/** + * A service for adding, removing and notifying observers of notifications. + * Wraps the nsIObserverService interface. + * + * @version 0.2 + */ + +var service = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + +/** + * A cache of observers that have been added. + * + * We use this to remove observers when a caller calls |Observers.remove|. + */ +var cache = []; + +/** + * Topics specifically available to Jetpack-generated extensions. + * + * Using these predefined consts instead of the platform strings is good: + * - allows us to scope topics specifically for Jetpacks + * - addons aren't dependent on strings nor behavior of core platform topics + * - the core platform topics are not clearly named + * + */ +exports.topics = { + /** + * A topic indicating that the application is in a state usable + * by add-ons. + */ + get APPLICATION_READY() packaging.jetpackID + "_APPLICATION_READY" +}; + +/** + * Register the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic to observe + * + * @param callback {Object} + * the callback; an Object that implements nsIObserver or a Function + * that gets called when the notification occurs + * + * @param thisObject {Object} [optional] + * the object to use as |this| when calling a Function callback + * + * @returns the observer + */ +var add = exports.add = function add(topic, callback, thisObject) { + var observer = new Observer(topic, callback, thisObject); + service.addObserver(observer, topic, true); + cache.push(observer); + + return observer; +}; + +/** + * Unregister the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic being observed + * + * @param callback {Object} + * the callback doing the observing + * + * @param thisObject {Object} [optional] + * the object being used as |this| when calling a Function callback + */ +var remove = exports.remove = function remove(topic, callback, thisObject) { + // This seems fairly inefficient, but I'm not sure how much better + // we can make it. We could index by topic, but we can't index by callback + // or thisObject, as far as I know, since the keys to JavaScript hashes + // (a.k.a. objects) can apparently only be primitive values. + let observers = cache.filter(function(v) { + return (v.topic == topic && + v.callback == callback && + v.thisObject == thisObject); + }); + + if (observers.length) { + service.removeObserver(observers[0], topic); + cache.splice(cache.indexOf(observers[0]), 1); + } +}; + +/** + * Notify observers about something. + * + * @param topic {String} + * the topic to notify observers about + * + * @param subject {Object} [optional] + * some information about the topic; can be any JS object or primitive + * + * @param data {String} [optional] [deprecated] + * some more information about the topic; deprecated as the subject + * is sufficient to pass all needed information to the JS observers + * that this module targets; if you have multiple values to pass to + * the observer, wrap them in an object and pass them via the subject + * parameter (i.e.: { foo: 1, bar: "some string", baz: myObject }) + */ +var notify = exports.notify = function notify(topic, subject, data) { + subject = (typeof subject == "undefined") ? null : new Subject(subject); + data = (typeof data == "undefined") ? null : data; + service.notifyObservers(subject, topic, data); +}; + +function Observer(topic, callback, thisObject) { + memory.track(this); + this.topic = topic; + this.callback = callback; + this.thisObject = thisObject; +} + +Observer.prototype = { + QueryInterface: xpcom.utils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + observe: function(subject, topic, data) { + // Extract the wrapped object for subjects that are one of our + // wrappers around a JS object. This way we support both wrapped + // subjects created using this module and those that are real + // XPCOM components. + if (subject && typeof subject == "object" && + ("wrappedJSObject" in subject) && + ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) + subject = subject.wrappedJSObject.object; + + try { + if (typeof this.callback == "function") { + if (this.thisObject) + this.callback.call(this.thisObject, subject, data); + else + this.callback(subject, data); + } else // typeof this.callback == "object" (nsIObserver) + this.callback.observe(subject, topic, data); + } catch (e) { + console.exception(e); + } + } +}; + +function Subject(object) { + // Double-wrap the object and set a property identifying the + // wrappedJSObject as one of our wrappers to distinguish between + // subjects that are one of our wrappers (which we should unwrap + // when notifying our observers) and those that are real JS XPCOM + // components (which we should pass through unaltered). + this.wrappedJSObject = { + observersModuleSubjectWrapper: true, + object: object + }; +} + +Subject.prototype = { + QueryInterface: xpcom.utils.generateQI([]), + getHelperForLanguage: function() {}, + getInterfaces: function() {} +}; + +require("./unload").when( + function removeAllObservers() { + // Make a copy of cache first, since cache will be changing as we + // iterate through it. + cache.slice().forEach( + function(observer) { + remove(observer.topic, observer.callback, observer.thisObject); + }); + }); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/passwords/utils.js b/tools/addon-sdk-1.4/packages/api-utils/lib/passwords/utils.js new file mode 100644 index 0000000..950aac2 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/passwords/utils.js @@ -0,0 +1,134 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack Packages. + * + * The Initial Developer of the Original Code is Red Hat. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Matěj Cepl <mcepl@redhat.com> (Original Author) + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Cc, Ci, components: { Constructor: CConstructor } } = require("chrome"); +const { uri: ADDON_URI } = require("self"); +const loginManager = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); +const { URL: parseURL } = require("../url"); +const LoginInfo = CConstructor("@mozilla.org/login-manager/loginInfo;1", + "nsILoginInfo", "init"); + +function filterMatchingLogins(loginInfo) + Object.keys(this).every(function(key) loginInfo[key] === this[key], this); + +/** + * Removes `user`, `password` and `path` fields from the given `url` if it's + * 'http', 'https' or 'ftp'. All other URLs are returned unchanged. + * @example + * http://user:pass@www.site.com/foo/?bar=baz#bang -> http://www.site.com + */ +function normalizeURL(url) { + let { scheme, host, port } = parseURL(url); + // We normalize URL only if it's `http`, `https` or `ftp`. All other types of + // URLs (`resource`, `chrome`, etc..) should not be normalized as they are + // used with add-on associated credentials path. + return scheme === "http" || scheme === "https" || scheme === "ftp" ? + scheme + "://" + (host || "") + (port ? ":" + port : "") : + url +} + +function Login(options) { + let login = Object.create(Login.prototype); + Object.keys(options || {}).forEach(function(key) { + if (key === 'url') + login.hostname = normalizeURL(options.url); + else if (key === 'formSubmitURL') + login.formSubmitURL = options.formSubmitURL ? + normalizeURL(options.formSubmitURL) : null; + else if (key === 'realm') + login.httpRealm = options.realm; + else + login[key] = options[key]; + }); + + return login; +} +Login.prototype.toJSON = function toJSON() { + return { + url: this.hostname || ADDON_URI, + realm: this.httpRealm || null, + formSubmitURL: this.formSubmitURL || null, + username: this.username || null, + password: this.password || null, + usernameField: this.usernameField || '', + passwordField: this.passwordField || '', + } +}; +Login.prototype.toLoginInfo = function toLoginInfo() { + let { url, realm, formSubmitURL, username, password, usernameField, + passwordField } = this.toJSON(); + + return new LoginInfo(url, formSubmitURL, realm, username, password, + usernameField, passwordField); +}; + +function loginToJSON(value) Login(value).toJSON() + +/** + * Returns array of `nsILoginInfo` objects that are stored in the login manager + * and have all the properties with matching values as a given `options` object. + * @param {Object} options + * @returns {nsILoginInfo[]} + */ +exports.search = function search(options) { + return loginManager.getAllLogins() + .filter(filterMatchingLogins, Login(options)) + .map(loginToJSON); +}; + +/** + * Stores login info created from the given `options` to the applications + * built-in login management system. + * @param {Object} options. + */ +exports.store = function store(options) { + loginManager.addLogin(Login(options).toLoginInfo()); +}; + +/** + * Removes login info from the applications built-in login management system. + * _Please note: When removing a login info the specified properties must + * exactly match to the one that is already stored or exception will be thrown._ + * @param {Object} options. + */ +exports.remove = function remove(options) { + loginManager.removeLogin(Login(options).toLoginInfo()); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/plain-text-console.js b/tools/addon-sdk-1.4/packages/api-utils/lib/plain-text-console.js new file mode 100644 index 0000000..f7ea74e --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/plain-text-console.js @@ -0,0 +1,114 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci} = require("chrome"); + +function stringify(arg) { + try { + return String(arg); + } + catch(ex) { + return "<toString() error>"; + } +} + +function stringifyArgs(args) { + return Array.map(args, stringify).join(" "); +} + +function message(print, level, args) { + print(level + ": " + stringifyArgs(args) + "\n", level); +} + +var Console = exports.PlainTextConsole = function PlainTextConsole(print) { + if (!print) + print = dump; + if (print === dump) { + // If we're just using dump(), auto-enable preferences so + // that the developer actually sees the console output. + var prefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + prefs.setBoolPref("browser.dom.window.dump.enabled", true); + } + this.print = print; + + // Binding all the public methods to an instance so that they can be used + // as callback / listener functions straightaway. + this.log = this.log.bind(this); + this.info = this.info.bind(this); + this.warn = this.warn.bind(this); + this.error = this.error.bind(this); + this.debug = this.debug.bind(this); + this.exception = this.exception.bind(this); + this.trace = this.trace.bind(this); +}; + +Console.prototype = { + log: function log() { + message(this.print, "info", arguments); + }, + + info: function info() { + message(this.print, "info", arguments); + }, + + warn: function warn() { + message(this.print, "warning", arguments); + }, + + error: function error() { + message(this.print, "error", arguments); + }, + + debug: function debug() { + message(this.print, "debug", arguments); + }, + + exception: function exception(e) { + var fullString = ("An exception occurred.\n" + + require("./traceback").format(e) + "\n" + e); + this.error(fullString); + }, + + trace: function trace() { + var traceback = require("./traceback"); + var stack = traceback.get(); + stack.splice(-1, 1); + message(this.print, "info", [traceback.format(stack)]); + } +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/preferences-service.js b/tools/addon-sdk-1.4/packages/api-utils/lib/preferences-service.js new file mode 100644 index 0000000..127a1cf --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/preferences-service.js @@ -0,0 +1,138 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Preferences. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez <myk@mozilla.org> + * Daniel Aquino <mr.danielaquino@gmail.com> + * Atul Varma <atul@mozilla.com> + * Erik Vold <erikvvold@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +// The minimum and maximum integers that can be set as preferences. +// The range of valid values is narrower than the range of valid JS values +// because the native preferences code treats integers as NSPR PRInt32s, +// which are 32-bit signed integers on all platforms. +const MAX_INT = 0x7FFFFFFF; +const MIN_INT = -0x80000000; + +const {Cc,Ci,Cr} = require("chrome"); + +var prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService).getBranch(null); + +var get = exports.get = function get(name, defaultValue) { + switch (prefSvc.getPrefType(name)) { + case Ci.nsIPrefBranch.PREF_STRING: + return prefSvc.getComplexValue(name, Ci.nsISupportsString).data; + + case Ci.nsIPrefBranch.PREF_INT: + return prefSvc.getIntPref(name); + + case Ci.nsIPrefBranch.PREF_BOOL: + return prefSvc.getBoolPref(name); + + case Ci.nsIPrefBranch.PREF_INVALID: + return defaultValue; + + default: + // This should never happen. + throw new Error("Error getting pref " + name + + "; its value's type is " + + prefSvc.getPrefType(name) + + ", which I don't know " + + "how to handle."); + } +}; + +var set = exports.set = function set(name, value) { + var prefType; + if (typeof value != "undefined" && value != null) + prefType = value.constructor.name; + + switch (prefType) { + case "String": + { + var string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = value; + prefSvc.setComplexValue(name, Ci.nsISupportsString, string); + } + break; + + case "Number": + // We throw if the number is outside the range or not an integer, since + // the result will not be what the consumer wanted to store. + if (value > MAX_INT || value < MIN_INT) + throw new Error("you cannot set the " + name + + " pref to the number " + value + + ", as number pref values must be in the signed " + + "32-bit integer range -(2^31) to 2^31-1. " + + "To store numbers outside that range, store " + + "them as strings."); + if (value % 1 != 0) + throw new Error("cannot store non-integer number: " + value); + prefSvc.setIntPref(name, value); + break; + + case "Boolean": + prefSvc.setBoolPref(name, value); + break; + + default: + throw new Error("can't set pref " + name + " to value '" + value + + "'; it isn't a string, integer, or boolean"); + } +}; + +var has = exports.has = function has(name) { + return (prefSvc.getPrefType(name) != Ci.nsIPrefBranch.PREF_INVALID); +}; + +var isSet = exports.isSet = function isSet(name) { + return (has(name) && prefSvc.prefHasUserValue(name)); +}; + +var reset = exports.reset = function reset(name) { + try { + prefSvc.clearUserPref(name); + } catch (e if e.result == Cr.NS_ERROR_UNEXPECTED) { + // The pref service throws NS_ERROR_UNEXPECTED when the caller tries + // to reset a pref that doesn't exist or is already set to its default + // value. This interface fails silently in those cases, so callers + // can unconditionally reset a pref without having to check if it needs + // resetting first or trap exceptions after the fact. It passes through + // other exceptions, however, so callers know about them, since we don't + // know what other exceptions might be thrown and what they might mean. + } +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/process.js b/tools/addon-sdk-1.4/packages/api-utils/lib/process.js new file mode 100644 index 0000000..3f429eb --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/process.js @@ -0,0 +1,95 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Cc, Ci } = require("chrome"); +const { createRemoteBrowser } = require("api-utils/window-utils"); +const { channel } = require("./channel"); +const { setTimout } = require('./timer'); +const packaging = require('@packaging'); +const { when } = require('./unload'); + +const addonService = '@mozilla.org/addon/service;1' in Cc ? + Cc['@mozilla.org/addon/service;1'].getService(Ci.nsIAddonService) : null + +const ENABLE_E10S = packaging.enable_e10s; + +function loadScript(target, uri, sync) { + return 'loadScript' in target ? target.loadScript(uri, sync) + : target.loadFrameScript(uri, sync) +} + +function process(target, id, path, scope) { + // Please note that even though `loadScript`, is executed before channel is + // returned, users still are able to subscribe for messages before any message + // will be sent. That's because `loadScript` queues script execution on the + // other process, which means they will execute async (on the next turn of + // event loop), while the channel for messages is returned immediately (in + // the same turn of event loop). + + loadScript(target, packaging.uriPrefix + packaging.loader, false); + loadScript(target, 'data:,let loader = Loader.new(' + + JSON.stringify(packaging) + ');\n' + + 'loader.main("' + id + '", "' + path + '");', false); + + when(function (reason) { + // Please note that it's important to unload remote loader + // synchronously (using synchronous frame script), to make sure that we + // don't stop during unload. + loadScript(target, 'data:,loader.unload("' + reason + '")', true); + }); + + return { channel: channel.bind(null, scope, target) } +} + +exports.spawn = function spawn(id, path) { + return function promise(deliver) { + // If `nsIAddonService` is available we use it to create an add-on process, + // otherwise we fallback to the remote browser's message manager. + if (ENABLE_E10S && addonService) { + console.log('!!!!!!!!!!!!!!!!!!!! Using addon process !!!!!!!!!!!!!!!!!!'); + deliver(process(addonService.createAddon(), id, path)); + } else { + createRemoteBrowser(ENABLE_E10S)(function(browser) { + let messageManager = browser.QueryInterface(Ci.nsIFrameLoaderOwner). + frameLoader.messageManager + let window = browser.ownerDocument.defaultView; + deliver(process(messageManager, id, path, window)); + }); + } + }; +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/runtime.js b/tools/addon-sdk-1.4/packages/api-utils/lib/runtime.js new file mode 100644 index 0000000..135617e --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/runtime.js @@ -0,0 +1,48 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Cc, Ci } = require("chrome"); +const runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); + +exports.inSafeMode = runtime.inSafeMode; +exports.OS = runtime.OS; +exports.processType = runtime.processType; +exports.widgetToolkit = runtime.widgetToolkit; +exports.XPCOMABI = runtime.XPCOMABI; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/self!.js b/tools/addon-sdk-1.4/packages/api-utils/lib/self!.js new file mode 100644 index 0000000..bd9b6da --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/self!.js @@ -0,0 +1,82 @@ +/* vim:st=2:sts=2:sw=2: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Brian Warner <warner@mozilla.com> + * Erik Vold <erikvvold@gmail.com> + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { CC } = require('chrome'); +const { jetpackID, name, manifest, metadata, uriPrefix } = require('@packaging'); + +const XMLHttpRequest = CC('@mozilla.org/xmlextras/xmlhttprequest;1', + 'nsIXMLHttpRequest'); + +// Utility function that synchronously reads local resource from the given +// `uri` and returns content string. +function readURI(uri) { + let request = XMLHttpRequest(); + request.open('GET', uri, false); + request.overrideMimeType('text/plain'); + request.send(); + return request.responseText; +} + +// Some XPCOM APIs require valid URIs as an argument for certain operations (see +// `nsILoginManager` for example). This property represents add-on associated +// unique URI string that can be used for that. +const uri = 'addon:' + jetpackID + +function url(root, path) root + (path || "") +function read(root, path) readURI(url(root, path)) + +exports.create = function create(base) { + let moduleData = manifest[base] && manifest[base].requirements['self']; + let root = uriPrefix + moduleData.dataURIPrefix; + return Object.freeze({ + id: 'self', + exports: Object.freeze({ + id: jetpackID, + uri: uri, + name: name, + version: metadata[name].version, + data: { + url: url.bind(null, root), + load: read.bind(null, root) + } + }) + }); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/system.js b/tools/addon-sdk-1.4/packages/api-utils/lib/system.js new file mode 100644 index 0000000..70896e7 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/system.js @@ -0,0 +1,131 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +'use strict'; + +const { Cc, Ci, CC } = require('chrome'); +const options = require('@packaging'); +const file = require('./file'); + +const appStartup = Cc['@mozilla.org/toolkit/app-startup;1']. + getService(Ci.nsIAppStartup); +const appInfo = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULAppInfo); +const runtime = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULRuntime); + +const { eAttemptQuit: E_ATTEMPT, eForceQuit: E_FORCE } = appStartup; + +/** + * Parsed JSON object that was passed via `cfx --static-args "{ foo: 'bar' }"` + */ +exports.staticArgs = options.staticArgs; + +/** + * Environment variables. Environment variables are non-enumerable properties + * of this object (key is name and value is value). + */ +exports.env = require('./environment').env; + +/** + * Ends the process with the specified `code`. If omitted, exit uses the + * 'success' code 0. To exit with failure use `1`. + * TODO: Improve platform to actually quit with an exit code. + */ +exports.exit = function exit(code) { + // This is used by 'cfx' to find out exit code. + if ('resultFile' in options) { + let stream = file.open(options.resultFile, 'w'); + stream.write(code ? 'FAIL' : 'OK'); + stream.close(); + } + + appStartup.quit(code ? E_ATTEMPT : E_FORCE); +}; + +/** + * What platform you're running on (all lower case string). + * For possible values see: + * https://developer.mozilla.org/en/OS_TARGET + */ +exports.platform = runtime.OS.toLowerCase(); + +/** + * What processor architecture you're running on: + * `'arm', 'ia32', or 'x64'`. + */ +exports.architecture = runtime.XPCOMABI.split('_')[0]; + +/** + * What compiler used for build: + * `'msvc', 'n32', 'gcc2', 'gcc3', 'sunc', 'ibmc'...` + */ +exports.compiler = runtime.XPCOMABI.split('_')[1]; + +/** + * The application's build ID/date, for example "2004051604". + */ +exports.build = appInfo.appBuildID; + +/** + * The XUL application's UUID. + * This has traditionally been in the form + * `{AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}` but for some applications it may + * be: "appname@vendor.tld". + */ +exports.id = appInfo.ID; + +/** + * The name of the application. + */ +exports.name = appInfo.name; + +/** + * The XUL application's version, for example "0.8.0+" or "3.7a1pre". + */ +exports.version = appInfo.version; + +/** + * XULRunner version. + */ +exports.platformVersion = runtime.platformVersion; + + +/** + * The name of the application vendor, for example "Mozilla". + */ +exports.vendor = appInfo.vendor; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/tab-browser.js b/tools/addon-sdk-1.4/packages/api-utils/lib/tab-browser.js new file mode 100644 index 0000000..272a7c6 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/tab-browser.js @@ -0,0 +1,761 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * Dietrich Ayala <dietrich@mozilla.com> + * Felipe Gomes <felipc@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; +const {Cc,Ci,Cu} = require("chrome"); +var NetUtil = {}; +Cu.import("resource://gre/modules/NetUtil.jsm", NetUtil); +NetUtil = NetUtil.NetUtil; +const errors = require("./errors"); +const windowUtils = require("./window-utils"); +const apiUtils = require("./api-utils"); +const collection = require("./collection"); + +// TODO: The hard-coding of app-specific info here isn't very nice; +// ideally such app-specific info should be more decoupled, and the +// module should be extensible, allowing for support of new apps at +// runtime, perhaps by inspecting supported packages (e.g. via +// dynamically-named modules or package-defined extension points). + +if (!require("./xul-app").is("Firefox")) { + throw new Error([ + "The tab-browser module currently supports only Firefox. In the future ", + "it will support other applications. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +function onBrowserLoad(callback, event) { + if (event.target && event.target.defaultView == this) { + this.removeEventListener("load", onBrowserLoad, true); + try { + require("timer").setTimeout(function () { + callback(event); + }, 10); + } catch (e) { console.exception(e); } + } +} + +// Utility function to open a new browser window. +function openBrowserWindow(callback, url) { + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + let urlString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + urlString.data = url; + let window = ww.openWindow(null, "chrome://browser/content/browser.xul", + "_blank", "chrome,all,dialog=no", urlString); + if (callback) + window.addEventListener("load", onBrowserLoad.bind(window, callback), true); + + return window; +} + +// Open a URL in a new tab +exports.addTab = function addTab(url, options) { + if (!options) + options = {}; + options.url = url; + + options = apiUtils.validateOptions(options, { + // TODO: take URL object instead of string (bug 564524) + url: { + is: ["string"], + ok: function (v) !!v, + msg: "The url parameter must have be a non-empty string." + }, + inNewWindow: { + is: ["undefined", "null", "boolean"] + }, + inBackground: { + is: ["undefined", "null", "boolean"] + }, + onLoad: { + is: ["undefined", "null", "function"] + }, + isPinned: { + is: ["undefined", "boolean"] + } + }); + + var wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + var win = wm.getMostRecentWindow("navigator:browser"); + if (!win || options.inNewWindow) { + openBrowserWindow(function(e) { + if(options.isPinned) { + //get the active tab in the recently created window + let mainWindow = e.target.defaultView; + mainWindow.gBrowser.pinTab(mainWindow.gBrowser.selectedTab); + } + require("./errors").catchAndLog(function(e) options.onLoad(e))(e); + }, options.url); + } else { + let tab = win.gBrowser.addTab(options.url); + if (!options.inBackground) + win.gBrowser.selectedTab = tab; + if (options.onLoad) { + let tabBrowser = win.gBrowser.getBrowserForTab(tab); + tabBrowser.addEventListener("load", function onLoad(e) { + if (e.target.defaultView.content.location == "about:blank") + return; + // remove event handler from addTab - don't want notified + // for subsequent loads in same tab. + tabBrowser.removeEventListener("load", onLoad, true); + require("./errors").catchAndLog(function(e) options.onLoad(e))(e); + }, true); + } + } +} + +// Iterate over a window's tabbrowsers +function tabBrowserIterator(window) { + var browsers = window.document.querySelectorAll("tabbrowser"); + for (var i = 0; i < browsers.length; i++) + yield browsers[i]; +} + +// Iterate over a tabbrowser's tabs +function tabIterator(tabbrowser) { + var tabs = tabbrowser.tabContainer; + for (var i = 0; i < tabs.children.length; i++) { + yield tabs.children[i]; + } +} + +// Tracker for all tabbrowsers across all windows, +// or a single tabbrowser if the window is given. +function Tracker(delegate, window) { + this._delegate = delegate; + this._browsers = []; + this._window = window; + this._windowTracker = new windowUtils.WindowTracker(this); + + require("./unload").ensure(this); +} +Tracker.prototype = { + __iterator__: function __iterator__() { + for (var i = 0; i < this._browsers.length; i++) + yield this._browsers[i]; + }, + get: function get(index) { + return this._browsers[index]; + }, + onTrack: function onTrack(window) { + if (this._window && window != this._window) + return; + + for (let browser in tabBrowserIterator(window)) + this._browsers.push(browser); + if (this._delegate) + for (let browser in tabBrowserIterator(window)) + this._delegate.onTrack(browser); + }, + onUntrack: function onUntrack(window) { + if (this._window && window != this._window) + return; + + for (let browser in tabBrowserIterator(window)) { + let index = this._browsers.indexOf(browser); + if (index != -1) + this._browsers.splice(index, 1); + else + console.error("internal error: browser tab not found"); + } + if (this._delegate) + for (let browser in tabBrowserIterator(window)) + this._delegate.onUntrack(browser); + }, + get length() { + return this._browsers.length; + }, + unload: function unload() { + this._windowTracker.unload(); + } +}; +exports.Tracker = apiUtils.publicConstructor(Tracker); + +// Tracker for all tabs across all windows, +// or a single window if it's given. +function TabTracker(delegate, window) { + this._delegate = delegate; + this._tabs = []; + this._tracker = new Tracker(this, window); + require("./unload").ensure(this); +} +TabTracker.prototype = { + _TAB_EVENTS: ["TabOpen", "TabClose"], + _safeTrackTab: function safeTrackTab(tab) { + this._tabs.push(tab); + try { + this._delegate.onTrack(tab); + } catch (e) { + console.exception(e); + } + }, + _safeUntrackTab: function safeUntrackTab(tab) { + var index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + this._tabs.splice(index, 1); + try { + this._delegate.onUntrack(tab); + } catch (e) { + console.exception(e); + } + }, + handleEvent: function handleEvent(event) { + switch (event.type) { + case "TabOpen": + this._safeTrackTab(event.target); + break; + case "TabClose": + this._safeUntrackTab(event.target); + break; + default: + throw new Error("internal error: unknown event type: " + + event.type); + } + }, + onTrack: function onTrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeTrackTab(tab); + var self = this; + this._TAB_EVENTS.forEach( + function(eventName) { + tabbrowser.tabContainer.addEventListener(eventName, self, true); + }); + }, + onUntrack: function onUntrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeUntrackTab(tab); + var self = this; + this._TAB_EVENTS.forEach( + function(eventName) { + tabbrowser.tabContainer.removeEventListener(eventName, self, true); + }); + }, + unload: function unload() { + this._tracker.unload(); + } +}; +exports.TabTracker = apiUtils.publicConstructor(TabTracker); + +exports.whenContentLoaded = function whenContentLoaded(callback) { + var cb = require("./errors").catchAndLog(function eventHandler(event) { + if (event.target && event.target.defaultView) + callback(event.target.defaultView); + }); + + var tracker = new Tracker({ + onTrack: function(tabBrowser) { + tabBrowser.addEventListener("DOMContentLoaded", cb, false); + }, + onUntrack: function(tabBrowser) { + tabBrowser.removeEventListener("DOMContentLoaded", cb, false); + } + }); + + return tracker; +}; + +exports.__defineGetter__("activeTab", function() { + const wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + let mainWindow = wm.getMostRecentWindow("navigator:browser"); + return mainWindow.gBrowser.selectedTab; +}); + +/******************* TabModule *********************/ + +// Supported tab events +const events = [ + "onActivate", + "onDeactivate", + "onOpen", + "onClose", + "onReady", + "onLoad", + "onPaint" +]; +exports.tabEvents = events; + +/** + * TabModule + * + * Constructor for a module that implements the tabs API + */ +let TabModule = exports.TabModule = function TabModule(window) { + let self = this; + /** + * Tab + * + * Safe object representing a tab. + */ + let tabConstructor = apiUtils.publicConstructor(function(element) { + if (!element) + throw new Error("no tab element."); + let win = element.ownerDocument.defaultView; + if (!win) + throw new Error("element has no window."); + if (window && win != window) + throw new Error("module's window and element's window don't match."); + let browser = win.gBrowser.getBrowserForTab(element); + + this.__defineGetter__("title", function() browser.contentDocument.title); + this.__defineGetter__("location", function() browser.contentDocument.location); + this.__defineSetter__("location", function(val) browser.contentDocument.location = val); + this.__defineGetter__("contentWindow", function() browser.contentWindow); + this.__defineGetter__("contentDocument", function() browser.contentDocument); + this.__defineGetter__("favicon", function() { + let pageURI = NetUtil.newURI(browser.contentDocument.location); + let fs = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + let faviconURL; + try { + let faviconURI = fs.getFaviconForPage(pageURI); + faviconURL = fs.getFaviconDataAsDataURL(faviconURI); + } catch(ex) { + let data = getChromeURLContents("chrome://mozapps/skin/places/defaultFavicon.png"); + let encoded = browser.contentWindow.btoa(data); + faviconURL = "data:image/png;base64," + encoded; + } + return faviconURL; + }); + this.__defineGetter__("style", function() null); // TODO + this.__defineGetter__("index", function() win.gBrowser.getBrowserIndexForDocument(browser.contentDocument)); + this.__defineGetter__("thumbnail", function() getThumbnailCanvasForTab(element, browser.contentWindow)); + + this.close = function() win.gBrowser.removeTab(element); + this.move = function(index) { + win.gBrowser.moveTabTo(element, index); + }; + + this.__defineGetter__("isPinned", function() element.pinned); + this.pin = function() win.gBrowser.pinTab(element); + this.unpin = function() win.gBrowser.unpinTab(element); + + // Set up the event handlers + let tab = this; + events.filter(function(e) e != "onOpen").forEach(function(e) { + // create a collection for each event + collection.addCollectionProperty(tab, e); + // make tabs setter for each event, for adding via property assignment + tab.__defineSetter__(e, function(val) tab[e].add(val)); + }); + + // listen for events, filtered on this tab + eventsTabDelegate.addTabDelegate(this); + }); + + /** + * tabs.activeTab + */ + this.__defineGetter__("activeTab", function() { + try { + return window ? tabConstructor(window.gBrowser.selectedTab) + : tabConstructor(exports.activeTab); + } + catch (e) { } + return null; + }); + this.__defineSetter__("activeTab", function(tab) { + let [tabElement, win] = getElementAndWindowForTab(tab, window); + if (tabElement) { + // set as active tab + win.gBrowser.selectedTab = tabElement; + // focus the window + win.focus(); + } + }); + + this.open = function TM_open(options) { + open(options, tabConstructor, window); + } + + // Set up the event handlers + events.forEach(function(eventHandler) { + // create a collection for each event + collection.addCollectionProperty(self, eventHandler); + // make tabs setter for each event, for adding via property assignment + self.__defineSetter__(eventHandler, function(val) self[eventHandler].add(val)); + }); + + // Tracker that listens for tab events, and proxies + // them to registered event listeners. + let eventsTabDelegate = { + selectedTab: null, + tabs: [], + addTabDelegate: function TETT_addTabDelegate(tabObj) { + this.tabs.push(tabObj); + }, + pushTabEvent: function TETT_pushTabEvent(event, tab) { + for (let callback in self[event]) { + require("./errors").catchAndLog(function(tab) { + callback(new tabConstructor(tab)); + })(tab); + } + + if (event != "onOpen") { + this.tabs.forEach(function(tabObj) { + if (tabObj[event].length) { + let [tabEl,] = getElementAndWindowForTab(tabObj, window); + if (tabEl == tab) { + for (let callback in tabObj[event]) + require("./errors").catchAndLog(function() callback())(); + } + } + // if being closed, remove the tab object from the cache + // of tabs to notify about events. + if (event == "onClose") + this.tabs.splice(this.tabs.indexOf(tabObj), 1); + }, this); + } + }, + unload: function() { + this.selectedTab = null; + this.tabs.splice(0); + } + }; + require("./unload").ensure(eventsTabDelegate); + + let eventsTabTracker = new ModuleTabTracker({ + onTrack: function TETT_onTrack(tab) { + eventsTabDelegate.pushTabEvent("onOpen", tab); + }, + onUntrack: function TETT_onUntrack(tab) { + eventsTabDelegate.pushTabEvent("onClose", tab); + }, + onSelect: function TETT_onSelect(tab) { + if (eventsTabDelegate.selectedTab) + eventsTabDelegate.pushTabEvent("onDeactivate", tab); + + eventsTabDelegate.selectedTab = new tabConstructor(tab); + + eventsTabDelegate.pushTabEvent("onActivate", tab); + }, + onReady: function TETT_onReady(tab) { + eventsTabDelegate.pushTabEvent("onReady", tab); + }, + onLoad: function TETT_onLoad(tab) { + eventsTabDelegate.pushTabEvent("onLoad", tab); + }, + onPaint: function TETT_onPaint(tab) { + eventsTabDelegate.pushTabEvent("onPaint", tab); + } + }, window); + require("./unload").ensure(eventsTabTracker); + + // Iterator for all tabs + this.__iterator__ = function tabsIterator() { + for (let i = 0; i < eventsTabTracker._tabs.length; i++) + yield tabConstructor(eventsTabTracker._tabs[i]); + } + + this.__defineGetter__("length", function() eventsTabTracker._tabs.length); + + // Cleanup when unloaded + this.unload = function TM_unload() { + // Unregister tabs event listeners + events.forEach(function(e) self[e] = []); + } + require("./unload").ensure(this); + +} // End of TabModule constructor + +/** + * tabs.open - open a URL in a new tab + */ +function open(options, tabConstructor, window) { + if (typeof options === "string") + options = { url: options }; + + options = apiUtils.validateOptions(options, { + url: { + is: ["string"] + }, + inNewWindow: { + is: ["undefined", "boolean"] + }, + inBackground: { + is: ["undefined", "boolean"] + }, + isPinned: { + is: ["undefined", "boolean"] + }, + onOpen: { + is: ["undefined", "function"] + } + }); + + if (window) + options.inNewWindow = false; + + let win = window || require("./window-utils").activeBrowserWindow; + + if (!win || options.inNewWindow) + openURLInNewWindow(options, tabConstructor); + else + openURLInNewTab(options, win, tabConstructor); +} + +function openURLInNewWindow(options, tabConstructor) { + let addTabOptions = { + inNewWindow: true + }; + if (options.onOpen) { + addTabOptions.onLoad = function(e) { + let win = e.target.defaultView; + let tabEl = win.gBrowser.tabContainer.childNodes[0]; + let tabBrowser = win.gBrowser.getBrowserForTab(tabEl); + tabBrowser.addEventListener("load", function onLoad(e) { + tabBrowser.removeEventListener("load", onLoad, true); + let tab = tabConstructor(tabEl); + require("./errors").catchAndLog(function(e) options.onOpen(e))(tab); + }, true); + }; + } + if (options.isPinned) { + addTabOptions.isPinned = true; + } + exports.addTab(options.url.toString(), addTabOptions); +} + +function openURLInNewTab(options, window, tabConstructor) { + window.focus(); + let tabEl = window.gBrowser.addTab(options.url.toString()); + if (!options.inBackground) + window.gBrowser.selectedTab = tabEl; + if (options.isPinned) + window.gBrowser.pinTab(tabEl); + if (options.onOpen) { + let tabBrowser = window.gBrowser.getBrowserForTab(tabEl); + tabBrowser.addEventListener("load", function onLoad(e) { + // remove event handler from addTab - don't want to be notified + // for subsequent loads in same tab. + tabBrowser.removeEventListener("load", onLoad, true); + let tab = tabConstructor(tabEl); + require("./timer").setTimeout(function() { + require("./errors").catchAndLog(function(tab) options.onOpen(tab))(tab); + }, 10); + }, true); + } +} + +function getElementAndWindowForTab(tabObj, window) { + // iterate over open windows, or use single window if provided + let windowIterator = window ? function() { yield window; } + : require("./window-utils").windowIterator; + for (let win in windowIterator()) { + if (win.gBrowser) { + // find the tab element at tab.index + let index = win.gBrowser.getBrowserIndexForDocument(tabObj.contentDocument); + if (index > -1) + return [win.gBrowser.tabContainer.getItemAtIndex(index), win]; + } + } + return [null, null]; +} + +// Tracker for all tabs across all windows +// This is tab-browser.TabTracker, but with +// support for additional events added. +function ModuleTabTracker(delegate, window) { + this._delegate = delegate; + this._tabs = []; + this._tracker = new Tracker(this, window); + require("./unload").ensure(this); +} +ModuleTabTracker.prototype = { + _TAB_EVENTS: ["TabOpen", "TabClose", "TabSelect", "DOMContentLoaded", + "load", "MozAfterPaint"], + _safeTrackTab: function safeTrackTab(tab) { + tab.addEventListener("load", this, false); + tab.linkedBrowser.addEventListener("MozAfterPaint", this, false); + this._tabs.push(tab); + try { + this._delegate.onTrack(tab); + } catch (e) { + console.exception(e); + } + }, + _safeUntrackTab: function safeUntrackTab(tab) { + tab.removeEventListener("load", this, false); + tab.linkedBrowser.removeEventListener("MozAfterPaint", this, false); + var index = this._tabs.indexOf(tab); + if (index == -1) + throw new Error("internal error: tab not found"); + this._tabs.splice(index, 1); + try { + this._delegate.onUntrack(tab); + } catch (e) { + console.exception(e); + } + }, + _safeSelectTab: function safeSelectTab(tab) { + var index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onSelect) + this._delegate.onSelect(tab); + } catch (e) { + console.exception(e); + } + }, + _safeDOMContentLoaded: function safeDOMContentLoaded(event) { + let tabBrowser = event.currentTarget; + let tabBrowserIndex = tabBrowser.getBrowserIndexForDocument(event.target); + // TODO: I'm seeing this when loading data url images + if (tabBrowserIndex == -1) + return; + let tab = tabBrowser.tabContainer.getItemAtIndex(tabBrowserIndex); + let index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onReady) + this._delegate.onReady(tab); + } catch (e) { + console.exception(e); + } + }, + _safeLoad: function safeLoad(event) { + let tab = event.target; + let index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onLoad) + this._delegate.onLoad(tab); + } catch (e) { + console.exception(e); + } + }, + _safeMozAfterPaint: function safeMozAfterPaint(event) { + let win = event.currentTarget.ownerDocument.defaultView; + let tabIndex = win.gBrowser.getBrowserIndexForDocument(event.target.document); + if (tabIndex == -1) + return; + let tab = win.gBrowser.tabContainer.getItemAtIndex(tabIndex); + let index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onPaint) + this._delegate.onPaint(tab); + } catch (e) { + console.exception(e); + } + }, + handleEvent: function handleEvent(event) { + switch (event.type) { + case "TabOpen": + this._safeTrackTab(event.target); + break; + case "TabClose": + this._safeUntrackTab(event.target); + break; + case "TabSelect": + this._safeSelectTab(event.target); + break; + case "DOMContentLoaded": + this._safeDOMContentLoaded(event); + break; + case "load": + this._safeLoad(event); + break; + case "MozAfterPaint": + this._safeMozAfterPaint(event); + break; + default: + throw new Error("internal error: unknown event type: " + + event.type); + } + }, + onTrack: function onTrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeTrackTab(tab); + tabbrowser.tabContainer.addEventListener("TabOpen", this, false); + tabbrowser.tabContainer.addEventListener("TabClose", this, false); + tabbrowser.tabContainer.addEventListener("TabSelect", this, false); + tabbrowser.ownerDocument.defaultView.gBrowser.addEventListener("DOMContentLoaded", this, false); + }, + onUntrack: function onUntrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeUntrackTab(tab); + tabbrowser.tabContainer.removeEventListener("TabOpen", this, false); + tabbrowser.tabContainer.removeEventListener("TabClose", this, false); + tabbrowser.tabContainer.removeEventListener("TabSelect", this, false); + tabbrowser.ownerDocument.defaultView.gBrowser.removeEventListener("DOMContentLoaded", this, false); + }, + unload: function unload() { + this._tracker.unload(); + } +}; + +// Utility to get a thumbnail canvas from a tab object +function getThumbnailCanvasForTab(tabEl, window) { + var thumbnail = window.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + thumbnail.mozOpaque = true; + window = tabEl.linkedBrowser.contentWindow; + thumbnail.width = Math.ceil(window.screen.availWidth / 5.75); + var aspectRatio = 0.5625; // 16:9 + thumbnail.height = Math.round(thumbnail.width * aspectRatio); + var ctx = thumbnail.getContext("2d"); + var snippetWidth = window.innerWidth * .6; + var scale = thumbnail.width / snippetWidth; + ctx.scale(scale, scale); + ctx.drawWindow(window, window.scrollX, window.scrollY, snippetWidth, snippetWidth * aspectRatio, "rgb(255,255,255)"); + return thumbnail; +} + +// Utility to return the contents of the target of a chrome URL +function getChromeURLContents(chromeURL) { + let io = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let channel = io.newChannel(chromeURL, null, null); + let input = channel.open(); + let stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(input); + let str = stream.readBytes(input.available()); + stream.close(); + input.close(); + return str; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/events.js b/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/events.js new file mode 100644 index 0000000..96309bb --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/events.js @@ -0,0 +1,56 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const ON_PREFIX = "on"; +const TAB_PREFIX = "Tab"; + +const EVENTS = { + ready: "DOMContentLoaded", + open: "TabOpen", + close: "TabClose", + activate: "TabSelect", + deactivate: null +} +exports.EVENTS = EVENTS; + +Object.keys(EVENTS).forEach(function(name) { + EVENTS[name] = { + name: name, + listener: ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1), + dom: EVENTS[name] + } +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/observer.js b/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/observer.js new file mode 100644 index 0000000..f679917 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/observer.js @@ -0,0 +1,126 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { EventEmitterTrait: EventEmitter } = require("../events"); +const { DOMEventAssembler } = require("../events/assembler"); +const { Trait } = require("../light-traits"); +const { getActiveTab, getTabs, getTabContainers } = require("./utils"); +const { browserWindowIterator, isBrowser } = require("../window-utils"); +const { observer: windowObserver } = require("../windows/observer"); + +const EVENTS = { + "TabOpen": "open", + "TabClose": "close", + "TabSelect": "select", + "TabMove": "move", + "TabPinned": "pin", + "TabUnpinned": "unpin" +}; + + +// Event emitter objects used to register listeners and emit events on them +// when they occur. +const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ + /** + * Method is implemented by `EventEmitter` and is used just for emitting + * events on registered listeners. + */ + _emit: Trait.required, + /** + * Events that are supported and emitted by the module. + */ + supportedEventsTypes: Object.keys(EVENTS), + /** + * Function handles all the supported events on all the windows that are + * observed. Method is used to proxy events to the listeners registered on + * this event emitter. + * @param {Event} event + * Keyboard event being emitted. + */ + handleEvent: function handleEvent(event) { + this._emit(EVENTS[event.type], event.target, event); + } +}); + +// Currently gecko does not dispatches any event on the previously selected +// tab before / after "TabSelect" is dispatched. In order to work around this +// limitation we keep track of selected tab and emit "deactivate" event with +// that before emitting "activate" on selected tab. +var selectedTab = null; +function onTabSelect(tab) { + if (selectedTab !== tab) { + if (selectedTab) observer._emit("deactivate", selectedTab); + if (tab) observer._emit("activate", selectedTab = tab); + } +}; +observer.on("select", onTabSelect); + +// We also observe opening / closing windows in order to add / remove it's +// containers to the observed list. +function onWindowOpen(chromeWindow) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + getTabContainers(chromeWindow).forEach(function (container) { + observer.observe(container); + }); +} +windowObserver.on("open", onWindowOpen); + +function onWindowClose(chromeWindow) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + getTabContainers(chromeWindow).forEach(function (container) { + observer.ignore(container); + }); +} +windowObserver.on("close", onWindowClose); + + +// Currently gecko does not dispatches "TabSelect" events when different +// window gets activated. To work around this limitation we emulate "select" +// event for this case. +windowObserver.on("activate", function onWindowActivate(chromeWindow) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + observer._emit("select", getActiveTab(chromeWindow)); +}); + +// We should synchronize state, since probably we already have at least one +// window open. +for each (let window in browserWindowIterator()) onWindowOpen(window); + +exports.observer = observer; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/tab.js b/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/tab.js new file mode 100644 index 0000000..17e2ff6 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/tab.js @@ -0,0 +1,297 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const { Ci } = require('chrome'); +const { Trait } = require("../traits"); +const { EventEmitter } = require("../events"); +const { validateOptions } = require("../api-utils"); +const { Enqueued } = require("../utils/function"); +const { EVENTS } = require("./events"); +const { getThumbnailURIForWindow } = require("../utils/thumbnail"); +const { getFaviconURIForLocation } = require("../utils/data"); + + + +// Array of the inner instances of all the wrapped tabs. +const TABS = []; + +/** + * Trait used to create tab wrappers. + */ +const TabTrait = Trait.compose(EventEmitter, { + on: Trait.required, + _emit: Trait.required, + /** + * Tab DOM element that is being wrapped. + */ + _tab: null, + /** + * Window wrapper whose tab this object represents. + */ + window: null, + constructor: function Tab(options) { + this._onReady = this._onReady.bind(this); + this._tab = options.tab; + let window = this.window = options.window; + // Setting event listener if was passed. + for each (let type in EVENTS) { + let listener = options[type.listener]; + if (listener) + this.on(type.name, options[type.listener]); + if ('ready' != type.name) // window spreads this event. + window.tabs.on(type.name, this._onEvent.bind(this, type.name)); + } + + this.on(EVENTS.close.name, this.destroy.bind(this)); + this._browser.addEventListener(EVENTS.ready.dom, this._onReady, true); + + if (options.isPinned) + this.pin(); + + // Since we will have to identify tabs by a DOM elements facade function + // is used as constructor that collects all the instances and makes sure + // that they more then one wrapper is not created per tab. + return this; + }, + destroy: function destroy() { + for each (let type in EVENTS) + this._removeAllListeners(type.name); + this._browser.removeEventListener(EVENTS.ready.dom, this._onReady, + true); + }, + + /** + * Internal listener that emits public event 'ready' when the page of this + * tab is loaded. + */ + _onReady: function _onReady(event) { + // IFrames events will bubble so we need to ignore those. + if (event.target == this._contentDocument) + this._emit(EVENTS.ready.name, this._public); + }, + /** + * Internal tab event router. Window will emit tab related events for all it's + * tabs, this listener will propagate all the events for this tab to it's + * listeners. + */ + _onEvent: function _onEvent(type, tab) { + if (tab == this._public) + this._emit(type, tab); + }, + /** + * Browser DOM element where page of this tab is currently loaded. + */ + get _browser() this._window.gBrowser.getBrowserForTab(this._tab), + /** + * Window DOM element containing this tab. + */ + get _window() this._tab.ownerDocument.defaultView, + /** + * Document object of the page that is currently loaded in this tab. + */ + get _contentDocument() this._browser.contentDocument, + /** + * Window object of the page that is currently loaded in this tab. + */ + get _contentWindow() this._browser.contentWindow, + + /** + * The title of the page currently loaded in the tab. + * Changing this property changes an actual title. + * @type {String} + */ + get title() this._contentDocument.title, + set title(value) this._contentDocument.title = String(value), + /** + * Location of the page currently loaded in this tab. + * Changing this property will loads page under under the specified location. + * @type {String} + */ + get url() String(this._browser.currentURI.spec), + set url(value) this._changeLocation(String(value)), + // "TabOpen" event is fired when it's still "about:blank" is loaded in the + // changing `location` property of the `contentDocument` has no effect since + // seems to be either ignored or overridden by internal listener, there for + // location change is enqueued for the next turn of event loop. + _changeLocation: Enqueued(function(url) this._browser.loadURI(url)), + /** + * URI of the favicon for the page currently loaded in this tab. + * @type {String} + */ + get favicon() getFaviconURIForLocation(this.url), + /** + * The CSS style for the tab + */ + get style() null, // TODO + /** + * The index of the tab relative to other tabs in the application window. + * Changing this property will change order of the actual position of the tab. + * @type {Number} + */ + get index() + this._window.gBrowser.getBrowserIndexForDocument(this._contentDocument), + set index(value) this._window.gBrowser.moveTabTo(this._tab, value), + /** + * Thumbnail data URI of the page currently loaded in this tab. + * @type {String} + */ + getThumbnail: function getThumbnail() + getThumbnailURIForWindow(this._contentWindow), + /** + * Whether or not tab is pinned (Is an app-tab). + * @type {Boolean} + */ + get isPinned() this._tab.pinned, + pin: function pin() { + this._window.gBrowser.pinTab(this._tab); + }, + unpin: function unpin() { + this._window.gBrowser.unpinTab(this._tab); + }, + + /** + * Create a worker for this tab, first argument is options given to Worker. + * @type {Worker} + */ + attach: function attach(options) { + let { Worker } = require("../content/worker"); + options.window = this._contentWindow; + let worker = Worker(options); + worker.once("detach", function detach() { + worker.destroy(); + }); + return worker; + }, + + /** + * Make this tab active. + * Please note: That this function is called synchronous since in E10S that + * will be the case. Besides this function is called from a constructor where + * we would like to return instance before firing a 'TabActivated' event. + */ + activate: Enqueued(function activate() { + if (this._window) // Ignore if window is closed by the time this is invoked. + this._window.gBrowser.selectedTab = this._tab; + }), + /** + * Close the tab + */ + close: function close(callback) { + if (callback) + this.once(EVENTS.close.name, callback); + this._window.gBrowser.removeTab(this._tab); + }, + /** + * Reload the tab + */ + reload: function reload() { + this._window.gBrowser.reloadTab(this._tab); + } +}); + +function Tab(options) { + let chromeTab = options.tab; + for each (let tab in TABS) { + if (chromeTab == tab._tab) + return tab._public; + } + let tab = TabTrait(options); + TABS.push(tab); + return tab._public; +} +Tab.prototype = TabTrait.prototype; +exports.Tab = Tab; + +function Options(options) { + if ("string" === typeof options) + options = { url: options }; + + return validateOptions(options, { + url: { is: ["string"] }, + inBackground: { is: ["undefined", "boolean"] }, + isPinned: { is: ["undefined", "boolean"] }, + onOpen: { is: ["undefined", "function"] }, + onClose: { is: ["undefined", "function"] }, + onReady: { is: ["undefined", "function"] }, + onActivate: { is: ["undefined", "function"] }, + onDeactivate: { is: ["undefined", "function"] } + }); +} +exports.Options = Options; + + +exports.getTabForWindow = function (win) { + // Get browser window + let topWindow = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + if (!topWindow.gBrowser) return null; + + // Get top window object, in case we are in a content iframe + let topContentWindow; + try { + topContentWindow = win.top; + } catch(e) { + // It may throw if win is not a valid content window + return null; + } + + function getWindowID(obj) { + return obj.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID; + } + + // Search for related Tab + let topWindowId = getWindowID(topContentWindow); + for (let i = 0; i < topWindow.gBrowser.browsers.length; i++) { + let w = topWindow.gBrowser.browsers[i].contentWindow; + if (getWindowID(w) == topWindowId) { + return Tab({ + // TODO: api-utils should not depend on addon-kit! + window: require("addon-kit/windows").BrowserWindow({ window: topWindow }), + tab: topWindow.gBrowser.tabs[i] + }); + } + } + + // We were unable to find the related tab! + return null; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/utils.js b/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/utils.js new file mode 100644 index 0000000..030956c --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/tabs/utils.js @@ -0,0 +1,87 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +function getTabContainer(tabBrowser) { + return tabBrowser.tabContainer; +} +exports.getTabContainer = getTabContainer; + +function getTabBrowsers(window) { + return Array.slice(window.document.getElementsByTagName("tabbrowser")); +} +exports.getTabBrowsers = getTabBrowsers; + +function getTabContainers(window) { + return getTabBrowsers(window).map(getTabContainer); +} +exports.getTabContainers = getTabContainers; + +function getTabs(window) { + return getTabContainers(window).reduce(function (tabs, container) { + tabs.push.apply(tabs, container.children); + return tabs; + }, []); +} +exports.getTabs = getTabs; + +function getActiveTab(window) { + return window.gBrowser.selectedTab; +} +exports.getActiveTab = getActiveTab; + +function getOwnerWindow(tab) { + return tab.ownerDocument.defaultView; +} +exports.getOwnerWindow = getOwnerWindow; + +function openTab(window, url) { + return window.gBrowser.addTab(url); +} +exports.openTab = openTab; + +function closeTab(tab) { + return getOwnerWindow(tab).gBrowser.removeTab(tab); +} +exports.closeTab = closeTab; + +function activateTab(tab) { + getOwnerWindow(tab).gBrowser.selectedTab = tab; +} +exports.activateTab = activateTab; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/test.js b/tools/addon-sdk-1.4/packages/api-utils/lib/test.js new file mode 100644 index 0000000..6490630 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/test.js @@ -0,0 +1,140 @@ +/* vim:ts=2:sts=2:sw=2: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const BaseAssert = require("./test/assert").Assert; +const { isFunction, isObject } = require("./type"); + +function extend(target) { + let descriptor = {} + Array.slice(arguments, 1).forEach(function(source) { + Object.getOwnPropertyNames(source).forEach(function onEach(name) { + descriptor[name] = Object.getOwnPropertyDescriptor(source, name); + }); + }); + return Object.create(target, descriptor); +} + +/** + * Function takes test `suite` object in CommonJS format and defines all of the + * tests from that suite and nested suites in a jetpack format on a given + * `target` object. Optionally third argument `prefix` can be passed to prefix + * all the test names. + */ +function defineTestSuite(target, suite, prefix) { + prefix = prefix || ""; + // If suite defines `Assert` that's what `assert` object have to be created + // from and passed to a test function (This allows custom assertion functions) + // See for details: http://wiki.commonjs.org/wiki/Unit_Testing/1.1 + let Assert = suite.Assert || BaseAssert; + // Going through each item in the test suite and wrapping it into a + // Jetpack test format. + Object.keys(suite).forEach(function(key) { + // If name starts with test then it's a test function or suite. + if (key.indexOf("test") === 0) { + let test = suite[key]; + + // For each test function so we create a wrapper test function in a + // jetpack format and copy that to a `target` exports. + if (isFunction(test)) { + + // Since names of the test may match across suites we use full object + // path as a name to avoid overriding same function. + target[prefix + key] = function(options) { + + // Creating `assert` functions for this test. + let assert = Assert(options); + + // If CommonJS test function expects more than one argument + // it means that test is async and second argument is a callback + // to notify that test is finished. + if (1 < test.length) { + + // Letting test runner know that test is executed async and + // creating a callback function that CommonJS tests will call + // once it's done. + options.waitUntilDone(); + test(assert, function() { + options.done(); + }); + } + + // Otherwise CommonJS test is synchronous so we call it only with + // one argument. + else { + test(assert); + } + } + } + + // If it's an object then it's a test suite containing test function + // and / or nested test suites. In that case we just extend prefix used + // and call this function to copy and wrap tests from nested suite. + else if (isObject(test)) { + // We need to clone `tests` instead of modifying it, since it's very + // likely that it is frozen (usually test suites imported modules). + test = extend(Object.prototype, test, { + Assert: test.Assert || Assert + }); + defineTestSuite(target, test, prefix + key + "."); + } + } + }); +} + +/** + * This function is a CommonJS test runner function, but since Jetpack test + * runner and test format is different from CommonJS this function shims given + * `exports` with all its tests into a Jetpack test format so that the built-in + * test runner will be able to run CommonJS test without manual changes. + */ +exports.run = function run(exports) { + + // We can't leave old properties on exports since those are test in a CommonJS + // format that why we move everything to a new `suite` object. + let suite = {}; + Object.keys(exports).forEach(function(key) { + suite[key] = exports[key]; + delete exports[key]; + }); + + // Now we wrap all the CommonJS tests to a Jetpack format and define + // those to a given `exports` object since that where jetpack test runner + // will look for them. + defineTestSuite(exports, suite); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/test/assert.js b/tools/addon-sdk-1.4/packages/api-utils/lib/test/assert.js new file mode 100644 index 0000000..0c29d62 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/test/assert.js @@ -0,0 +1,360 @@ +/* vim:ts=2:sts=2:sw=2: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const { isFunction, isNull, isObject, isString, isRegExp, isArray, isDate, + isPrimitive, isUndefined, instanceOf, source } = require("../type"); + +/** + * The `AssertionError` is defined in assert. + * @extends Error + * @example + * new assert.AssertionError({ + * message: message, + * actual: actual, + * expected: expected + * }) + */ +function AssertionError(options) { + let assertionError = Object.create(AssertionError.prototype); + + if (isString(options)) + options = { message: options }; + if ("actual" in options) + assertionError.actual = options.actual; + if ("expected" in options) + assertionError.expected = options.expected; + if ("operator" in options) + assertionError.operator = options.operator; + + assertionError.message = options.message; + assertionError.stack = new Error().stack; + return assertionError; +} +AssertionError.prototype = Object.create(Error.prototype, { + constructor: { value: AssertionError }, + name: { value: "AssertionError", enumerable: true }, + toString: { value: function toString() { + let value; + if (this.message) { + value = this.name + " : " + this.message; + } + else { + value = [ + this.name + " : ", + source(this.expected), + this.operator, + source(this.actual) + ].join(" "); + } + return value; + }} +}); +exports.AssertionError = AssertionError; + +function Assert(logger) { + return Object.create(Assert.prototype, { _log: { value: logger }}); +} +Assert.prototype = { + fail: function fail(e) { + this._log.fail(e.message); + }, + pass: function pass(message) { + this._log.pass(message); + }, + error: function error(e) { + this._log.exception(e); + }, + ok: function ok(value, message) { + if (!!!value) { + this.fail({ + actual: value, + expected: true, + message: message, + operator: "==" + }); + } + else { + this.pass(message); + } + }, + + /** + * The equality assertion tests shallow, coercive equality with `==`. + * @example + * assert.equal(1, 1, "one is one"); + */ + equal: function equal(actual, expected, message) { + if (actual == expected) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "==" + }); + } + }, + + /** + * The non-equality assertion tests for whether two objects are not equal + * with `!=`. + * @example + * assert.notEqual(1, 2, "one is not two"); + */ + notEqual: function notEqual(actual, expected, message) { + if (actual != expected) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "!=", + }); + } + }, + + /** + * The equivalence assertion tests a deep (with `===`) equality relation. + * @example + * assert.deepEqual({ a: "foo" }, { a: "foo" }, "equivalent objects") + */ + deepEqual: function deepEqual(actual, expected, message) { + if (isDeepEqual(actual, expected)) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "deepEqual" + }); + } + }, + + /** + * The non-equivalence assertion tests for any deep (with `===`) inequality. + * @example + * assert.notDeepEqual({ a: "foo" }, Object.create({ a: "foo" }), + * "object's inherit from different prototypes"); + */ + notDeepEqual: function notDeepEqual(actual, expected, message) { + if (!isDeepEqual(actual, expected)) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "notDeepEqual" + }); + } + }, + + /** + * The strict equality assertion tests strict equality, as determined by + * `===`. + * @example + * assert.strictEqual(null, null, "`null` is `null`") + */ + strictEqual: function strictEqual(actual, expected, message) { + if (actual === expected) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "===" + }); + } + }, + + /** + * The strict non-equality assertion tests for strict inequality, as + * determined by `!==`. + * @example + * assert.notStrictEqual(null, undefined, "`null` is not `undefined`"); + */ + notStrictEqual: function notStrictEqual(actual, expected, message) { + if (actual !== expected) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "!==" + }) + } + }, + + /** + * The assertion whether or not given `block` throws an exception. If optional + * `Error` argument is provided and it's type of function thrown error is + * asserted to be an instance of it, if type of `Error` is string then message + * of throw exception is asserted to contain it. + * @param {Function} block + * Function that is expected to throw. + * @param {Error|RegExp} [Error] + * Error constructor that is expected to be thrown or a string that + * must be contained by a message of the thrown exception, or a RegExp + * matching a message of the thrown exception. + * @param {String} message + * Description message + * + * @examples + * + * assert.throws(function block() { + * doSomething(4) + * }, "Object is expected", "Incorrect argument is passed"); + * + * assert.throws(function block() { + * Object.create(5) + * }, TypeError, "TypeError is thrown"); + */ + throws: function throws(block, Error, message) { + let threw = false; + let exception = null; + + // If third argument is not provided and second argument is a string it + // means that optional `Error` argument was not passed, so we shift + // arguments. + if (isString(Error) && isUndefined(message)) { + message = Error; + Error = undefined; + } + + // Executing given `block`. + try { + block(); + } + catch (e) { + threw = true; + exception = e; + } + + // If exception was thrown and `Error` argument was not passed assert is + // passed. + if (threw && (isUndefined(Error) || + // If passed `Error` is RegExp using it's test method to + // assert thrown exception message. + (isRegExp(Error) && Error.test(exception.message)) || + // If passed `Error` is a constructor function testing if + // thrown exception is an instance of it. + (isFunction(Error) && instanceOf(exception, Error)))) + { + this.pass(message); + } + + // Otherwise we report assertion failure. + else { + let failure = { + message: message, + operator: "throws" + }; + + if (exception) + failure.actual = exception; + + if (Error) + failure.expected = Error; + + this.fail(failure); + } + } +}; +exports.Assert = Assert; + +function isDeepEqual(actual, expected) { + + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + } + + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + else if (isDate(actual) && isDate(expected)) { + return actual.getTime() === expected.getTime(); + } + + // XXX specification bug: this should be specified + else if (isPrimitive(actual) || isPrimitive(expected)) { + return expected === actual; + } + + // 7.3. Other pairs that do not both pass typeof value == "object", + // equivalence is determined by ==. + else if (!isObject(actual) && !isObject(expected)) { + return actual == expected; + } + + // 7.4. For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical "prototype" property. Note: this + // accounts for both named and indexed properties on Arrays. + else { + return actual.prototype === expected.prototype && + isEquivalent(actual, expected); + } +} + +function isEquivalent(a, b, stack) { + return isArrayEquivalent(Object.keys(a).sort(), + Object.keys(b).sort()) && + Object.keys(a).every(function(key) { + return isDeepEqual(a[key], b[key], stack) + }); +} + +function isArrayEquivalent(a, b, stack) { + return isArray(a) && isArray(b) && + a.every(function(value, index) { + return isDeepEqual(value, b[index]); + }); +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/text-streams.js b/tools/addon-sdk-1.4/packages/api-utils/lib/text-streams.js new file mode 100644 index 0000000..87d375d --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/text-streams.js @@ -0,0 +1,273 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci,Cu,components} = require("chrome"); +var NetUtil = {}; +Cu.import("resource://gre/modules/NetUtil.jsm", NetUtil); +NetUtil = NetUtil.NetUtil; + +// NetUtil.asyncCopy() uses this buffer length, and since we call it, for best +// performance we use it, too. +const BUFFER_BYTE_LEN = 0x8000; +const PR_UINT32_MAX = 0xffffffff; +const DEFAULT_CHARSET = "UTF-8"; + +exports.TextReader = TextReader; +exports.TextWriter = TextWriter; + +/** + * An input stream that reads text from a backing stream using a given text + * encoding. + * + * @param inputStream + * The stream is backed by this nsIInputStream. It must already be + * opened. + * @param charset + * Text in inputStream is expected to be in this character encoding. If + * not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl for + * documentation on how to determine other valid values for this. + */ +function TextReader(inputStream, charset) { + const self = this; + charset = checkCharset(charset); + + let stream = Cc["@mozilla.org/intl/converter-input-stream;1"]. + createInstance(Ci.nsIConverterInputStream); + stream.init(inputStream, charset, BUFFER_BYTE_LEN, + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + + let manager = new StreamManager(this, stream); + + /** + * Reads a string from the stream. If the stream is closed, an exception is + * thrown. + * + * @param numChars + * The number of characters to read. If not given, the remainder of + * the stream is read. + * @return The string read. If the stream is already at EOS, returns the + * empty string. + */ + this.read = function TextReader_read(numChars) { + manager.ensureOpened(); + + let readAll = false; + if (typeof(numChars) === "number") + numChars = Math.max(numChars, 0); + else + readAll = true; + + let str = ""; + let totalRead = 0; + let chunkRead = 1; + + // Read in numChars or until EOS, whichever comes first. Note that the + // units here are characters, not bytes. + while (true) { + let chunk = {}; + let toRead = readAll ? + PR_UINT32_MAX : + Math.min(numChars - totalRead, PR_UINT32_MAX); + if (toRead <= 0 || chunkRead <= 0) + break; + + // The converter stream reads in at most BUFFER_BYTE_LEN bytes in a call + // to readString, enough to fill its byte buffer. chunkRead will be the + // number of characters encoded by the bytes in that buffer. + chunkRead = stream.readString(toRead, chunk); + str += chunk.value; + totalRead += chunkRead; + } + + return str; + }; +} + +/** + * A buffered output stream that writes text to a backing stream using a given + * text encoding. + * + * @param outputStream + * The stream is backed by this nsIOutputStream. It must already be + * opened. + * @param charset + * Text will be written to outputStream using this character encoding. + * If not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl + * for documentation on how to determine other valid values for this. + */ +function TextWriter(outputStream, charset) { + const self = this; + charset = checkCharset(charset); + + let stream = outputStream; + + // Buffer outputStream if it's not already. + let ioUtils = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil); + if (!ioUtils.outputStreamIsBuffered(outputStream)) { + stream = Cc["@mozilla.org/network/buffered-output-stream;1"]. + createInstance(Ci.nsIBufferedOutputStream); + stream.init(outputStream, BUFFER_BYTE_LEN); + } + + // I'd like to use nsIConverterOutputStream. But NetUtil.asyncCopy(), which + // we use below in writeAsync(), naturally expects its sink to be an instance + // of nsIOutputStream, which nsIConverterOutputStream's only implementation is + // not. So we use uconv and manually convert all strings before writing to + // outputStream. + let uconv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + uconv.charset = charset; + + let manager = new StreamManager(this, stream); + + /** + * Flushes the backing stream's buffer. + */ + this.flush = function TextWriter_flush() { + manager.ensureOpened(); + stream.flush(); + }; + + /** + * Writes a string to the stream. If the stream is closed, an exception is + * thrown. + * + * @param str + * The string to write. + */ + this.write = function TextWriter_write(str) { + manager.ensureOpened(); + let istream = uconv.convertToInputStream(str); + let len = istream.available(); + while (len > 0) { + stream.writeFrom(istream, len); + len = istream.available(); + } + istream.close(); + }; + + /** + * Writes a string on a background thread. After the write completes, the + * backing stream's buffer is flushed, and both the stream and the backing + * stream are closed, also on the background thread. If the stream is already + * closed, an exception is thrown immediately. + * + * @param str + * The string to write. + * @param callback + * An optional function. If given, it's called as callback(error) when + * the write completes. error is an Error object or undefined if there + * was no error. Inside callback, |this| is the stream object. + */ + this.writeAsync = function TextWriter_writeAsync(str, callback) { + manager.ensureOpened(); + let istream = uconv.convertToInputStream(str); + NetUtil.asyncCopy(istream, stream, function (result) { + let err = components.isSuccessCode(result) ? undefined : + new Error("An error occured while writing to the stream: " + result); + if (err) + console.error(err); + + // asyncCopy() closes its output (and input) stream. + manager.opened = false; + + if (typeof(callback) === "function") { + try { + callback.call(self, err); + } + catch (exc) { + console.exception(exc); + } + } + }); + }; +} + +// This manages the lifetime of stream, a TextReader or TextWriter. It defines +// closed and close() on stream and registers an unload listener that closes +// rawStream if it's still opened. It also provides ensureOpened(), which +// throws an exception if the stream is closed. +function StreamManager(stream, rawStream) { + const self = this; + this.rawStream = rawStream; + this.opened = true; + + /** + * True iff the stream is closed. + */ + stream.__defineGetter__("closed", function stream_closed() { + return !self.opened; + }); + + /** + * Closes both the stream and its backing stream. If the stream is already + * closed, an exception is thrown. For TextWriters, this first flushes the + * backing stream's buffer. + */ + stream.close = function stream_close() { + self.ensureOpened(); + self.unload(); + }; + + require("./unload").ensure(this); +} + +StreamManager.prototype = { + ensureOpened: function StreamManager_ensureOpened() { + if (!this.opened) + throw new Error("The stream is closed and cannot be used."); + }, + unload: function StreamManager_unload() { + // TextWriter.writeAsync() causes rawStream to close and therefore sets + // opened to false, so check that we're still opened. + if (this.opened) { + // Calling close() on both an nsIUnicharInputStream and + // nsIBufferedOutputStream closes their backing streams. It also forces + // nsIOutputStreams to flush first. + this.rawStream.close(); + this.opened = false; + } + } +}; + +function checkCharset(charset) { + return typeof(charset) === "string" ? charset : DEFAULT_CHARSET; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/timer.js b/tools/addon-sdk-1.4/packages/api-utils/lib/timer.js new file mode 100644 index 0000000..052f506 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/timer.js @@ -0,0 +1,141 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * Drew Willcoxon <adw@mozilla.com> + * Irakli Gozalishvili <gozala@mozilla.com> + * Erik Vold <erikvvold@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci} = require("chrome"); +var xpcom = require("./xpcom"); + +var timerClass = Cc["@mozilla.org/timer;1"]; +var nextID = 1; +var timers = {}; + +function TimerCallback(timerID, callback, params) { + this._callback = callback; + this._params = params; +}; +TimerCallback.prototype = { + QueryInterface : xpcom.utils.generateQI([Ci.nsITimerCallback]) +}; + +function TimeoutCallback(timerID, callback, params) { + memory.track(this); + TimerCallback.apply(this, arguments) + this._timerID = timerID; +}; +TimeoutCallback.prototype = new TimerCallback(); +TimeoutCallback.prototype.notify = function notifyOnTimeout(timer) { + try { + delete timers[this._timerID]; + this._callback.apply(null, this._params); + } catch (e) { + console.exception(e); + } +}; + +function IntervalCallback(timerID, callback, params) { + memory.track(this); + TimerCallback.apply(this, arguments) +}; +IntervalCallback.prototype = new TimerCallback(); +IntervalCallback.prototype.notify = function notifyOnInterval() { + try { + this._callback.apply(null, this._params); + } catch (e) { + console.exception(e); + } +}; + + +var setTimeout = exports.setTimeout = function setTimeout(callback, delay) { + return makeTimer( + Ci.nsITimer.TYPE_ONE_SHOT, + callback, + TimeoutCallback, + delay, + Array.slice(arguments, 2)); +}; + +var clearTimeout = exports.clearTimeout = function clearTimeout(timerID) { + cancelTimer(timerID); +}; + +var setInterval = exports.setInterval = function setInterval(callback, delay) { + return makeTimer( + Ci.nsITimer.TYPE_REPEATING_SLACK, + callback, + IntervalCallback, + delay, + Array.slice(arguments, 2)); +}; + +var clearInterval = exports.clearInterval = function clearInterval(timerID) { + cancelTimer(timerID); +}; + +function makeTimer(type, callback, callbackType, delay, params) { + var timer = timerClass.createInstance(Ci.nsITimer); + + memory.track(timer, "nsITimer"); + + var timerID = nextID++; + timers[timerID] = timer; + + timer.initWithCallback( + new callbackType(timerID, callback, params), + delay || 0, + type + ); + return timerID; +} + +function cancelTimer(timerID) { + var timer = timers[timerID]; + if (timer) { + timer.cancel(); + delete timers[timerID]; + } +} + +require("./unload").when( + function cancelAllPendingTimers() { + var timerIDs = [timerID for (timerID in timers)]; + timerIDs.forEach(function(timerID) { cancelTimer(timerID); }); + }); + diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/traceback.js b/tools/addon-sdk-1.4/packages/api-utils/lib/traceback.js new file mode 100644 index 0000000..fdedd99 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/traceback.js @@ -0,0 +1,155 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci,components} = require("chrome"); + +// Undo the auto-parentification of URLs done in bug 418356. +function deParentifyURL(url) { + return url ? url.split(" -> ").slice(-1)[0] : url; +} + +// TODO: We might want to move this function to url or some similar +// module. +function getLocalFile(path) { + var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + var channel = ios.newChannel(path, null, null); + var iStream = channel.open(); + var siStream = Cc['@mozilla.org/scriptableinputstream;1'] + .createInstance(Ci.nsIScriptableInputStream); + siStream.init(iStream); + var data = new String(); + data += siStream.read(-1); + siStream.close(); + iStream.close(); + return data; +} + +function safeGetFileLine(path, line) { + try { + var scheme = require("./url").URL(path).scheme; + // TODO: There should be an easier, more accurate way to figure out + // what's the case here. + if (!(scheme == "http" || scheme == "https")) + return getLocalFile(path).split("\n")[line - 1]; + } catch (e) {} + return null; +} + +function errorStackToJSON(stack) { + var lines = stack.split("\n"); + + var frames = []; + lines.forEach( + function(line) { + if (!line) + return; + var atIndex = line.indexOf("@"); + var colonIndex = line.lastIndexOf(":"); + var filename = deParentifyURL(line.slice(atIndex + 1, colonIndex)); + var lineNo = parseInt(line.slice(colonIndex + 1)); + var funcSig = line.slice(0, atIndex); + var funcName = funcSig.slice(0, funcSig.indexOf("(")); + frames.unshift({filename: filename, + funcName: funcName, + lineNo: lineNo}); + }); + + return frames; +}; + +function nsIStackFramesToJSON(frame) { + var stack = []; + + while (frame) { + if (frame.filename) { + var filename = deParentifyURL(frame.filename); + stack.splice(0, 0, {filename: filename, + lineNo: frame.lineNumber, + funcName: frame.name}); + } + frame = frame.caller; + } + + return stack; +}; + +var fromException = exports.fromException = function fromException(e) { + if (e instanceof Ci.nsIException) + return nsIStackFramesToJSON(e.location); + if (e.stack && e.stack.length) + return errorStackToJSON(e.stack); + if (e.fileName && typeof(e.lineNumber == "number")) + return [{filename: deParentifyURL(e.fileName), + lineNo: e.lineNumber, + funcName: null}]; + return []; +}; + +var get = exports.get = function get() { + return nsIStackFramesToJSON(components.stack.caller); +}; + +var format = exports.format = function format(tbOrException) { + if (tbOrException === undefined) { + tbOrException = get(); + tbOrException.splice(-1, 1); + } + + var tb; + if (typeof(tbOrException) == "object" && + tbOrException.constructor.name == "Array") + tb = tbOrException; + else + tb = fromException(tbOrException); + + var lines = ["Traceback (most recent call last):"]; + + tb.forEach( + function(frame) { + if (!(frame.filename || frame.lineNo || frame.funcName)) + return; + lines.push(' File "' + frame.filename + '", line ' + + frame.lineNo + ', in ' + frame.funcName); + var sourceLine = safeGetFileLine(frame.filename, frame.lineNo); + if (sourceLine) + lines.push(' ' + sourceLine.trim()); + }); + + return lines.join("\n"); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/traits.js b/tools/addon-sdk-1.4/packages/api-utils/lib/traits.js new file mode 100644 index 0000000..59193aa --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/traits.js @@ -0,0 +1,215 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { + compose: _compose, + override: _override, + resolve: _resolve, + trait: _trait, + //create: _create, + required, +} = require('./traits/core'); + +const defineProperties = Object.defineProperties, + freeze = Object.freeze, + create = Object.create; + +/** + * Work around bug 608959 by defining the _create function here instead of + * importing it from traits/core. For docs on this function, see the create + * function in that module. + * + * FIXME: remove this workaround in favor of importing the function once that + * bug has been fixed. + */ +function _create(proto, trait) { + let properties = {}, + keys = Object.getOwnPropertyNames(trait); + for each(let key in keys) { + let descriptor = trait[key]; + if (descriptor.required && + !Object.prototype.hasOwnProperty.call(proto, key)) + throw new Error('Missing required property: ' + key); + else if (descriptor.conflict) + throw new Error('Remaining conflicting property: ' + key); + else + properties[key] = descriptor; + } + return Object.create(proto, properties); +} + +/** + * Placeholder for `Trait.prototype` + */ +let TraitProto = Object.prototype; + +function Get(key) this[key] +function Set(key, value) this[key] = value + +/** + * Creates anonymous trait descriptor from the passed argument, unless argument + * is a trait constructor. In later case trait's already existing properties + * descriptor is returned. + * This is module's internal function and is used as a gateway to a trait's + * internal properties descriptor. + * @param {Function} $ + * Composed trait's constructor. + * @returns {Object} + * Private trait property of the composition. + */ +function TraitDescriptor(object) + ( + 'function' == typeof object && + (object.prototype == TraitProto || object.prototype instanceof Trait) + ) ? object._trait(TraitDescriptor) : _trait(object) + +function Public(instance, trait) { + let result = {}, + keys = Object.getOwnPropertyNames(trait); + for each (let key in keys) { + if ('_' === key.charAt(0) && '__iterator__' !== key ) + continue; + let property = trait[key], + descriptor = { + configurable: property.configurable, + enumerable: property.enumerable + }; + if (property.get) + descriptor.get = property.get.bind(instance); + if (property.set) + descriptor.set = property.set.bind(instance); + if ('value' in property) { + let value = property.value; + if ('function' === typeof value) { + descriptor.value = property.value.bind(instance); + descriptor.writable = property.writable; + } else { + descriptor.get = Get.bind(instance, key); + descriptor.set = Set.bind(instance, key); + } + } + result[key] = descriptor; + } + return result; +} + +/** + * This is private function that composes new trait with privates. + */ +function Composition(trait) { + function Trait() { + let self = _create({}, trait); + self._public = create(Trait.prototype, Public(self, trait)); + delete self._public.constructor; + if (Object === self.constructor) + self.constructor = Trait; + else + return self.constructor.apply(self, arguments) || self._public; + return self._public; + } + defineProperties(Trait, { + prototype: { value: freeze(create(TraitProto, { + constructor: { value: constructor, writable: true } + }))}, // writable is `true` to avoid getters in custom ES5 + displayName: { value: (trait.constructor || constructor).name }, + compose: { value: compose, enumerable: true }, + override: { value: override, enumerable: true }, + resolve: { value: resolve, enumerable: true }, + required: { value: required, enumerable: true }, + _trait: { value: function _trait(caller) + caller === TraitDescriptor ? trait : undefined + } + }); + return freeze(Trait); +} + +/** + * Composes new trait out of itself and traits / property maps passed as an + * arguments. If two or more traits / property maps have properties with the + * same name, the new trait will contain a "conflict" property for that name. + * This is a commutative and associative operation, and the order of its + * arguments is not significant. + * @params {Object|Function} + * List of Traits or property maps to create traits from. + * @returns {Function} + * New trait containing the combined properties of all the traits. + */ +function compose() { + let traits = Array.slice(arguments, 0); + traits.push(this); + return Composition(_compose.apply(null, traits.map(TraitDescriptor))); +} + +/** + * Composes a new trait with all of the combined properties of `this` and the + * argument traits. In contrast to `compose`, `override` immediately resolves + * all conflicts resulting from this composition by overriding the properties of + * later traits. Trait priority is from left to right. I.e. the properties of + * the leftmost trait are never overridden. + * @params {Object} trait + * @returns {Object} + */ +function override() { + let traits = Array.slice(arguments, 0); + traits.push(this); + return Composition(_override.apply(null, traits.map(TraitDescriptor))); +} + +/** + * Composes new resolved trait, with all the same properties as this + * trait, except that all properties whose name is an own property of + * `resolutions` will be renamed to `resolutions[name]`. If it is + * `resolutions[name]` is `null` value is changed into a required property + * descriptor. + */ +function resolve(resolutions) + Composition(_resolve(resolutions, TraitDescriptor(this))) + +/** + * Base Trait, that all the traits are composed of. + */ +const Trait = Composition({ + /** + * Internal property holding public API of this instance. + */ + _public: { value: null, configurable: true, writable: true }, + toString: { value: function() '[object ' + this.constructor.name + ']' } +}); +TraitProto = Trait.prototype; +exports.Trait = Trait; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/traits/core.js b/tools/addon-sdk-1.4/packages/api-utils/lib/traits/core.js new file mode 100644 index 0000000..bc8ae14 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/traits/core.js @@ -0,0 +1,349 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; +// Design inspired by: http://www.traitsjs.org/ + +// shortcuts +const getOwnPropertyNames = Object.getOwnPropertyNames, + getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor, + hasOwn = Object.prototype.hasOwnProperty, + _create = Object.create; + +function doPropertiesMatch(object1, object2, name) { + // If `object1` has property with the given `name` + return name in object1 ? + // then `object2` should have it with the same value. + name in object2 && object1[name] === object2[name] : + // otherwise `object2` should not have property with the given `name`. + !(name in object2); +} + +/** + * Compares two trait custom property descriptors if they are the same. If + * both are `conflict` or all the properties of descriptor are equal returned + * value will be `true`, otherwise it will be `false`. + * @param {Object} desc1 + * @param {Object} desc2 + */ +function areSame(desc1, desc2) { + return ('conflict' in desc1 && desc1.conflict && + 'conflict' in desc2 && desc2.conflict) || + (doPropertiesMatch(desc1, desc2, 'get') && + doPropertiesMatch(desc1, desc2, 'set') && + doPropertiesMatch(desc1, desc2, 'value') && + doPropertiesMatch(desc1, desc2, 'enumerable') && + doPropertiesMatch(desc1, desc2, 'required') && + doPropertiesMatch(desc1, desc2, 'conflict')); +} + +/** + * Converts array to an object whose own property names represent + * values of array. + * @param {String[]} names + * @returns {Object} + * @example + * Map(['foo', ...]) => { foo: true, ...} + */ +function Map(names) { + let map = {}; + for each (let name in names) + map[name] = true; + return map; +} + + +const ERR_CONFLICT = 'Remaining conflicting property: ', + ERR_REQUIRED = 'Missing required property: '; +/** + * Constant singleton, representing placeholder for required properties. + * @type {Object} + */ +const required = { toString: function()'<Trait.required>' }; +exports.required = required; + +/** + * Generates custom **required** property descriptor. Descriptor contains + * non-standard property `required` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function Required(name) { + function required() { throw new Error(ERR_REQUIRED + name) } + return { + get: required, + set: required, + required: true + }; +} + +/** + * Generates custom **conflicting** property descriptor. Descriptor contains + * non-standard property `conflict` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function Conflict(name) { + function conflict() { throw new Error(ERR_CONFLICT + name) } + return { + get: conflict, + set: conflict, + conflict: true + }; +} + +/** + * Function generates custom properties descriptor of the `object`s own + * properties. All the inherited properties are going to be ignored. + * Properties with values matching `required` singleton will be marked as + * 'required' properties. + * @param {Object} object + * Set of properties to generate trait from. + * @returns {Object} + * Properties descriptor of all of the `object`'s own properties. + */ +function trait(properties) { + let result = {}, + keys = getOwnPropertyNames(properties); + for each (let key in keys) { + let descriptor = getOwnPropertyDescriptor(properties, key); + result[key] = (required === descriptor.value) ? Required(key) : descriptor; + } + return result; +} +exports.Trait = exports.trait = trait; + +/** + * Composes new trait. If two or more traits have own properties with the + * same name, the new trait will contain a 'conflict' property for that name. + * 'compose' is a commutative and associative operation, and the order of its + * arguments is not significant. + * + * @params {Object} trait + * Takes traits as an arguments + * @returns {Object} + * New trait containing the combined own properties of all the traits. + * @example + * var newTrait = compose(trait_1, trait_2, ..., trait_N); + */ +function compose(trait1, trait2) { + let traits = Array.slice(arguments, 0), + result = {}; + for each (let trait in traits) { + let keys = getOwnPropertyNames(trait); + for each (let key in keys) { + let descriptor = trait[key]; + // if property already exists and it's not a requirement + if (hasOwn.call(result, key) && !result[key].required) { + if (descriptor.required) + continue; + if (!areSame(descriptor, result[key])) + result[key] = Conflict(key); + } else { + result[key] = descriptor; + } + } + } + return result; +} +exports.compose = compose; + +/** + * Composes new trait with the same own properties as the original trait, + * except that all property names appearing in the first argument are replaced + * by 'required' property descriptors. + * @param {String[]} keys + * Array of strings property names. + * @param {Object} trait + * A trait some properties of which should be excluded. + * @returns {Object} + * @example + * var newTrait = exclude(['name', ...], trait) + */ +function exclude(keys, trait) { + let exclusions = Map(keys), + result = {}; + + keys = getOwnPropertyNames(trait); + + for each (let key in keys) { + if (!hasOwn.call(exclusions, key) || trait[key].required) + result[key] = trait[key]; + else + result[key] = Required(key); + } + return result; +} + +/** + * Composes a new trait with all of the combined properties of the argument + * traits. In contrast to `compose`, `override` immediately resolves all + * conflicts resulting from this composition by overriding the properties of + * later traits. Trait priority is from left to right. I.e. the properties of + * the leftmost trait are never overridden. + * @params {Object} trait + * @returns {Object} + * @examples + * // override is associative: + * override(t1,t2,t3) + * // is equivalent to + * override(t1, override(t2, t3)) + * // or + * to override(override(t1, t2), t3) + * + * // override is not commutative: + * override(t1,t2) + * // is not equivalent to + * override(t2,t1) + */ +function override() { + let traits = Array.slice(arguments, 0), + result = {}; + for each (let trait in traits) { + let keys = getOwnPropertyNames(trait); + for each(let key in keys) { + let descriptor = trait[key]; + if (!hasOwn.call(result, key) || result[key].required) + result[key] = descriptor; + } + } + return result; +} +exports.override = override; + +/** + * Composes a new trait with the same properties as the original trait, except + * that all properties whose name is an own property of map will be renamed to + * map[name], and a 'required' property for name will be added instead. + * @param {Object} map + * An object whose own properties serve as a mapping from old names to new + * names. + * @param {Object} trait + * A trait object + * @returns {Object} + * @example + * var newTrait = rename(map, trait); + */ +function rename(map, trait) { + let result = {}, + keys = getOwnPropertyNames(trait); + for each(let key in keys) { + // must be renamed & it's not requirement + if (hasOwn.call(map, key) && !trait[key].required) { + let alias = map[key]; + if (hasOwn.call(result, alias) && !result[alias].required) + result[alias] = Conflict(alias); + else + result[alias] = trait[key]; + if (!hasOwn.call(result, key)) + result[key] = Required(key); + } else { // must not be renamed or its a requirement + // property is not in result trait yet + if (!hasOwn.call(result, key)) + result[key] = trait[key]; + // property is already in resulted trait & it's not requirement + else if (!trait[key].required) + result[key] = Conflict(key); + } + } + return result; +} + +/** +* Composes new resolved trait, with all the same properties as the original +* trait, except that all properties whose name is an own property of +* resolutions will be renamed to `resolutions[name]`. If it is +* `resolutions[name]` is `null` value is changed into a required property +* descriptor. +* function can be implemented as `rename(map,exclude(exclusions, trait))` +* where map is the subset of mappings from oldName to newName and exclusions +* is an array of all the keys that map to `null`. +* Note: it's important to **first** `exclude`, **then** `rename`, since +* `exclude` and rename are not associative. +* @param {Object} resolutions +* An object whose own properties serve as a mapping from old names to new +* names, or to `null` if the property should be excluded. +* @param {Object} trait +* A trait object +* @returns {Object} +* Resolved trait with the same own properties as the original trait. +*/ +function resolve(resolutions, trait) { + let renames = {}, + exclusions = [], + keys = getOwnPropertyNames(resolutions); + for each (let key in keys) { // pre-process renamed and excluded properties + if (resolutions[key]) // old name -> new name + renames[key] = resolutions[key]; + else // name -> undefined + exclusions.push(key); + } + return rename(renames, exclude(exclusions, trait)); +} +exports.resolve = resolve; + +/** + * `create` is like `Object.create`, except that it ensures that: + * - an exception is thrown if 'trait' still contains required properties + * - an exception is thrown if 'trait' still contains conflicting + * properties + * @param {Object} + * prototype of the completed object + * @param {Object} trait + * trait object to be turned into a complete object + * @returns {Object} + * An object with all of the properties described by the trait. + */ +function create(proto, trait) { + let properties = {}, + keys = getOwnPropertyNames(trait); + for each(let key in keys) { + let descriptor = trait[key]; + if (descriptor.required && !hasOwn.call(proto, key)) + throw new Error(ERR_REQUIRED + key); + else if (descriptor.conflict) + throw new Error(ERR_CONFLICT + key); + else + properties[key] = descriptor; + } + return _create(proto, properties); +} +exports.create = create; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/type.js b/tools/addon-sdk-1.4/packages/api-utils/lib/type.js new file mode 100644 index 0000000..012d2d1 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/type.js @@ -0,0 +1,372 @@ +/* vim:ts=2:sts=2:sw=2: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +/** + * Returns `true` if `value` is `undefined`. + * @examples + * var foo; isUndefined(foo); // true + * isUndefined(0); // false + */ +function isUndefined(value) { + return value === undefined; +} +exports.isUndefined = isUndefined; + +/** + * Returns `true` if value is `null`. + * @examples + * isNull(null); // true + * isNull(undefined); // false + */ +function isNull(value) { + return value === null; +} +exports.isNull = isNull; + +/** + * Returns `true` if value is a string. + * @examples + * isString("moe"); // true + */ +function isString(value) { + return typeof value === "string"; +} +exports.isString = isString; + +/** + * Returns `true` if `value` is a number. + * @examples + * isNumber(8.4 * 5); // true + */ +function isNumber(value) { + return typeof value === "number"; +} +exports.isNumber = isNumber; + +/** + * Returns `true` if `value` is a `RegExp`. + * @examples + * isRegExp(/moe/); // true + */ +function isRegExp(value) { + return isObject(value) && instanceOf(value, RegExp); +} +exports.isRegExp = isRegExp; + +/** + * Returns true if `value` is a `Date`. + * @examples + * isDate(new Date()); // true + */ +function isDate(value) { + return isObject(value) && instanceOf(value, Date); +} +exports.isDate = isDate; + +/** + * Returns true if object is a Function. + * @examples + * isFunction(function foo(){}) // true + */ +function isFunction(value) { + return typeof value === "function"; +} +exports.isFunction = isFunction; + +/** + * Returns `true` if `value` is an object (please note that `null` is considered + * to be an atom and not an object). + * @examples + * isObject({}) // true + * isObject(null) // false + */ +function isObject(value) { + return typeof value === "object" && value !== null; +} +exports.isObject = isObject; + +/** + * Returns true if `value` is an Array. + * @examples + * isArray([1, 2, 3]) // true + * isArray({ 0: 'foo', length: 1 }) // false + */ +var isArray = Array.isArray || function isArray(value) { + Object.prototype.toString.call(value) === "[object Array]"; +} +exports.isArray = isArray; + +/** + * Returns `true` if `value` is an Arguments object. + * @examples + * (function(){ return isArguments(arguments); })(1, 2, 3); // true + * isArguments([1,2,3]); // false + */ +function isArguments(value) { + Object.prototype.toString.call(value) === "[object Arguments]"; +} +exports.isArguments = isArguments; + +/** + * Returns true if it is a primitive `value`. (null, undefined, number, + * boolean, string) + * @examples + * isPrimitive(3) // true + * isPrimitive('foo') // true + * isPrimitive({ bar: 3 }) // false + */ +function isPrimitive(value) { + return !isFunction(value) && !isObject(value); +} +exports.isPrimitive = isPrimitive; + +/** + * Returns `true` if given `object` is flat (it is direct decedent of + * `Object.prototype` or `null`). + * @examples + * isFlat({}) // true + * isFlat(new Type()) // false + */ +function isFlat(object) { + return isObject(object) && (isNull(Object.getPrototypeOf(object)) || + isNull(Object.getPrototypeOf( + Object.getPrototypeOf(object)))); +} +exports.isFlat = isFlat; + +/** + * Returns `true` if object contains no values. + */ +function isEmpty(object) { + if (isObject(object)) { + for (var key in object) + return false; + return true; + } + return false; +} +exports.isEmpty = isEmpty; + +/** + * Returns `true` if `value` is an array / flat object containing only atomic + * values and other flat objects. + */ +function isJSON(value, visited) { + // Adding value to array of visited values. + (visited || (visited = [])).push(value); + // If `value` is an atom return `true` cause it's valid JSON. + return isPrimitive(value) || + // If `value` is an array of JSON values that has not been visited + // yet. + (isArray(value) && value.every(function(element) { + return isJSON(element, visited); + })) || + // If `value` is a plain object containing properties with a JSON + // values it's a valid JSON. + (isFlat(value) && Object.keys(value).every(function(key) { + var $ = Object.getOwnPropertyDescriptor(value, key); + // Check every proprety of a plain object to verify that + // it's neither getter nor setter, but a JSON value, that + // has not been visited yet. + return ((!isObject($.value) || !~visited.indexOf($.value)) && + !('get' in $) && !('set' in $) && + isJSON($.value, visited)); + })); +} +exports.isJSON = function (value) { + return isJSON(value); +}; + +/** + * Returns if `value` is an instance of a given `Type`. This is exactly same as + * `value instanceof Type` with a difference that `Type` can be from a scope + * that has a different top level object. (Like in case where `Type` is a + * function from different iframe / jetpack module / sandbox). + */ +function instanceOf(value, Type) { + var isConstructorNameSame; + var isConstructorSourceSame; + + // If `instanceof` returned `true` we know result right away. + var isInstanceOf = value instanceof Type; + + // If `instanceof` returned `false` we do ducktype check since `Type` may be + // from a different sandbox. If a constructor of the `value` or a constructor + // of the value's prototype has same name and source we assume that it's an + // instance of the Type. + if (!isInstanceOf && value) { + isConstructorNameSame = value.constructor.name === Type.name; + isConstructorSourceSame = String(value.constructor) == String(Type); + isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) || + instanceOf(Object.getPrototypeOf(value), Type); + } + return isInstanceOf; +} +exports.instanceOf = instanceOf; + +/** + * Function returns textual representation of a value passed to it. Function + * takes additional `indent` argument that is used for indentation. Also + * optional `limit` argument may be passed to limit amount of detail returned. + * @param {Object} value + * @param {String} [indent=" "] + * @param {Number} [limit] + */ +function source(value, indent, limit, offset, visited) { + var result; + var names; + var nestingIndex; + var isCompact = !isUndefined(limit); + + indent = indent || " "; + offset = (offset || ""); + result = ""; + visited = visited || []; + + if (isUndefined(value)) { + result += "undefined"; + } + else if (isNull(value)) { + result += "null"; + } + else if (isString(value)) { + result += '"' + value + '"'; + } + else if (isFunction(value)) { + value = String(value).split("\n"); + if (isCompact && value.length > 2) { + value = value.splice(0, 2); + value.push("...}"); + } + result += value.join("\n" + offset); + } + else if (isArray(value)) { + if ((nestingIndex = (visited.indexOf(value) + 1))) { + result = "#" + nestingIndex + "#"; + } + else { + visited.push(value); + + if (isCompact) + value = value.slice(0, limit); + + result += "[\n"; + result += value.map(function(value) { + return offset + indent + source(value, indent, limit, offset + indent, + visited); + }).join(",\n"); + result += isCompact && value.length > limit ? + ",\n" + offset + "...]" : "\n" + offset + "]"; + } + } + else if (isObject(value)) { + if ((nestingIndex = (visited.indexOf(value) + 1))) { + result = "#" + nestingIndex + "#" + } + else { + visited.push(value) + + names = Object.keys(value); + + result += "{ // " + value + "\n"; + result += (isCompact ? names.slice(0, limit) : names).map(function(name) { + var _limit = isCompact ? limit - 1 : limit; + var descriptor = Object.getOwnPropertyDescriptor(value, name); + var result = offset + indent + "// "; + var accessor; + if (0 <= name.indexOf(" ")) + name = '"' + name + '"'; + + if (descriptor.writable) + result += "writable "; + if (descriptor.configurable) + result += "configurable "; + if (descriptor.enumerable) + result += "enumerable "; + + result += "\n"; + if ("value" in descriptor) { + result += offset + indent + name + ": "; + result += source(descriptor.value, indent, _limit, indent + offset, + visited); + } + else { + + if (descriptor.get) { + result += offset + indent + "get " + name + " "; + accessor = source(descriptor.get, indent, _limit, indent + offset, + visited); + result += accessor.substr(accessor.indexOf("{")); + } + + if (descriptor.set) { + result += offset + indent + "set " + name + " "; + accessor = source(descriptor.set, indent, _limit, indent + offset, + visited); + result += accessor.substr(accessor.indexOf("{")); + } + } + return result; + }).join(",\n"); + + if (isCompact) { + if (names.length > limit && limit > 0) { + result += ",\n" + offset + indent + "//..."; + } + } + else { + if (names.length) + result += ","; + + result += "\n" + offset + indent + '"__proto__": '; + result += source(Object.getPrototypeOf(value), indent, 0, + offset + indent); + } + + result += "\n" + offset + "}"; + } + } + else { + result += String(value); + } + return result; +} +exports.source = function (value, indentation, limit) { + return source(value, indentation, limit); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/unit-test-finder.js b/tools/addon-sdk-1.4/packages/api-utils/lib/unit-test-finder.js new file mode 100644 index 0000000..a8dc14a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/unit-test-finder.js @@ -0,0 +1,106 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const file = require("./file"); +const packaging = require('@packaging'); +const suites = packaging.allTestModules; + +const NOT_TESTS = ['setup', 'teardown']; + +var TestFinder = exports.TestFinder = function TestFinder(options) { + memory.track(this); + this.filter = options.filter; + this.testInProcess = options.testInProcess === false ? false : true; + this.testOutOfProcess = options.testOutOfProcess === true ? true : false; +}; + +TestFinder.prototype = { + _makeTest: function _makeTest(suite, name, test) { + function runTest(runner) { + console.info("executing '" + suite + "." + name + "'"); + test(runner); + } + return runTest; + }, + + findTests: function findTests(cb) { + var self = this; + var tests = []; + var filter; + // A filter string is {fileNameRegex}[:{testNameRegex}] - ie, a colon + // optionally separates a regex for the test fileName from a regex for the + // testName. + if (this.filter) { + var colonPos = this.filter.indexOf(':'); + var filterFileRegex, filterNameRegex; + if (colonPos === -1) { + filterFileRegex = new RegExp(self.filter); + } else { + filterFileRegex = new RegExp(self.filter.substr(0, colonPos)); + filterNameRegex = new RegExp(self.filter.substr(colonPos + 1)); + } + // This function will first be called with just the filename; if + // it returns true the module will be loaded then the function + // called again with both the filename and the testname. + filter = function(filename, testname) { + return filterFileRegex.test(filename) && + ((testname && filterNameRegex) ? filterNameRegex.test(testname) + : true); + }; + } else + filter = function() {return true}; + + suites.forEach( + function(suite) { + var module = require(suite); + if (self.testInProcess) + for each (let name in Object.keys(module).sort()) { + if(NOT_TESTS.indexOf(name) === -1 && filter(suite, name)) { + tests.push({ + setup: module.setup, + teardown: module.teardown, + testFunction: self._makeTest(suite, name, module[name]), + name: suite + "." + name + }); + } + } + }); + + cb(tests); + } +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/unit-test.js b/tools/addon-sdk-1.4/packages/api-utils/lib/unit-test.js new file mode 100644 index 0000000..4b3ac89 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/unit-test.js @@ -0,0 +1,466 @@ +/* vim:st=2:sts=2:sw=2: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; +var timer = require("./timer"); + +exports.findAndRunTests = function findAndRunTests(options) { + var TestFinder = require("./unit-test-finder").TestFinder; + var finder = new TestFinder({ + filter: options.filter, + testInProcess: options.testInProcess, + testOutOfProcess: options.testOutOfProcess + }); + var runner = new TestRunner({fs: options.fs}); + finder.findTests( + function (tests) { + runner.startMany({tests: tests, + onDone: options.onDone}); + }); +}; + +var TestRunner = exports.TestRunner = function TestRunner(options) { + if (options) { + this.fs = options.fs; + } + this.console = (options && "console" in options) ? options.console : console; + memory.track(this); + this.passed = 0; + this.failed = 0; + this.testRunSummary = []; + this.expectFailNesting = 0; +}; + +TestRunner.prototype = { + toString: function toString() "[object TestRunner]", + + DEFAULT_PAUSE_TIMEOUT: 10000, + PAUSE_DELAY: 500, + + _logTestFailed: function _logTestFailed(why) { + this.test.errors[why]++; + if (!this.testFailureLogged) { + this.console.error("TEST FAILED: " + this.test.name + " (" + why + ")"); + this.testFailureLogged = true; + } + }, + + pass: function pass(message) { + if(!this.expectFailure) { + this.console.info("pass:", message); + this.passed++; + this.test.passed++; + } + else { + this.expectFailure = false; + this.fail('Failure Expected: ' + message); + } + }, + + fail: function fail(message) { + if(!this.expectFailure) { + this._logTestFailed("failure"); + this.console.error("fail:", message); + this.console.trace(); + this.failed++; + this.test.failed++; + } + else { + this.expectFailure = false; + this.pass(message); + } + }, + + expectFail: function(callback) { + this.expectFailure = true; + callback(); + this.expectFailure = false; + }, + + exception: function exception(e) { + this._logTestFailed("exception"); + this.console.exception(e); + this.failed++; + this.test.failed++; + }, + + assertMatches: function assertMatches(string, regexp, message) { + if (regexp.test(string)) { + if (!message) + message = uneval(string) + " matches " + uneval(regexp); + this.pass(message); + } else { + var no = uneval(string) + " doesn't match " + uneval(regexp); + if (!message) + message = no; + else + message = message + " (" + no + ")"; + this.fail(message); + } + }, + + assertRaises: function assertRaises(func, predicate, message) { + try { + func(); + if (message) + this.fail(message + " (no exception thrown)"); + else + this.fail("function failed to throw exception"); + } catch (e) { + var errorMessage; + if (typeof(e) == "string") + errorMessage = e; + else + errorMessage = e.message; + if (typeof(predicate) == "string") + this.assertEqual(errorMessage, predicate, message); + else + this.assertMatches(errorMessage, predicate, message); + } + }, + + assert: function assert(a, message) { + if (!a) { + if (!message) + message = "assertion failed, value is " + a; + this.fail(message); + } else + this.pass(message || "assertion successful"); + }, + + assertNotEqual: function assertNotEqual(a, b, message) { + if (a != b) { + if (!message) + message = "a != b != " + uneval(a); + this.pass(message); + } else { + var equality = uneval(a) + " == " + uneval(b); + if (!message) + message = equality; + else + message += " (" + equality + ")"; + this.fail(message); + } + }, + + assertEqual: function assertEqual(a, b, message) { + if (a == b) { + if (!message) + message = "a == b == " + uneval(a); + this.pass(message); + } else { + var inequality = uneval(a) + " != " + uneval(b); + if (!message) + message = inequality; + else + message += " (" + inequality + ")"; + this.fail(message); + } + }, + + assertNotStrictEqual: function assertNotStrictEqual(a, b, message) { + if (a !== b) { + if (!message) + message = "a !== b !== " + uneval(a); + this.pass(message); + } else { + var equality = uneval(a) + " === " + uneval(b); + if (!message) + message = equality; + else + message += " (" + equality + ")"; + this.fail(message); + } + }, + + assertStrictEqual: function assertStrictEqual(a, b, message) { + if (a === b) { + if (!message) + message = "a === b === " + uneval(a); + this.pass(message); + } else { + var inequality = uneval(a) + " !== " + uneval(b); + if (!message) + message = inequality; + else + message += " (" + inequality + ")"; + this.fail(message); + } + }, + + assertFunction: function assertFunction(a, message) { + this.assertStrictEqual('function', typeof a, message); + }, + + assertUndefined: function(a, message) { + this.assertStrictEqual('undefined', typeof a, message); + }, + + assertNotUndefined: function(a, message) { + this.assertNotStrictEqual('undefined', typeof a, message); + }, + + assertNull: function(a, message) { + this.assertStrictEqual(null, a, message); + }, + + assertNotNull: function(a, message) { + this.assertNotStrictEqual(null, a, message); + }, + + assertObject: function(a, message) { + this.assertStrictEqual('[object Object]', Object.prototype.toString.apply(a), message); + }, + + assertString: function(a, message) { + this.assertStrictEqual('[object String]', Object.prototype.toString.apply(a), message); + }, + + assertArray: function(a, message) { + this.assertStrictEqual('[object Array]', Object.prototype.toString.apply(a), message); + }, + + assertNumber: function(a, message) { + this.assertStrictEqual('[object Number]', Object.prototype.toString.apply(a), message); + }, + + done: function done() { + if (!this.isDone) { + this.isDone = true; + if(this.test.teardown) { + this.test.teardown(this); + } + if (this.waitTimeout !== null) { + timer.clearTimeout(this.waitTimeout); + this.waitTimeout = null; + } + if (this.test.passed == 0 && this.test.failed == 0) { + this._logTestFailed("empty test"); + this.failed++; + this.test.failed++; + } + + this.testRunSummary.push({ + name: this.test.name, + passed: this.test.passed, + failed: this.test.failed, + errors: [error for (error in this.test.errors)].join(", ") + }); + + if (this.onDone !== null) { + var onDone = this.onDone; + var self = this; + this.onDone = null; + timer.setTimeout(function() { onDone(self); }, 0); + } + } + }, + + // Set of assertion functions to wait for an assertion to become true + // These functions take the same arguments as the TestRunner.assert* methods. + waitUntil: function waitUntil() { + return this._waitUntil(this.assert, arguments); + }, + + waitUntilNotEqual: function waitUntilNotEqual() { + return this._waitUntil(this.assertNotEqual, arguments); + }, + + waitUntilEqual: function waitUntilEqual() { + return this._waitUntil(this.assertEqual, arguments); + }, + + waitUntilMatches: function waitUntilMatches() { + return this._waitUntil(this.assertMatches, arguments); + }, + + /** + * Internal function that waits for an assertion to become true. + * @param {Function} assertionMethod + * Reference to a TestRunner assertion method like test.assert, + * test.assertEqual, ... + * @param {Array} args + * List of arguments to give to the previous assertion method. + * All functions in this list are going to be called to retrieve current + * assertion values. + */ + _waitUntil: function waitUntil(assertionMethod, args) { + let count = 0; + let maxCount = this.DEFAULT_PAUSE_TIMEOUT / this.PAUSE_DELAY; + + // We need to ensure that test is asynchronous + if (!this.waitTimeout) + this.waitUntilDone(this.DEFAULT_PAUSE_TIMEOUT); + + let callback = null; + let finished = false; + + let test = this; + + // capture a traceback before we go async. + let traceback = require("./traceback"); + let stack = traceback.get(); + stack.splice(-2, 2); + let currentWaitStack = traceback.format(stack); + let timeout = null; + + function loop(stopIt) { + timeout = null; + + // Build a mockup object to fake TestRunner API and intercept calls to + // pass and fail methods, in order to retrieve nice error messages + // and assertion result + let mock = { + pass: function (msg) { + test.pass(msg); + test.waitUntilCallback = null; + if (callback && !stopIt) + callback(); + finished = true; + }, + fail: function (msg) { + // If we are called on test timeout, we stop the loop + // and print which test keeps failing: + if (stopIt) { + test.console.error("test assertion never became true:\n", + msg + "\n", + currentWaitStack); + if (timeout) + timer.clearTimeout(timeout); + return; + } + timeout = timer.setTimeout(loop, test.PAUSE_DELAY); + } + }; + + // Automatically call args closures in order to build arguments for + // assertion function + let appliedArgs = []; + for (let i = 0, l = args.length; i < l; i++) { + let a = args[i]; + if (typeof a == "function") { + try { + a = a(); + } + catch(e) { + test.fail("Exception when calling asynchronous assertion: " + e); + finished = true; + return; + } + } + appliedArgs.push(a); + } + + // Finally call assertion function with current assertion values + assertionMethod.apply(mock, appliedArgs); + } + loop(); + this.waitUntilCallback = loop; + + // Return an object with `then` method, to offer a way to execute + // some code when the assertion passed or failed + return { + then: function (c) { + callback = c; + + // In case of immediate positive result, we need to execute callback + // immediately here: + if (finished) + callback(); + } + }; + }, + + waitUntilDone: function waitUntilDone(ms) { + if (ms === undefined) + ms = this.DEFAULT_PAUSE_TIMEOUT; + + var self = this; + + function tiredOfWaiting() { + self._logTestFailed("timed out"); + if (self.waitUntilCallback) { + self.waitUntilCallback(true); + self.waitUntilCallback = null; + } + self.failed++; + self.test.failed++; + self.done(); + } + + // We may already have registered a timeout callback + if (this.waitTimeout) + timer.clearTimeout(this.waitTimeout); + + this.waitTimeout = timer.setTimeout(tiredOfWaiting, ms); + }, + + startMany: function startMany(options) { + function runNextTest(self) { + var test = options.tests.shift(); + if (test) + self.start({test: test, onDone: runNextTest}); + else + options.onDone(self); + } + runNextTest(this); + }, + + start: function start(options) { + this.test = options.test; + this.test.passed = 0; + this.test.failed = 0; + this.test.errors = {}; + + this.isDone = false; + this.onDone = options.onDone; + this.waitTimeout = null; + this.testFailureLogged = false; + + try { + if(this.test.setup) { + this.test.setup(this); + } + this.test.testFunction(this); + } catch (e) { + this.exception(e); + } + if (this.waitTimeout === null) + this.done(); + } +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/unload.js b/tools/addon-sdk-1.4/packages/api-utils/lib/unload.js new file mode 100644 index 0000000..3bbeb38 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/unload.js @@ -0,0 +1,59 @@ +// Parts of this module were taken from narwhal: +// +// http://narwhaljs.org + +var observers = []; +var unloaders = []; + +var when = exports.when = function when(observer) { + if (observers.indexOf(observer) != -1) + return; + observers.unshift(observer); +}; + +var send = exports.send = function send(reason, onError) { + onError = onError || console.exception; + observers.forEach(function (observer) { + try { + observer(reason); + } catch (e) { + onError(e); + } + }); +}; + +var ensure = exports.ensure = function ensure(obj, destructorName) { + if (!destructorName) + destructorName = "unload"; + if (!(destructorName in obj)) + throw new Error("object has no '" + destructorName + "' property"); + + let called = false; + let originalDestructor = obj[destructorName]; + + function unloadWrapper(reason) { + if (!called) { + called = true; + let index = unloaders.indexOf(unloadWrapper); + if (index == -1) + throw new Error("internal error: unloader not found"); + unloaders.splice(index, 1); + originalDestructor.call(obj, reason); + originalDestructor = null; + destructorName = null; + obj = null; + } + }; + + unloaders.push(unloadWrapper); + + obj[destructorName] = unloadWrapper; +}; + +when( + function(reason) { + unloaders.slice().forEach( + function(unloadWrapper) { + unloadWrapper(reason); + }); + }); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/url.js b/tools/addon-sdk-1.4/packages/api-utils/lib/url.js new file mode 100644 index 0000000..4502129 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/url.js @@ -0,0 +1,123 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci,Cr} = require("chrome"); + +var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + +var resProt = ios.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + +function newURI(uriStr, base) { + try { + let baseURI = base ? ios.newURI(base, null, null) : null; + return ios.newURI(uriStr, null, baseURI); + } + catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) { + throw new Error("malformed URI: " + uriStr); + } + catch (e if (e.result == Cr.NS_ERROR_FAILURE || + e.result == Cr.NS_ERROR_ILLEGAL_VALUE)) { + throw new Error("invalid URI: " + uriStr); + } +} + +function resolveResourceURI(uri) { + var resolved; + try { + resolved = resProt.resolveURI(uri); + } catch (e if e.result == Cr.NS_ERROR_NOT_AVAILABLE) { + throw new Error("resource does not exist: " + uri.spec); + }; + return resolved; +} + +let fromFilename = exports.fromFilename = function fromFilename(path) { + var file = Cc['@mozilla.org/file/local;1'] + .createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return ios.newFileURI(file).spec; +}; + +let toFilename = exports.toFilename = function toFilename(url) { + var uri = newURI(url); + if (uri.scheme == "resource") + uri = newURI(resolveResourceURI(uri)); + if (uri.scheme == "chrome") { + var channel = ios.newChannelFromURI(uri); + try { + channel = channel.QueryInterface(Ci.nsIFileChannel); + return channel.file.path; + } catch (e if e.result == Cr.NS_NOINTERFACE) { + throw new Error("chrome url isn't on filesystem: " + url); + } + } + if (uri.scheme == "file") { + var file = uri.QueryInterface(Ci.nsIFileURL).file; + return file.path; + } + throw new Error("cannot map to filename: " + url); +}; + +function URL(url, base) { + var uri = newURI(url, base); + + var userPass = null; + try { + userPass = uri.userPass ? uri.userPass : null; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + var host = null; + try { + host = uri.host; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + var port = null; + try { + port = uri.port == -1 ? null : uri.port; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + this.__defineGetter__("scheme", function() uri.scheme); + this.__defineGetter__("userPass", function() userPass); + this.__defineGetter__("host", function() host); + this.__defineGetter__("port", function() port); + this.__defineGetter__("path", function() uri.path); + this.toString = function URL_toString() uri.spec; +}; +exports.URL = require("./api-utils").publicConstructor(URL); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/utils/data.js b/tools/addon-sdk-1.4/packages/api-utils/lib/utils/data.js new file mode 100644 index 0000000..bf621ad --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/utils/data.js @@ -0,0 +1,104 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Cc, Ci, Cu } = require("chrome"); +const IOService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); +const AppShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); + +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm"); +const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + +const PNG_B64 = "data:image/png;base64,"; +const DEF_FAVICON_URI = "chrome://mozapps/skin/places/defaultFavicon.png"; +let DEF_FAVICON = null; + +/** + * Takes URI of the page and returns associated favicon URI. + * If page under passed uri has no favicon then base64 encoded data URI of + * default faveicon is returned. + * @param {String} uri + * @returns {String} + */ +exports.getFaviconURIForLocation = function getFaviconURIForLocation(uri) { + let pageURI = NetUtil.newURI(uri); + try { + return FaviconService.getFaviconDataAsDataURL( + FaviconService.getFaviconForPage(pageURI)); + } + catch(e) { + if (!DEF_FAVICON) { + DEF_FAVICON = PNG_B64 + + base64Encode(getChromeURIContent(DEF_FAVICON_URI)); + } + return DEF_FAVICON; + } +} + +/** + * Takes chrome URI and returns content under that URI. + * @param {String} chromeURI + * @returns {String} + */ +function getChromeURIContent(chromeURI) { + let channel = IOService.newChannel(chromeURI, null, null); + let input = channel.open(); + let stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(input); + let content = stream.readBytes(input.available()); + stream.close(); + input.close(); + return content; +} +exports.getChromeURIContent = getChromeURIContent; + +/** + * Creates a base-64 encoded ASCII string from a string of binary data. + */ +function base64Encode(data) AppShellService.hiddenDOMWindow.btoa(String(data)); +exports.base64Encode = base64Encode; + +/** + * Decodes a string of data which has been encoded using base-64 encoding. + */ +function base64Decode(data) AppShellService.hiddenDOMWindow.atob(String(data)); +exports.base64Decode = base64Decode; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/utils/function.js b/tools/addon-sdk-1.4/packages/api-utils/lib/utils/function.js new file mode 100644 index 0000000..ba43d59 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/utils/function.js @@ -0,0 +1,64 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +var { setTimeout } = require("../timer"); + +/** + * Takes a function and returns a wrapped one instead, calling which will call + * original function in the next turn of event loop. This is basically utility + * to do `setTimeout(function() { ... }, 0)`, with a difference that returned + * function is reused, instead of creating a new one each time. This also allows + * to use this functions as event listeners. + */ +function Enqueued(callee) { + return function enqueued() + setTimeout(invoke, 0, callee, arguments, this); +} +exports.Enqueued = Enqueued; + +/** + * Invokes `callee` by passing `params` as an arguments and `self` as `this` + * pseudo-variable. Returns value that is returned by a callee. + * @param {Function} callee + * Function to invoke. + * @param {Array} params + * Arguments to invoke function with. + * @param {Object} self + * Object to be passed as a `this` pseudo variable. + */ +function invoke(callee, params, self) callee.apply(self, params); +exports.invoke = invoke; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/utils/registry.js b/tools/addon-sdk-1.4/packages/api-utils/lib/utils/registry.js new file mode 100644 index 0000000..6cd11b2 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/utils/registry.js @@ -0,0 +1,90 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const { EventEmitter } = require('../events'); +const unload = require('../unload'); + +const Registry = EventEmitter.compose({ + _registry: null, + _constructor: null, + constructor: function Registry(constructor) { + this._registry = []; + this._constructor = constructor; + this.on('error', this._onError = this._onError.bind(this)); + unload.ensure(this, "_destructor"); + }, + _destructor: function _destructor() { + let _registry = this._registry.slice(0); + for each (let instance in _registry) + this._emit('remove', instance); + this._registry.splice(0); + }, + _onError: function _onError(e) { + if (!this._listeners('error').length) + console.error(e); + }, + has: function has(instance) { + let _registry = this._registry; + return ( + (0 <= _registry.indexOf(instance)) || + (instance && instance._public && 0 <= _registry.indexOf(instance._public)) + ); + }, + add: function add(instance) { + let { _constructor, _registry } = this; + if (!(instance instanceof _constructor)) + instance = new _constructor(instance); + if (0 > _registry.indexOf(instance)) { + _registry.push(instance); + this._emit('add', instance); + } + return instance; + }, + remove: function remove(instance) { + let _registry = this._registry; + let index = _registry.indexOf(instance) + if (0 <= index) { + this._emit('remove', instance); + _registry.splice(index, 1); + } + } +}); +exports.Registry = Registry; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/utils/thumbnail.js b/tools/addon-sdk-1.4/packages/api-utils/lib/utils/thumbnail.js new file mode 100644 index 0000000..d9012e1 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/utils/thumbnail.js @@ -0,0 +1,76 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Cc, Ci, Cu } = require("chrome"); +const AppShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); + +const NS = "http://www.w3.org/1999/xhtml"; +const COLOR = "rgb(255,255,255)"; + +/** + * Creates canvas element with a thumbnail of the passed window. + * @param {Window} window + * @returns {Element} + */ +function getThumbnailCanvasForWindow(window) { + let aspectRatio = 0.5625; // 16:9 + let thumbnail = AppShellService.hiddenDOMWindow.document + .createElementNS(NS, "canvas"); + thumbnail.mozOpaque = true; + thumbnail.width = Math.ceil(window.screen.availWidth / 5.75); + thumbnail.height = Math.round(thumbnail.width * aspectRatio); + let ctx = thumbnail.getContext("2d"); + let snippetWidth = window.innerWidth * .6; + let scale = thumbnail.width / snippetWidth; + ctx.scale(scale, scale); + ctx.drawWindow(window, window.scrollX, window.scrollY, snippetWidth, + snippetWidth * aspectRatio, COLOR); + return thumbnail; +} +exports.getThumbnailCanvasForWindow = getThumbnailCanvasForWindow; + +/** + * Creates Base64 encoded data URI of the thumbnail for the passed window. + * @param {Window} window + * @returns {String} + */ +exports.getThumbnailURIForWindow = function getThumbnailURIForWindow(window) { + return getThumbnailCanvasForWindow(window).toDataURL() +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/window-utils.js b/tools/addon-sdk-1.4/packages/api-utils/lib/window-utils.js new file mode 100644 index 0000000..2d123a8 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/window-utils.js @@ -0,0 +1,270 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Cc, Ci } = require("chrome"); +const { EventEmitter } = require('./events'), + { Trait } = require('./traits'); +const errors = require("./errors"); + +const gWindowWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); +const appShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); + +const XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + +/** + * An iterator for XUL windows currently in the application. + * + * @return A generator that yields XUL windows exposing the + * nsIDOMWindow interface. + */ +var windowIterator = exports.windowIterator = function windowIterator() { + let winEnum = gWindowWatcher.getWindowEnumerator(); + while (winEnum.hasMoreElements()) + yield winEnum.getNext().QueryInterface(Ci.nsIDOMWindow); +}; + +/** + * An iterator for browser windows currently open in the application. + * @returns {Function} + * A generator that yields browser windows exposing the `nsIDOMWindow` + * interface. + */ +function browserWindowIterator() { + for each (let window in windowIterator()) { + if (isBrowser(window)) + yield window; + } +} +exports.browserWindowIterator = browserWindowIterator; + +var WindowTracker = exports.WindowTracker = function WindowTracker(delegate) { + this.delegate = delegate; + this._loadingWindows = []; + for (let window in windowIterator()) + this._regWindow(window); + gWindowWatcher.registerNotification(this); + require("./unload").ensure(this); +}; + +WindowTracker.prototype = { + _regLoadingWindow: function _regLoadingWindow(window) { + this._loadingWindows.push(window); + window.addEventListener("load", this, true); + }, + + _unregLoadingWindow: function _unregLoadingWindow(window) { + var index = this._loadingWindows.indexOf(window); + + if (index != -1) { + this._loadingWindows.splice(index, 1); + window.removeEventListener("load", this, true); + } + }, + + _regWindow: function _regWindow(window) { + if (window.document.readyState == "complete") { + this._unregLoadingWindow(window); + this.delegate.onTrack(window); + } else + this._regLoadingWindow(window); + }, + + _unregWindow: function _unregWindow(window) { + if (window.document.readyState == "complete") { + if (this.delegate.onUntrack) + this.delegate.onUntrack(window); + } else { + this._unregLoadingWindow(window); + } + }, + + unload: function unload() { + gWindowWatcher.unregisterNotification(this); + for (let window in windowIterator()) + this._unregWindow(window); + }, + + handleEvent: function handleEvent(event) { + if (event.type == "load" && event.target) { + var window = event.target.defaultView; + if (window) + this._regWindow(window); + } + }, + + observe: function observe(subject, topic, data) { + var window = subject.QueryInterface(Ci.nsIDOMWindow); + if (topic == "domwindowopened") + this._regWindow(window); + else + this._unregWindow(window); + } +}; + +errors.catchAndLogProps(WindowTracker.prototype, ["handleEvent", "observe"]); + +const WindowTrackerTrait = Trait.compose({ + _onTrack: Trait.required, + _onUntrack: Trait.required, + constructor: function WindowTrackerTrait() { + new WindowTracker({ + onTrack: this._onTrack.bind(this), + onUntrack: this._onUntrack.bind(this) + }); + } +}); +exports.WindowTrackerTrait = WindowTrackerTrait; + +var gDocsToClose = []; + +function onDocUnload(event) { + var index = gDocsToClose.indexOf(event.target); + if (index == -1) + throw new Error("internal error: unloading document not found"); + var document = gDocsToClose.splice(index, 1)[0]; + // Just in case, let's remove the event listener too. + document.defaultView.removeEventListener("unload", onDocUnload, false); +} + +onDocUnload = require("./errors").catchAndLog(onDocUnload); + +exports.closeOnUnload = function closeOnUnload(window) { + window.addEventListener("unload", onDocUnload, false); + gDocsToClose.push(window.document); +}; + +exports.__defineGetter__("activeWindow", function() { + return Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow(null); +}); +exports.__defineSetter__("activeWindow", function(window) { + try { + window.focus(); + } + catch (e) { } +}); + +exports.__defineGetter__("activeBrowserWindow", function() { + return Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow("navigator:browser"); +}); + +/** + * Returns the ID of the window's current inner window. + */ +exports.getInnerId = function getInnerId(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; +}; + +/** + * Returns the ID of the window's outer window. + */ +exports.getOuterId = function getOuterId(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).outerWindowID; +}; + +function isBrowser(window) { + return window.document.documentElement.getAttribute("windowtype") === + "navigator:browser"; +}; +exports.isBrowser = isBrowser; + +exports.hiddenWindow = appShellService.hiddenDOMWindow; + +function createHiddenXULFrame() { + return function promise(deliver) { + let window = appShellService.hiddenDOMWindow; + let document = window.document; + let isXMLDoc = (document.contentType == "application/xhtml+xml" || + document.contentType == "application/vnd.mozilla.xul+xml") + + if (isXMLDoc) { + deliver(window) + } + else { + let frame = document.createElement('iframe'); + // This is ugly but we need window for XUL document in order to create + // browser elements. + frame.setAttribute('src', 'chrome://browser/content/hiddenWindow.xul'); + frame.addEventListener('DOMContentLoaded', function onLoad(event) { + frame.removeEventListener('DOMContentLoaded', onLoad, false); + deliver(frame.contentWindow); + }, false); + document.documentElement.appendChild(frame); + } + } +}; +exports.createHiddenXULFrame = createHiddenXULFrame; + +exports.createRemoteBrowser = function createRemoteBrowser(remote) { + return function promise(deliver) { + createHiddenXULFrame()(function(hiddenWindow) { + let document = hiddenWindow.document; + let browser = document.createElementNS(XUL, "browser"); + // Remote="true" enable everything here: + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1347 + if (remote !== false) + browser.setAttribute("remote","true"); + // Type="content" is mandatory to enable stuff here: + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1776 + browser.setAttribute("type","content"); + // We remove XBL binding to avoid execution of code that is not going to work + // because browser has no docShell attribute in remote mode (for example) + browser.setAttribute("style","-moz-binding: none;"); + // Flex it in order to be visible (optional, for debug purpose) + browser.setAttribute("flex", "1"); + document.documentElement.appendChild(browser); + + // Return browser + deliver(browser); + }); + }; +}; + +require("./unload").when( + function() { + gDocsToClose.slice().forEach( + function(doc) { doc.defaultView.close(); }); + }); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/windows/dom.js b/tools/addon-sdk-1.4/packages/api-utils/lib/windows/dom.js new file mode 100644 index 0000000..d74e314 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/windows/dom.js @@ -0,0 +1,60 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +'use strict'; + +const { Trait } = require('../traits'); + +const WindowDom = Trait.compose({ + _window: Trait.required, + get title() { + let window = this._window; + return window && window.document ? window.document.title : null + }, + close: function close() { + let window = this._window; + if (window) window.close(); + return this._public; + }, + activate: function activate() { + let window = this._window; + if (window) window.focus(); + return this._public; + } +}); +exports.WindowDom = WindowDom; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/windows/loader.js b/tools/addon-sdk-1.4/packages/api-utils/lib/windows/loader.js new file mode 100644 index 0000000..6b43fb6 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/windows/loader.js @@ -0,0 +1,152 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const { Cc, Ci } = require('chrome'), + { setTimeout } = require("../timer"), + { Trait } = require('../traits'), + + WM = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator), + + URI_BROWSER = 'chrome://browser/content/browser.xul', + NAME = '_blank', + FEATURES = 'chrome,all,dialog=no', + PARAMS = [ URI_BROWSER, NAME, FEATURES ], + ON_LOAD = 'load', + ON_UNLOAD = 'unload', + STATE_LOADED = 'complete', + BROWSER = 'navigator:browser'; + +/** + * Trait provides private `_window` property and requires `_onLoad` property + * that will be called when `_window` is loaded. If `_window` property value + * is changed with already loaded window `_onLoad` still will be called. + */ +const WindowLoader = Trait.compose({ + /** + * Internal listener that is called when window is loaded. + * Please keep in mind that this trait will not handle exceptions that may + * be thrown by this method so method itself should take care of + * handling them. + * @param {nsIWindow} window + */ + _onLoad: Trait.required, + _tabOptions: Trait.required, + /** + * Internal listener that is called when `_window`'s DOM 'unload' event + * is dispatched. Please note that this trait will not handle exceptions that + * may be thrown by this method so method itself should take care of + * handling them. + */ + _onUnload: Trait.required, + _load: function _load() { + if (this.__window) return; + let params = PARAMS.slice() + params.push(this._tabOptions.map(function(options) options.url).join("|")) + let browser = WM.getMostRecentWindow(BROWSER); + this._window = browser.openDialog.apply(browser, params); + }, + /** + * Private window who's load event is being tracked. Once window is loaded + * `_onLoad` is called. + * @type {nsIWindow} + */ + get _window() this.__window, + set _window(window) { + let _window = this.__window; + if (!window) window = null; + if (window !== _window) { + if (_window) { + _window.removeEventListener(ON_UNLOAD, this.__unloadListener, false); + _window.removeEventListener(ON_LOAD, this.__loadListener, false); + } + if (window) { + window.addEventListener( + ON_UNLOAD, + this.__unloadListener || + (this.__unloadListener = this._unloadListener.bind(this)) + , + false + ); + this.__window = window; + // If window is not loaded yet setting up a listener. + if (STATE_LOADED != window.document.readyState) { + window.addEventListener( + ON_LOAD, + this.__loadListener || + (this.__loadListener = this._loadListener.bind(this)) + , + false + ); + } + else { // If window is loaded calling listener next turn of event loop. + this._onLoad(window) + } + } + } + }, + __window: null, + /** + * Internal method used for listening 'load' event on the `_window`. + * Method takes care of removing itself from 'load' event listeners once + * event is being handled. + */ + _loadListener: function _loadListener(event) { + let window = this._window; + if (!event.target || event.target.defaultView != window) return; + window.removeEventListener(ON_LOAD, this.__loadListener, false); + this._onLoad(window); + }, + __loadListener: null, + /** + * Internal method used for listening 'unload' event on the `_window`. + * Method takes care of removing itself from 'unload' event listeners once + * event is being handled. + */ + _unloadListener: function _unloadListener(event) { + let window = this._window; + if (!event.target + || event.target.defaultView != window + || STATE_LOADED != window.document.readyState + ) return; + window.removeEventListener(ON_UNLOAD, this.__unloadListener, false); + this._onUnload(window); + }, + __unloadListener: null +}); +exports.WindowLoader = WindowLoader; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/windows/observer.js b/tools/addon-sdk-1.4/packages/api-utils/lib/windows/observer.js new file mode 100644 index 0000000..d7b01e6 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/windows/observer.js @@ -0,0 +1,86 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { EventEmitterTrait: EventEmitter } = require("../events"); +const { WindowTracker, windowIterator } = require("../window-utils"); +const { DOMEventAssembler } = require("../events/assembler"); +const { Trait } = require("../light-traits"); + +// Event emitter objects used to register listeners and emit events on them +// when they occur. +const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ + /** + * Method is implemented by `EventEmitter` and is used just for emitting + * events on registered listeners. + */ + _emit: Trait.required, + /** + * Events that are supported and emitted by the module. + */ + supportedEventsTypes: [ "activate", "deactivate" ], + /** + * Function handles all the supported events on all the windows that are + * observed. Method is used to proxy events to the listeners registered on + * this event emitter. + * @param {Event} event + * Keyboard event being emitted. + */ + handleEvent: function handleEvent(event) { + this._emit(event.type, event.target, event); + } +}); + +// Using `WindowTracker` to track window events. +new WindowTracker({ + onTrack: function onTrack(chromeWindow) { + observer._emit("open", chromeWindow); + observer.observe(chromeWindow); + }, + onUntrack: function onUntrack(chromeWindow) { + observer._emit("close", chromeWindow); + observer.ignore(chromeWindow); + } +}); + +// Making observer aware of already opened windows. +for each (let window in windowIterator()) + observer.observe(window); + +exports.observer = observer; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/windows/tabs.js b/tools/addon-sdk-1.4/packages/api-utils/lib/windows/tabs.js new file mode 100644 index 0000000..551b1ce --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/windows/tabs.js @@ -0,0 +1,207 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const { Trait } = require("../traits"); +const { List } = require("../list"); +const { Tab, Options } = require("../tabs/tab"); +const { EventEmitter } = require("../events"); +const { EVENTS } = require("../tabs/events"); +const { getOwnerWindow, getActiveTab, getTabs, + openTab, activateTab } = require("../tabs/utils"); +const { observer: tabsObserver } = require("../tabs/observer"); + +const TAB_BROWSER = "tabbrowser"; + +/** + * This is a trait that is used in composition of window wrapper. Trait tracks + * tab related events of the wrapped window in order to keep track of open + * tabs and maintain their wrappers. Every new tab gets wrapped and jetpack + * type event is emitted. + */ +const WindowTabTracker = Trait.compose({ + /** + * Chrome window whose tabs are tracked. + */ + _window: Trait.required, + /** + * Function used to emit events. + */ + _emit: EventEmitter.required, + _tabOptions: Trait.required, + /** + * Function to add event listeners. + */ + on: EventEmitter.required, + removeListener: EventEmitter.required, + /** + * Initializes tab tracker for a browser window. + */ + _initWindowTabTracker: function _initWindowTabTracker() { + // Ugly hack that we have to remove at some point (see Bug 658059). At this + // point it is necessary to invoke lazy `tabs` getter on the windows object + // which creates a `TabList` instance. + this.tabs; + // Binding all methods used as event listeners to the instance. + this._onTabReady = this._emitEvent.bind(this, "ready"); + this._onTabOpen = this._onTabEvent.bind(this, "open"); + this._onTabClose = this._onTabEvent.bind(this, "close"); + this._onTabActivate = this._onTabEvent.bind(this, "activate"); + this._onTabDeactivate = this._onTabEvent.bind(this, "deactivate"); + + for each (let tab in getTabs(this._window)) { + // We emulate "open" events for all open tabs since gecko does not emits + // them on the tabs that new windows are open with. Also this is + // necessary to synchronize tabs lists with an actual state. + this._onTabOpen(tab); + } + // We also emulate "activate" event so that it's picked up by a tab list. + this._onTabActivate(getActiveTab(this._window)); + + // Setting up event listeners + tabsObserver.on("open", this._onTabOpen); + tabsObserver.on("close", this._onTabClose); + tabsObserver.on("activate", this._onTabActivate); + tabsObserver.on("deactivate", this._onTabDeactivate); + }, + _destroyWindowTabTracker: function _destroyWindowTabTracker() { + // We emulate close events on all tabs, since gecko does not emits such + // events by itself. + for each (let tab in this.tabs) + this._emitEvent("close", tab); + + this._tabs._clear(); + + tabsObserver.removeListener("open", this._onTabOpen); + tabsObserver.removeListener("close", this._onTabClose); + tabsObserver.removeListener("activate", this._onTabActivate); + tabsObserver.removeListener("deactivate", this._onTabDeactivate); + }, + _onTabEvent: function _onTabEvent(type, tab) { + if (this._window === getOwnerWindow(tab)) { + let options = this._tabOptions.shift() || {}; + options.tab = tab; + options.window = this._public; + // creating tab wrapper and adding listener to "ready" events. + let wrappedTab = Tab(options); + + // Setting up an event listener for ready events. + if (type === "open") + wrappedTab.on("ready", this._onTabReady); + + this._emitEvent(type, wrappedTab); + } + }, + _emitEvent: function _emitEvent(type, tab) { + // Notifies combined tab list that tab was added / removed. + tabs._emit(type, tab); + // Notifies contained tab list that window was added / removed. + this._tabs._emit(type, tab); + } +}); +exports.WindowTabTracker = WindowTabTracker; + +/** + * This trait is used to create live representation of open tab lists. Each + * window wrapper's tab list is represented by an object created from this + * trait. It is also used to represent list of all the open windows. Trait is + * composed out of `EventEmitter` in order to emit 'TabOpen', 'TabClose' events. + * **Please note** that objects created by this trait can't be exposed outside + * instead you should expose it's `_public` property, see comments in + * constructor for details. + */ +const TabList = List.resolve({ constructor: "_init" }).compose( + // This is ugly, but necessary. Will be removed by #596248 + EventEmitter.resolve({ toString: null }), + Trait.compose({ + on: Trait.required, + _emit: Trait.required, + constructor: function TabList(options) { + this._window = options.window; + // Add new items to the list + this.on(EVENTS.open.name, this._add.bind(this)); + // Remove closed items from the list + this.on(EVENTS.close.name, this._remove.bind(this)); + + // Set value whenever new tab becomes active. + this.on("activate", function onTabActivate(tab) { + this._activeTab = tab; + }.bind(this)); + // Initialize list. + this._init(); + // This list is not going to emit any events, object holding this list + // will do it instead, to make that possible we return a private API. + return this; + }, + get activeTab() this._activeTab, + _activeTab: null, + + open: function open(options) { + options = Options(options); + this._window._tabOptions.push(options); + let tab = openTab(this._window._window, options.url); + if (!options.inBackground) + activateTab(tab); + } + // This is ugly, but necessary. Will be removed by #596248 + }).resolve({ toString: null }) +); + +/** + * Combined list of all open tabs on all the windows. + * type {TabList} + */ +var tabs = TabList({ window: null }); +exports.tabs = tabs._public; + +/** + * Trait is a part of composition that represents window wrapper. This trait is + * composed out of `WindowTabTracker` that allows it to keep track of open tabs + * on the window being wrapped. + */ +const WindowTabs = Trait.compose( + WindowTabTracker, + Trait.compose({ + _window: Trait.required, + /** + * List of tabs + */ + get tabs() (this._tabs || (this._tabs = TabList({ window: this })))._public, + _tabs: null, + }) +); +exports.WindowTabs = WindowTabs; diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/xhr.js b/tools/addon-sdk-1.4/packages/api-utils/lib/xhr.js new file mode 100644 index 0000000..10b83db --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/xhr.js @@ -0,0 +1,181 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci} = require("chrome"); + +// ## Implementation Notes ## +// +// Making `XMLHttpRequest` objects available to Jetpack code involves a +// few key principles universal to all low-level module implementations: +// +// * **Unloadability**. A Jetpack-based extension using this module can be +// asked to unload itself at any time, e.g. because the user decides to +// uninstall or disable the extension. This means we need to keep track of +// all in-progress reqests and abort them on unload. +// +// * **Developer-Ergonomic Tracebacks**. Whenever an exception is raised +// by a Jetpack-based extension, we want it to be logged in a +// place that is specific to that extension--so that a developer +// can distinguish it from an error on a web page or in another +// extension, for instance. We also want it to be logged with a +// full stack traceback, which the Mozilla platform doesn't usually +// do. +// +// Because of this, we don't actually want to give the Mozilla +// platform's "real" XHR implementation to clients, but instead provide +// a simple wrapper that trivially delegates to the implementation in +// all cases except where callbacks are involved: whenever Mozilla +// platform code calls into the extension, such as during the XHR's +// `onreadystatechange` callback, we want to wrap the client's callback +// in a try-catch clause that traps any exceptions raised by the +// callback and logs them via console.exception() instead of allowing +// them to propagate back into Mozilla platform code. + +// This is a private list of all active requests, so we know what to +// abort if we're asked to unload. +var requests = []; + +// Events on XHRs that we should listen for, so we know when to remove +// a request from our private list. +const TERMINATE_EVENTS = ["load", "error", "abort"]; + +// Read-only properties of XMLHttpRequest objects that we want to +// directly delegate to. +const READ_ONLY_PROPS = ["readyState", "responseText", "responseXML", + "status", "statusText"]; + +// Methods of XMLHttpRequest that we want to directly delegate to. +const DELEGATED_METHODS = ["abort", "getAllResponseHeaders", + "getResponseHeader", "overrideMimeType", + "send", "sendAsBinary", "setRequestHeader", + "open"]; + +var getRequestCount = exports.getRequestCount = function getRequestCount() { + return requests.length; +}; + +var XMLHttpRequest = exports.XMLHttpRequest = function XMLHttpRequest() { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + // For the sake of simplicity, don't tie this request to any UI. + req.mozBackgroundRequest = true; + + memory.track(req, "XMLHttpRequest"); + + this._req = req; + this._orsc = null; + + requests.push(this); + + var self = this; + + this._boundCleanup = function _boundCleanup() { + self._cleanup(); + }; + + TERMINATE_EVENTS.forEach( + function(name) { + self._req.addEventListener(name, self._boundCleanup, false); + }); +}; + +XMLHttpRequest.prototype = { + _cleanup: function _cleanup() { + this.onreadystatechange = null; + var index = requests.indexOf(this); + if (index != -1) { + var self = this; + TERMINATE_EVENTS.forEach( + function(name) { + self._req.removeEventListener(name, self._boundCleanup, false); + }); + requests.splice(index, 1); + } + }, + _unload: function _unload() { + this._req.abort(); + this._cleanup(); + }, + addEventListener: function addEventListener() { + throw new Error("not implemented"); + }, + removeEventListener: function removeEventListener() { + throw new Error("not implemented"); + }, + set upload(newValue) { + throw new Error("not implemented"); + }, + get onreadystatechange() { + return this._orsc; + }, + set onreadystatechange(cb) { + this._orsc = cb; + if (cb) { + var self = this; + this._req.onreadystatechange = function() { + try { + self._orsc.apply(self, arguments); + } catch (e) { + console.exception(e); + } + }; + } else + this._req.onreadystatechange = null; + } +}; + +READ_ONLY_PROPS.forEach( + function(name) { + XMLHttpRequest.prototype.__defineGetter__( + name, + function() { + return this._req[name]; + }); + }); + +DELEGATED_METHODS.forEach( + function(name) { + XMLHttpRequest.prototype[name] = function() { + return this._req[name].apply(this._req, arguments); + }; + }); + +require("./unload").when( + function() { + requests.slice().forEach(function(request) { request._unload(); }); + }); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/xpcom.js b/tools/addon-sdk-1.4/packages/api-utils/lib/xpcom.js new file mode 100644 index 0000000..10f1d6b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/xpcom.js @@ -0,0 +1,152 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * Drew Willcoxon <adw@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc,Ci,Cm,Cr,Cu} = require("chrome"); + +var jsm = {}; +Cu.import("resource://gre/modules/XPCOMUtils.jsm", jsm); +var utils = exports.utils = jsm.XPCOMUtils; + +Cm.QueryInterface(Ci.nsIComponentRegistrar); + +var factories = []; + +function Factory(options) { + memory.track(this); + + this.wrappedJSObject = this; + this.create = options.create; + this.uuid = options.uuid; + this.name = options.name; + this.contractID = options.contractID; + + Cm.registerFactory(this.uuid, + this.name, + this.contractID, + this); + + var self = this; + + factories.push(this); +} + +Factory.prototype = { + createInstance: function(outer, iid) { + try { + if (outer) + throw Cr.NS_ERROR_NO_AGGREGATION; + return (new this.create()).QueryInterface(iid); + } catch (e) { + console.exception(e); + if (e instanceof Ci.nsIException) + throw e; + else + throw Cr.NS_ERROR_FAILURE; + } + }, + unregister: function() { + var index = factories.indexOf(this); + if (index == -1) + throw new Error("factory already unregistered"); + + var self = this; + + factories.splice(index, 1); + Cm.unregisterFactory(this.uuid, this); + }, + QueryInterface: utils.generateQI([Ci.nsIFactory]) +}; + +var makeUuid = exports.makeUuid = function makeUuid() { + var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + var uuid = uuidGenerator.generateUUID(); + return uuid; +}; + +var autoRegister = exports.autoRegister = function autoRegister(path) { + // TODO: This assumes that the url points to a directory + // that contains subdirectories corresponding to OS/ABI and then + // further subdirectories corresponding to Gecko platform version. + // we should probably either behave intelligently here or allow + // the caller to pass-in more options if e.g. there aren't + // Gecko-specific binaries for a component (which will be the case + // if only frozen interfaces are used). + + var appInfo = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULAppInfo); + var runtime = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULRuntime); + + var osDirName = runtime.OS + "_" + runtime.XPCOMABI; + var platformVersion = appInfo.platformVersion.substring(0, 5); + + var file = Cc['@mozilla.org/file/local;1'] + .createInstance(Ci.nsILocalFile); + file.initWithPath(path); + file.append(osDirName); + file.append(platformVersion); + + if (!(file.exists() && file.isDirectory())) + throw new Error("component not available for OS/ABI " + + osDirName + " and platform " + platformVersion); + + Cm.QueryInterface(Ci.nsIComponentRegistrar); + Cm.autoRegister(file); +}; + +var register = exports.register = function register(options) { + options = {__proto__: options}; + if (!options.uuid) + options.uuid = makeUuid(); + return new Factory(options); +}; + +var getClass = exports.getClass = function getClass(contractID, iid) { + if (!iid) + iid = Ci.nsISupports; + return Cm.getClassObjectByContractID(contractID, iid); +}; + +require("./unload").when( + function() { + var copy = factories.slice(); + copy.reverse(); + copy.forEach(function(factory) { factory.unregister(); }); + }); diff --git a/tools/addon-sdk-1.4/packages/api-utils/lib/xul-app.js b/tools/addon-sdk-1.4/packages/api-utils/lib/xul-app.js new file mode 100644 index 0000000..c118682 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/lib/xul-app.js @@ -0,0 +1,95 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const {Cc, Ci} = require("chrome"); + +var appInfo = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULAppInfo); + +var ID = exports.ID = appInfo.ID; +var name = exports.name = appInfo.name; +var version = exports.version = appInfo.version; +var platformVersion = exports.platformVersion = appInfo.platformVersion; + +// The following mapping of application names to GUIDs was taken from: +// +// https://addons.mozilla.org/en-US/firefox/pages/appversions +// +// Using the GUID instead of the app's name is preferable because sometimes +// re-branded versions of a product have different names: for instance, +// Firefox, Minefield, Iceweasel, and Shiretoko all have the same +// GUID. +// This mapping is duplicated in `app-extensions/bootstrap.js`. They should keep +// in sync, so if you change one, change the other too! + +var ids = exports.ids = { + Firefox: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + Mozilla: "{86c18b42-e466-45a9-ae7a-9b95ba6f5640}", + Sunbird: "{718e30fb-e89b-41dd-9da7-e25a45638b28}", + SeaMonkey: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + Fennec: "{a23983c0-fd0e-11dc-95ff-0800200c9a66}", + Thunderbird: "{3550f703-e582-4d05-9a08-453d09bdfdc6}" +}; + +var is = exports.is = function is(name) { + if (!(name in ids)) + throw new Error("Unkown Mozilla Application: " + name); + return ID == ids[name]; +}; + +var isOneOf = exports.isOneOf = function isOneOf(names) { + for (var i = 0; i < names.length; i++) + if (is(names[i])) + return true; + return false; +}; + +/** + * Use this to check whether the given version (e.g. xulApp.platformVersion) + * is in the given range. Versions must be in version comparator-compatible + * format. See MDC for details: + * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIVersionComparator + */ +var versionInRange = exports.versionInRange = +function versionInRange(version, lowInclusive, highExclusive) { + var vc = Cc["@mozilla.org/xpcom/version-comparator;1"] + .getService(Ci.nsIVersionComparator); + return (vc.compare(version, lowInclusive) >= 0) && + (vc.compare(version, highExclusive) < 0); +} + diff --git a/tools/addon-sdk-1.4/packages/api-utils/package.json b/tools/addon-sdk-1.4/packages/api-utils/package.json new file mode 100644 index 0000000..d734856 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/package.json @@ -0,0 +1,14 @@ +{ + "name": "api-utils", + "description": "Foundational infrastructure and utilities.", + "keywords": ["javascript", "engine", "platform", "xulrunner", + "jetpack-low-level"], + "author": "Atul Varma (http://toolness.com/) <atul@mozilla.com>", + "contributors": [ + "Myk Melez (http://melez.com/) <myk@mozilla.org>", + "Daniel Aquino <mr.danielaquino@gmail.com>" + ], + "license": "MPL 1.1/GPL 2.0/LGPL 2.1", + "dependencies": ["addon-kit"], + "loader": "lib/cuddlefish.js" +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/commonjs-test-adapter/asserts.js b/tools/addon-sdk-1.4/packages/api-utils/tests/commonjs-test-adapter/asserts.js new file mode 100644 index 0000000..83abd23 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/commonjs-test-adapter/asserts.js @@ -0,0 +1,50 @@ +"use strict"; + +const AssertBase = require("test/assert").Assert; + +/** + * Generates custom assertion constructors that may be bundled with a test + * suite. + * @params {String} + * names of assertion function to be added to the generated Assert. + */ +function Assert() { + let assertDescriptor = {}; + Array.forEach(arguments, function(name) { + assertDescriptor[name] = { value: function(message) { + this.pass(message); + }} + }); + + return function Assert() { + return Object.create(AssertBase.apply(null, arguments), assertDescriptor); + }; +} + +exports["test suite"] = { + Assert: Assert("foo"), + "test that custom assertor is passed to test function": function(assert) { + assert.ok("foo" in assert, "custom assertion function `foo` is defined"); + assert.foo("custom assertion function `foo` is called"); + }, + "test sub suite": { + "test that `Assert` is inherited by sub suits": function(assert) { + assert.ok("foo" in assert, "assertion function `foo` is not defined"); + }, + "test sub sub suite": { + Assert: Assert("bar"), + "test that custom assertor is passed to test function": function(assert) { + assert.ok("bar" in assert, + "custom assertion function `bar` is defined"); + assert.bar("custom assertion function `bar` is called"); + }, + "test that `Assert` is not inherited by sub sub suits": function(assert) { + assert.ok(!("foo" in assert), + "assertion function `foo` is not defined"); + } + } + } +}; + +if (module == require.main) + require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/fixtures/es5.js b/tools/addon-sdk-1.4/packages/api-utils/tests/fixtures/es5.js new file mode 100644 index 0000000..6e5b1ea --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/fixtures/es5.js @@ -0,0 +1,4 @@ +"use strict"; +exports.frozen = Object.freeze({}); +exports.sealed = Object.seal({}); +exports.inextensible = Object.preventExtensions({}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/helpers.js b/tools/addon-sdk-1.4/packages/api-utils/tests/helpers.js new file mode 100644 index 0000000..bbfe911 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/helpers.js @@ -0,0 +1,19 @@ +"use strict"; + +const { Loader } = require("@loader"); + +exports.Loader = function(module, globals) { + var options = JSON.parse(JSON.stringify(require("@packaging"))); + options.globals = globals; + let loader = Loader.new(options); + return Object.create(loader, { + require: { value: Loader.require.bind(loader, module.path) }, + sandbox: { value: function sandbox(id) { + let path = options.manifest[module.path].requirements[id].path; + return loader.sandboxes[path].sandbox; + }}, + unload: { value: function unload(reason, callback) { + loader.unload(reason, callback); + }} + }) +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/loader/fixture.js b/tools/addon-sdk-1.4/packages/api-utils/tests/loader/fixture.js new file mode 100644 index 0000000..d097dea --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/loader/fixture.js @@ -0,0 +1,2 @@ +exports.foo = foo; +console.log('testing', 1, [2, 3, 4]); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/add.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/add.js new file mode 100644 index 0000000..5825e08 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/add.js @@ -0,0 +1,5 @@ +define('modules/add', function () { + return function (a, b) { + return a + b; + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/async1.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/async1.js new file mode 100644 index 0000000..cb51500 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/async1.js @@ -0,0 +1,10 @@ +define(['./traditional2', './async2'], function () { + var traditional2 = require('./traditional2'); + return { + name: 'async1', + traditional1Name: traditional2.traditional1Name, + traditional2Name: traditional2.name, + async2Name: require('./async2').name, + async2Traditional2Name: require('./async2').traditional2Name + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/async2.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/async2.js new file mode 100644 index 0000000..c0281e5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/async2.js @@ -0,0 +1,4 @@ +define(['./traditional2', 'exports'], function (traditional2, exports) { + exports.name = 'async2'; + exports.traditional2Name = traditional2.name; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/badExportAndReturn.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/badExportAndReturn.js new file mode 100644 index 0000000..0844be1 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/badExportAndReturn.js @@ -0,0 +1,6 @@ +// This is a bad module, it asks for exports but also returns a value from +// the define defintion function. +define(['exports'], function (exports) { + return 'badExportAndReturn'; +}); + diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/badFirst.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/badFirst.js new file mode 100644 index 0000000..c3e4c36 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/badFirst.js @@ -0,0 +1,5 @@ +define(['./badSecond'], function (badSecond) { + return { + name: 'badFirst' + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/badSecond.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/badSecond.js new file mode 100644 index 0000000..213c7b8 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/badSecond.js @@ -0,0 +1,4 @@ +var first = require('./badFirst'); + +exports.name = 'badSecond'; +exports.badFirstName = first.name; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/blue.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/blue.js new file mode 100644 index 0000000..af3a193 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/blue.js @@ -0,0 +1,5 @@ +define(function () { + return { + name: 'blue' + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/castor.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/castor.js new file mode 100644 index 0000000..c2d40b7 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/castor.js @@ -0,0 +1,6 @@ +define(['exports', './pollux'], function(exports, pollux) { + exports.name = 'castor'; + exports.getPolluxName = function () { + return pollux.name; + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/cheetah.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/cheetah.js new file mode 100644 index 0000000..ad24e3a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/cheetah.js @@ -0,0 +1,5 @@ +define(function () { + return function () { + return 'cheetah'; + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/color.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/color.js new file mode 100644 index 0000000..e1fe374 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/color.js @@ -0,0 +1,3 @@ +define({ + type: 'color' +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/dupe.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/dupe.js new file mode 100644 index 0000000..f5ce8c9 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/dupe.js @@ -0,0 +1,11 @@ +define({ + name: 'dupe' +}); + +// This is wrong and should not be allowed. Only one call to +// define per file. +define([], function () { + return { + name: 'dupe2' + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/dupeNested.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/dupeNested.js new file mode 100644 index 0000000..85ecb8d --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/dupeNested.js @@ -0,0 +1,11 @@ + +define(function () { + // This is wrong and should not be allowed. + define('dupeNested2', { + name: 'dupeNested2' + }); + + return { + name: 'dupeNested' + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/dupeSetExports.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/dupeSetExports.js new file mode 100644 index 0000000..8ad3417 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/dupeSetExports.js @@ -0,0 +1,4 @@ +define({name: "dupeSetExports"}); + +// so this should cause a failure +module.setExports("no no no"); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/exportsEquals.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/exportsEquals.js new file mode 100644 index 0000000..a9bbdd8 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/exportsEquals.js @@ -0,0 +1 @@ +module.exports = 4; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/green.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/green.js new file mode 100644 index 0000000..8bca33c --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/green.js @@ -0,0 +1,6 @@ +define('modules/green', ['./color'], function (color) { + return { + name: 'green', + parentType: color.type + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/lion.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/lion.js new file mode 100644 index 0000000..f3962c1 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/lion.js @@ -0,0 +1,3 @@ +define(function(require) { + return 'lion'; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/orange.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/orange.js new file mode 100644 index 0000000..d983a35 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/orange.js @@ -0,0 +1,6 @@ +define(['./color'], function (color) { + return { + name: 'orange', + parentType: color.type + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/pollux.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/pollux.js new file mode 100644 index 0000000..e49370b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/pollux.js @@ -0,0 +1,6 @@ +define(['exports', './castor'], function(exports, castor) { + exports.name = 'pollux'; + exports.getCastorName = function () { + return castor.name; + }; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/red.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/red.js new file mode 100644 index 0000000..eb58660 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/red.js @@ -0,0 +1,12 @@ +define(function (require) { + // comment fake-outs for require finding. + // require('bad1'); + return { + name: 'red', + parentType: require('./color').type + }; + + /* + require('bad2'); + */ +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/setExports.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/setExports.js new file mode 100644 index 0000000..290a3cb --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/setExports.js @@ -0,0 +1 @@ +module.setExports(5); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/subtract.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/subtract.js new file mode 100644 index 0000000..2743132 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/subtract.js @@ -0,0 +1,5 @@ +define(function () { + return function (a, b) { + return a - b; + } +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/tiger.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/tiger.js new file mode 100644 index 0000000..9a98b76 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/tiger.js @@ -0,0 +1,4 @@ +define(function (require, exports) { + exports.name = 'tiger'; + exports.type = require('modules/types/cat').type; +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/traditional1.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/traditional1.js new file mode 100644 index 0000000..d2e720d --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/traditional1.js @@ -0,0 +1,8 @@ +exports.name = 'traditional1' + +var async1 = require('./async1'); + +exports.traditional2Name = async1.traditional2Name; +exports.traditional1Name = async1.traditional1Name; +exports.async2Name = async1.async2Name; +exports.async2Traditional2Name = async1.async2Traditional2Name; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/traditional2.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/traditional2.js new file mode 100644 index 0000000..8363404 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/traditional2.js @@ -0,0 +1,2 @@ +exports.name = 'traditional2'; +exports.traditional1Name = require('./traditional1').name; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/modules/types/cat.js b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/types/cat.js new file mode 100644 index 0000000..24a1c59 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/modules/types/cat.js @@ -0,0 +1 @@ +exports.type = 'cat'; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-api-utils.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-api-utils.js new file mode 100644 index 0000000..fc072bb --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-api-utils.js @@ -0,0 +1,274 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const apiUtils = require("api-utils/api-utils"); + +exports.testPublicConstructor = function (test) { + function PrivateCtor() {} + PrivateCtor.prototype = {}; + + let PublicCtor = apiUtils.publicConstructor(PrivateCtor); + test.assert( + PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype), + "PrivateCtor.prototype should be prototype of PublicCtor.prototype" + ); + + function testObj(useNew) { + let obj = useNew ? new PublicCtor() : PublicCtor(); + test.assert(obj instanceof PublicCtor, + "Object should be instance of PublicCtor"); + test.assert(obj instanceof PrivateCtor, + "Object should be instance of PrivateCtor"); + test.assert(PublicCtor.prototype.isPrototypeOf(obj), + "PublicCtor's prototype should be prototype of object"); + test.assertEqual(obj.constructor, PublicCtor, + "Object constructor should be PublicCtor"); + } + testObj(true); + testObj(false); +}; + +exports.testValidateOptionsEmpty = function (test) { + let val = apiUtils.validateOptions(null, {}); + assertObjsEqual(test, val, {}); + + val = apiUtils.validateOptions(null, { foo: {} }); + assertObjsEqual(test, val, {}); + + val = apiUtils.validateOptions({}, {}); + assertObjsEqual(test, val, {}); + + val = apiUtils.validateOptions({}, { foo: {} }); + assertObjsEqual(test, val, {}); +}; + +exports.testValidateOptionsNonempty = function (test) { + let val = apiUtils.validateOptions({ foo: 123 }, {}); + assertObjsEqual(test, val, {}); + + val = apiUtils.validateOptions({ foo: 123, bar: 456 }, + { foo: {}, bar: {}, baz: {} }); + assertObjsEqual(test, val, { foo: 123, bar: 456 }); +}; + +exports.testValidateOptionsMap = function (test) { + let val = apiUtils.validateOptions({ foo: 3, bar: 2 }, { + foo: { map: function (v) v * v }, + bar: { map: function (v) undefined } + }); + assertObjsEqual(test, val, { foo: 9, bar: undefined }); +}; + +exports.testValidateOptionsMapException = function (test) { + let val = apiUtils.validateOptions({ foo: 3 }, { + foo: { map: function () { throw new Error(); }} + }); + assertObjsEqual(test, val, { foo: 3 }); +}; + +exports.testValidateOptionsOk = function (test) { + let val = apiUtils.validateOptions({ foo: 3, bar: 2, baz: 1 }, { + foo: { ok: function (v) v }, + bar: { ok: function (v) v } + }); + assertObjsEqual(test, val, { foo: 3, bar: 2 }); + + test.assertRaises( + function () apiUtils.validateOptions({ foo: 2, bar: 2 }, { + bar: { ok: function (v) v > 2 } + }), + 'The option "bar" is invalid.', + "ok should raise exception on invalid option" + ); + + test.assertRaises( + function () apiUtils.validateOptions(null, { foo: { ok: function (v) v }}), + 'The option "foo" is invalid.', + "ok should raise exception on invalid option" + ); +}; + +exports.testValidateOptionsIs = function (test) { + let opts = { + array: [], + boolean: true, + func: function () {}, + nul: null, + number: 1337, + object: {}, + string: "foo", + undef1: undefined + }; + let requirements = { + array: { is: ["array"] }, + boolean: { is: ["boolean"] }, + func: { is: ["function"] }, + nul: { is: ["null"] }, + number: { is: ["number"] }, + object: { is: ["object"] }, + string: { is: ["string"] }, + undef1: { is: ["undefined"] }, + undef2: { is: ["undefined"] } + }; + let val = apiUtils.validateOptions(opts, requirements); + assertObjsEqual(test, val, opts); + + test.assertRaises( + function () apiUtils.validateOptions(null, { + foo: { is: ["object", "number"] } + }), + 'The option "foo" must be one of the following types: object, number', + "Invalid type should raise exception" + ); +}; + +exports.testValidateOptionsMapIsOk = function (test) { + let [map, is, ok] = [false, false, false]; + let val = apiUtils.validateOptions({ foo: 1337 }, { + foo: { + map: function (v) v.toString(), + is: ["string"], + ok: function (v) v.length > 0 + } + }); + assertObjsEqual(test, val, { foo: "1337" }); + + let requirements = { + foo: { + is: ["object"], + ok: function () test.fail("is should have caused us to throw by now") + } + }; + test.assertRaises( + function () apiUtils.validateOptions(null, requirements), + 'The option "foo" must be one of the following types: object', + "is should be used before ok is called" + ); +}; + +exports.testValidateOptionsErrorMsg = function (test) { + test.assertRaises( + function () apiUtils.validateOptions(null, { + foo: { ok: function (v) v, msg: "foo!" } + }), + "foo!", + "ok should raise exception with customized message" + ); +}; + +exports.testValidateMapWithMissingKey = function (test) { + let val = apiUtils.validateOptions({ }, { + foo: { + map: function (v) v || "bar" + } + }); + assertObjsEqual(test, val, { foo: "bar" }); + + val = apiUtils.validateOptions({ }, { + foo: { + map: function (v) { throw "bar" } + } + }); + assertObjsEqual(test, val, { }); +}; + +exports.testAddIterator = function testAddIterator(test) { + let obj = {}; + let keys = ["foo", "bar", "baz"]; + let vals = [1, 2, 3]; + let keysVals = [["foo", 1], ["bar", 2], ["baz", 3]]; + apiUtils.addIterator( + obj, + function keysValsGen() { + for each (let keyVal in keysVals) + yield keyVal; + } + ); + + let keysItr = []; + for (let key in obj) + keysItr.push(key); + test.assertEqual(keysItr.length, keys.length, + "the keys iterator returns the correct number of items"); + for (let i = 0; i < keys.length; i++) + test.assertEqual(keysItr[i], keys[i], "the key is correct"); + + let valsItr = []; + for each (let val in obj) + valsItr.push(val); + test.assertEqual(valsItr.length, vals.length, + "the vals iterator returns the correct number of items"); + for (let i = 0; i < vals.length; i++) + test.assertEqual(valsItr[i], vals[i], "the val is correct"); + + let keysValsItr = []; + for (let keyVal in Iterator(obj)) + keysValsItr.push(keyVal); + test.assertEqual(keysValsItr.length, keysVals.length, "the keys/vals " + + "iterator returns the correct number of items"); + for (let i = 0; i < keysVals.length; i++) { + test.assertEqual(keysValsItr[i][0], keysVals[i][0], "the key is correct"); + test.assertEqual(keysValsItr[i][1], keysVals[i][1], "the val is correct"); + } + + let keysOnlyItr = []; + for (let key in Iterator(obj, true /* keysonly */)) + keysOnlyItr.push(key); + test.assertEqual(keysOnlyItr.length, keysVals.length, "the keys only " + + "iterator returns the correct number of items"); + for (let i = 0; i < keysVals.length; i++) + test.assertEqual(keysOnlyItr[i], keysVals[i][0], "the key is correct"); +}; + +function assertObjsEqual(test, obj1, obj2) { + var items = 0; + for (let [key, val] in Iterator(obj1)) { + items++; + test.assert(key in obj2, "obj1 key should be present in obj2"); + test.assertEqual(obj2[key], val, "obj1 value should match obj2 value"); + } + for (let [key, val] in Iterator(obj2)) { + items++; + test.assert(key in obj1, "obj2 key should be present in obj1"); + test.assertEqual(obj1[key], val, "obj2 value should match obj1 value"); + } + if (!items) + test.assertEqual(JSON.stringify(obj1), JSON.stringify(obj2), + "obj1 should have same JSON representation as obj2"); +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-app-strings.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-app-strings.js new file mode 100644 index 0000000..3c41929 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-app-strings.js @@ -0,0 +1,58 @@ +const { Cc,Ci } = require("chrome"); + +let StringBundle = require("app-strings").StringBundle; +exports.testStringBundle = function(test) { + let url = "chrome://global/locale/security/caps.properties"; + + let strings = StringBundle(url); + + test.assertEqual(strings.url, url, + "'url' property contains correct URL of string bundle"); + + let appLocale = Cc["@mozilla.org/intl/nslocaleservice;1"]. + getService(Ci.nsILocaleService). + getApplicationLocale(); + + let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(url, appLocale); + + let (name = "Yes") { + test.assertEqual(strings.get(name), stringBundle.GetStringFromName(name), + "getting a string returns the string"); + } + + let (name = "ExtensionCapability", args = ["foo"]) { + test.assertEqual(strings.get(name, args), + stringBundle.formatStringFromName(name, args, args.length), + "getting a formatted string returns the formatted string"); + } + + test.assertRaises(function () strings.get("nonexistentString"), + "String 'nonexistentString' could not be retrieved from " + + "the bundle due to an unknown error (it doesn't exist?).", + "retrieving a nonexistent string throws an exception"); + + let a = [], b = []; + let enumerator = stringBundle.getSimpleEnumeration(); + while (enumerator.hasMoreElements()) { + let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + a.push([elem.key, elem.value]); + } + for (let keyVal in Iterator(strings)) + b.push(keyVal); + + // Sort the arrays, because we don't assume enumeration has a set order. + // Sort compares [key, val] as string "key,val", which sorts the way we want + // it to, so there is no need to provide a custom compare function. + a.sort(); + b.sort(); + + test.assertEqual(a.length, b.length, + "the iterator returns the correct number of items"); + for (let i = 0; i < a.length; i++) { + test.assertEqual(a[i][0], b[i][0], "the iterated string's name is correct"); + test.assertEqual(a[i][1], b[i][1], + "the iterated string's value is correct"); + } +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-array.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-array.js new file mode 100644 index 0000000..88aed24 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-array.js @@ -0,0 +1,36 @@ +var array = require("array"); + +exports.testHas = function(test) { + var testAry = [1, 2, 3]; + test.assertEqual(array.has([1, 2, 3], 1), true); + test.assertEqual(testAry.length, 3); + test.assertEqual(testAry[0], 1); + test.assertEqual(testAry[1], 2); + test.assertEqual(testAry[2], 3); + test.assertEqual(array.has(testAry, 2), true); + test.assertEqual(array.has(testAry, 3), true); + test.assertEqual(array.has(testAry, 4), false); + test.assertEqual(array.has(testAry, "1"), false); +}; + +exports.testAdd = function(test) { + var testAry = [1]; + test.assertEqual(array.add(testAry, 1), false); + test.assertEqual(testAry.length, 1); + test.assertEqual(testAry[0], 1); + test.assertEqual(array.add(testAry, 2), true); + test.assertEqual(testAry.length, 2); + test.assertEqual(testAry[0], 1); + test.assertEqual(testAry[1], 2); +}; + +exports.testRemove = function(test) { + var testAry = [1, 2]; + test.assertEqual(array.remove(testAry, 3), false); + test.assertEqual(testAry.length, 2); + test.assertEqual(testAry[0], 1); + test.assertEqual(testAry[1], 2); + test.assertEqual(array.remove(testAry, 2), true); + test.assertEqual(testAry.length, 1); + test.assertEqual(testAry[0], 1); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-byte-streams.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-byte-streams.js new file mode 100644 index 0000000..4eae428 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-byte-streams.js @@ -0,0 +1,203 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const byteStreams = require("byte-streams"); +const file = require("file"); +const url = require("url"); +const { Loader } = require("./helpers"); + +const STREAM_CLOSED_ERROR = "The stream is closed and cannot be used."; + +// This should match the constant of the same name in byte-streams.js. +const BUFFER_BYTE_LEN = 0x8000; + +exports.testWriteRead = function (test) { + let fname = dataFileFilename(); + + // Write a small string less than the stream's buffer size... + let str = "exports.testWriteRead data!"; + let stream = open(test, fname, true); + test.assert(!stream.closed, "stream.closed after open should be false"); + stream.write(str); + stream.close(); + test.assert(stream.closed, "Stream should be closed after stream.close"); + test.assertRaises(function () stream.write("This shouldn't be written!"), + STREAM_CLOSED_ERROR, + "stream.write after close should raise error"); + + // ... and read it. + stream = open(test, fname); + test.assertEqual(stream.read(), str, + "stream.read should return string written"); + test.assertEqual(stream.read(), "", + "stream.read at EOS should return empty string"); + stream.close(); + test.assert(stream.closed, "Stream should be closed after stream.close"); + test.assertRaises(function () stream.read(), + STREAM_CLOSED_ERROR, + "stream.read after close should raise error"); + + file.remove(fname); +}; + +// Write a big string many times the size of the stream's buffer and read it. +exports.testWriteReadBig = function (test) { + let str = ""; + let bufLen = BUFFER_BYTE_LEN; + let fileSize = bufLen * 10; + for (let i = 0; i < fileSize; i++) + str += i % 10; + let fname = dataFileFilename(); + let stream = open(test, fname, true); + stream.write(str); + stream.close(); + stream = open(test, fname); + test.assertEqual(stream.read(), str, + "stream.read should return string written"); + stream.close(); + file.remove(fname); +}; + +// The same, but write and read in chunks. +exports.testWriteReadChunks = function (test) { + let str = ""; + let bufLen = BUFFER_BYTE_LEN; + let fileSize = bufLen * 10; + for (let i = 0; i < fileSize; i++) + str += i % 10; + let fname = dataFileFilename(); + let stream = open(test, fname, true); + let i = 0; + while (i < str.length) { + // Use a chunk length that spans buffers. + let chunk = str.substr(i, bufLen + 1); + stream.write(chunk); + i += bufLen + 1; + } + stream.close(); + stream = open(test, fname); + let readStr = ""; + bufLen = BUFFER_BYTE_LEN; + let readLen = bufLen + 1; + do { + var frag = stream.read(readLen); + readStr += frag; + } while (frag); + stream.close(); + test.assertEqual(readStr, str, + "stream.write and read in chunks should work as expected"); + file.remove(fname); +}; + +exports.testReadLengths = function (test) { + let fname = dataFileFilename(); + let str = "exports.testReadLengths data!"; + let stream = open(test, fname, true); + stream.write(str); + stream.close(); + + stream = open(test, fname); + test.assertEqual(stream.read(str.length * 1000), str, + "stream.read with big byte length should return string " + + "written"); + stream.close(); + + stream = open(test, fname); + test.assertEqual(stream.read(0), "", + "string.read with zero byte length should return empty " + + "string"); + stream.close(); + + stream = open(test, fname); + test.assertEqual(stream.read(-1), "", + "string.read with negative byte length should return " + + "empty string"); + stream.close(); + + file.remove(fname); +}; + +exports.testTruncate = function (test) { + let fname = dataFileFilename(); + let str = "exports.testReadLengths data!"; + let stream = open(test, fname, true); + stream.write(str); + stream.close(); + + stream = open(test, fname); + test.assertEqual(stream.read(), str, + "stream.read should return string written"); + stream.close(); + + stream = open(test, fname, true); + stream.close(); + + stream = open(test, fname); + test.assertEqual(stream.read(), "", + "stream.read after truncate should be empty"); + stream.close(); + + file.remove(fname); +}; + +exports.testUnload = function (test) { + let loader = Loader(module); + let file = loader.require("file"); + + let filename = url.toFilename(module.uri); + let stream = file.open(filename, "b"); + + loader.unload(); + test.assert(stream.closed, "Stream should be closed after module unload"); +}; + +// Returns the name of a file that should be used to test writing and reading. +function dataFileFilename() { + let dir = file.dirname(url.toFilename(module.uri)); + return file.join(dir, "test-byte-streams-data"); +} + +// Opens and returns the given file and ensures it's of the correct class. +function open(test, filename, forWriting) { + let stream = file.open(filename, forWriting ? "wb" : "b"); + let klass = forWriting ? "ByteWriter" : "ByteReader"; + test.assert(stream instanceof byteStreams[klass], + "Opened stream should be a " + klass); + return stream; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-collection.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-collection.js new file mode 100644 index 0000000..9db638e --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-collection.js @@ -0,0 +1,160 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const collection = require("collection"); + +exports.testAddRemove = function (test) { + let coll = new collection.Collection(); + compare(test, coll, []); + addRemove(test, coll, [], false); +}; + +exports.testAddRemoveBackingArray = function (test) { + let items = ["foo"]; + let coll = new collection.Collection(items); + compare(test, coll, items); + addRemove(test, coll, items, true); + + items = ["foo", "bar"]; + coll = new collection.Collection(items); + compare(test, coll, items); + addRemove(test, coll, items, true); +}; + +exports.testProperty = function (test) { + let obj = makeObjWithCollProp(); + compare(test, obj.coll, []); + addRemove(test, obj.coll, [], false); + + // Test single-value set. + let items = ["foo"]; + obj.coll = items[0]; + compare(test, obj.coll, items); + addRemove(test, obj.coll, items, false); + + // Test array set. + items = ["foo", "bar"]; + obj.coll = items; + compare(test, obj.coll, items); + addRemove(test, obj.coll, items, false); +}; + +exports.testPropertyBackingArray = function (test) { + let items = ["foo"]; + let obj = makeObjWithCollProp(items); + compare(test, obj.coll, items); + addRemove(test, obj.coll, items, true); + + items = ["foo", "bar"]; + obj = makeObjWithCollProp(items); + compare(test, obj.coll, items); + addRemove(test, obj.coll, items, true); +}; + +// Adds some values to coll and then removes them. initialItems is an array +// containing coll's initial items. isBacking is true if initialItems is coll's +// backing array; the point is that updates to coll should affect initialItems +// if that's the case. +function addRemove(test, coll, initialItems, isBacking) { + let items = isBacking ? initialItems : initialItems.slice(0); + let numInitialItems = items.length; + + // Test add(val). + let numInsertions = 5; + for (let i = 0; i < numInsertions; i++) { + compare(test, coll, items); + coll.add(i); + if (!isBacking) + items.push(i); + } + compare(test, coll, items); + + // Add the items we just added to make sure duplicates aren't added. + for (let i = 0; i < numInsertions; i++) + coll.add(i); + compare(test, coll, items); + + // Test remove(val). Do a kind of shuffled remove. Remove item 1, then + // item 0, 3, 2, 5, 4, ... + for (let i = 0; i < numInsertions; i++) { + let val = i % 2 ? i - 1 : + i === numInsertions - 1 ? i : i + 1; + coll.remove(val); + if (!isBacking) + items.splice(items.indexOf(val), 1); + compare(test, coll, items); + } + test.assertEqual(coll.length, numInitialItems, + "All inserted items should be removed"); + + // Remove the items we just removed. coll should be unchanged. + for (let i = 0; i < numInsertions; i++) + coll.remove(i); + compare(test, coll, items); + + // Test add and remove([val1, val2]). + let newItems = [0, 1]; + coll.add(newItems); + compare(test, coll, isBacking ? items : items.concat(newItems)); + coll.remove(newItems); + compare(test, coll, items); + test.assertEqual(coll.length, numInitialItems, + "All inserted items should be removed"); +} + +// Asserts that the items in coll are the items of array. +function compare(test, coll, array) { + test.assertEqual(coll.length, array.length, + "Collection length should be correct"); + let numItems = 0; + for (let item in coll) { + test.assertEqual(item, array[numItems], "Items should be equal"); + numItems++; + } + test.assertEqual(numItems, array.length, + "Number of items in iteration should be correct"); +} + +// Returns a new object with a collection property named "coll". backingArray, +// if defined, will create the collection with that backing array. +function makeObjWithCollProp(backingArray) { + let obj = {}; + collection.addCollectionProperty(obj, "coll", backingArray); + return obj; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-commonjs-test-adapter.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-commonjs-test-adapter.js new file mode 100644 index 0000000..71fc4a0 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-commonjs-test-adapter.js @@ -0,0 +1,7 @@ +"use strict"; + +exports["test custom `Assert`'s"] = require("./commonjs-test-adapter/asserts"); + +// Disabling this check since it is not yet supported by jetpack. +// if (module == require.main) + require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-loader.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-loader.js new file mode 100644 index 0000000..0e91ee5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-loader.js @@ -0,0 +1,223 @@ +"use strict"; +const { Loader } = require('content/loader'); +const self = require("self"); + +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.', + 'Must throw an exception if `contentURL` is not URL.' + ); + test.assertRaises( + function() loader.contentURL = { toString: function() 'Oops' }, + 'The `contentURL` option must be a valid URL.', + 'Must throw an exception if `contentURL` is not URL.' + ); + + function listener(e) { + emitted ++; + test.assert( + 'contentURL' in e, + 'emitted event must contain "content" property' + ); + test.assert( + value, + '' + e.contentURL, + 'content property of an event must match value' + ); + } + loader.on('propertyChange', listener); + + test.assertEqual( + null, + loader.contentURL, + 'default value is `null`' + ); + loader.contentURL = value = 'data:text/html,<html><body>Hi</body><html>'; + test.assertEqual( + value, + '' + loader.contentURL, + 'data uri is ok' + ); + test.assertEqual( + ++changes, + emitted, + 'had to emit `propertyChange`' + ); + loader.contentURL = value; + test.assertEqual( + changes, + emitted, + 'must not emit `propertyChange` if same value is set' + ); + + loader.contentURL = value = 'http://google.com/'; + test.assertEqual( + value, + '' + loader.contentURL, + 'value must be set' + ); + test.assertEqual( + ++ changes, + emitted, + 'had to emit `propertyChange`' + ); + loader.contentURL = value; + test.assertEqual( + changes, + emitted, + 'must not emit `propertyChange` if same value is set' + ); + + loader.removeListener('propertyChange', listener); + loader.contentURL = value = 'about:blank'; + test.assertEqual( + value, + '' + loader.contentURL, + 'contentURL must be an actual value' + ); + test.assertEqual( + changes, + emitted, + 'listener had to be romeved' + ); +}; + +exports['test:contentScriptWhen'] = function(test) { + let loader = Loader(); + test.assertEqual( + 'end', + loader.contentScriptWhen, + '`contentScriptWhen` defaults to "end"' + ); + loader.contentScriptWhen = "end"; + test.assertEqual( + "end", + loader.contentScriptWhen + ); + try { + loader.contentScriptWhen = 'boom'; + test.fail('must throw when wrong value is set'); + } catch(e) { + test.assertEqual( + 'The `contentScriptWhen` option must be either "start", "ready" or "end".', + e.message + ); + } + loader.contentScriptWhen = null; + test.assertEqual( + 'end', + loader.contentScriptWhen, + '`contentScriptWhen` defaults to "end"' + ); + loader.contentScriptWhen = "ready"; + test.assertEqual( + "ready", + loader.contentScriptWhen + ); + loader.contentScriptWhen = "start"; + test.assertEqual( + 'start', + loader.contentScriptWhen + ); +}; + +exports['test:contentScript'] = function(test) { + let loader = Loader(), value; + test.assertEqual( + null, + loader.contentScript, + '`contentScript` defaults to `null`' + ); + loader.contentScript = value = 'let test = {};'; + test.assertEqual( + value, + loader.contentScript + ); + try { + loader.contentScript = { 1: value } + test.fail('must throw when wrong value is set'); + } catch(e) { + test.assertEqual( + 'The script option must be a string or an array of strings.', + e.message + ); + } + try { + loader.contentScript = ['oue', 2] + test.fail('must throw when wrong value is set'); + } catch(e) { + test.assertEqual( + 'The script option must be a string or an array of strings.', + e.message + ); + } + loader.contentScript = undefined; + test.assertEqual( + null, + loader.contentScript + ); + loader.contentScript = value = ["1;", "2;"]; + test.assertEqual( + value, + loader.contentScript + ); +}; + +exports['test:contentScriptFile'] = function(test) { + let loader = Loader(), value, uri = self.data.url("test-content-loader.js"); + test.assertEqual( + null, + loader.contentScriptFile, + '`contentScriptFile` defaults to `null`' + ); + loader.contentScriptFile = value = uri; + test.assertEqual( + value, + loader.contentScriptFile + ); + try { + loader.contentScriptFile = { 1: uri } + test.fail('must throw when wrong value is set'); + } catch(e) { + test.assertEqual( + 'The `contentScriptFile` option must be a local file URL or an array of' + + 'URLs.', + e.message + ); + } + try { + loader.contentScriptFile = ['oue', uri] + test.fail('must throw when wrong value is set'); + } catch(e) { + test.assertEqual( + 'The `contentScriptFile` option must be a local file URL or an array of' + + 'URLs.', + e.message + ); + } + loader.contentScriptFile = undefined; + test.assertEqual( + null, + loader.contentScriptFile + ); + loader.contentScriptFile = value = [uri]; + test.assertEqual( + value, + loader.contentScriptFile + ); +}; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-proxy.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-proxy.js new file mode 100644 index 0000000..1689c81 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-proxy.js @@ -0,0 +1,754 @@ +const hiddenFrames = require("hidden-frame"); +const xulApp = require("xul-app"); + +const { Loader } = require('./helpers'); + +/* + * Utility function that allow to easily run a proxy test with a clean + * new HTML document. See first unit test for usage. + */ +function createProxyTest(html, callback) { + return function (test) { + test.waitUntilDone(); + + let url = 'data:text/html,' + encodeURI(html); + + let hiddenFrame = hiddenFrames.add(hiddenFrames.HiddenFrame({ + onReady: function () { + + function onDOMReady() { + hiddenFrame.element.removeEventListener("DOMContentLoaded", onDOMReady, + false); + + let xrayWindow = hiddenFrame.element.contentWindow; + let rawWindow = xrayWindow.wrappedJSObject; + + let done = false; + let helper = { + xrayWindow: xrayWindow, + rawWindow: rawWindow, + createWorker: function (contentScript) { + return createWorker(test, xrayWindow, contentScript, helper.done); + }, + done: function () { + if (done) + return; + done = true; + hiddenFrames.remove(hiddenFrame); + test.done(); + } + } + callback(helper, test); + } + + hiddenFrame.element.addEventListener("DOMContentLoaded", onDOMReady, false); + hiddenFrame.element.setAttribute("src", url); + + } + })); + }; +} + +function createWorker(test, xrayWindow, contentScript, done) { + // We have to use Sandboxed loader in order to get access to the private + // unlock key `PRIVATE_KEY`. This key should not be used anywhere else. + // See `PRIVATE_KEY` definition in worker.js + let loader = Loader(module); + let Worker = loader.require("api-utils/content/worker").Worker; + let key = loader.sandbox("api-utils/content/worker").PRIVATE_KEY; + let worker = Worker({ + exposeUnlockKey : key, + window: xrayWindow, + contentScript: [ + 'new ' + function () { + assert = function assert(v, msg) { + self.port.emit("assert", {assertion:v, msg:msg}); + } + done = function done() { + self.port.emit("done"); + } + }, + contentScript + ] + }); + + worker.port.on("done", done); + worker.port.on("assert", function (data) { + test.assert(data.assertion, data.msg); + }); + + return worker; +} + +/* Examples for the `createProxyTest` uses */ + +let html = "<script>var documentGlobal = true</script>"; +exports.testCreateProxyTest = createProxyTest(html, function (helper, test) { + // You can get access to regular `test` object in second argument of + // `createProxyTest` method: + test.assert(helper.rawWindow.documentGlobal, + "You have access to a raw window reference via `helper.rawWindow`"); + test.assert(!("documentGlobal" in helper.xrayWindow), + "You have access to an XrayWrapper reference via `helper.xrayWindow`"); + + // If you do not create a Worker, you have to call helper.done(), + // in order to say when your test is finished + helper.done(); +}); + +exports.testCreateProxyTestWithWorker = createProxyTest("", function (helper) { + + helper.createWorker( + "new " + function WorkerScope() { + assert(true, "You can do assertions in your content script"); + // And if you create a worker, you either have to call `done` + // from content script or helper.done() + done(); + } + ); + +}); + +exports.testCreateProxyTestWithEvents = createProxyTest("", function (helper, test) { + + let worker = helper.createWorker( + "new " + function WorkerScope() { + self.port.emit("foo"); + } + ); + + worker.port.on("foo", function () { + test.pass("You can use events"); + // And terminate your test with helper.done: + helper.done(); + }); + +}); + +// Verify that the attribute `exposeUnlockKey`, that allow this test +// to identify proxies, works correctly. +// See `PRIVATE_KEY` definition in worker.js +exports.testKeyAccess = createProxyTest("", function(helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + assert("UNWRAP_ACCESS_KEY" in window, "have access to `UNWRAP_ACCESS_KEY`"); + done(); + } + ); + +}); + + +// Bug 714778: There was some issue around `toString` functions +// that ended up being shared between content scripts +exports.testSharedToStringProxies = createProxyTest("", function(helper) { + + let worker = helper.createWorker( + 'new ' + function ContentScriptScope() { + assert(document.location.toString() == "data:text/html,", + "document.location.toString()"); + self.postMessage("next"); + } + ); + worker.on("message", function () { + helper.createWorker( + 'new ' + function ContentScriptScope2() { + assert(document.location.toString() == "data:text/html,", + "document.location.toString()"); + done(); + } + ); + }); +}); + + +// Ensure that postMessage is working correctly across documents with an iframe +let html = '<iframe id="iframe" name="test" src="data:text/html," />'; +exports.testPostMessage = createProxyTest(html, function (helper, test) { + let ifWindow = helper.xrayWindow.document.getElementById("iframe").contentWindow; + // Listen without proxies, to check that it will work in regular case + // simulate listening from a web document. + ifWindow.addEventListener("message", function listener(event) { + //if (event.source.wrappedJSObject == helper.rawWindow) return; + ifWindow.removeEventListener("message", listener, false); + // As we are in system principal, event is an XrayWrapper + test.assertEqual(event.source, ifWindow, + "event.source is the iframe window"); + test.assertEqual(event.origin, "null", "origin is null"); + + test.assertEqual(event.data, "{\"foo\":\"bar\\n \\\"escaped\\\".\"}", + "message data is correct"); + + helper.done(); + }, false); + + helper.createWorker( + 'new ' + function ContentScriptScope() { + assert(postMessage === postMessage, + "verify that we doesn't generate multiple functions for the same method"); + + var json = JSON.stringify({foo : "bar\n \"escaped\"."}); + + document.getElementById("iframe").contentWindow.postMessage(json, "*"); + } + ); +}); + +let html = '<input id="input2" type="checkbox" />'; +exports.testObjectListener = createProxyTest(html, function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Test objects being given as event listener + let input = document.getElementById("input2"); + let myClickListener = { + called: false, + handleEvent: function(event) { + assert(this === myClickListener, "`this` is the original object"); + assert(!this.called, "called only once"); + this.called = true; + assert(event.valueOf() !== event.valueOf(UNWRAP_ACCESS_KEY), "event is wrapped"); + assert(event.target, input, "event.target is the wrapped window"); + done(); + } + }; + + window.addEventListener("click", myClickListener, true); + input.click(); + window.removeEventListener("click", myClickListener, true); + } + ); + +}); + +exports.testObjectListener2 = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Verify object as DOM event listener + let myMessageListener = { + called: false, + handleEvent: function(event) { + window.removeEventListener("message", myMessageListener, true); + + assert(this == myMessageListener, "`this` is the original object"); + assert(!this.called, "called only once"); + this.called = true; + assert(event.valueOf() !== event.valueOf(UNWRAP_ACCESS_KEY), "event is wrapped"); + assert(event.target == document.defaultView, "event.target is the wrapped window"); + assert(event.source == document.defaultView, "event.source is the wrapped window"); + assert(event.origin == "null", "origin is null"); + assert(event.data == "ok", "message data is correct"); + done(); + } + }; + + window.addEventListener("message", myMessageListener, true); + document.defaultView.postMessage("ok", '*'); + } + ); + +}); + +let html = '<input id="input" type="text" /><input id="input3" type="checkbox" />' + + '<input id="input2" type="checkbox" />'; +exports.testStringOverload = createProxyTest(html, function (helper, test) { + // Proxy - toString error + let originalString = "string"; + let p = Proxy.create({ + get: function(receiver, name) { + if (name == "binded") + return originalString.toString.bind(originalString); + return originalString[name]; + } + }); + test.assertRaises(function () { + p.toString(); + }, + /String.prototype.toString called on incompatible Proxy/, + "toString can't be called with this being the proxy"); + test.assertEqual(p.binded(), "string", "but it works if we bind this to the original string"); + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // RightJS is hacking around String.prototype, and do similar thing: + // Pass `this` from a String prototype method. + // It is funny because typeof this == "object"! + // So that when we pass `this` to a native method, + // our proxy code can fail on another even more crazy thing. + // See following test to see what fails around proxies. + String.prototype.update = function () { + assert(typeof this == "object", "in update, `this` is an object"); + assert(this.toString() == "input", "in update, `this.toString works"); + return document.querySelectorAll(this); + }; + assert("input".update().length == 3, "String.prototype overload works"); + done(); + } + ); +}); + +exports.testMozMatchedSelector = createProxyTest("", function (helper) { + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Check mozMatchesSelector XrayWrappers bug: + // mozMatchesSelector returns bad results when we are not calling it from the node itself + // SEE BUG 658909: mozMatchesSelector returns incorrect results with XrayWrappers + assert(document.createElement( "div" ).mozMatchesSelector("div"), + "mozMatchesSelector works while being called from the node"); + assert(document.documentElement.mozMatchesSelector.call( + document.createElement( "div" ), + "div" + ), + "mozMatchesSelector works while being called from a " + + "function reference to " + + "document.documentElement.mozMatchesSelector.call"); + done(); + } + ); +}); + +exports.testEventsOverload = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // If we add a "____proxy" attribute on XrayWrappers in order to store + // the related proxy to create an unique proxy for each wrapper; + // we end up setting this attribute to prototype objects :x + // And so, instances created with such prototype will be considered + // as equal to the prototype ... + // // Internal method that return the proxy for a given XrayWrapper + // function proxify(obj) { + // if (obj._proxy) return obj._proxy; + // return obj._proxy = Proxy.create(...); + // } + // + // // Get a proxy of an XrayWrapper prototype object + // let proto = proxify(xpcProto); + // + // // Use this proxy as a prototype + // function Constr() {} + // Constr.proto = proto; + // + // // Try to create an instance using this prototype + // let xpcInstance = new Constr(); + // let wrapper = proxify(xpcInstance) + // + // xpcProto._proxy = proto and as xpcInstance.__proto__ = xpcProto, + // xpcInstance._proxy = proto ... and profixy(xpcInstance) = proto :( + // + let proto = window.document.createEvent('HTMLEvents').__proto__; + window.Event.prototype = proto; + let event = document.createEvent('HTMLEvents'); + assert(event !== proto, "Event should not be equal to its prototype"); + event.initEvent('dataavailable', true, true); + assert(event.type === 'dataavailable', "Events are working fine"); + done(); + } + ); + +}); + +exports.testNestedAttributes = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // XrayWrappers has a bug when you set an attribute on it, + // in some cases, it creates an unnecessary wrapper that introduces + // a different object that refers to the same original object + // Check that our wrappers don't reproduce this bug + // SEE BUG 658560: Fix identity problem with CrossOriginWrappers + let o = {sandboxObject:true}; + window.nested = o; + o.foo = true; + assert(o === window.nested, "Nested attribute to sandbox object should not be proxified"); + window.nested = document; + assert(window.nested === document, "Nested attribute to proxy should not be double proxified"); + done(); + } + ); + +}); + +exports.testFormNodeName = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + let body = document.body; + // Check form[nodeName] + let form = document.createElement("form"); + let input = document.createElement("input"); + input.setAttribute("name", "test"); + form.appendChild(input); + body.appendChild(form); + assert(form.test == input, "form[nodeName] is valid"); + body.removeChild(form); + done(); + } + ); + +}); + +exports.testLocalStorage = createProxyTest("", function (helper, test) { + + let worker = helper.createWorker( + 'new ' + function ContentScriptScope() { + // Check localStorage: + assert(window.localStorage, "has access to localStorage"); + window.localStorage.name = 1; + assert(window.localStorage.name == 1, "localStorage appears to work"); + + self.port.on("step2", function () { + window.localStorage.clear(); + assert(window.localStorage.name == undefined, "localStorage really, really works"); + done(); + }); + self.port.emit("step1"); + } + ); + + worker.port.on("step1", function () { + test.assertEqual(helper.rawWindow.localStorage.name, 1, "localStorage really works"); + worker.port.emit("step2"); + }); + +}); + +exports.testAutoUnwrapCustomAttributes = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + let body = document.body; + // Setting a custom object to a proxy attribute is not wrapped when we get it afterward + let object = {custom: true, enumerable: false}; + body.customAttribute = object; + assert(body.customAttribute.valueOf() === body.customAttribute.valueOf(UNWRAP_ACCESS_KEY), "custom JS attributes are not wrapped"); + assert(object === body.customAttribute, "custom JS attributes are not wrapped"); + done(); + } + ); + +}); + +exports.testObjectTag = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // <object>, <embed> and other tags return typeof 'function' + let flash = document.createElement("object"); + assert(typeof flash == "function", "<object> is typeof 'function'"); + assert(flash.toString().match(/\[object HTMLObjectElement.*\]/), "<object> is HTMLObjectElement"); + assert("setAttribute" in flash, "<object> has a setAttribute method"); + done(); + } + ); + +}); + +exports.testHighlightToStringBehavior = createProxyTest("", function (helper, test) { + // We do not have any workaround this particular use of toString + // applied on <object> elements. So disable this test until we found one! + //test.assertEqual(helper.rawWindow.Object.prototype.toString.call(flash), "[object HTMLObjectElement]", "<object> is HTMLObjectElement"); + function f() {}; + test.assertMatches(Object.prototype.toString.call(f), /\[object Function.*\]/, "functions are functions 1"); + // This is how jquery call toString: + test.assertMatches(helper.rawWindow.Object.prototype.toString.call(""), /\[object String.*\]/, "strings are strings"); + test.assertMatches(helper.rawWindow.Object.prototype.toString.call({}), /\[object Object.*\]/, "objects are objects"); + + // Make sure to pass a function from the same compartments + // or toString will return [object Object] on FF8+ + let f2 = helper.rawWindow.eval("(function () {})"); + test.assertMatches(helper.rawWindow.Object.prototype.toString.call(f2), /\[object Function.*\]/, "functions are functions 2"); + + helper.done(); +}); + +exports.testDocumentTagName = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + let body = document.body; + // Check document[tagName] + let div = document.createElement("div"); + div.setAttribute("name", "test"); + body.appendChild(div); + assert(!document.test, "document[divName] is undefined"); + body.removeChild(div); + + let form = document.createElement("form"); + form.setAttribute("name", "test"); + body.appendChild(form); + assert(document.test == form, "document[formName] is valid"); + body.removeChild(form); + + let img = document.createElement("img"); + img.setAttribute("name", "test"); + body.appendChild(img); + assert(document.test == img, "document[imgName] is valid"); + body.removeChild(img); + done(); + } + ); + +}); + +let html = '<iframe id="iframe" name="test" src="data:text/html," />'; +exports.testWindowFrames = createProxyTest(html, function (helper) { + + helper.createWorker( + 'let glob = this; new ' + function ContentScriptScope() { + // Check window[frameName] and window.frames[i] + let iframe = document.getElementById("iframe"); + //assert(window.frames.length == 1, "The iframe is reported in window.frames check1"); + //assert(window.frames[0] == iframe.contentWindow, "The iframe is reported in window.frames check2"); + //console.log(window.test+ "-"+iframe.contentWindow); + //console.log(window); + assert(window.test == iframe.contentWindow, "window[frameName] is valid"); + done(); + } + ); + +}); + +exports.testCollections = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Highlight XPCNativeWrapper bug with HTMLCollection + // tds[0] is only defined on first access :o + let body = document.body; + let div = document.createElement("div"); + body.appendChild(div); + div.innerHTML = "<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>"; + let tds = div.getElementsByTagName("td"); + assert(tds[0] == tds[0], "We can get array element multiple times"); + body.removeChild(div); + done(); + } + ); + +}); + +let html = '<input id="input" type="text" /><input id="input3" type="checkbox" />' + + '<input id="input2" type="checkbox" />'; +exports.testCollections2 = createProxyTest(html, function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Verify that NodeList/HTMLCollection are working fine + let body = document.body; + let inputs = body.getElementsByTagName("input"); + assert(body.childNodes.length == 3, "body.childNodes length is correct"); + assert(inputs.length == 3, "inputs.length is correct"); + assert(body.childNodes[0] == inputs[0], "body.childNodes[0] is correct"); + assert(body.childNodes[1] == inputs[1], "body.childNodes[1] is correct"); + assert(body.childNodes[2] == inputs[2], "body.childNodes[2] is correct"); + let count = 0; + for(let i in body.childNodes) { + count++; + } + assert(count == 3, "body.childNodes is iterable"); + done(); + } + ); + +}); + +exports.testValueOf = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Check internal use of valueOf() + assert(window.valueOf().toString().match(/\[object Window.*\]/), "proxy.valueOf() returns the wrapped version"); + assert(window.valueOf({}).toString().match(/\[object Window.*\]/), "proxy.valueOf({}) returns the wrapped version"); + assert(window.valueOf(UNWRAP_ACCESS_KEY).toString().match(/\[object XrayWrapper \[object Window.*\].*\]/), "proxy.valueOf(UNWRAP_ACCESS_KEY) returns the unwrapped version"); + done(); + } + ); + +}); + +exports.testXMLHttpRequest = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // XMLHttpRequest doesn't support XMLHttpRequest.apply, + // that may break our proxy code + assert(window.XMLHttpRequest(), "we are able to instantiate XMLHttpRequest object"); + done(); + } + ); + +}); + +exports.testXPathResult = createProxyTest("", function (helper, test) { + + // Check XPathResult bug with constants being undefined on + // XPCNativeWrapper + let value = helper.rawWindow.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE; + let xpcXPathResult = helper.xrayWindow.XPathResult; + test.assertEqual(xpcXPathResult.wrappedJSObject. + UNORDERED_NODE_SNAPSHOT_TYPE, + value, + "XPathResult's constants are valid on unwrapped node"); + + if (xulApp.versionInRange(xulApp.platformVersion, "10.0a1", "*")) { + test.assertEqual(xpcXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, 6, + "XPathResult's constants are defined on " + + "XPCNativeWrapper (platform bug #)"); + } + else { + test.assertEqual(xpcXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, + undefined, + "XPathResult's constants are undefined on " + + "XPCNativeWrapper (platform bug #665279)"); + } + + let worker = helper.createWorker( + 'new ' + function ContentScriptScope() { + self.port.on("value", function (value) { + // Check that our work around is working: + assert(window.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE === value, + "XPathResult works correctly on Proxies"); + done(); + }); + } + ); + worker.port.emit("value", value); + +}); + +exports.testPrototypeInheritance = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Verify that inherited prototype function like initEvent + // are handled correctly. (e2.type will return an error if it's not the case) + let event1 = document.createEvent( 'MouseEvents' ); + event1.initEvent( "click", true, true ); + let event2 = document.createEvent( 'MouseEvents' ); + event2.initEvent( "click", true, true ); + assert(event2.type == "click", "We are able to create an event"); + done(); + } + ); + +}); + +exports.testFunctions = createProxyTest("", function (helper) { + + helper.rawWindow.callFunction = function (f) f(); + helper.rawWindow.isEqual = function (a, b) a == b; + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Check basic usage of functions + let closure2 = function () {return "ok";}; + assert(window.wrappedJSObject.callFunction(closure2) == "ok", "Function references work"); + + // Ensure that functions are cached when being wrapped to native code + let closure = function () {}; + assert(window.wrappedJSObject.isEqual(closure, closure), "Function references are cached before being wrapped to native"); + done(); + } + ); + +}); + +let html = '<input id="input2" type="checkbox" />'; +exports.testListeners = createProxyTest(html, function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Verify listeners: + let input = document.getElementById("input2"); + assert(input, "proxy.getElementById works"); + + function onclick() {}; + input.onclick = onclick; + assert(input.onclick === onclick, "on* attributes are equal to original function set"); + + let addEventListenerCalled = false; + let expandoCalled = false; + input.addEventListener("click", function onclick(event) { + input.removeEventListener("click", onclick, true); + + assert(!addEventListenerCalled, "closure given to addEventListener is called once"); + if (addEventListenerCalled) + return; + addEventListenerCalled = true; + + assert(!event.target.ownerDocument.defaultView.documentGlobal, "event object is still wrapped and doesn't expose document globals"); + assert("__isWrappedProxy" in event.target, "event object is a proxy"); + + let input2 = document.getElementById("input2"); + + input.onclick = function (event) { + input.onclick = null; + assert(!expandoCalled, "closure set to expando is called once"); + if (expandoCalled) return; + expandoCalled = true; + + assert(!event.target.ownerDocument.defaultView.documentGlobal, "event object is still wrapped and doesn't expose document globals"); + assert("__isWrappedProxy" in event.target, "event object is a proxy"); + + setTimeout(function () { + input.click(); + done(); + }, 0); + + } + + setTimeout(function () { + input.click(); + }, 0); + + }, true); + + input.click(); + } + ); + +}); + +exports.testMozRequestAnimationFrame = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + window.mozRequestAnimationFrame(function callback() { + assert(callback == this, "callback is equal to `this`"); + done(); + }); + } + ); + +}); + +exports.testGlobalScope = createProxyTest("", function (helper) { + + helper.createWorker( + 'let toplevelScope = true;' + + 'assert(window.toplevelScope, "variables in toplevel scope are set to `window` object");' + + 'assert(this.toplevelScope, "variables in toplevel scope are set to `this` object");' + + 'done();' + ); + +}); + +// Bug 671016: Typed arrays should not be proxified +exports.testTypedArrays = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + let canvas = document.createElement("canvas"); + let context = canvas.getContext("2d"); + let imageData = context.getImageData(0,0, 1, 1); + let unwrappedData = imageData.valueOf(UNWRAP_ACCESS_KEY).data; + let data = imageData.data; + assert(unwrappedData === data, "Typed array isn't proxified") + done(); + } + ); + +}); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-symbiont.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-symbiont.js new file mode 100644 index 0000000..3bd2890 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-symbiont.js @@ -0,0 +1,186 @@ +"use strict"; + +const { Cc, Ci } = require('chrome'); +const { Symbiont } = require('content/symbiont'); +const self = require("self"); + +function makeWindow() { + let content = + '<?xml version="1.0"?>' + + '<window ' + + 'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">' + + '<iframe id="content" type="content"/>' + + '</window>'; + var url = "data:application/vnd.mozilla.xul+xml," + + encodeURIComponent(content); + var features = ["chrome", "width=10", "height=10"]; + + return Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher). + openWindow(null, url, null, features.join(","), null); +} + +exports['test:constructing symbiont && validating API'] = function(test) { + let window = makeWindow(); + window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad, false); + let frame = window.document.getElementById("content"); + // TODO: support arrays ?? + let contentScripts = ["1;", "2;"]; + let contentSymbiont = Symbiont({ + frame: frame, + contentScriptFile: self.data.url("test-content-symbiont.js"), + contentScript: contentScripts, + contentScriptWhen: "start" + }); + + test.assertEqual( + self.data.url("test-content-symbiont.js"), + contentSymbiont.contentScriptFile, + "There is one contentScriptFile, as specified in options." + ); + test.assertEqual( + contentScripts.length, + contentSymbiont.contentScript.length, + "There are two contentScripts, as specified in options." + ); + test.assertEqual( + contentScripts[0], + contentSymbiont.contentScript[0], + "There are two contentScripts, as specified in options." + ); + test.assertEqual( + contentScripts[1], + contentSymbiont.contentScript[1], + "There are two contentScripts, as specified in options." + ) + test.assertEqual( + contentSymbiont.contentScriptWhen, + "start", + "contentScriptWhen is as specified in options." + ); + + test.done(); + window.close(); + frame.setAttribute("src", "data:text/html,<html><body></body></html>"); + }, false); + test.waitUntilDone(); +}; + +exports["test:communication with worker global scope"] = function(test) { + let window = makeWindow(); + let contentSymbiont; + + function onMessage1(message) { + test.assertEqual(message, 1, "Program gets message via onMessage."); + contentSymbiont.removeListener('message', onMessage1); + contentSymbiont.on('message', onMessage2); + contentSymbiont.postMessage(2); + }; + + function onMessage2(message) { + if (5 == message) { + test.done(); + } else { + test.assertEqual(message, 3, "Program gets message via onMessage2."); + contentSymbiont.postMessage(4) + } + } + + window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad, false); + let frame = window.document.getElementById("content"); + contentSymbiont = Symbiont({ + frame: frame, + contentScript: 'new ' + function() { + self.postMessage(1); + self.on("message", function onMessage(message) { + if (message === 2) + self.postMessage(3); + if (message === 4) + self.postMessage(5); + }); + } + '()', + contentScriptWhen: 'ready', + onMessage: onMessage1 + }); + + frame.setAttribute("src", "data:text/html,<html><body></body></html>"); + }, false); + test.waitUntilDone(); +}; + +exports['test:pageWorker'] = function(test) { + test.waitUntilDone(); + let worker = Symbiont({ + contentURL: 'about:buildconfig', + contentScript: 'new ' + function WorkerScope() { + self.on('message', function(data) { + if (data.valid) + self.postMessage('bye!'); + }) + self.postMessage(window.location.toString()); + }, + onMessage: function(msg) { + if (msg == 'bye!') { + test.done() + } else { + test.assertEqual( + worker.contentURL + '', + msg + ); + worker.postMessage({ valid: true }); + } + } + }); +}; + +exports["test:document element present on 'start'"] = function(test) { + test.waitUntilDone(); + let xulApp = require("xul-app"); + let worker = Symbiont({ + contentURL: "about:buildconfig", + contentScript: "self.postMessage(!!document.documentElement)", + contentScriptWhen: "start", + onMessage: function(message) { + if (xulApp.versionInRange(xulApp.platformVersion, "2.0b6", "*")) + test.assert(message, "document element present on 'start'"); + else + test.pass("document element not necessarily present on 'start'"); + test.done(); + } + }); +}; + +exports["test:direct communication with trusted document"] = function(test) { + test.waitUntilDone(); + + let worker = Symbiont({ + contentURL: require("self").data.url("test-trusted-document.html") + }); + + worker.port.on('document-to-addon', function (arg) { + test.assertEqual(arg, "ok", "Received an event from the document"); + worker.destroy(); + test.done(); + }); + worker.port.emit('addon-to-document', 'ok'); +}; + +exports["test:`addon` is not available when a content script is set"] = function(test) { + test.waitUntilDone(); + + let worker = Symbiont({ + contentURL: require("self").data.url("test-trusted-document.html"), + contentScript: "new " + function ContentScriptScope() { + self.port.emit("cs-to-addon", "addon" in unsafeWindow); + } + }); + + worker.port.on('cs-to-addon', function (hasAddon) { + test.assertEqual(hasAddon, false, + "`addon` is not available"); + worker.destroy(); + test.done(); + }); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-worker.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-worker.js new file mode 100644 index 0000000..ced0b60 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-content-worker.js @@ -0,0 +1,374 @@ +"use stirct"; + +const { Cc, Ci } = require('chrome'); +const timer = require('timer'); + +function makeWindow(contentURL) { + let content = + '<?xml version="1.0"?>' + + '<window ' + + 'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">' + + '<iframe id="content" type="content" src="' + + encodeURIComponent(contentURL) + '"/>' + + '<script>var documentValue=true;</script>' + + '</window>'; + var url = "data:application/vnd.mozilla.xul+xml," + + encodeURIComponent(content); + var features = ["chrome", "width=10", "height=10"]; + + return Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher). + openWindow(null, url, null, features.join(","), null); +} + +const { Worker } = require('content/worker'); +exports['test:sample'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + // As window has just being created, its document is still loading, + // and we have about:blank document before the expected one + test.assertEqual(window.document.location.href, "about:blank", + "window starts by loading about:blank"); + + // We need to wait for the load/unload of temporary about:blank + // or our worker is going to be automatically destroyed + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + test.assertNotEqual(window.document.location.href, "about:blank", + "window is now on the right document"); + + let worker = Worker({ + window: window, + contentScript: 'new ' + function WorkerScope() { + // window is accessible + let myLocation = window.location.toString(); + self.on('message', function(data) { + if (data == 'hi!') + self.postMessage('bye!'); + }); + }, + contentScriptWhen: 'ready', + onMessage: function(msg) { + test.assertEqual('bye!', msg); + test.assertEqual(worker.url, window.document.location.href, + "worker.url still works"); + test.done(); + } + }); + + test.assertEqual(worker.url, window.document.location.href, + "worker.url works"); + worker.postMessage('hi!'); + + }, true); + +} + +exports['test:emit'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + let worker = Worker({ + window: window, + contentScript: 'new ' + function WorkerScope() { + // Validate self.on and self.emit + self.port.on('addon-to-content', function (data) { + self.port.emit('content-to-addon', data); + }); + + // Check for global pollution + //if (typeof on != "undefined") + // self.postMessage("`on` is in globals"); + if (typeof once != "undefined") + self.postMessage("`once` is in globals"); + if (typeof emit != "undefined") + self.postMessage("`emit` is in globals"); + + }, + onMessage: function(msg) { + test.fail("Got an unexpected message : "+msg); + } + }); + + // Validate worker.port + worker.port.on('content-to-addon', function (data) { + test.assertEqual(data, "event data"); + test.done(); + }); + worker.port.emit('addon-to-content', 'event data'); + +} + +exports['test:emit hack message'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + let worker = Worker({ + window: window, + contentScript: 'new ' + function WorkerScope() { + // Validate self.port + self.port.on('message', function (data) { + self.port.emit('message', data); + }); + // We should not receive message on self, but only on self.port + self.on('message', function (data) { + self.postMessage('message', data); + }); + }, + onError: function(e) { + test.fail("Got exception: "+e); + } + }); + + worker.port.on('message', function (data) { + test.assertEqual(data, "event data"); + test.done(); + }); + worker.on('message', function (data) { + test.fail("Got an unexpected message : "+msg); + }); + worker.port.emit('message', 'event data'); + +} + +exports['test:n-arguments emit'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + let worker = Worker({ + window: window, + contentScript: 'new ' + function WorkerScope() { + // Validate self.on and self.emit + self.port.on('addon-to-content', function (a1, a2, a3) { + self.port.emit('content-to-addon', a1, a2, a3); + }); + } + }); + + // Validate worker.port + worker.port.on('content-to-addon', function (arg1, arg2, arg3) { + test.assertEqual(arg1, "first argument"); + test.assertEqual(arg2, "second"); + test.assertEqual(arg3, "third"); + test.done(); + }); + worker.port.emit('addon-to-content', 'first argument', 'second', 'third'); +} + +exports['test:post-json-values-only'] = function(test) { + let window = makeWindow("data:text/html,"); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let worker = Worker({ + window: window.document.getElementById("content").contentWindow, + contentScript: 'new ' + function WorkerScope() { + self.on('message', function (message) { + self.postMessage([ message.fun === undefined, + typeof message.w, + message.w && "port" in message.w, + message.w.url, + Array.isArray(message.array), + JSON.stringify(message.array)]); + }); + } + }); + + // Validate worker.onMessage + let array = [1, 2, 3]; + worker.on('message', function (message) { + test.assert(message[0], "function becomes undefined"); + test.assertEqual(message[1], "object", "object stays object"); + test.assert(message[2], "object's attributes are enumerable"); + test.assertEqual(message[3], "about:blank", "jsonable attributes are accessible"); + // See bug 714891, Arrays may be broken over compartements: + test.assert(message[4], "Array keeps being an array"); + test.assertEqual(message[5], JSON.stringify(array), + "Array is correctly serialized"); + test.done(); + }); + worker.postMessage({ fun: function () {}, w: worker, array: array }); + + }, true); + +}; + +exports['test:emit-json-values-only'] = function(test) { + let window = makeWindow("data:text/html,"); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let win = window.document.getElementById("content").contentWindow; + let worker = Worker({ + window: win, + contentScript: 'new ' + function WorkerScope() { + // Validate self.on and self.emit + self.port.on('addon-to-content', function (fun, w, obj, array) { + self.port.emit('content-to-addon', [ + fun === null, + typeof w, + "port" in w, + w.url, + "fun" in obj, + Object.keys(obj.dom).length, + Array.isArray(array), + JSON.stringify(array) + ]); + }); + } + }); + + // Validate worker.port + let array = [1, 2, 3]; + worker.port.on('content-to-addon', function (result) { + test.assert(result[0], "functions become null"); + test.assertEqual(result[1], "object", "objects stay objects"); + test.assert(result[2], "object's attributes are enumerable"); + test.assertEqual(result[3], "about:blank", "json attribute is accessible"); + test.assert(!result[4], "function as object attribute is removed"); + test.assertEqual(result[5], 0, "DOM nodes are converted into empty object"); + // See bug 714891, Arrays may be broken over compartements: + test.assert(result[6], "Array keeps being an array"); + test.assertEqual(result[7], JSON.stringify(array), + "Array is correctly serialized"); + test.done(); + }); + let obj = { + fun: function () {}, + dom: win.document.documentElement + }; + worker.port.emit('addon-to-content', function () {}, worker, obj, array); + + }, true); + +} + +exports['test:content is wrapped'] = function(test) { + let contentURL = 'data:text/html,<script>var documentValue=true;</script>'; + let window = makeWindow(contentURL); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let worker = Worker({ + window: window.document.getElementById("content").contentWindow, + contentScript: 'new ' + function WorkerScope() { + self.postMessage(!window.documentValue); + }, + contentScriptWhen: 'ready', + onMessage: function(msg) { + test.assert(msg, + "content script has a wrapped access to content document"); + test.done(); + } + }); + + }, true); + +} + +exports['test:chrome is unwrapped'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let worker = Worker({ + window: window, + contentScript: 'new ' + function WorkerScope() { + self.postMessage(window.documentValue); + }, + contentScriptWhen: 'ready', + onMessage: function(msg) { + test.assert(msg, + "content script has an unwrapped access to chrome document"); + test.done(); + } + }); + + }, true); + +} + +exports['test:setTimeout can\'t be cancelled by content'] = function(test) { + let contentURL = 'data:text/html,<script>var documentValue=true;</script>'; + let window = makeWindow(contentURL); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let worker = Worker({ + window: window.document.getElementById("content").contentWindow, + contentScript: 'new ' + function WorkerScope() { + let id = setTimeout(function () { + self.postMessage("timeout"); + }, 100); + unsafeWindow.eval("clearTimeout("+id+");"); + }, + contentScriptWhen: 'ready', + onMessage: function(msg) { + test.assert(msg, + "content didn't managed to cancel our setTimeout"); + test.done(); + } + }); + + }, true); + +} + +exports['test:setTimeout are unregistered on content unload'] = function(test) { + let contentURL = 'data:text/html,foo'; + let window = makeWindow(contentURL); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let iframe = window.document.getElementById("content"); + let originalDocument = iframe.contentDocument; + let worker = Worker({ + window: iframe.contentWindow, + contentScript: 'new ' + function WorkerScope() { + document.title = "ok"; + let i = 0; + setInterval(function () { + document.title = i++; + }, 10); + }, + contentScriptWhen: 'ready' + }); + + // Change location so that content script is destroyed, + // and all setTimeout/setInterval should be unregistered. + // Wait some cycles in order to execute some intervals. + timer.setTimeout(function () { + // Bug 689621: Wait for the new document load so that we are sure that + // previous document cancelled its intervals + iframe.addEventListener("load", function onload() { + iframe.removeEventListener("load", onload, true); + let titleAfterLoad = originalDocument.title; + // Wait additional cycles to verify that intervals are really cancelled + timer.setTimeout(function () { + test.assertEqual(iframe.contentDocument.title, "final", + "New document has not been modified"); + test.assertEqual(originalDocument.title, titleAfterLoad, + "Nor previous one"); + test.done(); + }, 100); + }, true); + iframe.setAttribute("src", "data:text/html,<title>final</title>"); + }, 100); + + }, true); + +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-cortex.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-cortex.js new file mode 100644 index 0000000..11ed397 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-cortex.js @@ -0,0 +1,118 @@ +// vim:set ts=2 sw=2 sts=2 + +"use strict"; + +var Cortex = require("cortex").Cortex; + +exports["test property changes propagate"] = function (assert) { + var source = { + _foo: "secret", + get foo() { + return this._foo; + }, + set foo(value) { + this._foo = value; + }, + get getOnly() { + return this._foo; + }, + set setOnly(value) { + this._setOnly = value; + }, + bar: "public", + method: function method(a, b) { + return this._foo + a + b + } + }; + var fixture = Cortex(source); + + assert.ok(!('_foo' in fixture), + "properties that start with `_` are omitted"); + assert.equal(fixture.foo, "secret", "get accessor alias works"); + fixture.foo = "new secret"; + assert.equal(fixture.foo, "new secret", "set accessor alias works"); + assert.equal(source.foo, "new secret", "accessor delegates to the source"); + assert.equal(fixture.bar, "public", "data property alias works"); + fixture.bar = "bar"; + assert.equal(source.bar, "bar", "data property change propagates"); + source.bar = "foo" + assert.equal(fixture.bar, "foo", "data property change probagets back"); + assert.equal(fixture.method("a", "b"), "new secretab", + "public methods are callable"); + assert.equal(fixture.method.call({ _foo: "test" }, " a,", "b"), + "new secret a,b", + "`this` pseudo-variable can not be passed through call."); + assert.equal(fixture.method.apply({ _foo: "test" }, [" a,", "b"]), + "new secret a,b", + "`this` pseudo-variable can not be passed through apply."); + assert.equal(fixture.getOnly, source._foo, + "getter returned property of wrapped object"); + fixture.setOnly = 'bar' + assert.equal(source._setOnly, 'bar', "setter modified wrapped object") +}; + + +exports["test immunity of inheritance"] = function(assert) { + function Type() {} + Type.prototype = { + constructor: Type, + _bar: 2, + bar: 3, + get_Foo: function getFoo() { + return this._foo; + } + } + var source = Object.create(Type.prototype, { + _foo: { value: 'secret' }, + getBar: { value: function get_Bar() { + return this.bar + }}, + get_Bar: { value: function getBar() { + return this._bar + }} + }); + + var fixture = Cortex(source); + + assert.ok(Cortex({}, null, Type.prototype) instanceof Type, + "if custom prototype is providede cortex will inherit from it"); + assert.ok(fixture instanceof Type, + "if no prototype is given cortex inherits from object's prototype"); + + source.bar += 1; + assert.notEqual(fixture.bar, source.bar, + "chages of properties don't propagate to non-aliases"); + assert.equal(fixture.getBar(), source.bar, + "methods accessing public properties are bound to the source"); + + fixture._bar += 1; + assert.notEqual(fixture._bar, source._bar, + "changes of non aliased properties don't propagate"); + assert.equal(fixture.get_Bar(), source._bar, + "methods accessing privates are bound to the source"); + assert.notEqual(fixture.get_Foo(), source._foo, + "prototoype methods are not bound to the source"); +} + +exports["test customized public properties"] = function(assert) { + var source = { + _a: 'a', + b: 'b', + get: function get(name) { + return this[name]; + } + }; + + var fixture = Cortex(source, ['_a', 'get']); + fixture._a += "#change"; + + + assert.ok(!("b" in fixture), "non-public own property is not defined"); + assert.equal(fixture.get("b"), source.b, + "public methods preserve access to the private properties"); + assert.equal(fixture._a, source._a, + "custom public property changes propagate"); +} + +//if (require.main == module) + require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-dom.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-dom.js new file mode 100644 index 0000000..57b1afe --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-dom.js @@ -0,0 +1,84 @@ +"use strict"; + +const events = require("dom/events"); +const { activeBrowserWindow: { document } } = require("window-utils"); +const window = document.window; + +exports["test on / emit"] = function (assert, done) { + let element = document.createElement("div"); + events.on(element, "click", function listener(event) { + assert.equal(event.target, element, "event has correct target"); + events.removeListener(element, "click", listener); + done(); + }); + + events.emit(element, "click", { + category: "MouseEvents", + settings: [ + true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null + ] + }); +}; + +exports["test remove"] = function (assert, done) { + let element = document.createElement("span"); + let l1 = 0; + let l2 = 0; + let options = { + category: "MouseEvents", + settings: [ + true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null + ] + }; + + events.on(element, "click", function listener1(event) { + l1 ++; + assert.equal(event.target, element, "event has correct target"); + events.removeListener(element, "click", listener1); + }); + + events.on(element, "click", function listener2(event) { + l2 ++; + if (l1 < l2) { + assert.equal(l1, 1, "firs listener was called and then romeved"); + events.removeListener(element, "click", listener2); + done(); + } + events.emit(element, "click", options); + }); + + events.emit(element, "click", options); +}; + +exports["test once"] = function (assert, done) { + let element = document.createElement("h1"); + let l1 = 0; + let l2 = 0; + let options = { + category: "MouseEvents", + settings: [ + true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null + ] + }; + + + events.once(element, "click", function listener(event) { + assert.equal(event.target, element, "event target is a correct element"); + l1 ++; + }); + + events.on(element, "click", function listener(event) { + l2 ++; + if (l2 > 3) { + events.removeListener(element, "click", listener); + assert.equal(event.target, element, "event has correct target"); + assert.equal(l1, 1, "once was called only once"); + done(); + } + events.emit(element, "click", options); + }); + + events.emit(element, "click", options); +} + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-e10s-porting.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-e10s-porting.js new file mode 100644 index 0000000..3dc4d32 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-e10s-porting.js @@ -0,0 +1,73 @@ +// This file just runs all test suites we've white-listed as being +// compatible with E10s. Once we're done with the porting effort, +// we'll just enable cfx's '--e10s' option by default and remove +// this file. + +// This is just to serve as an indicator not to run these tests in +// the addon process. +require("chrome"); + +const E10S_COMPATIBLE_TEST_SUITES = [ + 'test-api-utils.js', + 'test-traits-core.js', + 'test-traits.js', + 'test-list.js', + 'test-self.js' +]; + +exports.runE10SCompatibleTestSuites = function(test) { + var xulApp = require("xul-app"); + if (xulApp.is("Firefox") && + xulApp.versionInRange(xulApp.version, "4.0b7", "4.0b8pre")) { + test.pass("Due to bug 609066, Firefox 4.0b7 will never pass this test, " + + "so we'll skip it."); + return; + } + + // If the "jetpack/service" XPCOM component is not present, then the host + // application does not support e10s, so we can't run any e10s-compatible + // test suites under e10s mode. + if (!require("chrome").Cc["@mozilla.org/jetpack/service;1"]) { + test.pass("This application does not support e10s."); + return; + } + + if (packaging.enableE10s) { + // Don't worry about running these E10S-compatible test + // suites, cfx will find them by default because its + // '--e10s' option is enabled. + test.pass("'cfx --e10s' detected, skipping this test."); + return; + } + + var {TestFinder} = require("unit-test-finder"); + var {TestRunner} = require("unit-test"); + var url = require("url"); + + var thisDir = url.toFilename(url.URL('./', __url__)); + var finder = new TestFinder({ + dirs: [thisDir], + filter: function(name) { + return E10S_COMPATIBLE_TEST_SUITES.indexOf(name) != -1; + }, + testInProcess: false, + testOutOfProcess: true + }); + var runner = new TestRunner(); + finder.findTests(function(tests) { + test.assert(tests.length >= 1, "must find at least one test"); + runner.startMany({ + tests: tests, + onDone: function(runner) { + test.assertEqual(runner.failed, 0, + "No tests in addon process should have failed"); + test.assert(runner.passed > 0, + "Some tests in addon process must have been run"); + test.failed += runner.failed; + test.passed += runner.passed; + test.done(); + } + }); + }); + test.waitUntilDone(); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-environment.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-environment.js new file mode 100644 index 0000000..24077a6 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-environment.js @@ -0,0 +1,46 @@ +'use strict'; + +const { env } = require('api-utils/environment'); +const { Cc, Ci } = require('chrome'); +const { get, set, exists } = Cc['@mozilla.org/process/environment;1']. + getService(Ci.nsIEnvironment); + +exports['test exists'] = function(assert) { + assert.equal('PATH' in env, exists('PATH'), + 'PATH environment variable is defined'); + assert.equal('FOO1' in env, exists('FOO1'), + 'FOO1 environment variable is not defined'); + set('FOO1', 'foo'); + assert.equal('FOO1' in env, true, + 'FOO1 environment variable was set'); + set('FOO1', null); + assert.equal('FOO1' in env, false, + 'FOO1 environment variable was unset'); +}; + +exports['test get'] = function(assert) { + assert.equal(env.PATH, get('PATH'), 'PATH env variable matches'); + assert.equal(env.BAR2, undefined, 'BAR2 env variable is not defined'); + set('BAR2', 'bar'); + assert.equal(env.BAR2, 'bar', 'BAR2 env variable was set'); + set('BAR2', null); + assert.equal(env.BAR2, undefined, 'BAR2 env variable was unset'); +}; + +exports['test set'] = function(assert) { + assert.equal(get('BAZ3'), '', 'BAZ3 env variable is not set'); + assert.equal(env.BAZ3, undefined, 'BAZ3 is not set'); + env.BAZ3 = 'baz'; + assert.equal(env.BAZ3, get('BAZ3'), 'BAZ3 env variable is set'); + assert.equal(get('BAZ3'), 'baz', 'BAZ3 env variable was set to "baz"'); +}; + +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, undefined, 'BLA4 env variable is unset'); + assert.equal('BLA4' in env, false, 'BLA4 env variable no longer exists' ); +}; + +require('test').run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-errors.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-errors.js new file mode 100644 index 0000000..6db9c87 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-errors.js @@ -0,0 +1,66 @@ +var errors = require("errors"); + +exports.testCatchAndLog = function(test) { + var caught = []; + function dummyLog(e) { caught.push(e); } + + var wrapped = errors.catchAndLog(function(x) { + throw Error("blah" + x + this); + }, + "boop", + dummyLog); + test.assertEqual(wrapped.call("hi", 1), "boop", + "exceptions should be trapped, def. resp. returned"); + test.assertEqual(caught.length, 1, + "logging function should be called"); + test.assertEqual(caught[0].message, "blah1hi", + "args and this should be passed to wrapped func"); +}; + +exports.testCatchAndLogProps = function(test) { + var caught = []; + function dummyLog(e) { caught.push(e); } + + var thing = { + foo: function(x) { throw Error("nowai" + x); }, + bar: function() { throw Error("blah"); }, + baz: function() { throw Error("fnarg"); } + }; + + errors.catchAndLogProps(thing, "foo", "ugh", dummyLog); + + test.assertEqual(thing.foo(1), "ugh", + "props should be wrapped"); + test.assertEqual(caught.length, 1, + "logging function should be called"); + test.assertEqual(caught[0].message, "nowai1", + "args should be passed to wrapped func"); + test.assertRaises(function() { thing.bar(); }, + "blah", + "non-wrapped props should be wrapped"); + + errors.catchAndLogProps(thing, ["bar", "baz"], "err", dummyLog); + test.assert((thing.bar() == thing.baz()) && + (thing.bar() == "err"), + "multiple props should be wrapped if array passed in"); +}; + +exports.testCatchAndReturn = function(test) { + var wrapped = errors.catchAndReturn(function(x) { + if (x == 1) + return "one"; + if (x == 2) + throw new Error("two"); + return this + x; + }); + + test.assertEqual(wrapped(1).returnValue, "one", + "arg should be passed; return value should be returned"); + test.assert(wrapped(2).exception, "exception should be returned"); + test.assertEqual(wrapped(2).exception.message, "two", "message is correct"); + test.assert(wrapped(2).exception.fileName.indexOf("test-errors.js") != -1, + "filename is present"); + test.assert(wrapped(2).exception.stack, "stack is available"); + test.assertEqual(wrapped.call("hi", 3).returnValue, "hi3", + "`this` should be set correctly"); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-events.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-events.js new file mode 100644 index 0000000..8bd846b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-events.js @@ -0,0 +1,182 @@ +'use strict'; + +// Exposing private methods as public in order to test +const EventEmitter = require('events').EventEmitter.compose({ + listeners: function(type) this._listeners(type), + emit: function() this._emit.apply(this, arguments), + emitOnObject: function() this._emitOnObject.apply(this, arguments), + removeAllListeners: function(type) this._removeAllListeners(type) +}); + +exports['test:add listeners'] = function(test) { + let e = new EventEmitter(); + + let events_new_listener_emited = []; + let times_hello_emited = 0; + + e.on("newListener", function (event, listener) { + events_new_listener_emited.push(event) + }) + + e.on("hello", function (a, b) { + times_hello_emited += 1 + test.assertEqual("a", a) + test.assertEqual("b", b) + test.assertEqual(this, e, '`this` pseudo-variable is bound to instance'); + }) + + e.emit("hello", "a", "b") +}; + +exports['test:remove listeners'] = function(test) { + let count = 0; + + function listener1 () { + count++; + } + function listener2 () { + count++; + } + function listener3 () { + count++; + } + + let e1 = new EventEmitter(); + e1.on("hello", listener1); + test.assertEqual(1, e1.listeners('hello').length); + e1.removeListener("hello", listener1); + test.assertEqual(0, e1.listeners('hello').length); + + let e2 = new EventEmitter(); + e2.on("hello", listener1); + test.assertEqual(1, e2.listeners('hello').length); + e2.removeListener("hello", listener2); + test.assertEqual(1, e2.listeners('hello').length); + test.assertEqual(listener1, e2.listeners('hello')[0]); + + let e3 = new EventEmitter(); + e3.on("hello", listener1); + test.assertEqual(1, e3.listeners('hello').length); + e3.on("hello", listener2); + test.assertEqual(2, e3.listeners('hello').length); + e3.removeListener("hello", listener1); + test.assertEqual(1, e3.listeners('hello').length); + test.assertEqual(listener2, e3.listeners('hello')[0]); +}; + +exports['test: modify in emit'] = function(test) { + let callbacks_called = [ ]; + let e = new EventEmitter(); + + function callback1() { + callbacks_called.push("callback1"); + e.on("foo", callback2); + e.on("foo", callback3); + e.removeListener("foo", callback1); + } + function callback2() { + callbacks_called.push("callback2"); + e.removeListener("foo", callback2); + } + function callback3() { + callbacks_called.push("callback3"); + e.removeListener("foo", callback3); + } + + e.on("foo", callback1); + test.assertEqual(1, e.listeners("foo").length); + + e.emit("foo"); + test.assertEqual(2, e.listeners("foo").length); + test.assertEqual(1, callbacks_called.length); + test.assertEqual('callback1', callbacks_called[0]); + + e.emit("foo"); + test.assertEqual(0, e.listeners("foo").length); + test.assertEqual(3, callbacks_called.length); + test.assertEqual('callback1', callbacks_called[0]); + test.assertEqual('callback2', callbacks_called[1]); + test.assertEqual('callback3', callbacks_called[2]); + + e.emit("foo"); + test.assertEqual(0, e.listeners("foo").length); + test.assertEqual(3, callbacks_called.length); + test.assertEqual('callback1', callbacks_called[0]); + test.assertEqual('callback2', callbacks_called[1]); + test.assertEqual('callback3', callbacks_called[2]); + + e.on("foo", callback1); + e.on("foo", callback2); + test.assertEqual(2, e.listeners("foo").length); + e.removeAllListeners("foo"); + test.assertEqual(0, e.listeners("foo").length); + + // Verify that removing callbacks while in emit allows emits to propagate to + // all listeners + callbacks_called = [ ]; + + e.on("foo", callback2); + e.on("foo", callback3); + test.assertEqual(2, e.listeners("foo").length); + e.emit("foo"); + test.assertEqual(2, callbacks_called.length); + test.assertEqual('callback2', callbacks_called[0]); + test.assertEqual('callback3', callbacks_called[1]); + test.assertEqual(0, e.listeners("foo").length); +}; + +exports['test:adding same listener'] = function(test) { + function foo() {} + let e = new EventEmitter(); + e.on("foo", foo); + e.on("foo", foo); + test.assertEqual( + 1, + e.listeners("foo").length, + "listener reregistration is ignored" + ); +} + +exports['test:errors are reported if listener throws'] = function(test) { + let e = new EventEmitter(), + reported = false; + e.on('error', function(e) reported = true) + e.on('boom', function() { throw new Error('Boom!') }); + e.emit('boom', 3); + test.assert(reported, 'error should be reported through event'); +}; + +exports['test:emitOnObject'] = function(test) { + let e = new EventEmitter(); + + e.on("foo", function() { + test.assertEqual(this, e, "`this` should be emitter"); + }); + e.emitOnObject(e, "foo"); + + e.on("bar", function() { + test.assertEqual(this, obj, "`this` should be other object"); + }); + let obj = {}; + e.emitOnObject(obj, "bar"); +}; + +exports['test:once'] = function(test) { + let e = new EventEmitter(); + let called = false; + + e.once('foo', function(value) { + test.assert(!called, "listener called only once"); + test.assertEqual(value, "bar", "correct argument was passed"); + }); + + e.emit('foo', 'bar'); + e.emit('foo', 'baz'); +}; + +exports["test romeving once"] = function(test) { + let e = require("events").EventEmitterTrait.create(); + e.once("foo", function() { test.pass("listener was called"); }); + e.once("error", function() { test.fail("error event was emitted"); }); + e._emit("foo", "bug-656684"); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-file.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-file.js new file mode 100644 index 0000000..5b1314a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-file.js @@ -0,0 +1,276 @@ +var file = require("file"); +var url = require("url"); +var byteStreams = require("byte-streams"); +var textStreams = require("text-streams"); + +const ERRORS = { + FILE_NOT_FOUND: /^path does not exist: .+$/, + NOT_A_DIRECTORY: /^path is not a directory: .+$/, + NOT_A_FILE: /^path is not a file: .+$/, +}; + +var myurl = module.uri; +var mydir = myurl.slice(0, -("test-file.js".length)); +var otherdir = mydir + "modules/"; + +exports.testDirName = function(test) { + var aDir = url.toFilename(otherdir); + test.assertEqual(file.dirname(aDir), + aDir.slice(0, aDir.lastIndexOf("modules")-1), + "file.dirname() of dir should return parent dir"); + + aDir = url.toFilename(myurl); + test.assertEqual(file.dirname(aDir), + aDir.slice(0, aDir.lastIndexOf("test-file")-1), + "file.dirname() of file should return its dir"); + + while (aDir) + aDir = file.dirname(aDir); + test.assertEqual(aDir, "", + "dirname should return empty string when dir has no parent"); +}; + +exports.testBasename = function(test) { + // Get the top-most path -- the path with no basename. E.g., on Unix-like + // systems this will be /. We'll use it below to build up some test paths. + // We have to go to this trouble because file.join() needs a legal path as a + // base case; join("foo", "bar") doesn't work unfortunately. + var topPath = url.toFilename(myurl); + var parentPath = file.dirname(topPath); + while (parentPath) { + topPath = parentPath; + parentPath = file.dirname(topPath); + } + + var path = topPath; + test.assertEqual(file.basename(path), "", + "basename should work on paths with no components"); + + path = file.join(path, "foo"); + test.assertEqual(file.basename(path), "foo", + "basename should work on paths with a single component"); + + path = file.join(path, "bar"); + test.assertEqual(file.basename(path), "bar", + "basename should work on paths with multiple components"); +}; + +exports.testList = function(test) { + var list = file.list(url.toFilename(otherdir)); + var found = [true for each (name in list) + if (name == "add.js")]; + if (found.length > 1) + test.fail("a dir can't contain two files of the same name!"); + test.assertEqual(found[0], true, "file.list() should work"); + + test.assertRaises( + function() { file.list(url.toFilename(module.uri)); }, + ERRORS.NOT_A_DIRECTORY, + "file.list() on non-dir should raise error" + ); + + test.assertRaises( + function() { file.list(url.toFilename(mydir + "foo/")); }, + ERRORS.FILE_NOT_FOUND, + "file.list() on nonexistent dir should raise error" + ); +}; + +exports.testRead = function(test) { + var filename = url.toFilename(module.uri); + var contents = file.read(filename); + test.assertMatches(contents, /file\.read\(\) should work/, + "file.read() should work"); + + test.assertRaises( + function() { file.read(filename + "blah"); }, + ERRORS.FILE_NOT_FOUND, + "file.read() on nonexistent file should raise error" + ); + + test.assertRaises( + function() { file.read(url.toFilename(otherdir)); }, + ERRORS.NOT_A_FILE, + "file.read() on dir should raise error" + ); +}; + +exports.testJoin = function(test) { + var filename = url.toFilename(myurl); + var baseDir = file.dirname(filename); + + test.assertEqual(file.join(baseDir, "test-file.js"), + filename, + "file.join() should work"); +}; + +exports.testOpenNonexistentForRead = function (test) { + var filename = dataFileFilename(test); + test.assertRaises(function () file.open(filename), + ERRORS.FILE_NOT_FOUND, + "file.open() on nonexistent file should raise error"); + test.assertRaises(function () file.open(filename, "r"), + ERRORS.FILE_NOT_FOUND, + "file.open('r') on nonexistent file should raise error"); + test.assertRaises(function () file.open(filename, "zzz"), + ERRORS.FILE_NOT_FOUND, + "file.open('zzz') on nonexistent file should raise error"); +}; + +exports.testOpenNonexistentForWrite = function (test) { + var filename = dataFileFilename(test); + + var stream = file.open(filename, "w"); + stream.close(); + + test.assert(file.exists(filename), + "file.exists() should return true after file.open('w')"); + file.remove(filename); + test.assert(!file.exists(filename), + "file.exists() should return false after file.remove()"); + + stream = file.open(filename, "rw"); + stream.close(); + + test.assert(file.exists(filename), + "file.exists() should return true after file.open('rw')"); + file.remove(filename); + test.assert(!file.exists(filename), + "file.exists() should return false after file.remove()"); +}; + +exports.testOpenDirectory = function (test) { + var dir = file.dirname(url.toFilename(module.uri)); + test.assertRaises(function () file.open(dir), + ERRORS.NOT_A_FILE, + "file.open() on directory should raise error"); + test.assertRaises(function () file.open(dir, "w"), + ERRORS.NOT_A_FILE, + "file.open('w') on directory should raise error"); +}; + +exports.testOpenTypes = function (test) { + var filename = dataFileFilename(test); + + // Do the opens first to create the data file. + var stream = file.open(filename, "w"); + test.assert(stream instanceof textStreams.TextWriter, + "open(w) should return a TextWriter"); + stream.close(); + + stream = file.open(filename, "wb"); + test.assert(stream instanceof byteStreams.ByteWriter, + "open(wb) should return a ByteWriter"); + stream.close(); + + stream = file.open(filename); + test.assert(stream instanceof textStreams.TextReader, + "open() should return a TextReader"); + stream.close(); + + stream = file.open(filename, "r"); + test.assert(stream instanceof textStreams.TextReader, + "open(r) should return a TextReader"); + stream.close(); + + stream = file.open(filename, "b"); + test.assert(stream instanceof byteStreams.ByteReader, + "open(b) should return a ByteReader"); + stream.close(); + + stream = file.open(filename, "rb"); + test.assert(stream instanceof byteStreams.ByteReader, + "open(rb) should return a ByteReader"); + stream.close(); + + file.remove(filename); +}; + +exports.testMkpathRmdir = function (test) { + var basePath = file.dirname(url.toFilename(module.uri)); + var dirs = []; + for (var i = 0; i < 3; i++) + dirs.push("test-file-dir"); + var paths = []; + for (var i = 0; i < dirs.length; i++) { + var args = [basePath].concat(dirs.slice(0, i + 1)); + paths.unshift(file.join.apply(null, args)); + } + for (i = 0; i < paths.length; i++) { + test.assert(!file.exists(paths[i]), + "Sanity check: path should not exist: " + paths[i]); + } + file.mkpath(paths[0]); + test.assert(file.exists(paths[0]), "mkpath should create path: " + paths[0]); + for (i = 0; i < paths.length; i++) { + file.rmdir(paths[i]); + test.assert(!file.exists(paths[i]), + "rmdir should remove path: " + paths[i]); + } +}; + +exports.testMkpathTwice = function (test) { + var dir = file.dirname(url.toFilename(module.uri)); + var path = file.join(dir, "test-file-dir"); + test.assert(!file.exists(path), + "Sanity check: path should not exist: " + path); + file.mkpath(path); + test.assert(file.exists(path), "mkpath should create path: " + path); + file.mkpath(path); + test.assert(file.exists(path), + "After second mkpath, path should still exist: " + path); + file.rmdir(path); + test.assert(!file.exists(path), "rmdir should remove path: " + path); +}; + +exports.testMkpathExistingNondirectory = function (test) { + var fname = dataFileFilename(test); + file.open(fname, "w").close(); + test.assert(file.exists(fname), "File should exist"); + test.assertRaises(function () file.mkpath(fname), + /^The path already exists and is not a directory: .+$/, + "mkpath on file should raise error"); + file.remove(fname); +}; + +exports.testRmdirNondirectory = function (test) { + var fname = dataFileFilename(test); + file.open(fname, "w").close(); + test.assert(file.exists(fname), "File should exist"); + test.assertRaises(function () file.rmdir(fname), + ERRORS.NOT_A_DIRECTORY, + "rmdir on file should raise error"); + file.remove(fname); + test.assert(!file.exists(fname), "File should not exist"); + test.assertRaises(function () file.rmdir(fname), + ERRORS.FILE_NOT_FOUND, + "rmdir on non-existing file should raise error"); +}; + +exports.testRmdirNonempty = function (test) { + var dir = file.dirname(url.toFilename(module.uri)); + var path = file.join(dir, "test-file-dir"); + test.assert(!file.exists(path), + "Sanity check: path should not exist: " + path); + file.mkpath(path); + var filePath = file.join(path, "file"); + file.open(filePath, "w").close(); + test.assert(file.exists(filePath), + "Sanity check: path should exist: " + filePath); + test.assertRaises(function () file.rmdir(path), + /^The directory is not empty: .+$/, + "rmdir on non-empty directory should raise error"); + file.remove(filePath); + file.rmdir(path); + test.assert(!file.exists(path), "Path should not exist"); +}; + +// Returns the name of a file that should be used to test writing and reading. +function dataFileFilename(test) { + var dir = file.dirname(url.toFilename(module.uri)); + var fname = file.join(dir, "test-file-data"); + test.assert(!file.exists(fname), + "Sanity check: the file that this test assumes does not " + + "exist should really not exist!"); + return fname; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-function-utils.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-function-utils.js new file mode 100644 index 0000000..292380a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-function-utils.js @@ -0,0 +1,23 @@ +const { invoke, Enqueued } = require('utils/function'); + +exports['test forwardApply'] = function(test) { + function sum(b, c) this.a + b + c + test.assertEqual(invoke(sum, [2, 3], { a: 1 }), 6, + 'passed arguments and pseoude-variable are used'); + test.assertEqual(invoke(sum.bind({ a: 2 }), [2, 3], { a: 1 }), 7, + 'bounded `this` pseoudo variable is used') +} + +exports['test enqueued function'] = function(test) { + test.waitUntilDone(); + let nextTurn = false; + function sum(b, c) { + test.assert(nextTurn, 'enqueued is called in next turn of event loop'); + test.assertEqual(this.a + b + c, 6, + 'passed arguments an pseoude-variable are used'); + test.done(); + } + let fixture = { a: 1, method: Enqueued(sum) } + fixture.method(2, 3); + nextTurn = true; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-globals.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-globals.js new file mode 100644 index 0000000..97cb774 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-globals.js @@ -0,0 +1,21 @@ +Object.defineProperty(this, "global", { value: this }); + +exports.testGlobals = function(test) { + // the only globals in module scope should be: + // module, exports, require, dump, console + test.assertObject(module, "have 'module', good"); + test.assertObject(exports, "have 'exports', good"); + test.assertFunction(require, "have 'require', good"); + test.assertFunction(dump, "have 'dump', good"); + test.assertObject(console, "have 'console', good"); + + // in particular, these old globals should no longer be present + test.assert(!('packaging' in global), "no 'packaging', good"); + // this will be fixed by bug 620559 + test.expectFail(function() { + test.assert(!('memory' in global), "no 'memory', good"); + }); + + test.assertMatches(module.uri, /test-globals\.js$/, + 'should contain filename'); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-hidden-frame.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-hidden-frame.js new file mode 100644 index 0000000..c92046b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-hidden-frame.js @@ -0,0 +1,47 @@ +let tests = {}, hiddenFrames, HiddenFrame; + +tests.testFrame = function(test) { + let url = "data:text/html,<!DOCTYPE%20html>"; + test.waitUntilDone(); + let hiddenFrame = hiddenFrames.add(HiddenFrame({ + onReady: function () { + test.assertEqual(this.element.contentWindow.location, "about:blank", + "HiddenFrame loads about:blank by default."); + + function onDOMReady() { + hiddenFrame.element.removeEventListener("DOMContentLoaded", onDOMReady, + false); + test.assertEqual(hiddenFrame.element.contentWindow.location, url, + "HiddenFrame loads the specified content."); + test.done(); + } + this.element.addEventListener("DOMContentLoaded", onDOMReady, false); + this.element.setAttribute("src", url); + } + })); +}; + +let hiddenFrameSupported = true; + +try { + hiddenFrames = require("hidden-frame"); + HiddenFrame = hiddenFrames.HiddenFrame; +} +catch(ex if ex.message == [ + "The hidden-frame module currently supports only Firefox and Thunderbird. ", + "In the future, we would like it to support other applications, however. ", + "Please see https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more ", + "information." + ].join("")) { + hiddenFrameSupported = false; +} + +if (hiddenFrameSupported) { + for (let test in tests) + exports[test] = tests[test]; +} +else { + exports.testHiddenFrameNotSupported = function(test) { + test.pass("The hidden-frame module is not supported on this app."); + } +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-httpd.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-httpd.js new file mode 100644 index 0000000..42734c1 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-httpd.js @@ -0,0 +1,28 @@ +exports.testBasicHTTPServer = function(test) { + var port = 8080; + var data = require("self").data; + var testFilePath = require("url").toFilename(data.url("test-httpd.txt")); + var basePath = require("file").dirname(testFilePath); + var {startServerAsync} = require("httpd"); + + var srv = startServerAsync(port, basePath); + + test.waitUntilDone(); + + // Request this very file. + var Request = require('request').Request; + Request({ + url: "http://localhost:" + port + "/test-httpd.txt", + onComplete: function (response) { + test.assertEqual(response.text, "This is the HTTPD test file.\n"); + done(); + } + }).get(); + + + function done() { + srv.stop(function() { + test.done(); + }); + } +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-keyboard-observer.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-keyboard-observer.js new file mode 100644 index 0000000..27d4276 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-keyboard-observer.js @@ -0,0 +1,32 @@ +"use strict"; + +const { keyPress } = require("api-utils/dom/events/keys"); +const { Loader } = require("./helpers"); + +exports["test unload keyboard observer"] = function(assert, done) { + let loader = Loader(module); + let element = loader.require("api-utils/window-utils"). + activeBrowserWindow.document.documentElement; + let observer = loader.require("api-utils/keyboard/observer"). + observer; + let called = 0; + + observer.on("keypress", function () { called++; }); + + // dispatching "keypress" event to trigger observer listeners. + keyPress(element, "accel-%"); + + // Unload the module. + loader.unload(); + + // dispatching "keypress" even once again. + keyPress(element, "accel-%"); + + // Enqueuing asserts to make sure that assertion is not performed early. + require("timer").setTimeout(function () { + assert.equal(called, 1, "observer was called before unload only."); + done(); + }, 0); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-keyboard-utils.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-keyboard-utils.js new file mode 100644 index 0000000..137e6b1 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-keyboard-utils.js @@ -0,0 +1,58 @@ +"use strict"; + +const utils = require("keyboard/utils"); +const runtime = require("runtime"); + +const isMac = runtime.OS === "Darwin"; + +exports["test toString"] = function(assert) { + assert.equal(utils.toString({ + key: "B", + modifiers: [ "Shift", "Ctrl" ] + }), "Shift-Ctrl-B", "toString does not normalizes JSON"); + + assert.equal(utils.toString({ + key: "C", + modifiers: [], + }), "C", "Works with objects with empty array of modifiers"); + + assert.equal(utils.toString(Object.create((function Type() {}).prototype, { + key: { value: "d" }, + modifiers: { value: [ "alt" ] }, + method: { value: function() {} } + })), "alt-d", "Works with non-json objects"); + + assert.equal(utils.toString({ + modifiers: [ "shift", "alt" ] + }), "shift-alt-", "works with only modifiers"); +}; + +exports["test toJSON"] = function(assert) { + assert.deepEqual(utils.toJSON("Shift-Ctrl-B"), { + key: "b", + modifiers: [ "control", "shift" ] + }, "toJSON normalizes input"); + + assert.deepEqual(utils.toJSON("Meta-Alt-option-C"), { + key: "c", + modifiers: [ "alt", "meta" ] + }, "removes dublicates"); + + assert.deepEqual(utils.toJSON("AccEl+sHiFt+Z", "+"), { + key: "z", + modifiers: isMac ? [ "meta", "shift" ] : [ "control", "shift" ] + }, "normalizes OS specific keys and adjustes seperator"); +}; + +exports["test normalize"] = function assert(assert) { + assert.equal(utils.normalize("Shift Ctrl A control ctrl", " "), + "control shift a", "removes reapeted modifiers"); + assert.equal(utils.normalize("shift-ctrl-left"), "control-shift-left", + "normilizes non printed characters"); + + assert.throws(function() { + utils.normalize("shift-alt-b-z"); + }, "throws if contains more then on non-modifier key"); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-light-traits.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-light-traits.js new file mode 100644 index 0000000..4ad97b5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-light-traits.js @@ -0,0 +1,7 @@ +"use strict"; + +exports["test traits from objects"] = require("./traits/object-tests"); +exports["test traits from descriptors"] = require("./traits/descriptor-tests"); +exports["test inheritance"] = require("./traits/inheritance-tests"); + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-list.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-list.js new file mode 100644 index 0000000..29abe1a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-list.js @@ -0,0 +1,203 @@ +"use strict"; + +function assertList(test, array, list) { + for (let i = 0, ii = array.length; i < ii; i < ii, i++) { + test.assertEqual( + array.length, + list.length, + 'list must contain same amount of elements as array' + ); + test.assertEqual( + 'List(' + array + ')', + list + '', + 'toString must output array like result' + ); + test.assert( + i in list, + 'must contain element with index: ' + i + ); + test.assertEqual( + array[i], + list[i], + 'element with index: ' + i + ' should match' + ); + } +} + +const { List } = require('api-utils/list'); + +exports['test:test for'] = function(test) { + let fixture = List(3, 2, 1); + + test.assertEqual(3, fixture.length, 'length is 3'); + let i = 0; + for (let key in fixture) { + test.assertEqual(i++, key, 'key should match'); + } +}; + +exports['test:test for each'] = function(test) { + let fixture = new List(3, 2, 1); + + test.assertEqual(3, fixture.length, 'length is 3'); + let i = 3; + for each (let value in fixture) { + test.assertEqual(i--, value, 'value should match'); + } +}; + +exports['test: for each using Iterator'] = function(test) { + let fixture = new List(3, 2, 1); + + test.assertEqual(3, fixture.length, 'length is 3'); + let v = 3, k = 0; + for each (let [key, value] in Iterator(fixture)) { + test.assertEqual(k++, key, 'key should match'); + test.assertEqual(v--, value, 'value should match'); + } +}; + +exports['test:test toString'] = function(test) { + let fixture = List(3, 2, 1); + + test.assertEqual( + 'List(3,2,1)', + fixture + '', + 'toString must output array like result' + ) +}; + +exports['test:test constructor with apply'] = function(test) { + let array = ['a', 'b', 'c']; + let fixture = List.apply(null, array); + + test.assertEqual( + 3, + fixture.length, + 'should have applied arguments' + ); +}; + +exports['test:direct element access'] = function(test) { + let array = [1, 'foo', 2, 'bar', {}, 'bar', function a() {}, test, 1]; + let fixture = List.apply(null, array); + array.splice(5, 1); + array.splice(7, 1); + + test.assertEqual( + array.length, + fixture.length, + 'list should omit duplicate elements' + ); + + test.assertEqual( + 'List(' + array + ')', + fixture.toString(), + 'elements should not be rearranged' + ); + + for (let key in array) { + test.assert(key in fixture,'should contain key for index:' + key); + test.assertEqual( + array[key], + fixture[key], + 'values should match for: ' + key + ); + } +}; + +exports['test:removing adding elements'] = function(test) { + let array = [1, 'foo', 2, 'bar', {}, 'bar', function a() {}, test, 1]; + let fixture = List.compose({ + add: function() this._add.apply(this, arguments), + remove: function() this._remove.apply(this, arguments), + clear: function() this._clear() + }).apply(null, array); + array.splice(5, 1); + array.splice(7, 1); + + assertList(test, array, fixture); + + array.splice(array.indexOf(2), 1); + fixture.remove(2); + assertList(test, array, fixture); + + array.splice(array.indexOf('foo'), 1); + fixture.remove('foo'); + array.splice(array.indexOf(1), 1); + fixture.remove(1); + array.push('foo'); + fixture.add('foo'); + assertList(test, array, fixture); + + array.splice(0); + fixture.clear(0); + assertList(test, array, fixture); + + array.push(1, 'foo', 2, 'bar', 3); + fixture.add(1); + fixture.add('foo'); + fixture.add(2); + fixture.add('bar'); + fixture.add(3); + + assertList(test, array, fixture); +}; + +exports['test: remove does not leave invalid numerical properties'] = function(test) { + let fixture = List.compose({ + remove: function() this._remove.apply(this, arguments), + }).apply(null, [1, 2, 3]); + + fixture.remove(1); + test.assertEqual(fixture[fixture.length], undefined); +} + +exports['test:add list item from Iterator'] = function(test) { + let array = [1, 2, 3, 4], sum = 0, added = false; + + let fixture = List.compose({ + add: function() this._add.apply(this, arguments), + }).apply(null, array); + + for each (let item in fixture) { + sum += item; + + if (!added) { + fixture.add(5); + added = true; + } + } + + test.assertEqual(sum, 1 + 2 + 3 + 4); +}; + +exports['test:remove list item from Iterator'] = function(test) { + let array = [1, 2, 3, 4], sum = 0; + + let fixture = List.compose({ + remove: function() this._remove.apply(this, arguments), + }).apply(null, array); + + for each (let item in fixture) { + sum += item; + fixture.remove(item); + } + + test.assertEqual(sum, 1 + 2 + 3 + 4); +}; + +exports['test:clear list from Iterator'] = function(test) { + let array = [1, 2, 3, 4], sum = 0; + + let fixture = List.compose({ + clear: function() this._clear() + }).apply(null, array); + + for each (let item in fixture) { + sum += item; + fixture.clear(); + } + + test.assertEqual(sum, 1 + 2 + 3 + 4); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-loader.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-loader.js new file mode 100644 index 0000000..796d1cb --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-loader.js @@ -0,0 +1,31 @@ +const { Loader } = require("./helpers"); + +exports.testLoader = function(test) { + var prints = []; + function print(message) { + prints.push(message); + } + + var loader = Loader(module, { dump: print, foo: 1 }); + + var fixture = loader.require('./loader/fixture'); + + test.assertEqual(fixture.foo, 1, "custom globals must work."); + + test.assertEqual(prints[0], "info: testing 1 2,3,4\n", + "global console must work."); + + var unloadsCalled = ''; + + loader.require("unload").when(function() { + unloadsCalled += 'a'; + }); + loader.require("unload.js").when(function() { + unloadsCalled += 'b'; + }); + + loader.unload(); + + test.assertEqual(unloadsCalled, 'ba', + "loader.unload() must call cb's in LIFO order."); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-match-pattern.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-match-pattern.js new file mode 100644 index 0000000..0352ba9 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-match-pattern.js @@ -0,0 +1,161 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original Author) + * Drew Willcoxon <adw@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const { MatchPattern } = require("match-pattern"); + +exports.testMatchPatternTestTrue = function(test) { + function ok(pattern, url) { + let mp = new MatchPattern(pattern); + test.assert(mp.test(url), pattern + " should match " + url); + } + + ok("*", "http://example.com"); + ok("*", "https://example.com"); + ok("*", "ftp://example.com"); + + ok("*.example.com", "http://example.com"); + ok("*.example.com", "http://hamburger.example.com"); + ok("*.example.com", "http://hotdog.hamburger.example.com"); + + ok("http://example.com*", "http://example.com"); + ok("http://example.com*", "http://example.com/"); + ok("http://example.com/*", "http://example.com/"); + ok("http://example.com/*", "http://example.com/potato-salad"); + ok("http://example.com/pickles/*", "http://example.com/pickles/"); + ok("http://example.com/pickles/*", "http://example.com/pickles/lemonade"); + + ok("http://example.com", "http://example.com"); + ok("http://example.com/ice-cream", "http://example.com/ice-cream"); + + ok(/.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok(/https:.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); +}; + +exports.testMatchPatternTestFalse = function(test) { + function ok(pattern, url) { + let mp = new MatchPattern(pattern); + test.assert(!mp.test(url), pattern + " should not match " + url); + } + + ok("*", null); + ok("*", ""); + ok("*", "bogus"); + ok("*", "chrome://browser/content/browser.xul"); + ok("*", "nttp://example.com"); + + ok("*.example.com", null); + ok("*.example.com", ""); + ok("*.example.com", "bogus"); + ok("*.example.com", "http://example.net"); + ok("*.example.com", "http://foo.com"); + ok("*.example.com", "http://example.com.foo"); + ok("*.example2.com", "http://example.com"); + + ok("http://example.com/*", null); + ok("http://example.com/*", ""); + ok("http://example.com/*", "bogus"); + ok("http://example.com/*", "http://example.com"); + ok("http://example.com/*", "http://foo.com/"); + + ok("http://example.com", null); + ok("http://example.com", ""); + ok("http://example.com", "bogus"); + ok("http://example.com", "http://example.com/"); + + ok(/zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok(/.*zilla/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok(/https:.*zilla/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); +}; + +exports.testMatchPatternErrors = function(test) { + test.assertRaises( + function() new MatchPattern("*.google.com/*"), + /There can be at most one/, + "MatchPattern throws when supplied multiple '*'" + ); + + test.assertRaises( + function() new MatchPattern("google.com"), + /expected to be either an exact URL/, + "MatchPattern throws when the wildcard doesn't use '*' and doesn't " + + "look like a URL" + ); + + test.assertRaises( + function() new MatchPattern("http://google*.com"), + /expected to be the first or the last/, + "MatchPattern throws when a '*' is in the middle of the wildcard" + ); + + test.assertRaises( + function() new MatchPattern(/ /g), + /^A RegExp match pattern cannot be set to `global` \(i\.e\. \/\/g\)\.$/, + "MatchPattern throws on a RegExp set to `global` (i.e. //g)." + ); + + test.assertRaises( + function() new MatchPattern(/ /i), + /^A RegExp match pattern cannot be set to `ignoreCase` \(i\.e\. \/\/i\)\.$/, + "MatchPattern throws on a RegExp set to `ignoreCase` (i.e. //i)." + ); + + test.assertRaises( + function() new MatchPattern( / /m ), + /^A RegExp match pattern cannot be set to `multiline` \(i\.e\. \/\/m\)\.$/, + "MatchPattern throws on a RegExp set to `multiline` (i.e. //m)." + ); +}; + +exports.testMatchPatternInternals = function(test) { + test.assertEqual( + new MatchPattern("http://google.com/test").exactURL, + "http://google.com/test" + ); + + test.assertEqual( + new MatchPattern("http://google.com/test/*").urlPrefix, + "http://google.com/test/" + ); + + test.assertEqual( + new MatchPattern("*.example.com").domain, + "example.com" + ); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-memory.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-memory.js new file mode 100644 index 0000000..7e172bf --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-memory.js @@ -0,0 +1,15 @@ +var memory = require("memory"); + +exports.testMemory = function(test) { + test.pass("Skipping this test until Gecko memory debugging issues " + + "are resolved (see bug 592774)."); + return; + + var obj = {}; + memory.track(obj, "testMemory.testObj"); + var objs = memory.getObjects("testMemory.testObj"); + test.assertEqual(objs[0].weakref.get(), obj); + obj = null; + memory.gc(); + test.assertEqual(objs[0].weakref.get(), null); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-modules.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-modules.js new file mode 100644 index 0000000..99297bc --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-modules.js @@ -0,0 +1,144 @@ +exports.testDefine = function(test) { + let tiger = require('./modules/tiger'); + test.assertEqual(tiger.name, 'tiger', 'name proprety was exported properly'); + test.assertEqual(tiger.type, 'cat', 'property form other module exported'); +}; + +exports.testDefineInoresNonFactory = function(test) { + let mod = require('./modules/async2'); + test.assertEqual(mod.name, 'async2', 'name proprety was exported properly'); + test.assertNotEqual(mod.traditional2Name, 'traditional2', '1st is ignored'); +}; +/* Disable test that require AMD specific functionality: + +// define() that exports a function as the module value, +// specifying a module name. +exports.testDefExport = function(test) { + var add = require('modules/add'); + test.assertEqual(add(1, 1), 2, 'Named define() exporting a function'); +}; + +// define() that exports function as a value, but is anonymous +exports.testAnonDefExport = function (test) { + var subtract = require('modules/subtract'); + test.assertEqual(subtract(4, 2), 2, + 'Anonymous define() exporting a function'); +} + +// using require([], function () {}) to load modules. +exports.testSimpleRequire = function (test) { + require(['modules/blue', 'modules/orange'], function (blue, orange) { + test.assertEqual(blue.name, 'blue', 'Simple require for blue'); + test.assertEqual(orange.name, 'orange', 'Simple require for orange'); + test.assertEqual(orange.parentType, 'color', + 'Simple require dependency check for orange'); + }); +} + +// using nested require([]) calls. +exports.testSimpleRequireNested = function (test) { + require(['modules/blue', 'modules/orange', 'modules/green'], + function (blue, orange, green) { + + require(['modules/orange', 'modules/red'], function (orange, red) { + test.assertEqual(red.name, 'red', 'Simple require for red'); + test.assertEqual(red.parentType, 'color', + 'Simple require dependency check for red'); + test.assertEqual(blue.name, 'blue', 'Simple require for blue'); + test.assertEqual(orange.name, 'orange', 'Simple require for orange'); + test.assertEqual(orange.parentType, 'color', + 'Simple require dependency check for orange'); + test.assertEqual(green.name, 'green', 'Simple require for green'); + test.assertEqual(green.parentType, 'color', + 'Simple require dependency check for green'); + }); + + }); +} + +// requiring a traditional module, that uses async, that use traditional and +// async, with a circular reference +exports.testMixedCircular = function (test) { + var t = require('modules/traditional1'); + test.assertEqual(t.name, 'traditional1', 'Testing name'); + test.assertEqual(t.traditional2Name, 'traditional2', + 'Testing dependent name'); + test.assertEqual(t.traditional1Name, 'traditional1', 'Testing circular name'); + test.assertEqual(t.async2Name, 'async2', 'Testing async2 name'); + test.assertEqual(t.async2Traditional2Name, 'traditional2', + 'Testing nested traditional2 name'); +} + +// Testing define()(function(require) {}) with some that use exports, +// some that use return. +exports.testAnonExportsReturn = function (test) { + var lion = require('modules/lion'); + require(['modules/tiger', 'modules/cheetah'], function (tiger, cheetah) { + test.assertEqual('lion', lion, 'Check lion name'); + test.assertEqual('tiger', tiger.name, 'Check tiger name'); + test.assertEqual('cat', tiger.type, 'Check tiger type'); + test.assertEqual('cheetah', cheetah(), 'Check cheetah name'); + }); +} + +// circular dependency +exports.testCircular = function (test) { + var pollux = require('modules/pollux'), + castor = require('modules/castor'); + + test.assertEqual(pollux.name, 'pollux', 'Pollux\'s name'); + test.assertEqual(pollux.getCastorName(), + 'castor', 'Castor\'s name from Pollux.'); + test.assertEqual(castor.name, 'castor', 'Castor\'s name'); + test.assertEqual(castor.getPolluxName(), 'pollux', + 'Pollux\'s name from Castor.'); +} + +// test a bad module that asks for exports but also does a define() return +exports.testBadExportAndReturn = function (test) { + var passed = false; + try { + var bad = require('modules/badExportAndReturn'); + } catch(e) { + passed = /cannot use exports and also return/.test(e.toString()); + } + test.assertEqual(passed, true, 'Make sure exports and return fail'); +} + +// test a bad circular dependency, where an exported value is needed, but +// the return value happens too late, a module already asked for the exported +// value. +exports.testBadExportAndReturnCircular = function (test) { + var passed = false; + try { + var bad = require('modules/badFirst'); + } catch(e) { + passed = /after another module has referenced its exported value/ + .test(e.toString()); + } + test.assertEqual(passed, true, 'Make sure return after an exported ' + + 'value is grabbed by another module fails.'); +} + +// only allow one define call per file. +exports.testOneDefine = function (test) { + var passed = false; + try { + var dupe = require('modules/dupe'); + } catch(e) { + passed = /Only one call to define/.test(e.toString()); + } + test.assertEqual(passed, true, 'Only allow one define call per module'); +} + +// only allow one define call per file, testing a bad nested define call. +exports.testOneDefineNested = function (test) { + var passed = false; + try { + var dupe = require('modules/dupeNested'); + } catch(e) { + passed = /Only one call to define/.test(e.toString()); + } + test.assertEqual(passed, true, 'Only allow one define call per module'); +} +*/ diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-namespace.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-namespace.js new file mode 100644 index 0000000..60ba23d --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-namespace.js @@ -0,0 +1,70 @@ +"use strict"; + +let { Namespace } = require("api-utils/namespace"); + +exports["test namsepace basics"] = function(assert) { + var privates = Namespace(); + var object = { foo: function foo() { return "hello foo"; } }; + + assert.notEqual(privates(object), object, + "namespaced object is not the same"); + assert.ok(!('foo' in privates(object)), + "public properties are not in the namespace"); + + assert.equal(privates(object), privates(object), + "same namespaced object is returned on each call"); +}; + +exports["test namespace overlays"] = function(assert) { + var _ = new Namespace(); + var object = { foo: 'foo' }; + + _(object).foo = 'bar'; + + assert.equal(_(object).foo, "bar", + "namespaced property `foo` changed value"); + + assert.equal(object.foo, "foo", + "public property `foo` has original value"); + + object.foo = "baz"; + assert.equal(_(object).foo, "bar", + "property changes do not affect namespaced properties"); + + object.bar = "foo"; + assert.ok(!("bar" in _(object)), + "new public properties are not reflected in namespace"); +}; + +exports["test shared namespaces"] = function(assert) { + var _ = new Namespace({ hello: 'hello world' }); + + var f1 = { hello: 1 }; + var f2 = { foo: 'foo' }; + + assert.equal(_(f1).hello, _(f2).hello, "namespace can be shared"); + assert.notEqual(f1.hello, _(f1).hello, "shared namespace can overlay"); + assert.notEqual(f2.hello, _(f2).hello, "target is not affected"); + + _(f1).hello = 2; + + assert.notEqual(_(f1).hello, _(f2).hello, + "namespaced property can be overided"); + assert.equal(_(f2).hello, _({}).hello, "namespace does not change"); +}; + +exports["test multi namespace"] = function(assert) { + var n1 = new Namespace(); + var n2 = new Namespace(); + var object = { baz: 1 }; + n1(object).foo = 1; + n2(object).foo = 2; + n1(object).bar = n2(object).bar = 3; + + assert.notEqual(n1(object).foo, n2(object).foo, + "object can have multiple namespaces"); + assert.equal(n1(object).bar, n2(object).bar, + "object can have matching props in diff namespaces"); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-observer-service.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-observer-service.js new file mode 100644 index 0000000..68cda74 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-observer-service.js @@ -0,0 +1,73 @@ +var observers = require("api-utils/observer-service"); +var {Cc,Ci} = require("chrome"); +const { Loader } = require("./helpers"); + +exports.testUnloadAndErrorLogging = function(test) { + var prints = []; + var loader = Loader(module, { dump: function print(message) { + prints.push(message); + }}); + var sbobsvc = loader.require("api-utils/observer-service"); + + var timesCalled = 0; + var cb = function(subject, data) { + timesCalled++; + }; + var badCb = function(subject, data) { + throw new Error("foo"); + }; + sbobsvc.add("blarg", cb); + observers.notify("blarg", "yo yo"); + test.assertEqual(timesCalled, 1); + sbobsvc.add("narg", badCb); + observers.notify("narg", "yo yo"); + var lines = prints[0].split("\n"); + test.assertEqual(lines[0], "error: An exception occurred."); + test.assertEqual(lines[1], "Traceback (most recent call last):"); + test.assertEqual(lines.slice(-2)[0], "Error: foo"); + + loader.unload(); + observers.notify("blarg", "yo yo"); + test.assertEqual(timesCalled, 1); +}; + +exports.testObserverService = function(test) { + var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + var service = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + var uri = ios.newURI("http://www.foo.com", null, null); + var timesCalled = 0; + var lastSubject = null; + var lastData = null; + + var cb = function(subject, data) { + timesCalled++; + lastSubject = subject; + lastData = data; + }; + + observers.add("blarg", cb); + service.notifyObservers(uri, "blarg", "some data"); + test.assertEqual(timesCalled, 1, + "observer-service.add() should call callback"); + test.assertEqual(lastSubject, uri, + "observer-service.add() should pass subject"); + test.assertEqual(lastData, "some data", + "observer-service.add() should pass data"); + + function customSubject() {} + function customData() {} + observers.notify("blarg", customSubject, customData); + test.assertEqual(timesCalled, 2, + "observer-service.notify() should work"); + test.assertEqual(lastSubject, customSubject, + "observer-service.notify() should pass+wrap subject"); + test.assertEqual(lastData, customData, + "observer-service.notify() should pass data"); + + observers.remove("blarg", cb); + service.notifyObservers(null, "blarg", "some data"); + test.assertEqual(timesCalled, 2, + "observer-service.remove() should work"); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-passwords-utils.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-passwords-utils.js new file mode 100644 index 0000000..651296f --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-passwords-utils.js @@ -0,0 +1,138 @@ +"use strict"; + +const { store, search, remove } = require("passwords/utils"); + +exports["test store requires `password` field"] = function(assert) { + assert.throws(function() { + store({ username: "foo", realm: "bar" }); + }, "`password` is required"); +}; + +exports["test store requires `username` field"] = function(assert) { + assert.throws(function() { + store({ password: "foo", realm: "bar" }); + }, "`username` is required"); +}; + +exports["test store requires `realm` field"] = function(assert) { + assert.throws(function() { + store({ username: "foo", password: "bar" }); + }, "`password` is required"); +}; + +exports["test can't store same login twice"] = function(assert) { + let options = { username: "user", password: "pass", realm: "realm" }; + store(options); + assert.throws(function() { + store(options); + }, "can't store same pass twice"); + remove(options); +}; + +exports["test remove throws if no login found"] = function(assert) { + assert.throws(function() { + remove({ username: "foo", password: "bar", realm: "baz" }); + }, "can't remove unstored credentials"); +}; + +exports["test addon associated credentials"] = function(assert) { + let options = { username: "foo", password: "bar", realm: "baz" }; + store(options); + + assert.ok(search().length, "credential was stored"); + assert.ok(search(options).length, "stored credential found"); + assert.ok(search({ username: options.username }).length, "found by username"); + assert.ok(search({ password: options.password }).length, "found by password"); + assert.ok(search({ realm: options.realm }).length, "found by realm"); + + let credential = search(options)[0]; + assert.equal(credential.url.indexOf("addon:"), 0, + "`addon:` uri is used for add-on associated credentials"); + assert.equal(credential.username, options.username, "username matches"); + assert.equal(credential.password, options.password, "password matches"); + assert.equal(credential.realm, options.realm, "realm matches"); + assert.equal(credential.formSubmitURL, null, + "`formSubmitURL` is `null` for add-on associated credentials"); + assert.equal(credential.usernameField, "", "usernameField is empty"); + assert.equal(credential.passwordField, "", "passwordField is empty"); + + remove(search(options)[0]); + assert.ok(!search(options).length, "remove worked"); +}; + +exports["test web page associated credentials"] = function(assert) { + let options = { + url: "http://www.example.com", + formSubmitURL: "http://login.example.com", + username: "user", + password: "pass", + usernameField: "user-f", + passwordField: "pass-f" + }; + store({ + url: "http://www.example.com/login", + formSubmitURL: "http://login.example.com/foo/authenticate.cgi", + username: options.username, + password: options.password, + usernameField: options.usernameField, + passwordField: options.passwordField + }); + + assert.ok(search().length, "credential was stored"); + assert.ok(search(options).length, "stored credential found"); + assert.ok(search({ username: options.username }).length, "found by username"); + assert.ok(search({ password: options.password }).length, "found by password"); + assert.ok(search({ formSubmitURL: options.formSubmitURL }).length, + "found by formSubmitURL"); + assert.ok(search({ usernameField: options.usernameField }).length, + "found by usernameField"); + assert.ok(search({ passwordField: options.passwordField }).length, + "found by passwordField"); + + let credential = search(options)[0]; + assert.equal(credential.url, options.url, "url matches"); + assert.equal(credential.username, options.username, "username matches"); + assert.equal(credential.password, options.password, "password matches"); + assert.equal(credential.realm, null, "realm is "); + assert.equal(credential.formSubmitURL, options.formSubmitURL, + "`formSubmitURL` matches"); + assert.equal(credential.usernameField, options.usernameField, + "usernameField matches"); + assert.equal(credential.passwordField, options.passwordField, + "passwordField matches"); + + remove(search(options)[0]); + assert.ok(!search(options).length, "remove worked"); +}; + +exports["test site authentication credentials"] = function(assert) { + let options = { + url: "http://test.authentication.com", + username: "u", + password: "p", + realm: "r" + }; + + store(options); + assert.ok(search().length, "credential was stored"); + assert.ok(search(options).length, "stored credential found"); + assert.ok(search({ username: options.username }).length, "found by username"); + assert.ok(search({ password: options.password }).length, "found by password"); + assert.ok(search({ realm: options.realm }).length, "found by realm"); + assert.ok(search({ url: options.url }).length, "found by url"); + + let credential = search(options)[0]; + assert.equal(credential.url, options.url, "url matches"); + assert.equal(credential.username, options.username, "username matches"); + assert.equal(credential.password, options.password, "password matches"); + assert.equal(credential.realm, options.realm, "realm matches"); + assert.equal(credential.formSubmitURL, null, + "`formSubmitURL` is `null` for site authentication credentials"); + assert.equal(credential.usernameField, "", "usernameField is empty"); + assert.equal(credential.passwordField, "", "passwordField is empty"); + + remove(search(options)[0]); + assert.ok(!search(options).length, "remove worked"); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-plain-text-console.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-plain-text-console.js new file mode 100644 index 0000000..a0c9dda --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-plain-text-console.js @@ -0,0 +1,64 @@ +exports.testPlainTextConsole = function(test) { + var prints = []; + function print(message) { + prints.push(message); + } + function lastPrint() { + var last = prints.slice(-1)[0]; + prints = []; + return last; + } + + var Console = require("plain-text-console").PlainTextConsole; + var con = new Console(print); + + test.pass("PlainTextConsole instantiates"); + + con.log('testing', 1, [2, 3, 4]); + test.assertEqual(lastPrint(), "info: testing 1 2,3,4\n", + "PlainTextConsole.log() must work."); + + con.info('testing', 1, [2, 3, 4]); + test.assertEqual(lastPrint(), "info: testing 1 2,3,4\n", + "PlainTextConsole.info() must work."); + + con.warn('testing', 1, [2, 3, 4]); + test.assertEqual(lastPrint(), "warning: testing 1 2,3,4\n", + "PlainTextConsole.warn() must work."); + + con.error('testing', 1, [2, 3, 4]); + test.assertEqual(lastPrint(), "error: testing 1 2,3,4\n", + "PlainTextConsole.error() must work."); + + con.debug('testing', 1, [2, 3, 4]); + test.assertEqual(lastPrint(), "debug: testing 1 2,3,4\n", + "PlainTextConsole.debug() must work."); + + con.log('testing', undefined); + test.assertEqual(lastPrint(), "info: testing undefined\n", + "PlainTextConsole.log() must stringify undefined."); + + con.log('testing', null); + test.assertEqual(lastPrint(), "info: testing null\n", + "PlainTextConsole.log() must stringify null."); + + con.log("testing", { toString: function() "obj.toString()" }); + test.assertEqual(lastPrint(), "info: testing obj.toString()\n", + "PlainTextConsole.log() must stringify custom toString."); + + con.log("testing", { toString: function() { throw "fail!"; } }); + test.assertEqual(lastPrint(), "info: testing <toString() error>\n", + "PlainTextConsole.log() must stringify custom bad toString."); + + con.exception(new Error("blah")); + var tbLines = prints[0].split("\n"); + test.assertEqual(tbLines[0], "error: An exception occurred."); + test.assertEqual(tbLines[1], "Traceback (most recent call last):"); + test.assertEqual(tbLines.slice(-2)[0], "Error: blah"); + + prints = []; + con.trace(); + tbLines = prints[0].split("\n"); + test.assertEqual(tbLines[0], "info: Traceback (most recent call last):"); + test.assertEqual(tbLines.slice(-2)[0].trim(), "con.trace();"); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-preferences-service.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-preferences-service.js new file mode 100644 index 0000000..dd0de38 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-preferences-service.js @@ -0,0 +1,87 @@ +var prefs = require("preferences-service"); +var {Cc,Ci} = require("chrome"); + +exports.testReset = function(test) { + prefs.reset("test_reset_pref"); + test.assertEqual(prefs.has("test_reset_pref"), false); + test.assertEqual(prefs.isSet("test_reset_pref"), false); + prefs.set("test_reset_pref", 5); + test.assertEqual(prefs.has("test_reset_pref"), true); + test.assertEqual(prefs.isSet("test_reset_pref"), true); +}; + +exports.testGetAndSet = function(test) { + let svc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(null); + svc.setCharPref("test_get_string_pref", "a normal string"); + test.assertEqual(prefs.get("test_get_string_pref"), "a normal string", + "preferences-service should read from " + + "application-wide preferences service"); + + prefs.set("test_set_get_pref.integer", 1); + test.assertEqual(prefs.get("test_set_get_pref.integer"), 1, + "set/get integer preference should work"); + + prefs.set("test_set_get_number_pref", 42); + test.assertRaises( + function() { prefs.set("test_set_get_number_pref", 3.14159); }, + "cannot store non-integer number: 3.14159", + "setting a float preference should raise an error" + ); + test.assertEqual(prefs.get("test_set_get_number_pref"), 42, + "bad-type write attempt should not overwrite"); + + // 0x80000000 (no), 0x7fffffff (yes), -0x80000000 (yes), -0x80000001 (no) + test.assertRaises( + function() { prefs.set("test_set_get_number_pref", Math.pow(2, 31)); }, + ("you cannot set the test_set_get_number_pref pref to the number " + + "2147483648, as number pref values must be in the signed 32-bit " + + "integer range -(2^31) to 2^31-1. To store numbers outside that " + + "range, store them as strings."), + "setting an int pref outside the range -(2^31) to 2^31-1 shouldn't work" + ); + test.assertEqual(prefs.get("test_set_get_number_pref"), 42, + "out-of-range write attempt should not overwrite 1"); + prefs.set("test_set_get_number_pref", Math.pow(2, 31)-1); + test.assertEqual(prefs.get("test_set_get_number_pref"), 0x7fffffff, + "in-range write attempt should work 1"); + prefs.set("test_set_get_number_pref", -Math.pow(2, 31)); + test.assertEqual(prefs.get("test_set_get_number_pref"), -0x80000000, + "in-range write attempt should work 2"); + test.assertRaises( + function() { prefs.set("test_set_get_number_pref", -0x80000001); }, + ("you cannot set the test_set_get_number_pref pref to the number " + + "-2147483649, as number pref values must be in the signed 32-bit " + + "integer range -(2^31) to 2^31-1. To store numbers outside that " + + "range, store them as strings."), + "setting an int pref outside the range -(2^31) to 2^31-1 shouldn't work" + ); + test.assertEqual(prefs.get("test_set_get_number_pref"), -0x80000000, + "out-of-range write attempt should not overwrite 2"); + + + prefs.set("test_set_get_pref.string", "foo"); + test.assertEqual(prefs.get("test_set_get_pref.string"), "foo", + "set/get string preference should work"); + + prefs.set("test_set_get_pref.boolean", true); + test.assertEqual(prefs.get("test_set_get_pref.boolean"), true, + "set/get boolean preference should work"); + + prefs.set("test_set_get_unicode_pref", String.fromCharCode(960)); + test.assertEqual(prefs.get("test_set_get_unicode_pref"), + String.fromCharCode(960), + "set/get unicode preference should work"); + + var unsupportedValues = [null, [], undefined]; + unsupportedValues.forEach( + function(value) { + test.assertRaises( + function() { prefs.set("test_set_pref", value); }, + ("can't set pref test_set_pref to value '" + value + "'; " + + "it isn't a string, integer, or boolean"), + "Setting a pref to " + uneval(value) + " should raise error" + ); + }); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-registry.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-registry.js new file mode 100644 index 0000000..8a251f7 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-registry.js @@ -0,0 +1,76 @@ +'use strict'; + +exports['test:add'] = function(test) { + function Class() {} + let fixture = require('utils/registry').Registry(Class); + let isAddEmitted = false; + fixture.on('add', function(item) { + test.assert( + item instanceof Class, + 'if object added is not an instance should construct instance from it' + ); + test.assert( + fixture.has(item), + 'callback is called after instance is added' + ); + test.assert( + !isAddEmitted, + 'callback should be called for the same item only once' + ); + isAddEmitted = true; + }); + + let object = fixture.add({}); + fixture.add(object); +}; + +exports['test:remove'] = function(test) { + function Class() {} + let fixture = require('utils/registry').Registry(Class); + fixture.on('remove', function(item) { + test.assert( + item instanceof Class, + 'if object removed can be only instance of Class' + ); + test.assert( + fixture.has(item), + 'callback is called before instance is removed' + ); + test.assert( + !isRemoveEmitted, + 'callback should be called for the same item only once' + ); + isRemoveEmitted = true; + }); + + fixture.remove({}); + let object = fixture.add({}); + fixture.remove(object); + fixture.remove(object); +}; + +exports['test:items'] = function(test) { + function Class() {} + let fixture = require('utils/registry').Registry(Class), + actual, + times = 0; + + function testItem(item) { + times ++; + test.assertEqual( + actual, + item, + 'item should match actual item being added/removed' + ); + } + + actual = fixture.add({}); + + fixture.on('add', testItem); + fixture.on('remove', testItem); + + fixture.remove(actual); + fixture.remove(fixture.add(actual = new Class())); + test.assertEqual(3, times, 'should notify listeners on each call'); +} + diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-require.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-require.js new file mode 100644 index 0000000..986a53c --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-require.js @@ -0,0 +1,25 @@ +const traceback = require("traceback"); + +exports.test_no_args = function(test) { + var passed = false; + try { + var oops = require(); // leave this on line 6! + } catch(e) { + let msg = e.toString(); + test.assertEqual(msg.indexOf("Error: you must provide a module name when calling require() from "), 0); + test.assertNotEqual(msg.indexOf("test-require"), -1, msg); + // we'd also like to assert that the right filename and linenumber is in + // the stack trace, but this currently doesn't work (see bugs 679591 and + // 551604) + if (0) { + let tb = traceback.fromException(e); + let lastFrame = tb[tb.length-1]; + test.assertNotEqual(lastFrame.filename.indexOf("test-require.js"), -1, + lastFrame.filename); + test.assertEqual(lastFrame.lineNo, 6); + test.assertEqual(lastFrame.funcName, "??"); + } + passed = true; + } + test.assert(passed, 'require() with no args should raise helpful error'); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-self.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-self.js new file mode 100644 index 0000000..3cc6df5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-self.js @@ -0,0 +1,29 @@ +exports.testSelf = function(test) { + var self = require("self"); + // We can't assert anything about the ID inside the unit test right now, + // because the ID we get depends upon how the test was invoked. The idea + // is that it is supposed to come from the main top-level package's + // package.json file, from the "id" key. + test.assertEqual(typeof(self.id), "string", "self.id is a string"); + test.assert(self.id.length > 0); + + var source = self.data.load("test-content-symbiont.js"); + test.assert(source.match(/test-content-symbiont/), "self.data.load() works"); + + // Likewise, we can't assert anything about the full URL, because that + // depends on self.id . We can only assert that it ends in the right + // thing. + var url = self.data.url("test-content-symbiont.js"); + test.assertEqual(typeof(url), "string", "self.data.url('x') returns string"); + test.assertEqual(/\/test-content-symbiont\.js$/.test(url), true); + + // Make sure 'undefined' is not in url when no string is provided. + url = self.data.url(); + test.assertEqual(typeof(url), "string", "self.data.url() returns string"); + test.assertEqual(/\/undefined$/.test(url), false); + + // When tests are run on just the api-utils package, self.name is + // api-utils. When they're run as 'cfx testall', self.name is testpkgs. + test.assert((self.name == "api-utils") || (self.name == "testpkgs"), + "self.name is api-utils or testpkgs"); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-set-exports.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-set-exports.js new file mode 100644 index 0000000..be89305 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-set-exports.js @@ -0,0 +1,31 @@ +let four = require("./modules/exportsEquals"); +exports.testExportsEquals = function(test) { + test.assertEqual(four, 4); +} + +/* TODO: Discuss idea of dropping support for this feature that was alternative + to `module.exports = ..` that failed. +let five = require("./modules/setExports"); +exports.testSetExports = function(test) { + test.assertEqual(five, 5); +} + +exports.testDupeSetExports = function(test) { + var passed = false; + try { + var dupe = require('./modules/dupeSetExports'); + } catch(e) { + passed = /define\(\) was used, so module\.exports= and module\.setExports\(\) may not be used/.test(e.toString()); + } + test.assertEqual(passed, true, 'define() or setExports(), not both'); +} +*/ + +exports.testModule = function(test) { + // module.id is not cast in stone yet. In the future, it may include the + // package name, or may possibly be a/ URL of some sort. For now, it's a + // URL that starts with resource: and ends with this module name, but the + // part in between varies depending upon how the test is run. + var found = /test-set-exports$/.test(module.id); + test.assertEqual(found, true, module.id+" ends with test-set-exports.js"); +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-tab-browser.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-tab-browser.js new file mode 100644 index 0000000..10d9614 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-tab-browser.js @@ -0,0 +1,525 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var timer = require("timer"); +var {Cc,Ci} = require("chrome"); + +function onBrowserLoad(callback, event) { + if (event.target && event.target.defaultView == this) { + this.removeEventListener("load", onBrowserLoad, true); + let browsers = this.document.getElementsByTagName("tabbrowser"); + try { + require("timer").setTimeout(function (window) { + callback(window, browsers[0]); + }, 10, this); + } catch (e) { console.exception(e); } + } +} +// Utility function to open a new browser window. +function openBrowserWindow(callback, url) { + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + let urlString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + urlString.data = url; + let window = ww.openWindow(null, "chrome://browser/content/browser.xul", + "_blank", "chrome,all,dialog=no", urlString); + if (callback) + window.addEventListener("load", onBrowserLoad.bind(window, callback), true); + + return window; +} + +// Helper for calling code at window close +function closeBrowserWindow(window, callback) { + require("timer").setTimeout(function() { + window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload, false); + callback(); + }, false); + window.close(); + }, 0); +} + +// Helper for opening two windows at once +function openTwoWindows(callback) { + openBrowserWindow(function (window1) { + openBrowserWindow(function (window2) { + callback(window1, window2); + }); + }); +} + +// Helper for closing two windows at once +function closeTwoWindows(window1, window2, callback) { + closeBrowserWindow(window1, function() { + closeBrowserWindow(window2, callback); + }); +} + +exports.testAddTab = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + const tabBrowser = require("tab-browser"); + + let cache = []; + let windowUtils = require("window-utils"); + new windowUtils.WindowTracker({ + onTrack: function(win) { + cache.push(win); + }, + onUntrack: function(win) { + cache.splice(cache.indexOf(win), 1) + } + }); + let startWindowCount = cache.length; + + // Test 1: add a tab + let firstUrl = "data:text/html,one"; + tabBrowser.addTab(firstUrl, { + onLoad: function(e) { + let win1 = cache[startWindowCount - 1]; + test.assertEqual(win1.content.location, firstUrl, "URL of new tab in first window matches"); + + // Test 2: add a tab in a new window + let secondUrl = "data:text/html,two"; + tabBrowser.addTab(secondUrl, { + inNewWindow: true, + onLoad: function(e) { + test.assertEqual(cache.length, startWindowCount + 1, "a new window was opened"); + let win2 = cache[startWindowCount]; + let gBrowser = win2.gBrowser; + gBrowser.addEventListener("DOMContentLoaded", function onLoad(e) { + gBrowser.removeEventListener("DOMContentLoaded", onLoad, false); + test.assertEqual(win2.content.location, secondUrl, "URL of new tab in the new window matches"); + + closeBrowserWindow(win2, function() { + closeBrowserWindow(win1, function() { + test.done(); + }); + }); + }, false); + } + }); + } + }); + }); +}; + +exports.testTrackerWithDelegate = function(test) { + test.waitUntilDone(); + const tabBrowser = require("tab-browser"); + + var delegate = { + state: "initializing", + onTrack: function onTrack(browser) { + if (this.state == "initializing") { + this.state = "waiting for browser window to open"; + } + else if (this.state == "waiting for browser window to open") { + this.state = "waiting for browser window to close"; + require("timer").setTimeout(function() { + closeBrowserWindow(browser.ownerDocument.defaultView, function() { + test.assertEqual(delegate.state, "deinitializing"); + tb.unload(); + test.done(); + }); + }, 0); + } + else + test.fail("invalid state"); + }, + onUntrack: function onUntrack(browser) { + if (this.state == "waiting for browser window to close") { + test.pass("proper state in onUntrack"); + this.state = "deinitializing"; + } + else if (this.state != "deinitializing") + test.fail("invalid state"); + } + }; + var tb = new tabBrowser.Tracker(delegate); + + delegate.state = "waiting for browser window to open"; + + openBrowserWindow(); +}; + +exports.testWhenContentLoaded = function(test) { + test.waitUntilDone(); + const tabBrowser = require("tab-browser"); + + var tracker = tabBrowser.whenContentLoaded( + function(window) { + var item = window.document.getElementById("foo"); + test.assertEqual(item.textContent, "bar", + "whenContentLoaded() works."); + tracker.unload(); + closeBrowserWindow(activeWindow(), function() { + test.done(); + }); + }); + + openBrowserWindow(function(browserWindow, browser) { + var html = '<div id="foo">bar</div>'; + browser.addTab("data:text/html," + html); + }); +}; + +exports.testTrackerWithoutDelegate = function(test) { + test.waitUntilDone(); + const tabBrowser = require("tab-browser"); + + openBrowserWindow(function(browserWindow, browser) { + var tb = new tabBrowser.Tracker(); + + if (tb.length == 0) + test.fail("expect at least one tab browser to exist."); + + for (var i = 0; i < tb.length; i++) + test.assertEqual(tb.get(i).nodeName, "tabbrowser", + "get() method and length prop should work"); + for (var b in tb) + test.assertEqual(b.nodeName, "tabbrowser", + "iterator should work"); + + var matches = [b for (b in tb) + if (b == browser)]; + test.assertEqual(matches.length, 1, + "New browser should be in tracker."); + tb.unload(); + + closeBrowserWindow(browserWindow, function() { + test.done(); + }); + }); +}; + +exports.testTabTracker = function(test) { + test.waitUntilDone(); + const tabBrowser = require("tab-browser"); + + openBrowserWindow(function(browserWindow, browser) { + var delegate = { + tracked: 0, + onTrack: function(tab) { + this.tracked++; + }, + onUntrack: function(tab) { + this.tracked--; + } + }; + + let tabTracker = tabBrowser.TabTracker(delegate); + + let tracked = delegate.tracked; + let url1 = "data:text/html,1"; + let url2 = "data:text/html,2"; + let url3 = "data:text/html,3"; + let tabCount = 0; + + function tabLoadListener(e) { + let loadedURL = e.target.defaultView.location; + if (loadedURL == url1) + tabCount++; + else if (loadedURL == url2) + tabCount++; + else if (loadedURL == url3) + tabCount++; + + if (tabCount == 3) { + test.assertEqual(delegate.tracked, tracked + 3, "delegate tracked tabs matched count"); + tabTracker.unload(); + closeBrowserWindow(browserWindow, function() { + require("timer").setTimeout(function() test.done(), 0); + }); + } + } + + tabBrowser.addTab(url1, { + onLoad: tabLoadListener + }); + tabBrowser.addTab(url2, { + onLoad: tabLoadListener + }); + tabBrowser.addTab(url3, { + onLoad: tabLoadListener + }); + }); +}; + +exports.testActiveTab = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(browserWindow, browser) { + const tabBrowser = require("tab-browser"); + const TabModule = require("tab-browser").TabModule; + let tm = new TabModule(browserWindow); + test.assertEqual(tm.length, 1); + let url1 = "data:text/html,foo"; + let url2 = "data:text/html,bar"; + + function tabURL(tab) tab.ownerDocument.defaultView.content.location.toString() + + tabBrowser.addTab(url1, { + onLoad: function(e) { + // make sure we're running in the right window. + test.assertEqual(tabBrowser.activeTab.ownerDocument.defaultView, browserWindow, "active window matches"); + browserWindow.focus(); + + test.assertEqual(tabURL(tabBrowser.activeTab), url1, "url1 matches"); + let tabIndex = browser.getBrowserIndexForDocument(e.target); + let tabAtIndex = browser.tabContainer.getItemAtIndex(tabIndex); + test.assertEqual(tabAtIndex, tabBrowser.activeTab, "activeTab element matches"); + + tabBrowser.addTab(url2, { + inBackground: true, + onLoad: function() { + test.assertEqual(tabURL(tabBrowser.activeTab), url1, "url1 still matches"); + let tabAtIndex = browser.tabContainer.getItemAtIndex(tabIndex); + test.assertEqual(tabAtIndex, tabBrowser.activeTab, "activeTab element matches"); + closeBrowserWindow(browserWindow, function() { + test.done() + }); + } + }); + } + }); + }); +}; + +// TabModule tests +exports.testEventsAndLengthStayInModule = function(test) { + test.waitUntilDone(); + let TabModule = require("tab-browser").TabModule; + + openTwoWindows(function(window1, window2) { + let tm1 = new TabModule(window1); + let tm2 = new TabModule(window2); + + let counter1 = 0, counter2 = 0; + let counterTabs = 0; + + function onOpenListener() { + ++counterTabs; + if (counterTabs < 5) + return; + test.assertEqual(counter1, 2, "Correct number of events fired from window 1"); + test.assertEqual(counter2, 3, "Correct number of events fired from window 2"); + test.assertEqual(counterTabs, 5, "Correct number of events fired from all windows"); + test.assertEqual(tm1.length, 3, "Correct number of tabs in window 1"); + test.assertEqual(tm2.length, 4, "Correct number of tabs in window 2"); + closeTwoWindows(window1, window2, function() test.done()); + } + + tm1.onOpen = function() ++counter1 && onOpenListener(); + tm2.onOpen = function() ++counter2 && onOpenListener(); + + let url = "data:text/html,default"; + tm1.open(url); + tm1.open(url); + + tm2.open(url); + tm2.open(url); + tm2.open(url); + }); +} + +exports.testTabModuleActiveTab_getterAndSetter = function(test) { + test.waitUntilDone(); + let TabModule = require("tab-browser").TabModule; + + openTwoWindows(function(window1, window2) { + let tm1 = new TabModule(window1); + let tm2 = new TabModule(window2); + + let tab1 = null; + tm1.open({ + url: "data:text/html,<title>window1,tab1</title>", + onOpen: function(tab) tab1 = tab, + }); + tm1.open("data:text/html,<title>window1,tab2</title>"); + + tm1.onActivate = function onActivate() { + tm1.onActivate.remove(onActivate); + require("timer").setTimeout(function() { + test.assertEqual(tm1.activeTab.title, "window1,tab1", "activeTab setter works"); + closeTwoWindows(window1, window2, function() test.done()); + }, 1000); + } + + tm2.open("data:text/html,<title>window2,tab1</title>"); + tm2.open({ + url: "data:text/html,<title>window2,tab2</title>", + onOpen: function(tab4) { + test.assertEqual(tm1.activeTab.title, "window1,tab2", "Correct active tab on window 1"); + test.assertEqual(tm2.activeTab.title, "window2,tab2", "Correct active tab on window 2"); + + tm1.activeTab = tab1; + tm1.activeTab = tab4; // Setting activeTab from another window should have no effect + } + }); + }); +} + +// test tabs iterator +exports.testTabModuleTabsIterator = function(test) { + test.waitUntilDone(); + let TabModule = require("tab-browser").TabModule; + + openBrowserWindow(function(window) { + let tm1 = new TabModule(window); + let url = "data:text/html,default"; + tm1.open(url); + tm1.open(url); + tm1.open({ + url: url, + onOpen: function(tab) { + let count = 0; + for each (let t in tm1) count++; + test.assertEqual(count, 4, "iterated tab count matches"); + test.assertEqual(count, tm1.length, "length tab count matches"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// inNewWindow parameter is ignored on single-window modules +exports.testTabModuleCantOpenInNewWindow = function(test) { + test.waitUntilDone(); + let TabModule = require("tab-browser").TabModule; + + openBrowserWindow(function(window) { + let tm = new TabModule(window); + let url = "data:text/html,default"; + tm.open({ + url: url, + inNewWindow: true, + onOpen: function() { + test.assertEqual(tm.length, 2, "Tab was open on same window"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// Test that having two modules attached to the same +// window won't duplicate events fired on each module +exports.testModuleListenersDontInteract = function(test) { + test.waitUntilDone(); + let TabModule = require("tab-browser").TabModule; + + openBrowserWindow(function(window) { + let tm1 = new TabModule(window); + let tm2 = new TabModule(window); + + let url = "data:text/html,foo"; + let eventCount = 0, eventModule1 = 0, eventModule2 = 0; + + + let listener1 = function() { + // this should be called twice: when tab is open and when + // the url location is changed + eventCount++; + eventModule1++; + } + tm1.onReady = listener1; + + tm2.open({ + url: "about:blank", + onOpen: function(tab) { + // add listener via property assignment + let listener2 = function() { + eventCount++; + eventModule2++; + }; + tab.onReady = listener2; + + // add listener via collection add + let listener3 = function() { + eventCount++; + eventModule2++; + }; + tab.onReady.add(listener3); + + tab.location = url; + + test.waitUntilEqual(function () eventCount, 4, + "Correct global number of events") + .then(function () { + test.assertEqual(eventModule1, 2, + "Correct number of events on module 1"); + test.assertEqual(eventModule2, 2, + "Correct number of events on module 2"); + + tm1.onReady.remove(listener1); + tab.onReady.remove(listener2); + tab.onReady.remove(listener3); + closeBrowserWindow(window, function() test.done()); + }); + } + }); + }); +}; + +/******************* helpers *********************/ + +// Helper for getting the active window +function activeWindow() { + return Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); +}; + +// If the module doesn't support the app we're being run in, require() will +// throw. In that case, remove all tests above from exports, and add one dummy +// test that passes. +try { + require("tab-browser"); +} +catch (err) { + // This bug should be mentioned in the error message. + let bug = "https://bugzilla.mozilla.org/show_bug.cgi?id=560716"; + if (err.message.indexOf(bug) < 0) + throw err; + for (let [prop, val] in Iterator(exports)) { + if (/^test/.test(prop) && typeof(val) === "function") + delete exports[prop]; + } + exports.testAppNotSupported = function (test) { + test.pass("the tab-browser module does not support this application."); + }; +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-tab-observer.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-tab-observer.js new file mode 100644 index 0000000..b56b1aa --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-tab-observer.js @@ -0,0 +1,35 @@ +"use strict"; + +const { openTab, closeTab } = require("api-utils/tabs/utils"); +const { Loader } = require("./helpers"); +const { setTimeout } = require("timer"); + +exports["test unload tab observer"] = function(assert, done) { + let loader = Loader(module); + + let window = loader.require("api-utils/window-utils").activeBrowserWindow; + let observer = loader.require("api-utils/tabs/observer").observer; + let opened = 0; + let closed = 0; + + observer.on("open", function onOpen(window) { opened++; }); + observer.on("close", function onClose(window) { closed++; }); + + // Open and close tab to trigger observers. + closeTab(openTab(window, "data:text/html,tab-1")); + + // Unload the module so that all listeners set by observer are removed. + loader.unload(); + + // Open and close tab once again. + closeTab(openTab(window, "data:text/html,tab-2")); + + // Enqueuing asserts to make sure that assertion is not performed early. + setTimeout(function () { + assert.equal(1, opened, "observer open was called before unload only"); + assert.equal(1, closed, "observer close was called before unload only"); + done(); + }, 0); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-tab.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-tab.js new file mode 100644 index 0000000..4015faa --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-tab.js @@ -0,0 +1,106 @@ +const tabAPI = require("tabs/tab"); +const tabs = require("tabs"); // From addon-kit +const windowUtils = require("window-utils"); + +// The primary test tab +var primaryTab; + +// We have an auxiliary tab to test background tabs. +var auxTab; + +// The window for the outer iframe in the primary test page +var iframeWin; + +exports.testGetTabForWindow = function(test) { + test.waitUntilDone(); + + test.assertEqual(tabAPI.getTabForWindow(windowUtils.activeWindow), null, + "getTabForWindow return null on topwindow"); + test.assertEqual(tabAPI.getTabForWindow(windowUtils.activeBrowserWindow), null, + "getTabForWindow return null on topwindow"); + + let subSubDocument = encodeURIComponent( + 'Sub iframe<br/>'+ + '<iframe id="sub-sub-iframe" src="data:text/html,SubSubIframe" />'); + let subDocument = encodeURIComponent( + 'Iframe<br/>'+ + '<iframe id="sub-iframe" src="data:text/html,'+subSubDocument+'" />'); + let url = 'data:text/html,' + encodeURIComponent( + 'Content<br/><iframe id="iframe" src="data:text/html,'+subDocument+'" />'); + + // Open up a new tab in the background. + // + // This lets us test whether GetTabForWindow works even when the tab in + // question is not active. + tabs.open({ + inBackground: true, + url: "about:mozilla", + onReady: function(tab) { auxTab = tab; step2(url, test);}, + onActivate: function(tab) { step3(test); } + }); +} + +function step2(url, test) { + + tabs.open({ + url: url, + onReady: function(tab) { + primaryTab = tab; + let window = windowUtils.activeBrowserWindow.content; + + let matchedTab = tabAPI.getTabForWindow(window); + test.assertEqual(matchedTab, tab, + "We are able to find the tab with his content window object"); + + let timer = require("timer"); + function waitForFrames() { + let iframe = window.document.getElementById("iframe"); + if (!iframe) { + timer.setTimeout(waitForFrames, 100); + return; + } + iframeWin = iframe.contentWindow; + let subIframe = iframeWin.document.getElementById("sub-iframe"); + if (!subIframe) { + timer.setTimeout(waitForFrames, 100); + return; + } + let subIframeWin = subIframe.contentWindow; + let subSubIframe = subIframeWin.document.getElementById("sub-sub-iframe"); + if (!subSubIframe) { + timer.setTimeout(waitForFrames, 100); + return; + } + let subSubIframeWin = subSubIframe.contentWindow; + + matchedTab = tabAPI.getTabForWindow(iframeWin); + test.assertEqual(matchedTab, tab, + "We are able to find the tab with an iframe window object"); + + matchedTab = tabAPI.getTabForWindow(subIframeWin); + test.assertEqual(matchedTab, tab, + "We are able to find the tab with a sub-iframe window object"); + + matchedTab = tabAPI.getTabForWindow(subSubIframeWin); + test.assertEqual(matchedTab, tab, + "We are able to find the tab with a sub-sub-iframe window object"); + + // Put our primary tab in the background and test again. + // The onActivate listener will take us to step3. + auxTab.activate(); + } + waitForFrames(); + } + }); +} + +function step3(test) { + + let matchedTab = tabAPI.getTabForWindow(iframeWin); + test.assertEqual(matchedTab, primaryTab, + "We get the correct tab even when it's in the background"); + + primaryTab.close(function () { + auxTab.close(function () { test.done();}); + }); +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-text-streams.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-text-streams.js new file mode 100644 index 0000000..2c1c80d --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-text-streams.js @@ -0,0 +1,190 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Drew Willcoxon <adw@mozilla.com> (Original Author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const file = require("file"); +const url = require("url"); +const { Loader } = require("./helpers"); + +const STREAM_CLOSED_ERROR = "The stream is closed and cannot be used."; + +// This should match the constant of the same name in text-streams.js. +const BUFFER_BYTE_LEN = 0x8000; + +exports.testWriteRead = function (test) { + let fname = dataFileFilename(); + + // Write a small string less than the stream's buffer size... + let str = "exports.testWriteRead data!"; + let stream = file.open(fname, "w"); + test.assert(!stream.closed, "stream.closed after open should be false"); + stream.write(str); + stream.close(); + test.assert(stream.closed, "stream.closed after close should be true"); + test.assertRaises(function () stream.close(), + STREAM_CLOSED_ERROR, + "stream.close after already closed should raise error"); + test.assertRaises(function () stream.write("This shouldn't be written!"), + STREAM_CLOSED_ERROR, + "stream.write after close should raise error"); + + // ... and read it. + stream = file.open(fname); + test.assert(!stream.closed, "stream.closed after open should be false"); + test.assertEqual(stream.read(), str, + "stream.read should return string written"); + test.assertEqual(stream.read(), "", + "stream.read at EOS should return empty string"); + stream.close(); + test.assert(stream.closed, "stream.closed after close should be true"); + test.assertRaises(function () stream.close(), + STREAM_CLOSED_ERROR, + "stream.close after already closed should raise error"); + test.assertRaises(function () stream.read(), + STREAM_CLOSED_ERROR, + "stream.read after close should raise error"); + + // Write a big string many times the size of the stream's buffer and read it. + // Since it comes after the previous test, this also ensures that the file is + // truncated when it's opened for writing. + str = ""; + let bufLen = BUFFER_BYTE_LEN; + let fileSize = bufLen * 10; + for (let i = 0; i < fileSize; i++) + str += i % 10; + stream = file.open(fname, "w"); + stream.write(str); + stream.close(); + stream = file.open(fname); + test.assertEqual(stream.read(), str, + "stream.read should return string written"); + stream.close(); + + // The same, but write and read in chunks. + stream = file.open(fname, "w"); + let i = 0; + while (i < str.length) { + // Use a chunk length that spans buffers. + let chunk = str.substr(i, bufLen + 1); + stream.write(chunk); + i += bufLen + 1; + } + stream.close(); + stream = file.open(fname); + let readStr = ""; + bufLen = BUFFER_BYTE_LEN; + let readLen = bufLen + 1; + do { + var frag = stream.read(readLen); + readStr += frag; + } while (frag); + stream.close(); + test.assertEqual(readStr, str, + "stream.write and read in chunks should work as expected"); + + // Read the same file, passing in strange numbers of bytes to read. + stream = file.open(fname); + test.assertEqual(stream.read(fileSize * 100), str, + "stream.read with big byte length should return string " + + "written"); + stream.close(); + + stream = file.open(fname); + test.assertEqual(stream.read(0), "", + "string.read with zero byte length should return empty " + + "string"); + stream.close(); + + stream = file.open(fname); + test.assertEqual(stream.read(-1), "", + "string.read with negative byte length should return " + + "empty string"); + stream.close(); + + file.remove(fname); +}; + +exports.testWriteAsync = function (test) { + test.waitUntilDone(); + + let fname = dataFileFilename(); + let str = "exports.testWriteAsync data!"; + let stream = file.open(fname, "w"); + test.assert(!stream.closed, "stream.closed after open should be false"); + + // Write. + stream.writeAsync(str, function (err) { + test.assertEqual(this, stream, "|this| should be the stream object"); + test.assertEqual(err, undefined, + "stream.writeAsync should not cause error"); + test.assert(stream.closed, "stream.closed after write should be true"); + test.assertRaises(function () stream.close(), + STREAM_CLOSED_ERROR, + "stream.close after already closed should raise error"); + test.assertRaises(function () stream.writeAsync("This shouldn't work!"), + STREAM_CLOSED_ERROR, + "stream.writeAsync after close should raise error"); + + // Read. + stream = file.open(fname, "r"); + test.assert(!stream.closed, "stream.closed after open should be false"); + let readStr = stream.read(); + test.assertEqual(readStr, str, + "string.read should yield string written"); + stream.close(); + file.remove(fname); + test.done(); + }); +}; + +exports.testUnload = function (test) { + let loader = Loader(module); + let file = loader.require("file"); + + let filename = url.toFilename(module.uri); + let stream = file.open(filename); + + loader.unload(); + test.assert(stream.closed, "stream should be closed after module unload"); +}; + +// Returns the name of a file that should be used to test writing and reading. +function dataFileFilename() { + let dir = file.dirname(url.toFilename(module.uri)); + return file.join(dir, "test-text-streams-data"); +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-timer.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-timer.js new file mode 100644 index 0000000..7c995e2 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-timer.js @@ -0,0 +1,127 @@ +var timer = require("timer"); +const { Loader } = require("./helpers"); + +exports.testSetTimeout = function(test) { + timer.setTimeout(function() { + test.pass("testSetTimeout passed"); + test.done(); + }, 1); + test.waitUntilDone(); +}; + +exports.testParamedSetTimeout = function(test) { + let params = [1, 'foo', { bar: 'test' }, null, undefined]; + timer.setTimeout.apply(null, [function() { + test.assertEqual(arguments.length, params.length); + for (let i = 0, ii = params.length; i < ii; i++) + test.assertEqual(params[i], arguments[i]); + test.done(); + }, 1].concat(params)); + test.waitUntilDone(); +}; + +exports.testClearTimeout = function(test) { + var myFunc = function myFunc() { + test.fail("myFunc() should not be called in testClearTimeout"); + }; + var id = timer.setTimeout(myFunc, 1); + timer.setTimeout(function() { + test.pass("testClearTimeout passed"); + test.done(); + }, 2); + timer.clearTimeout(id); + test.waitUntilDone(); +}; + +exports.testParamedClearTimeout = function(test) { + let params = [1, 'foo', { bar: 'test' }, null, undefined]; + var myFunc = function myFunc() { + test.fail("myFunc() should not be called in testClearTimeout"); + }; + var id = timer.setTimeout(myFunc, 1); + timer.setTimeout.apply(null, [function() { + test.assertEqual(arguments.length, params.length); + for (let i = 0, ii = params.length; i < ii; i++) + test.assertEqual(params[i], arguments[i]); + test.done(); + }, 1].concat(params)); + timer.clearTimeout(id); + test.waitUntilDone(); +}; + +exports.testSetInterval = function (test) { + var count = 0; + var id = timer.setInterval(function () { + count++; + if (count >= 5) { + timer.clearInterval(id); + test.pass("testSetInterval passed"); + test.done(); + } + }, 1); + test.waitUntilDone(); +}; + +exports.testParamedSetInerval = function(test) { + let params = [1, 'foo', { bar: 'test' }, null, undefined]; + let count = 0; + let id = timer.setInterval.apply(null, [function() { + count ++; + if (count < 5) { + test.assertEqual(arguments.length, params.length); + for (let i = 0, ii = params.length; i < ii; i++) + test.assertEqual(params[i], arguments[i]); + } else { + timer.clearInterval(id); + test.done(); + } + }, 1].concat(params)); + test.waitUntilDone(); +}; + +exports.testClearInterval = function (test) { + timer.clearInterval(timer.setInterval(function () { + test.fail("setInterval callback should not be called"); + }, 1)); + var id = timer.setInterval(function () { + timer.clearInterval(id); + test.pass("testClearInterval passed"); + test.done(); + }, 2); + test.waitUntilDone(); +}; + +exports.testParamedClearInterval = function(test) { + timer.clearInterval(timer.setInterval(function () { + test.fail("setInterval callback should not be called"); + }, 1, timer, {}, null)); + + let id = timer.setInterval(function() { + timer.clearInterval(id); + test.assertEqual(3, arguments.length); + test.done(); + }, 2, undefined, 'test', {}); + test.waitUntilDone(); +}; + + +exports.testUnload = function(test) { + var loader = Loader(module); + var sbtimer = loader.require("timer"); + + var myFunc = function myFunc() { + test.fail("myFunc() should not be called in testUnload"); + }; + + sbtimer.setTimeout(myFunc, 1); + sbtimer.setTimeout(myFunc, 1, 'foo', 4, {}, undefined); + sbtimer.setInterval(myFunc, 1); + sbtimer.setInterval(myFunc, 1, {}, null, 'bar', undefined, 87); + loader.unload(); + timer.setTimeout(function() { + test.pass("timer testUnload passed"); + test.done(); + }, 2); + test.waitUntilDone(); +}; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-traceback.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-traceback.js new file mode 100644 index 0000000..6cf50f0 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-traceback.js @@ -0,0 +1,114 @@ +var traceback = require("traceback"); +var {Cc,Ci,Cr,Cu} = require("chrome"); + +function throwNsIException() { + var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + ios.newURI("i'm a malformed URI", null, null); +} + +function throwError() { + throw new Error("foob"); +} + +exports.testFormatDoesNotFetchRemoteFiles = function(test) { + var observers = require("observer-service"); + ["http", "https"].forEach( + function(scheme) { + var httpRequests = 0; + function onHttp() { + httpRequests++; + } + + observers.add("http-on-modify-request", onHttp); + + try { + var tb = [{filename: scheme + "://www.mozilla.org/", + lineNo: 1, + funcName: "blah"}]; + traceback.format(tb); + } catch (e) { + test.exception(e); + } + + observers.remove("http-on-modify-request", onHttp); + + test.assertEqual(httpRequests, 0, + "traceback.format() does not make " + + scheme + " request"); + }); +}; + +exports.testFromExceptionWithString = function(test) { + try { + throw "foob"; + test.fail("an exception should've been thrown"); + } catch (e if e == "foob") { + var tb = traceback.fromException(e); + test.assertEqual(tb.length, 0); + } +}; + +exports.testFormatWithString = function(test) { + // This can happen if e.g. a thrown exception was + // a string instead of an Error instance. + test.assertEqual(traceback.format("blah"), + "Traceback (most recent call last):"); +}; + +exports.testFromExceptionWithError = function(test) { + try { + throwError(); + test.fail("an exception should've been thrown"); + } catch (e if e instanceof Error) { + var tb = traceback.fromException(e); + var xulApp = require("xul-app"); + test.assertEqual(tb.slice(-1)[0].funcName, "throwError"); + } +}; + +exports.testFromExceptionWithNsIException = function(test) { + try { + throwNsIException(); + test.fail("an exception should've been thrown"); + } catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) { + var tb = traceback.fromException(e); + test.assertEqual(tb.slice(-1)[0].funcName, + "throwNsIException"); + } +}; + +exports.testFormat = function(test) { + function getTraceback() { + return traceback.format(); + } + + var formatted = getTraceback(); + test.assertEqual(typeof(formatted), "string"); + var lines = formatted.split("\n"); + test.assertEqual(lines.slice(-2)[0].indexOf("getTraceback") > 0, + true, + "formatted traceback should include function name"); + test.assertEqual(lines.slice(-1)[0].trim(), + "return traceback.format();", + "formatted traceback should include source code"); +}; + +exports.testExceptionsWithEmptyStacksAreLogged = function(test) { + // Ensures that our fix to bug 550368 works. + var sandbox = Cu.Sandbox("http://www.foo.com"); + var excRaised = false; + try { + Cu.evalInSandbox("returns 1 + 2;", sandbox, "1.8", + "blah.js", 25); + } catch (e) { + excRaised = true; + var stack = traceback.fromException(e); + test.assertEqual(stack.length, 1, "stack should have one frame"); + test.assert(stack[0].filename, "blah.js", "frame should have filename"); + test.assert(stack[0].lineNo, 25, "frame should have line no"); + test.assertEqual(stack[0].funcName, null, "frame should have null function name"); + } + if (!excRaised) + test.fail("Exception should have been raised."); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-traits-core.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-traits-core.js new file mode 100644 index 0000000..ecc2c51 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-traits-core.js @@ -0,0 +1,834 @@ +"use strict"; + +const ERR_CONFLICT = 'Remaining conflicting property: ', + ERR_REQUIRED = 'Missing required property: '; + +function assertSametrait(test, trait1, trait2) { + let names1 = Object.getOwnPropertyNames(trait1), + names2 = Object.getOwnPropertyNames(trait2); + + test.assertEqual( + names1.length, + names2.length, + 'equal traits must have same amount of properties' + ); + + for (let i = 0; i < names1.length; i++) { + let name = names1[i]; + test.assertNotEqual( + -1, + names2.indexOf(name), + 'equal traits must contain same named properties: ' + name + ); + assertSameDescriptor(test, name, trait1[name], trait2[name]); + } +} + +function assertSameDescriptor(test, name, desc1, desc2) { + if (desc1.conflict || desc2.conflict) { + test.assertEqual( + desc1.conflict, + desc2.conflict, + 'if one of same descriptors has `conflict` another must have it: ' + + name + ); + } else if (desc1.required || desc2.required) { + test.assertEqual( + desc1.required, + desc2.required, + 'if one of same descriptors is has `required` another must have it: ' + + name + ); + } else { + test.assertEqual( + desc1.get, + desc2.get, + 'get must be the same on both descriptors: ' + name + ); + test.assertEqual( + desc1.set, + desc2.set, + 'set must be the same on both descriptors: ' + name + ); + test.assertEqual( + desc1.value, + desc2.value, + 'value must be the same on both descriptors: ' + name + ); + test.assertEqual( + desc1.enumerable, + desc2.enumerable, + 'enumerable must be the same on both descriptors: ' + name + ); + test.assertEqual( + desc1.required, + desc2.required, + 'value must be the same on both descriptors: ' + name + ); + } +} + +function Data(value, enumerable, confligurable, writable) { + return { + value: value, + enumerable: false !== enumerable, + confligurable: false !== confligurable, + writable: false !== writable + }; +} + +function Method(method, enumerable, confligurable, writable) { + return { + value: method, + enumerable: false !== enumerable, + confligurable: false !== confligurable, + writable: false !== writable + }; +} + +function Accessor(get, set, enumerable, confligurable) { + return { + get: get, + set: set, + enumerable: false !== enumerable, + confligurable: false !== confligurable, + }; +} + +function Required(name) { + function required() { throw new Error(ERR_REQUIRED + name) } + return { + get: required, + set: required, + required: true + }; +} + +function Conflict(name) { + function conflict() { throw new Error(ERR_CONFLICT + name) } + return { + get: conflict, + set: conflict, + conflict: true + }; +} + +function testMethod() {}; + +const { trait, compose, resolve, required, override, create } = + require('traits/core'); + + +exports['test:empty trait'] = function(test) { + assertSametrait( + test, + trait({}), + {} + ); +}; + +exports['test:simple trait'] = function(test) { + assertSametrait( + test, + trait({ + a: 0, + b: testMethod + }), + { + a: Data(0, true, true, true), + b: Method(testMethod, true, true, true) + } + ); +}; + +exports['test:simple trait with required prop'] = function(test) { + assertSametrait( + test, + trait({ + a: required, + b: 1 + }), + { + a: Required('a'), + b: Data(1) + } + ); +}; + +exports['test:ordering of trait properties is irrelevant'] = function(test) { + assertSametrait(test, + trait({ a: 0, b: 1, c: required }), + trait({ b: 1, c: required, a: 0 }) + ); +}; + +exports['test:trait with accessor property'] = function(test) { + let record = { get a() {}, set a(v) {} }; + let get = Object.getOwnPropertyDescriptor(record,'a').get; + let set = Object.getOwnPropertyDescriptor(record,'a').set; + assertSametrait(test, + trait(record), + { a: Accessor(get, set ) } + ); +}; + +exports['test:simple composition'] = function(test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1 }), + trait({ c: 2, d: testMethod }) + ), + { + a: Data(0), + b: Data(1), + c: Data(2), + d: Method(testMethod) + } + ); +}; + +exports['test:composition with conflict'] = function(test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1 }), + trait({ a: 2, c: testMethod }) + ), + { + a: Conflict('a'), + b: Data(1), + c: Method(testMethod) + } + ); +}; + +exports['test:composition of identical props does not cause conflict'] = +function(test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1 }), + trait({ a: 0, c: testMethod }) + ), + { + a: Data(0), + b: Data(1), + c: Method(testMethod) } + ) +}; + +exports['test:composition with identical required props'] = +function(test) { + assertSametrait(test, + compose( + trait({ a: required, b: 1 }), + trait({ a: required, c: testMethod }) + ), + { + a: Required(), + b: Data(1), + c: Method(testMethod) + } + ); +}; + +exports['test:composition satisfying a required prop'] = function (test) { + assertSametrait(test, + compose( + trait({ a: required, b: 1 }), + trait({ a: testMethod }) + ), + { + a: Method(testMethod), + b: Data(1) + } + ); +}; + +exports['test:compose is neutral wrt conflicts'] = function (test) { + assertSametrait(test, + compose( + compose( + trait({ a: 1 }), + trait({ a: 2 }) + ), + trait({ b: 0 }) + ), + { + a: Conflict('a'), + b: Data(0) + } + ); +}; + +exports['test:conflicting prop overrides required prop'] = function (test) { + assertSametrait(test, + compose( + compose( + trait({ a: 1 }), + trait({ a: 2 }) + ), + trait({ a: required }) + ), + { + a: Conflict('a') + } + ); +}; + +exports['test:compose is commutative'] = function (test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1 }), + trait({ c: 2, d: testMethod }) + ), + compose( + trait({ c: 2, d: testMethod }), + trait({ a: 0, b: 1 }) + ) + ); +}; + +exports['test:compose is commutative, also for required/conflicting props'] = +function (test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1, c: 3, e: required }), + trait({ c: 2, d: testMethod }) + ), + compose( + trait({ c: 2, d: testMethod }), + trait({ a: 0, b: 1, c: 3, e: required }) + ) + ); +}; +exports['test:compose is associative'] = function (test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1, c: 3, d: required }), + compose( + trait({ c: 3, d: required }), + trait({ c: 2, d: testMethod, e: 'foo' }) + ) + ), + compose( + compose( + trait({ a: 0, b: 1, c: 3, d: required }), + trait({ c: 3, d: required }) + ), + trait({ c: 2, d: testMethod, e: 'foo' }) + ) + ); +}; + +exports['test:diamond import of same prop does not generate conflict'] = +function (test) { + assertSametrait(test, + compose( + compose( + trait({ b: 2 }), + trait({ a: 1 }) + ), + compose( + trait({ c: 3 }), + trait({ a: 1 }) + ), + trait({ d: 4 }) + ), + { + a: Data(1), + b: Data(2), + c: Data(3), + d: Data(4) + } + ); +}; + +exports['test:resolve with empty resolutions has no effect'] = +function (test) { + assertSametrait(test, resolve({}, trait({ + a: 1, + b: required, + c: testMethod + })), { + a: Data(1), + b: Required(), + c: Method(testMethod) + }); +}; + +exports['test:resolve: renaming'] = function (test) { + assertSametrait(test, + resolve( + { a: 'A', c: 'C' }, + trait({ a: 1, b: required, c: testMethod }) + ), + { + A: Data(1), + b: Required(), + C: Method(testMethod), + a: Required(), + c: Required() + } + ); +}; + +exports['test:resolve: renaming to conflicting name causes conflict, order 1'] += function (test) { + assertSametrait(test, + resolve( + { a: 'b'}, + trait({ a: 1, b: 2 }) + ), + { + b: Conflict('b'), + a: Required() + } + ); +}; + +exports['test:resolve: renaming to conflicting name causes conflict, order 2'] += function (test) { + assertSametrait(test, + resolve( + { a: 'b' }, + trait({ b: 2, a: 1 }) + ), + { + b: Conflict('b'), + a: Required() + } + ); +}; + +exports['test:resolve: simple exclusion'] = function (test) { + assertSametrait(test, + resolve( + { a: undefined }, + trait({ a: 1, b: 2 }) + ), + { + a: Required(), + b: Data(2) + } + ); +}; + +exports['test:resolve: exclusion to "empty" trait'] = function (test) { + assertSametrait(test, + resolve( + { a: undefined, b: undefined }, + trait({ a: 1, b: 2 }) + ), + { + a: Required(), + b: Required() + } + ); +}; + +exports['test:resolve: exclusion and renaming of disjoint props'] = +function (test) { + assertSametrait(test, + resolve( + { a: undefined, b: 'c' }, + trait({ a: 1, b: 2 }) + ), + { + a: Required(), + c: Data(2), + b: Required() + } + ); +}; + +exports['test:resolve: exclusion and renaming of overlapping props'] = +function (test) { + assertSametrait(test, + resolve( + { a: undefined, b: 'a' }, + trait({ a: 1, b: 2 }) + ), + { + a: Data(2), + b: Required() + } + ); +}; + +exports['test:resolve: renaming to a common alias causes conflict'] = +function (test) { + assertSametrait(test, + resolve( + { a: 'c', b: 'c' }, + trait({ a: 1, b: 2 }) + ), + { + c: Conflict('c'), + a: Required(), + b: Required() + } + ); +}; + +exports['test:resolve: renaming overrides required target'] = +function (test) { + assertSametrait(test, + resolve( + { b: 'a' }, + trait({ a: required, b: 2 }) + ), + { + a: Data(2), + b: Required() + } + ); +}; + +exports['test:resolve: renaming required properties has no effect'] = +function (test) { + assertSametrait(test, + resolve( + { b: 'a' }, + trait({ a: 2, b: required }) + ), + { + a: Data(2), + b: Required() + } + ); +}; + +exports['test:resolve: renaming of non-existent props has no effect'] = +function (test) { + assertSametrait(test, + resolve( + { a: 'c', d: 'c' }, + trait({ a: 1, b: 2 }) + ), + { + c: Data(1), + b: Data(2), + a: Required() + } + ); +}; + +exports['test:resolve: exclusion of non-existent props has no effect'] = +function (test) { + assertSametrait(test, + resolve( + { b: undefined }, + trait({ a: 1 }) + ), + { + a: Data(1) + } + ); +}; + +exports['test:resolve is neutral w.r.t. required properties'] = +function (test) { + assertSametrait(test, + resolve( + { a: 'c', b: undefined }, + trait({ a: required, b: required, c: 'foo', d: 1 }) + ), + { + a: Required(), + b: Required(), + c: Data('foo'), + d: Data(1) + } + ); +}; + +exports['test:resolve supports swapping of property names, ordering 1'] = +function (test) { + assertSametrait(test, + resolve( + { a: 'b', b: 'a' }, + trait({ a: 1, b: 2 }) + ), + { + a: Data(2), + b: Data(1) + } + ); +}; + +exports['test:resolve supports swapping of property names, ordering 2'] = +function (test) { + assertSametrait(test, + resolve( + { b: 'a', a: 'b' }, + trait({ a: 1, b: 2 }) + ), + { + a: Data(2), + b: Data(1) + } + ); +}; + +exports['test:resolve supports swapping of property names, ordering 3'] = +function (test) { + assertSametrait(test, + resolve( + { b: 'a', a: 'b' }, + trait({ b: 2, a: 1 }) + ), + { + a: Data(2), + b: Data(1) + } + ); +}; + +exports['test:resolve supports swapping of property names, ordering 4'] = +function (test) { + assertSametrait(test, + resolve( + { a: 'b', b: 'a' }, + trait({ b: 2, a: 1 }) + ), + { + a: Data(2), + b: Data(1) + } + ); +}; + +exports['test:override of mutually exclusive traits'] = function (test) { + assertSametrait(test, + override( + trait({ a: 1, b: 2 }), + trait({ c: 3, d: testMethod }) + ), + { + a: Data(1), + b: Data(2), + c: Data(3), + d: Method(testMethod) + } + ); +}; + +exports['test:override of mutually exclusive traits is compose'] = +function (test) { + assertSametrait(test, + override( + trait({ a: 1, b: 2 }), + trait({ c: 3, d: testMethod }) + ), + compose( + trait({ d: testMethod, c: 3 }), + trait({ b: 2, a: 1 }) + ) + ); +}; + +exports['test:override of overlapping traits'] = function (test) { + assertSametrait(test, + override( + trait({ a: 1, b: 2 }), + trait({ a: 3, c: testMethod }) + ), + { + a: Data(1), + b: Data(2), + c: Method(testMethod) + } + ); +}; + +exports['test:three-way override of overlapping traits'] = function (test) { + assertSametrait(test, + override( + trait({ a: 1, b: 2 }), + trait({ b: 4, c: 3 }), + trait({ a: 3, c: testMethod, d: 5 }) + ), + { + a: Data(1), + b: Data(2), + c: Data(3), + d: Data(5) + } + ); +}; + +exports['test:override replaces required properties'] = function (test) { + assertSametrait(test, + override( + trait({ a: required, b: 2 }), + trait({ a: 1, c: testMethod }) + ), + { + a: Data(1), + b: Data(2), + c: Method(testMethod) + } + ); +}; + +exports['test:override is not commutative'] = function (test) { + assertSametrait(test, + override( + trait({ a: 1, b: 2 }), + trait({ a: 3, c: 4 }) + ), + { + a: Data(1), + b: Data(2), + c: Data(4) + } + ); + + assertSametrait(test, + override( + trait({ a: 3, c: 4 }), + trait({ a: 1, b: 2 }) + ), + { + a: Data(3), + b: Data(2), + c: Data(4) + } + ); +}; + +exports['test:override is associative'] = function (test) { + assertSametrait(test, + override( + override( + trait({ a: 1, b: 2 }), + trait({ a: 3, c: 4, d: 5 }) + ), + trait({ a: 6, c: 7, e: 8 }) + ), + override( + trait({ a: 1, b: 2 }), + override( + trait({ a: 3, c: 4, d: 5 }), + trait({ a: 6, c: 7, e: 8 }) + ) + ) + ); +}; + +exports['test:create simple'] = function(test) { + let o1 = create( + Object.prototype, + trait({ a: 1, b: function() { return this.a; } }) + ); + + test.assertEqual( + Object.prototype, + Object.getPrototypeOf(o1), + 'o1 prototype' + ); + test.assertEqual(1, o1.a, 'o1.a'); + test.assertEqual(1, o1.b(), 'o1.b()'); + test.assertEqual( + 2, + Object.getOwnPropertyNames(o1).length, + 'Object.keys(o1).length === 2' + ); +}; + +exports['test:create with Array.prototype'] = function(test) { + let o2 = create(Array.prototype, trait({})); + test.assertEqual( + Array.prototype, + Object.getPrototypeOf(o2), + "o2 prototype" + ); +}; + +exports['test:exception for incomplete required properties'] = +function(test) { + try { + create(Object.prototype, trait({ foo: required })); + test.fail('expected create to complain about missing required props'); + } catch(e) { + test.assertEqual( + 'Error: Missing required property: foo', + e.toString(), + 'required prop error' + ); + } +}; + +exports['test:exception for unresolved conflicts'] = function(test) { + try { + create({}, compose(trait({ a: 0 }), trait({ a: 1 }))); + test.fail('expected create to complain about unresolved conflicts'); + } catch(e) { + test.assertEqual( + 'Error: Remaining conflicting property: a', + e.toString(), + 'conflicting prop error' + ); + } +}; + +exports['test:verify that required properties are present but undefined'] = +function(test) { + try { + let o4 = Object.create(Object.prototype, trait({ foo: required })); + test.assertEqual(true, 'foo' in o4, 'required property present'); + try { + let foo = o4.foo; + test.fail('access to required property must throw'); + } catch(e) { + test.assertEqual( + 'Error: Missing required property: foo', + e.toString(), + 'required prop error' + ) + } + } catch(e) { + test.fail('did not expect create to complain about required props'); + } +}; + +exports['test:verify that conflicting properties are present'] = +function(test) { + try { + let o5 = Object.create( + Object.prototype, + compose(trait({ a: 0 }), trait({ a: 1 })) + ); + test.assertEqual(true, 'a' in o5, 'conflicting property present'); + try { + let a = o5.a; // accessors or data prop + test.fail('expected conflicting prop to cause exception'); + } catch (e) { + test.assertEqual( + 'Error: Remaining conflicting property: a', + e.toString(), + 'conflicting prop access error' + ); + } + } catch(e) { + test.fail('did not expect create to complain about conflicting props'); + } +}; + +exports['test diamond with conflicts'] = function(test) { + function makeT1(x) trait({ m: function() { return x; } }) + function makeT2(x) compose(trait({ t2: 'foo' }), makeT1(x)) + function makeT3(x) compose(trait({ t3: 'bar' }), makeT1(x)) + + let T4 = compose(makeT2(5), makeT3(5)); + try { + let o = create(Object.prototype, T4); + test.fail('expected diamond prop to cause exception'); + } catch(e) { + test.assertEqual( + 'Error: Remaining conflicting property: m', + e.toString(), + 'diamond prop conflict' + ); + } +}; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-traits.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-traits.js new file mode 100644 index 0000000..6940616 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-traits.js @@ -0,0 +1,394 @@ +"use strict"; + +const { Trait } = require('traits'); + +exports['test:simple compose'] = function(test) { + let List = Trait.compose({ + _list: null, + constructor: function List() { + this._list = []; + }, + list: function list() this._list.slice(0), + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + + test.assertNotEqual(undefined, List, 'should not be undefined'); + test.assertEqual('function', typeof List, 'type should be function'); + test.assertEqual( + Trait.compose, + List.compose, + 'should inherit static compose' + ); + test.assertEqual( + Trait.override, + List.override, + 'should inherit static override' + ); + test.assertEqual( + Trait.required, + List.required, + 'should inherit static required' + ); + test.assertEqual( + Trait.resolve, + List.resolve, + 'should inherit static resolve' + ); + + test.assert( + !('_list' in List.prototype), + 'should not expose private API' + ); +} +exports['test: compose trait instance and create instance'] = function(test) { + let List = Trait.compose({ + constructor: function List(options) { + this._list = []; + this._public.publicMember = options.publicMember; + }, + _privateMember: true, + get privateMember() this._privateMember, + get list() this._list.slice(0), + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list + let index = list.indexOf(item) + if (0 <= index) list.slice(index, 1) + } + }); + let list = List({ publicMember: true }); + + test.assertEqual('object', typeof list, 'should return an object') + test.assertEqual( + true, + list instanceof List, + 'should be instance of a List' + ); + + test.assertEqual( + undefined, + list._privateMember, + 'instance should not expose private API' + ); + + test.assertEqual( + true, + list.privateMember, + 'privates are accessible by public API' + ); + + list._privateMember = false; + + test.assertEqual( + true, + list.privateMember, + 'property changes on instance must not affect privates' + ); + + test.assert( + !('_list' in list), + 'instance should not expose private members' + ); + + test.assertEqual( + true, + list.publicMember, + 'public members are exposed' + ) + test.assertEqual( + 'function', + typeof list.add, + 'should be function' + ) + test.assertEqual( + 'function', + typeof list.remove, + 'should be function' + ); + + list.add(1); + test.assertEqual( + 1, + list.list[0], + 'exposed public API should be able of modifying privates' + ) +}; + + +exports['test:instances must not be hackable'] = function(test) { + let SECRET = 'There is no secret!', + secret = null; + + let Class = Trait.compose({ + _secret: null, + protect: function(data) this._secret = data + }); + + let i1 = Class(); + i1.protect(SECRET); + + test.assertEqual( + undefined, + (function() this._secret).call(i1), + 'call / apply can\'t access private state' + ); + + let proto = Object.getPrototypeOf(i1); + try { + proto.reveal = function() this._secret; + secret = i1.reveal(); + } catch(e) {} + test.assertNotEqual( + SECRET, + secret, + 'public __proto__ changes should not affect privates' + ); + secret = null; + + let Class2 = Trait.compose({ + _secret: null, + protect: function(data) this._secret = data + }); + let i2 = Class2(); + i2.protect(SECRET); + try { + Object.prototype.reveal = function() this._secret; + secret = i2.reveal(); + } catch(e) {} + test.assertNotEqual( + SECRET, + secret, + 'Object.prototype changes must not affect instances' + ); +} + +exports['test:instanceof'] = function(test) { + const List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = [] + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + + test.assert(List() instanceof List, 'Must be instance of List'); + test.assert(new List() instanceof List, 'Must be instance of List'); +}; + +exports['test:privates are unaccessible'] = function(test) { + const List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = []; + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + + let list = List(); + test.assert(!('_list' in list), 'no privates on instance'); + test.assert( + !('_list' in List.prototype), + 'no privates on prototype' + ); +}; + +exports['test:public API can access private API'] = function(test) { + const List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = []; + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + let list = List(); + + list.add('test'); + + test.assertEqual( + 1, + list.length, + 'should be able to add element and access it from public getter' + ); +}; + +exports['test:required'] = function(test) { + const Enumerable = Trait.compose({ + list: Trait.required, + forEach: function forEach(consumer) { + return this.list.forEach(consumer); + } + }); + + try { + let i = Enumerable(); + test.fail('should throw when creating instance with required properties'); + } catch(e) { + test.assertEqual( + 'Error: Missing required property: list', + e.toString(), + 'required prop error' + ); + } +}; + +exports['test:compose with required'] = function(test) { + const List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = []; + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + + const Enumerable = Trait.compose({ + list: Trait.required, + forEach: function forEach(consumer) { + return this.list.forEach(consumer); + } + }); + + const EnumerableList = Enumerable.compose({ + get list() this._list.slice(0) + }, List); + + let array = [1,2, 'ab'] + let l = EnumerableList(array); + array.forEach(function(element) l.add(element)); + let number = 0; + l.forEach(function(element, index) { + number ++; + test.assertEqual(array[index], element, 'should mach array element') + }); + test.assertEqual( + array.length, + number, + 'should perform as many asserts as elements in array' + ); +}; + +exports['test:resolve'] = function(test) { + const List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = []; + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + + const Range = List.resolve({ + constructor: null, + add: '_add', + }).compose({ + min: null, + max: null, + get list() this._list.slice(0), + constructor: function Range(min, max) { + this.min = min; + this.max = max; + this._list = []; + }, + add: function(item) { + if (item <= this.max && item >= this.min) + this._add(item) + } + }); + + let r = Range(0, 10); + + test.assertEqual( + 0, + r.min, + 'constructor must have set min' + ); + test.assertEqual( + 10, + r.max, + 'constructor must have set max' + ); + + test.assertEqual( + 0, + r.length, + 'should not contain any elements' + ); + + r.add(5); + + test.assertEqual( + 1, + r.length, + 'should add `5` to list' + ); + + r.add(12); + + test.assertEqual( + 1, + r.length, + 'should not add `12` to list' + ); +}; + +exports['test:custom iterator'] = function(test) { + let Sub = Trait.compose({ + foo: "foo", + bar: "bar", + baz: "baz", + __iterator__: function() { + yield 1; + yield 2; + yield 3; + } + }); + + let (i = 0, sub = Sub()) { + for (let item in sub) + test.assertEqual(++i, item, "iterated item has the right value"); + }; +}; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-type.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-type.js new file mode 100644 index 0000000..072075c --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-type.js @@ -0,0 +1,88 @@ +"use strict" + +var utils = require("type"); + +exports["test function"] = function (assert) { + assert.ok(utils.isFunction(function(){}), "value is function"); + assert.ok(utils.isFunction(Object), "Object is function"); + assert.ok(utils.isFunction(new Function("")), "Genertaed value is function"); + assert.ok(!utils.isFunction({}), "object is not a function"); + assert.ok(!utils.isFunction(4), "number is not a function"); +}; + +exports["test atoms"] = function (assert) { + assert.ok(utils.isPrimitive(2), "number is primitive"); + assert.ok(utils.isPrimitive(NaN), "`NaN` is primitve"); + assert.ok(utils.isPrimitive(undefined), "`undefined` is primitive"); + assert.ok(utils.isPrimitive(null), "`null` is primitive"); + assert.ok(utils.isPrimitive(Infinity), "`Infinity` is primitive"); + assert.ok(utils.isPrimitive("foo"), "strings are primitive"); + assert.ok(utils.isPrimitive(true) && utils.isPrimitive(false), + "booleans are primitive"); +}; + +exports["test object"] = function (assert) { + assert.ok(utils.isObject({}), "`{}` is object"); + assert.ok(!utils.isObject(null), "`null` is not an object"); + assert.ok(!utils.isObject(Object), "functions is not an object"); +}; + +exports["test flat objects"] = function (assert) { + assert.ok(utils.isFlat({}), "`{}` is a flat object"); + assert.ok(!utils.isFlat([]), "`[]` is not a flat object"); + assert.ok(!utils.isFlat(new function() {}), "derived objects are not flat"); + assert.ok(utils.isFlat(Object.prototype), "Object.prototype is flat"); +}; + +exports["test json atoms"] = function (assert) { + assert.ok(utils.isJSON(null), "`null` is JSON"); + assert.ok(utils.isJSON(undefined), "`undefined` is JSON"); + assert.ok(utils.isJSON(NaN), "`NaN` is JSON"); + assert.ok(utils.isJSON(Infinity), "`Infinity` is JSON"); + assert.ok(utils.isJSON(true) && utils.isJSON(false), "booleans are JSON"); + assert.ok(utils.isJSON(4), utils.isJSON(0), "numbers are JSON"); + assert.ok(utils.isJSON("foo bar"), "strings are JSON"); +}; + +exports["test instanceOf"] = function (assert) { + assert.ok(utils.instanceOf(assert, Object), + "assert is object from other sandbox"); + assert.ok(utils.instanceOf(new Date(), Date), "instance of date"); + assert.ok(!utils.instanceOf(null, Object), "null is not an instance"); +}; + +exports["test json"] = function (assert) { + assert.ok(!utils.isJSON(function(){}), "functions are not json"); + assert.ok(utils.isJSON({}), "`{}` is JSON"); + assert.ok(utils.isJSON({ + a: "foo", + b: 3, + c: undefined, + d: null, + e: { + f: { + g: "bar", + p: [{}, "oueou", 56] + }, + q: { nan: NaN, infinity: Infinity }, + "non standard name": "still works" + } + }), "JSON can contain nested objects"); + + var foo = {}; + var bar = { foo: foo }; + foo.bar = bar; + assert.ok(!utils.isJSON(foo), "recursive objects are not json"); + + + assert.ok(!utils.isJSON({ get foo() { return 5 } }), + "json can not have getter"); + + assert.ok(!utils.isJSON({ foo: "bar", baz: function () {} }), + "json can not contain functions"); + + assert.ok(!utils.isJSON(Object.create({})), + "json must be direct descendant of `Object.prototype`"); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-unit-test.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-unit-test.js new file mode 100644 index 0000000..eeef994 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-unit-test.js @@ -0,0 +1,247 @@ +const timer = require("timer"); +const { Loader } = require("./helpers"); + +var setupCalled = false, teardownCalled = false; + +exports.setup = function() { + setupCalled = true; +}; + +exports.teardown = function() { + teardownCalled = true; + setupCalled = false; +}; + +// Important note - unit tests are run in alphabetical order. The following +// unit tests for setup/teardown are order dependent, sometimes the result of +// one test is checked in the next test (testing for teardown does this). When +// tests are cohesively a single unit, they are named <test_name> - partN where +// N is their order in the sequence. Secondly, because these tests should be +// run before all others, they start with an A. +exports.testASetupTeardownSyncTestPart1 = function(test) { + test.assertEqual(true, setupCalled, 'setup function was called before this'); + test.assertEqual(false, teardownCalled, 'teardown function was not called before this'); +}; + +exports.testASetupTeardownSyncTestPart2 = function(test) { + test.assertEqual(true, setupCalled, 'setup was re-called before this'); + test.assertEqual(true, teardownCalled, 'teardown was called after first function'); +}; + +exports.testATeardownAsyncTestPart1 = function(test) { + teardownCalled = false; + + timer.setTimeout(function() { + test.assertEqual(false, teardownCalled, "teardown not called until done"); + test.done(); + }, 200); + test.waitUntilDone(); +}; + +exports.testATeardownAsyncTestPart2 = function(test) { + test.assertEqual(true, teardownCalled, "teardown called after done"); +}; + +exports.testWaitUntilInstant = function(test) { + test.waitUntilDone(); + + test.waitUntil(function () true, "waitUntil with instant true pass") + .then(function () test.done()); +} + +exports.testWaitUntil = function(test) { + test.waitUntilDone(); + let succeed = false; + + test.waitUntil(function () succeed, "waitUntil pass") + .then(function () test.done()); + + timer.setTimeout(function () { + succeed = true; + }, 200); +} + +exports.testWaitUntilEqual = function(test) { + test.waitUntilDone(); + let succeed = false; + + test.waitUntilEqual("foo", function () succeed ? "foo" : "bar", + "waitUntilEqual pass") + .then(function () test.done()); + + timer.setTimeout(function () { + succeed = true; + }, 200); +} + +exports.testWaitUntilNotEqual = function(test) { + test.waitUntilDone(); + let succeed = false; + + test.waitUntilNotEqual("foo", function () succeed ? "bar" : "foo", + "waitUntilNotEqual pass") + .then(function () test.done()); + + timer.setTimeout(function () { + succeed = true; + }, 200); +} + +exports.testWaitUntilMatches = function(test) { + test.waitUntilDone(); + let succeed = false; + + test.waitUntilMatches(function () succeed ? "foo" : "bar", + /foo/, "waitUntilEqual pass") + .then(function () test.done()); + + timer.setTimeout(function () { + succeed = true; + }, 200); +} + +exports.testWaitUntilErrorInCallback = function(test) { + test.waitUntilDone(); + + test.expectFail(function() { + test.waitUntil(function () {throw "oops"}, "waitUntil pass") + .then(function () test.done()); + }); +} + +exports.testWaitUntilTimeoutInCallback = function(test) { + test.waitUntilDone(1000); + + let runner = new (require("unit-test").TestRunner)({ + console: { + calls: 0, + error: function(msg) { + this.calls++; + if (this.calls == 1) + test.assertEqual(arguments[0], "TEST FAILED: wait4ever (timed out)"); + else if (this.calls == 2) { + test.assertEqual(arguments[0], "test assertion never became true:\n"); + test.assertEqual(arguments[1], "assertion failed, value is false\n"); + // We could additionally check that arguments[1] contains the correct + // stack, but it would be difficult to do so given that it contains + // resource: URLs with a randomly generated string embedded in them + // (the ID of the test addon created to run the tests). And in any + // case, checking the arguments seems sufficient. + + test.done(); + } + else { + test.fail("We got unexpected console.error() calls from waitUntil" + + " assertion callback: '" + arguments[1] + "'"); + } + }, + trace: function () {} + } + }); + + runner.start({ + test: { + name: "wait4ever", + testFunction: function(test) { + test.waitUntilDone(100); + test.waitUntil(function() false); + } + }, + onDone: function() {} + }); +}; + +exports.testExpectFail = function(test) { + test.expectFail(function() { + test.fail('expectFail masking .fail'); + }); + + test.expectFail(function() { + test.assert(false, 'expectFail masking .assert'); + }); + + test.assert(true, 'assert should pass with no expectFail'); +/* + test.expectFail(function() { + test.expectFail(function() { + test.fail('this should blow up'); + }); + }); +*/ +}; + +exports.testAssertFunction = function(test) { + test.assertFunction(function() {}, 'assertFunction with function'); + test.expectFail(function() { + test.assertFunction(null, 'assertFunction with non-function'); + }); +}; + +exports.testAssertUndefined = function(test) { + test.assertUndefined(undefined, 'assertUndefined with undefined'); + test.expectFail(function() { + test.assertUndefined(null, 'assertUndefined with null'); + }); + test.expectFail(function() { + test.assertUndefined(false, 'assertUndefined with false'); + }); + test.expectFail(function() { + test.assertUndefined(0, 'assertUndefined with 0'); + }); +}; + +exports.testAssertNotUndefined = function(test) { + test.expectFail(function() { + test.assertNotUndefined(undefined, 'assertNotUndefined with undefined'); + }); + test.assertNotUndefined(null, 'assertNotUndefined with null'); + test.assertNotUndefined(false, 'assertNotUndefined with false'); + test.assertNotUndefined(0, 'assertNotUndefined with 0'); +}; + +exports.testAssertNull = function(test) { + test.assertNull(null, 'assertNull with null'); + test.expectFail(function() { + test.assertNull(undefined, 'assertNull with undefined'); + }); + test.expectFail(function() { + test.assertNull(false, 'assertNull with false'); + }); + test.expectFail(function() { + test.assertNull(0, 'assertNull with 0'); + }); +}; + +exports.testAssertNotNull = function(test) { + test.assertNotNull(undefined, 'assertNotNull with undefined'); + test.assertNotNull(false, 'assertNotNull with false'); + test.assertNotNull(0, 'assertNotNull with 0'); + + test.expectFail(function() { + test.assertNotNull(null, 'testAssertNotNull with null'); + }); +}; + +exports.testAssertObject = function(test) { + test.assertObject({}, 'assertObject with {}' ); + test.assertObject(new Object(), 'assertObject with new Object'); + test.expectFail(function() { + test.assertObject('fail', 'assertObject with string'); + }); +}; + +exports.testAssertString = function(test) { + test.assertString('', 'assertString with ""'); + test.assertString(new String(), 'assertString with new String'); +}; + +exports.testAssertArray = function(test) { + test.assertArray([], 'assertArray with []'); + test.assertArray(new Array(), 'assertArray with new Array'); +}; + +exports.testNumber = function(test) { + test.assertNumber(1, 'assertNumber with 1'); + test.assertNumber(new Number('2'), 'assertNumber with new Number("2")' ); +}; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-unload.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-unload.js new file mode 100644 index 0000000..33e84d0 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-unload.js @@ -0,0 +1,196 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> (Original Author) + * Drew Willcoxon <adw@mozilla.com> + * Erik Vold <erikvvold@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var unload = require("unload"); +var { Loader } = require("./helpers"); + +exports.testUnloading = function(test) { + var loader = Loader(module); + var ul = loader.require("unload"); + var unloadCalled = 0; + var errorsReported = 0; + function unload() { + unloadCalled++; + throw new Error("error"); + } + ul.when(unload); + + // This should be ignored, as we already registered it + ul.when(unload); + + function unload2() { unloadCalled++; } + ul.when(unload2); + loader.unload(undefined, function onError() { errorsReported++; }); + test.assertEqual(unloadCalled, 2, + "Unloader functions are called on unload."); + test.assertEqual(errorsReported, 1, + "One unload handler threw exception"); +}; + +exports.testEnsure = function(test) { + test.assertRaises(function() { unload.ensure({}); }, + "object has no 'unload' property", + "passing obj with no unload prop should fail"); + test.assertRaises(function() { unload.ensure({}, "destroy"); }, + "object has no 'destroy' property", + "passing obj with no custom unload prop should fail"); + + var called = 0; + var obj = {unload: function() { called++; }}; + + unload.ensure(obj); + obj.unload(); + test.assertEqual(called, 1, + "unload() should be called"); + obj.unload(); + test.assertEqual(called, 1, + "unload() should be called only once"); +}; + +/** + * Check that destructors are called only once with Traits. + * - check that public API is calling the destructor and unregister it, + * - check that composed traits with multiple ensure calls, leads to only + * one destructor call. + */ +exports.testEnsureWithTraits = function(test) { + + let { Trait } = require("traits"); + let loader = Loader(module); + let ul = loader.require("unload"); + + let called = 0; + let composedCalled = 0; + let composedTrait = Trait.compose({ + constructor: function () { + // We have to give "public interface" of this trait, as we want to + // call public `unload` method and ensure that we call it only once, + // either when we call this public function manually or on add-on unload + ul.ensure(this._public); + }, + unload: function unload() { + composedCalled++; + } + }); + let obj = Trait.compose( + composedTrait.resolve({ + constructor: "_constructor", + unload : "_unload" + }), { + constructor: function constructor() { + // Same thing applies here, we need to pass public interface + ul.ensure(this._public); + this._constructor(); + }, + unload: function unload() { + called++; + this._unload(); + } + })(); + + obj.unload(); + test.assertEqual(called, 1, + "unload() should be called"); + + test.assertEqual(composedCalled, 1, + "composed object unload() should be called"); + + obj.unload(); + test.assertEqual(called, 1, + "unload() should be called only once"); + test.assertEqual(composedCalled, 1, + "composed object unload() should be called only once"); + + loader.unload(); + test.assertEqual(called, 1, + "unload() should be called only once, after addon unload"); + test.assertEqual(composedCalled, 1, + "composed object unload() should be called only once, " + + "after addon unload"); +}; + +exports.testEnsureWithTraitsPrivate = function(test) { + + let { Trait } = require("traits"); + let loader = Loader(module); + let ul = loader.require("unload"); + + let called = 0; + let privateObj = null; + let obj = Trait.compose({ + constructor: function constructor() { + // This time wa don't have to give public interface, + // as we want to call a private method: + ul.ensure(this, "_unload"); + privateObj = this; + }, + _unload: function unload() { + called++; + this._unload(); + } + })(); + + loader.unload(); + test.assertEqual(called, 1, + "unload() should be called"); + + privateObj._unload(); + test.assertEqual(called, 1, + "_unload() should be called only once, after addon unload"); +}; + +exports.testReason = function (test) { + var reason = "Reason doesn't actually have to be anything in particular."; + var loader = Loader(module); + var ul = loader.require("unload"); + ul.when(function (rsn) { + test.assertEqual(rsn, reason, + "when() reason should be reason given to loader"); + }); + var obj = { + unload: function (rsn) { + test.assertEqual(rsn, reason, + "ensure() reason should be reason given to loader"); + } + }; + ul.ensure(obj); + loader.unload(reason); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-url.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-url.js new file mode 100644 index 0000000..3307769 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-url.js @@ -0,0 +1,157 @@ +var url = require("url"); + +exports.testResolve = function(test) { + test.assertEqual(url.URL("bar", "http://www.foo.com/").toString(), + "http://www.foo.com/bar"); + + test.assertEqual(url.URL("bar", "http://www.foo.com"), + "http://www.foo.com/bar"); + + test.assertEqual(url.URL("http://bar.com/", "http://foo.com/"), + "http://bar.com/", + "relative should override base"); + + test.assertRaises(function() { url.URL("blah"); }, + "malformed URI: blah", + "url.resolve() should throw malformed URI on base"); + + test.assertRaises(function() { url.URL("chrome://global"); }, + "invalid URI: chrome://global", + "url.resolve() should throw invalid URI on base"); + + test.assertRaises(function() { url.URL("chrome://foo/bar"); }, + "invalid URI: chrome://foo/bar", + "url.resolve() should throw on bad chrome URI"); + + test.assertEqual(url.URL("", "http://www.foo.com"), + "http://www.foo.com/", + "url.resolve() should add slash to end of domain"); +}; + +exports.testParseHttp = function(test) { + var info = url.URL("http://foo.com/bar"); + test.assertEqual(info.scheme, "http"); + test.assertEqual(info.host, "foo.com"); + test.assertEqual(info.port, null); + test.assertEqual(info.userPass, null); + test.assertEqual(info.path, "/bar"); +}; + +exports.testParseHttpWithPort = function(test) { + var info = url.URL("http://foo.com:5/bar"); + test.assertEqual(info.port, 5); +}; + +exports.testParseChrome = function(test) { + var info = url.URL("chrome://global/content/blah"); + test.assertEqual(info.scheme, "chrome"); + test.assertEqual(info.host, "global"); + test.assertEqual(info.port, null); + test.assertEqual(info.userPass, null); + test.assertEqual(info.path, "/content/blah"); +}; + +exports.testParseAbout = function(test) { + var info = url.URL("about:boop"); + test.assertEqual(info.scheme, "about"); + test.assertEqual(info.host, null); + test.assertEqual(info.port, null); + test.assertEqual(info.userPass, null); + test.assertEqual(info.path, "boop"); +}; + +exports.testParseFTP = function(test) { + var info = url.URL("ftp://1.2.3.4/foo"); + test.assertEqual(info.scheme, "ftp"); + test.assertEqual(info.host, "1.2.3.4"); + test.assertEqual(info.port, null); + test.assertEqual(info.userPass, null); + test.assertEqual(info.path, "/foo"); +}; + +exports.testParseFTPWithUserPass = function(test) { + var info = url.URL("ftp://user:pass@1.2.3.4/foo"); + test.assertEqual(info.userPass, "user:pass"); +}; + +exports.testToFilename = function(test) { + test.assertRaises( + function() { url.toFilename("resource://nonexistent"); }, + "resource does not exist: resource://nonexistent/", + "url.toFilename() on nonexistent resources should throw" + ); + + test.assertMatches(url.toFilename(module.uri), + /.*test-url\.js$/, + "url.toFilename() on resource: URIs should work"); + + test.assertRaises( + function() { url.toFilename("http://foo.com/"); }, + "cannot map to filename: http://foo.com/", + "url.toFilename() on http: URIs should raise error" + ); + + try { + test.assertMatches( + url.toFilename("chrome://global/content/console.xul"), + /.*console\.xul$/, + "url.toFilename() w/ console.xul works when it maps to filesystem" + ); + } catch (e) { + if (/chrome url isn\'t on filesystem/.test(e.message)) + test.pass("accessing console.xul in jar raises exception"); + else + test.fail("accessing console.xul raises " + e); + } + + // TODO: Are there any chrome URLs that we're certain exist on the + // filesystem? + // test.assertMatches(url.toFilename("chrome://myapp/content/main.js"), + // /.*main\.js$/); +}; + +exports.testFromFilename = function(test) { + var fileUrl = url.fromFilename(url.toFilename(module.uri)); + test.assertEqual(url.URL(fileUrl).scheme, 'file', + 'url.toFilename() should return a file: url'); + test.assertEqual(url.fromFilename(url.toFilename(fileUrl)), + fileUrl); +}; + +exports.testURL = function(test) { + let URL = url.URL; + test.assert(URL("h:foo") instanceof URL, "instance is of correct type"); + test.assertRaises(function() URL(), + "malformed URI: undefined", + "url.URL should throw on undefined"); + test.assertRaises(function() URL(""), + "malformed URI: ", + "url.URL should throw on empty string"); + test.assertRaises(function() URL("foo"), + "malformed URI: foo", + "url.URL should throw on invalid URI"); + test.assert(URL("h:foo").scheme, "has scheme"); + test.assertEqual(URL("h:foo").toString(), + "h:foo", + "toString should roundtrip"); + // test relative + base + test.assertEqual(URL("mypath", "http://foo").toString(), + "http://foo/mypath", + "relative URL resolved to base"); + // test relative + no base + test.assertRaises(function() URL("path").toString(), + "malformed URI: path", + "no base for relative URI should throw"); + + let a = URL("h:foo"); + let b = URL(a); + test.assertEqual(b.toString(), + "h:foo", + "a URL can be initialized from another URL"); + test.assertNotStrictEqual(a, b, + "a URL initialized from another URL is not the same object"); + test.assert(a == "h:foo", + "toString is implicit when a URL is compared to a string via =="); + test.assertStrictEqual(a + "", "h:foo", + "toString is implicit when a URL is concatenated to a string"); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-window-loader.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-window-loader.js new file mode 100644 index 0000000..fc6f144 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-window-loader.js @@ -0,0 +1,152 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Irakli Gozalishvili <gozala@mozilla.com> (Original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const { WindowLoader } = require('windows/loader'), + { Trait } = require('traits'); + +const Loader = Trait.compose( + WindowLoader, + { + constructor: function Loader(options) { + this._onLoad = options.onLoad; + this._onUnload = options.onUnload; + if ('window' in options) + this._window = options.window; + this._load(); + this.window = this._window; + }, + window: null, + _onLoad: null, + _onUnload: null, + _tabOptions: [] + } +); + +exports['test compositions with missing required properties'] = function(test) { + test.assertRaises( + function() WindowLoader.compose({})(), + 'Missing required property: _onLoad', + 'should throw missing required property exception' + ); + test.assertRaises( + function() WindowLoader.compose({ _onLoad: null, _tabOptions: null })(), + 'Missing required property: _onUnload', + 'should throw missing required property `_onUnload`' + ); + test.assertRaises( + function() WindowLoader.compose({ _onUnload: null, _tabOptions: null })(), + 'Missing required property: _onLoad', + 'should throw missing required property `_onLoad`' + ); + test.assertRaises( + function() WindowLoader.compose({ _onUnload: null, _onLoad: null })(), + 'Missing required property: _tabOptions', + 'should throw missing required property `_tabOptions`' + ); +}; + +exports['test `load` events'] = function(test) { + test.waitUntilDone(); + let onLoadCalled = false; + Loader({ + onLoad: function(window) { + onLoadCalled = true; + test.assertEqual( + window, this._window, 'windows should match' + ); + test.assertEqual( + window.document.readyState, 'complete', 'window must be fully loaded' + ); + window.close(); + }, + onUnload: function(window) { + test.assertEqual( + window, this._window, 'windows should match' + ); + test.assertEqual( + window.document.readyState, 'complete', 'window must be fully loaded' + ); + test.assert(onLoadCalled, 'load callback is supposed to be called'); + test.done(); + } + }); +}; + +exports['test removeing listeners'] = function(test) { + test.waitUntilDone(); + Loader({ + onLoad: function(window) { + test.assertEqual( + window, this._window, 'windows should match' + ); + window.close(); + }, + onUnload: function(window) { + test.done(); + } + }); +}; + +exports['test create loader from opened window'] = function(test) { + test.waitUntilDone(); + let onUnloadCalled = false; + Loader({ + onLoad: function(window) { + test.assertEqual( + window, this._window, 'windows should match' + ); + test.assertEqual( + window.document.readyState, 'complete', 'window must be fully loaded' + ); + Loader({ + window: window, + onLoad: function(win) { + test.assertEqual(win, window, 'windows should match'); + window.close(); + }, + onUnload: function(window) { + test.assert(onUnloadCalled, 'first handler should be called already'); + test.done(); + } + }); + }, + onUnload: function(window) { + onUnloadCalled = true; + } + }); +}; + diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-window-observer.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-window-observer.js new file mode 100644 index 0000000..3f3bfc2 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-window-observer.js @@ -0,0 +1,44 @@ +"use strict"; + +const { Loader } = require("./helpers"); + +exports["test unload window observer"] = function(assert, done) { + // Hacky way to be able to create unloadable modules via makeSandboxedLoader. + let loader = Loader(module); + + let utils = loader.require("api-utils/window-utils"); + let { isBrowser, activeBrowserWindow: activeWindow } = utils; + let observer = loader.require("api-utils/windows/observer").observer; + let opened = 0; + let closed = 0; + + observer.on("open", function onOpen(window) { + // Ignoring non-browser windows + if (isBrowser(window)) + opened++; + }); + observer.on("close", function onClose(window) { + // Ignore non-browser windows & already opened `activeWindow` (unload will + // emit close on it even though it is not actually closed). + if (isBrowser(window) && window !== activeWindow) + closed++; + }); + + // Open window and close it to trigger observers. + activeWindow.open().close(); + + // Unload the module so that all listeners set by observer are removed. + loader.unload(); + + // Open and close window once again. + activeWindow.open().close(); + + // Enqueuing asserts to make sure that assertion is not performed early. + require("timer").setTimeout(function () { + assert.equal(1, opened, "observer open was called before unload only"); + assert.equal(1, closed, "observer close was called before unload only"); + done(); + }, 0); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-window-utils.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-window-utils.js new file mode 100644 index 0000000..eff9f2b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-window-utils.js @@ -0,0 +1,271 @@ +var windowUtils = require("window-utils"); +var timer = require("timer"); +var {Cc,Ci} = require("chrome"); +var { Loader } = require("./helpers"); + +function makeEmptyWindow() { + var xulNs = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var blankXul = ('<?xml version="1.0"?>' + + '<?xml-stylesheet href="chrome://global/skin/" ' + + ' type="text/css"?>' + + '<window xmlns="' + xulNs + '" windowtype="test:window">' + + '</window>'); + var url = "data:application/vnd.mozilla.xul+xml," + escape(blankXul); + var features = ["chrome", "width=10", "height=10"]; + + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Ci.nsIWindowWatcher); + return ww.openWindow(null, url, null, features.join(","), null); +} + +exports.testCloseOnUnload = function(test) { + var timesClosed = 0; + var fakeWindow = { + _listeners: [], + addEventListener: function(name, func, bool) { + this._listeners.push(func); + }, + removeEventListener: function(name, func, bool) { + var index = this._listeners.indexOf(func); + if (index == -1) + throw new Error("event listener not found"); + this._listeners.splice(index, 1); + }, + close: function() { + timesClosed++; + this._listeners.forEach( + function(func) { + func({target: fakeWindow.document}); + }); + }, + document: { + get defaultView() { return fakeWindow; } + } + }; + + let loader = Loader(module); + loader.require("window-utils").closeOnUnload(fakeWindow); + test.assertEqual(fakeWindow._listeners.length, 1, + "unload listener added on closeOnUnload()"); + test.assertEqual(timesClosed, 0, + "window not closed when registered."); + loader.require("unload").send(); + test.assertEqual(timesClosed, 1, + "window closed on module unload."); + test.assertEqual(fakeWindow._listeners.length, 0, + "unload event listener removed on module unload"); + + timesClosed = 0; + loader.require("window-utils").closeOnUnload(fakeWindow); + test.assertEqual(timesClosed, 0, + "window not closed when registered."); + fakeWindow.close(); + test.assertEqual(timesClosed, 1, + "window closed when close() called."); + test.assertEqual(fakeWindow._listeners.length, 0, + "unload event listener removed on window close"); + loader.require("unload").send(); + test.assertEqual(timesClosed, 1, + "window not closed again on module unload."); + loader.unload(); +}; + +exports.testWindowWatcher = function(test) { + var myWindow; + var finished = false; + + var delegate = { + onTrack: function(window) { + if (window == myWindow) { + test.pass("onTrack() called with our test window"); + timer.setTimeout(function() { myWindow.close(); }, 1); + } + }, + onUntrack: function(window) { + if (window == myWindow) { + test.pass("onUntrack() called with our test window"); + timer.setTimeout(function() { + if (!finished) { + finished = true; + myWindow = null; + wt.unload(); + test.done(); + } else + test.fail("finishTest() called multiple times."); + }, 1); + } + } + }; + + var wt = new windowUtils.WindowTracker(delegate); + myWindow = makeEmptyWindow(); + test.waitUntilDone(5000); +}; + +// test that _unregWindow calls _unregLoadingWindow +exports.testWindowWatcherUnregs4LoadingWindows = function(test) { + var myWindow; + var finished = false; + let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow("navigator:browser"); + var counter = 0; + + var delegate = { + onTrack: function(window) { + var type = window.document.documentElement.getAttribute("windowtype"); + if (type == "test:window") + test.fail("onTrack shouldn't have been executed."); + } + }; + var wt = new windowUtils.WindowTracker(delegate); + + // make a new window + myWindow = makeEmptyWindow(); + + // make sure that the window hasn't loaded yet + test.assertNotEqual( + myWindow.document.readyState, + "complete", + "window hasn't loaded yet."); + + // unload WindowTracker + wt.unload(); + + // make sure that the window still hasn't loaded, which means that the onTrack + // would have been removed successfully assuming that it doesn't execute. + test.assertNotEqual( + myWindow.document.readyState, + "complete", + "window still hasn't loaded yet."); + + // wait for the window to load and then close it. onTrack wouldn't be called + // until the window loads, so we must let it load before closing it to be + // certain that onTrack was removed. + myWindow.addEventListener("load", function() { + // allow all of the load handles to execute before closing + myWindow.setTimeout(function() { + myWindow.addEventListener("unload", function() { + // once the window unloads test is done + test.done(); + }, false); + myWindow.close(); + }, 0); + }, false); + + test.waitUntilDone(5000); +} + +exports.testWindowWatcherWithoutUntracker = function(test) { + var myWindow; + var finished = false; + + var delegate = { + onTrack: function(window) { + if (window == myWindow) { + test.pass("onTrack() called with our test window"); + timer.setTimeout(function() { + myWindow.close(); + + if (!finished) { + finished = true; + myWindow = null; + wt.unload(); + test.done(); + } else { + test.fail("onTrack() called multiple times."); + } + }, 1); + } + } + }; + + var wt = new windowUtils.WindowTracker(delegate); + myWindow = makeEmptyWindow(); + test.waitUntilDone(5000); +}; + +exports.testActiveWindow = function(test) { + test.waitUntilDone(5000); + + let testRunnerWindow = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow("test:runner"); + let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow("navigator:browser"); + + test.assertEqual(windowUtils.activeBrowserWindow, browserWindow, + "Browser window is the active browser window."); + + + let testSteps = [ + function() { + windowUtils.activeWindow = browserWindow; + continueAfterFocus(browserWindow); + }, + function() { + test.assertEqual(windowUtils.activeWindow, browserWindow, + "Correct active window [1]"); + continueAfterFocus(windowUtils.activeWindow = testRunnerWindow); + }, + function() { + test.assertEqual(windowUtils.activeWindow, testRunnerWindow, + "Correct active window [2]"); + test.assertEqual(windowUtils.activeBrowserWindow, browserWindow, + "Correct active browser window [3]"); + continueAfterFocus(windowUtils.activeWindow = browserWindow); + }, + function() { + test.assertEqual(windowUtils.activeWindow, browserWindow, + "Correct active window [4]"); + continueAfterFocus(windowUtils.activeWindow = testRunnerWindow); + }, + function() { + test.assertEqual(windowUtils.activeWindow, testRunnerWindow, + "Correct active window [5]"); + test.assertEqual(windowUtils.activeBrowserWindow, browserWindow, + "Correct active browser window [6]"); + testRunnerWindow = null; + browserWindow = null; + test.done() + } + ]; + + let nextTest = function() { + let func = testSteps.shift(); + if (func) { + func(); + } + } + + function continueAfterFocus(targetWindow) { + + // Based on SimpleTest.waitForFocus + var fm = Cc["@mozilla.org/focus-manager;1"]. + getService(Ci.nsIFocusManager); + + var childTargetWindow = {}; + fm.getFocusedElementForWindow(targetWindow, true, childTargetWindow); + childTargetWindow = childTargetWindow.value; + + var focusedChildWindow = {}; + if (fm.activeWindow) { + fm.getFocusedElementForWindow(fm.activeWindow, true, focusedChildWindow); + focusedChildWindow = focusedChildWindow.value; + } + + var focused = (focusedChildWindow == childTargetWindow); + if (focused) { + nextTest(); + } else { + childTargetWindow.addEventListener("focus", function focusListener() { + childTargetWindow.removeEventListener("focus", focusListener, true); + nextTest(); + }, true); + } + + } + + nextTest(); +} diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-xhr.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-xhr.js new file mode 100644 index 0000000..2912139 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-xhr.js @@ -0,0 +1,70 @@ +var xhr = require("xhr"); +var timer = require("timer"); +var { Loader } = require("./helpers"); + +/* Test is intentionally disabled until platform bug 707256 is fixed. +exports.testAbortedXhr = function(test) { + var req = new xhr.XMLHttpRequest(); + test.assertEqual(xhr.getRequestCount(), 1); + req.abort(); + test.assertEqual(xhr.getRequestCount(), 0); +}; +*/ + +exports.testLocalXhr = function(test) { + var req = new xhr.XMLHttpRequest(); + req.overrideMimeType("text/plain"); + req.open("GET", module.uri); + req.onreadystatechange = function() { + if (req.readyState == 4 && req.status == 0) { + test.assertMatches(req.responseText, + /onreadystatechange/, + "XMLHttpRequest should get local files"); + timer.setTimeout( + function() { test.assertEqual(xhr.getRequestCount(), 0); + test.done(); }, + 0 + ); + } + }; + req.send(null); + test.assertEqual(xhr.getRequestCount(), 1); + test.waitUntilDone(4000); +}; + +exports.testUnload = function(test) { + var loader = Loader(module); + var sbxhr = loader.require("xhr"); + var req = new sbxhr.XMLHttpRequest(); + req.overrideMimeType("text/plain"); + req.open("GET", module.uri); + req.send(null); + test.assertEqual(sbxhr.getRequestCount(), 1); + loader.unload(); + test.assertEqual(sbxhr.getRequestCount(), 0); +}; + +exports.testDelegatedReturns = function(test) { + var req = new xhr.XMLHttpRequest(); + req.overrideMimeType("text/plain"); + req.open("GET", module.uri); + req.onreadystatechange = function() { + if (req.readyState == 4 && req.status == 0) { + // This response isn't going to have any headers, so the return value + // should be null. Previously it wasn't returning anything, and thus was + // undefined. + + // Depending on whether Bug 608939 has been applied + // to the platform, getAllResponseHeaders() may return + // null or the empty string; accept either. + var headers = req.getAllResponseHeaders(); + test.assert(headers === null || headers === "", + "XHR's delegated methods should return"); + test.done(); + } + }; + req.send(null); + test.assertEqual(xhr.getRequestCount(), 1); + test.waitUntilDone(4000); +} + diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-xpcom.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-xpcom.js new file mode 100644 index 0000000..fdcb6e8 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-xpcom.js @@ -0,0 +1,107 @@ +var traceback = require("traceback"); +var xpcom = require("xpcom"); +var {Cc,Ci,Cm,Cr} = require("chrome"); +var { Loader } = require("./helpers"); + +exports.testRegister = function(test, text) { + if (!text) + text = "hai2u"; + + function Component() {} + + Component.prototype = { + newChannel : function(aURI) { + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + var channel = ios.newChannel( + "data:text/plain," + text, + null, + null + ); + + channel.originalURI = aURI; + return channel; + }, + getURIFlags: function(aURI) { + return Ci.nsIAboutModule.ALLOW_SCRIPT; + }, + QueryInterface: xpcom.utils.generateQI([Ci.nsIAboutModule]) + }; + + var contractID = "@mozilla.org/network/protocol/about;1?what=boop"; + + var factory = xpcom.register({name: "test about:boop page", + contractID: contractID, + create: Component}); + + var manager = Cm.QueryInterface(Ci.nsIComponentRegistrar); + test.assertEqual(manager.isCIDRegistered(factory.uuid), true); + + // We don't want to use Cc[contractID] here because it's immutable, + // so it can't accept updated versions of a contractID during the + // same application session. + var aboutFactory = xpcom.getClass(contractID, Ci.nsIFactory); + + test.assertNotEqual(aboutFactory.wrappedJSObject, + undefined, + "Factory wrappedJSObject should exist."); + + var about = aboutFactory.createInstance(null, Ci.nsIAboutModule); + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + test.assertEqual( + about.getURIFlags(ios.newURI("http://foo.com", null, null)), + Ci.nsIAboutModule.ALLOW_SCRIPT + ); + + var aboutURI = ios.newURI("about:boop", null, null); + var channel = ios.newChannelFromURI(aboutURI); + var iStream = channel.open(); + var siStream = Cc['@mozilla.org/scriptableinputstream;1'] + .createInstance(Ci.nsIScriptableInputStream); + siStream.init(iStream); + var data = new String(); + data += siStream.read(-1); + siStream.close(); + iStream.close(); + test.assertEqual(data, text); + + factory.unregister(); + test.assertEqual(manager.isCIDRegistered(factory.uuid), false); +}; + +exports.testReRegister = function(test) { + exports.testRegister(test, "hai2u again"); +}; + +exports.testMakeUuid = function(test) { + var first = xpcom.makeUuid().toString(); + var second = xpcom.makeUuid().toString(); + test.assertMatches(first, /{[0-9a-f\-]+}/); + test.assertMatches(second, /{[0-9a-f\-]+}/); + test.assertNotEqual(first, second); +}; + +exports.testUnload = function(test) { + var loader = Loader(module); + var sbxpcom = loader.require("xpcom"); + + function Component() {} + + Component.prototype = { + QueryInterface: sbxpcom.utils.generateQI([Ci.nsISupports]) + }; + + var contractID = "@mozilla.org/blargle;1"; + var factory = sbxpcom.register({name: "test component", + contractID: contractID, + create: Component}); + + var manager = Cm.QueryInterface(Ci.nsIComponentRegistrar); + test.assertEqual(manager.isCIDRegistered(factory.uuid), true); + + loader.unload(); + + test.assertEqual(manager.isCIDRegistered(factory.uuid), false); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/test-xul-app.js b/tools/addon-sdk-1.4/packages/api-utils/tests/test-xul-app.js new file mode 100644 index 0000000..cda4a2e --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/test-xul-app.js @@ -0,0 +1,41 @@ +var xulApp = require("xul-app"); + +exports.testXulApp = function(test) { + test.assertEqual(typeof(xulApp.ID), "string", + "ID is a string"); + test.assertEqual(typeof(xulApp.name), "string", + "name is a string"); + test.assertEqual(typeof(xulApp.version), "string", + "version is a string"); + test.assertEqual(typeof(xulApp.platformVersion), "string", + "platformVersion is a string"); + + test.assertRaises(function() { xulApp.is("blargy"); }, + "Unkown Mozilla Application: blargy", + "is() throws error on bad app name"); + test.assertRaises(function() { xulApp.isOneOf(["blargy"]); }, + "Unkown Mozilla Application: blargy", + "isOneOf() throws error on bad app name"); + + function testSupport(name) { + var item = xulApp.is(name); + test.assert(item === true || item === false, + "is('" + name + "') is true or false."); + } + + var apps = ["Firefox", "Mozilla", "Sunbird", "SeaMonkey", + "Fennec", "Thunderbird"]; + + apps.forEach(function(name) { testSupport(name); }); + + test.assert(xulApp.isOneOf(apps) == true || + xulApp.isOneOf(apps) == false, + "isOneOf() returns true or false."); + + test.assertEqual(xulApp.versionInRange(xulApp.platformVersion, "1.9", "*"), + true, "platformVersion in range [1.9, *)"); + test.assertEqual(xulApp.versionInRange("3.6.4", "3.6.4", "3.6.*"), + true, "3.6.4 in [3.6.4, 3.6.*)"); + test.assertEqual(xulApp.versionInRange("1.9.3", "1.9.2", "1.9.3"), + false, "1.9.3 not in [1.9.2, 1.9.3)"); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/traits/assert.js b/tools/addon-sdk-1.4/packages/api-utils/tests/traits/assert.js new file mode 100644 index 0000000..dd662a4 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/traits/assert.js @@ -0,0 +1,94 @@ +"use strict"; + +var BaseAssert = require("test/assert").Assert; + +/** + * Whether or not given property descriptors are equivalent. They are + * equivalent either if both are marked as "conflict" or "required" property + * or if all the properties of descriptors are equal. + * @param {Object} actual + * @param {Object} expected + */ +function equivalentDescriptors(actual, expected) { + return (actual.conflict && expected.conflict) || + (actual.required && expected.required) || + equalDescriptors(actual, expected); +} + +function equalDescriptors(actual, expected) { + return actual.get === expected.get && + actual.set === expected.set && + actual.value === expected.value && + !!actual.enumerable === !!expected.enumerable && + !!actual.configurable === !!expected.configurable && + !!actual.writable === !!expected.writable; +} + +/** + * Whether or not given `target` array contains all the element + * from a given `source` array. + */ +function containsSet(source, target) { + return source.some(function(element) { + return 0 > target.indexOf(element); + }); +} + +/** + * Whether or not given two arrays contain all elements from another. + */ +function equivalentSets(source, target) { + return containsSet(source, target) && containsSet(target, source); +} + +/** + * Finds name of the property from `source` property descriptor map, that + * is not equivalent of the name named property in the `target` property + * descriptor map. If not found `null` is returned instead. + */ +function findNonEquivalentPropertyName(source, target) { + var value = null; + Object.getOwnPropertyNames(source).some(function(key) { + var areEquivalent = false; + if (!equivalentDescriptors(source[key], target[key])) { + value = key; + areEquivalent = true; + } + return areEquivalent; + }); + return value; +} + +var AssertDescriptor = { + equalTraits: { + value: function equivalentTraits(actual, expected, message) { + var difference; + var actualKeys = Object.getOwnPropertyNames(actual); + var expectedKeys = Object.getOwnPropertyNames(expected); + + if (equivalentSets(actualKeys, expectedKeys)) { + this.fail({ + operator: "equalTraits", + message: "Traits define different properties", + actual: actualKeys.sort().join(","), + expected: expectedKeys.sort().join(","), + }); + } + else if ((difference = findNonEquivalentPropertyName(actual, expected))) { + this.fail({ + operator: "equalTraits", + message: "Traits define non-equivalent property `" + difference + "`", + actual: actual[difference], + expected: expected[difference] + }); + } + else { + this.pass(message || "Traits are equivalent."); + } + } + } +}; + +exports.Assert = function Assert() { + return Object.create(BaseAssert.apply(null, arguments), AssertDescriptor); +}; diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/traits/descriptor-tests.js b/tools/addon-sdk-1.4/packages/api-utils/tests/traits/descriptor-tests.js new file mode 100644 index 0000000..7c27ac4 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/traits/descriptor-tests.js @@ -0,0 +1,331 @@ +"use strict"; + +var Trait = require("light-traits").Trait; +var utils = require("./utils"); +var Data = utils.Data; +var Method = utils.Method; +var Accessor = utils.Accessor; +var Required = utils.Required; +var Conflict = utils.Conflict; + +function method() {} + +exports.Assert = require("./assert").Assert +exports["test simple composition"] = function(assert) { + var actual = Trait.compose( + Trait({ a: 0, b: 1 }), + { c: { value: 2 }, d: { value: method, enumerable: true } } + ); + + var expected = { + a: Data(0), + b: Data(1), + c: Data(2, false, false, false), + d: Method(method, true, false, false) + }; + + assert.equalTraits(actual, expected); +}; + +exports["test composition with conflict"] = function(assert) { + var actual = Trait.compose( + Trait({ a: 0, b: 1 }), + { + a: { + value: 2, + writable: true, + configurable: true, + enumerable: true + }, + c: { + value: method, + configurable: true + } + } + ); + + var expected = { + a: Conflict("a"), + b: Data(1), + c: Method(method, false, true, false) + }; + + assert.equalTraits(actual, expected); +}; + +exports["test identical props does not cause conflict"] = function(assert) { + var actual = Trait.compose( + { + a: { + value: 0, + writable: true, + configurable: true, + enumerable: true + }, + b: { + value: 1 + } + }, + Trait({ + a: 0, + c: method + }) + ); + + var expected = { + a: Data(0), + b: Data(1, false, false, false), + c: Method(method) + } + + assert.equalTraits(actual, expected); +}; + +exports["test composition with identical required props"] = function(assert) { + var actual = Trait.compose( + Trait({ a: Trait.required, b: 1 }), + { a: { required: true }, c: { value: method } } + ); + + var expected = { + a: Required(), + b: Data(1), + c: Method(method, false, false, false) + }; + + assert.equalTraits(actual, expected); +}; + +exports["test composition satisfying a required prop"] = function(assert) { + var actual = Trait.compose( + Trait({ a: Trait.required, b: 1 }), + { a: { value: method, enumerable: true } } + ); + + var expected = { + a: Method(method, true, false, false), + b: Data(1) + }; + + assert.equalTraits(actual, expected); +}; + +exports["test compose is neutral wrt conflicts"] = function(assert) { + var actual = Trait.compose( + Trait({ a: { value: 1 } }, Trait({ a: 2 })), + { b: { value: 0, writable: true, configurable: true, enumerable: false } } + ); + + var expected = { a: Conflict("a"), b: Data(0, false) }; + + assert.equalTraits(actual, expected); +}; + +exports["test conflicting prop overrides Trait.required"] = function(assert) { + var actual = Trait.compose( + Trait.compose( + Trait({ a: 1 }), + { a: { value: 2 } } + ), + { a: { value: Trait.required } } + ); + + var expected = { a: Conflict("a") }; + + assert.equalTraits(actual, expected); +}; + +exports["test compose is commutative"] = function(assert) { + var actual = Trait.compose( + Trait({ a: 0, b: 1 }), + { c: { value: 2 }, d: { value: method } } + ); + + var expected = Trait.compose( + { c: { value: 2 }, d: { value: method } }, + Trait({ a: 0, b: 1 }) + ); + + assert.equalTraits(actual, expected); +} + +exports["test compose is commutative, also for required/conflicting props"] = function(assert) { + var actual = Trait.compose( + { + a: { value: 0 }, + b: { value: 1 }, + c: { value: 3 }, + e: { value: Trait.required } + }, + { + c: { value: 2 }, + d: { get: method } + } + ); + + var expected = Trait.compose( + Trait({ c: 3 }), + { + c: { value: 2 }, + d: { get: method }, + a: { value: 0 }, + b: { value: 1 }, + e: { value: Trait.required }, + } + ); + + assert.equalTraits(actual, expected); +}; + +exports["test compose is associative"] = function(assert) { + var actual = Trait.compose( + { + a: { value: 0 }, + b: { value: 1 }, + c: { value: 3 }, + d: { value: Trait.required } + }, + Trait.compose( + { c: { value: 3 }, d: { value: Trait.required } }, + { c: { value: 2 }, d: { value: method }, e: { value: "foo" } } + ) + ); + + var expected = Trait.compose( + Trait.compose( + { + a: { value: 0 }, + b: { value: 1 }, + c: { value: 3 }, + d: { value: Trait.required } + }, + { + c: { value: 3 }, + d: { value: Trait.required } + } + ), + { + c: { value: 2 }, + d: { value: method }, + e: { value: "foo" } + } + ); + + assert.equalTraits(actual, expected); +}; + +exports["test diamond import of same prop do not conflict"] = function(assert) { + var actual = Trait.compose( + Trait.compose( + { b: { value: 2 } }, + { a: { value: 1, enumerable: true, configurable: true, writable: true } } + ), + Trait.compose( + { c: { value: 3 } }, + Trait({ a: 1 }) + ), + Trait({ d: 4 }) + ); + + var expected = { + a: Data(1), + b: Data(2, false, false, false), + c: Data(3, false, false, false), + d: Data(4) + }; + + assert.equalTraits(actual, expected); +}; + +exports["test create simple"] = function(assert) { + var o1 = Trait.compose( + Trait({ a: 1 }), + { + b: { + value: function() { + return this.a; + } + } + } + ).create(Object.prototype); + + assert.equal(Object.getPrototypeOf(o1), Object.prototype, "o1 prototype"); + assert.equal(1, o1.a, "o1.a"); + assert.equal(1, o1.b(), "o1.b()"); + assert.equal(Object.keys(o1).length, 1, "Object.keys(o1).length === 2"); +}; + +exports["test create with Array.prototype"] = function(assert) { + var o2 = Trait.compose({}, {}).create(Array.prototype); + assert.equal(Object.getPrototypeOf(o2), Array.prototype, "o2 prototype"); +}; + +exports["test exception for incomplete required properties"] = function(assert) { + assert.throws(function() { + Trait({ foo: Trait.required }).create(Object.prototype) + }, /Missing required property: `foo`/, "required prop error"); +} + +exports["test exception for unresolved conflicts"] = function(assert) { + assert.throws(function() { + Trait(Trait({ a: 0 }), Trait({ a: 1 })).create({}) + }, /Remaining conflicting property: `a`/, "conflicting prop error"); +} + +exports["test conflicting properties are present"] = function(assert) { + var o5 = Object.create(Object.prototype, Trait.compose( + { a: { value: 0 } }, + { a: { value: 1 } } + )); + + assert.ok("a" in o5, "conflicting property present"); + assert.throws(function() { + o5.a + }, /Remaining conflicting property: `a`/, "conflicting prop access error"); +}; + +exports["test diamond with conflicts"] = function(assert) { + function makeT1(x) { + return { + m: { + value: function() { + return x + } + } + }; + }; + + function makeT2(x) { + return Trait.compose( + Trait({ t2: "foo" }), + makeT1(x) + ); + }; + + function makeT3(x) { + return Trait.compose( + { + t3: { value: "bar" } + }, + makeT1(x) + ); + }; + + var T4 = Trait.compose(makeT2(5), makeT3(5)); + + assert.throws(function() { + T4.create(Object.prototype); + }, /Remaining conflicting property: `m`/, "diamond prop conflict"); +}; + +exports["test providing requirements through proto"] = function(assert) { + var t = Trait.compose( + {}, + { required: { required: true } } + ).create({ required: "test" }); + + assert.equal(t.required, "test", "property from proto is inherited"); +}; + +if (module == require.main) + require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/traits/inheritance-tests.js b/tools/addon-sdk-1.4/packages/api-utils/tests/traits/inheritance-tests.js new file mode 100644 index 0000000..73a23b7 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/traits/inheritance-tests.js @@ -0,0 +1,100 @@ +"use strict"; + +var Trait = require("light-traits").Trait; + +exports["test custom constructor and inherited toString"] = function(assert) { + function Type() { + return Object.create(Type.prototype); + } + Type.prototype = Trait({ + method: function method() { + return 2; + } + }).create(Object.freeze(Type.prototype)); + + var fixture = Type(); + + assert.equal(fixture.constructor, Type, "must override constructor"); + assert.equal(fixture.toString(), "[object Type]", "must inherit toString"); +}; + +exports["test custom toString and inherited constructor"] = function(assert) { + function Type() { + return Object.create(Type.prototype); + } + Type.prototype = Trait({ + toString: function toString() { + return "<toString>"; + } + }).create(); + + var fixture = Type(); + + assert.equal(fixture.constructor, Trait, "must inherit constructor Trait"); + assert.equal(fixture.toString(), "<toString>", "Must override toString"); +}; + +exports["test custom toString and constructor"] = function(assert) { + function Type() { + return TypeTrait.create(Type.prototype); + } + Object.freeze(Type.prototype); + var TypeTrait = Trait({ + toString: function toString() { + return "<toString>"; + } + }); + + var fixture = Type(); + + assert.equal(fixture.constructor, Type, "constructor is provided to create"); + assert.equal(fixture.toString(), "<toString>", "toString was overridden"); +}; + +exports["test resolve constructor"] = function (assert) { + function Type() {} + var T1 = Trait({ constructor: Type }).resolve({ constructor: '_foo' }); + var f1 = T1.create(); + + assert.equal(f1._foo, Type, "constructor was resolved"); + assert.equal(f1.constructor, Trait, "constructor of prototype is inherited"); + assert.equal(f1.toString(), "[object Trait]", "toString is inherited"); +}; + +exports["test compose read-only"] = function (assert) { + function Type() {} + Type.prototype = Trait.compose(Trait({}), { + constructor: { value: Type }, + a: { value: "b", enumerable: true } + }).resolve({ a: "b" }).create({ a: "a" }); + + var f1 = new Type(); + + assert.equal(Object.getPrototypeOf(f1), Type.prototype, "inherits correctly"); + assert.equal(f1.constructor, Type, "constructor was overridden"); + assert.equal(f1.toString(), "[object Type]", "toString was inherited"); + assert.equal(f1.a, "a", "property a was resolved"); + assert.equal(f1.b, "b", "property a was renamed to b"); + assert.ok(!Object.getOwnPropertyDescriptor(Type.prototype, "a"), + "a is not on the prototype of the instance"); + + var proto = Object.getPrototypeOf(Type.prototype); + var dc = Object.getOwnPropertyDescriptor(Type.prototype, "constructor"); + var db = Object.getOwnPropertyDescriptor(Type.prototype, "b"); + var da = Object.getOwnPropertyDescriptor(proto, "a"); + + assert.ok(!dc.writable, "constructor is not writable"); + assert.ok(!dc.enumerable, "constructor is not enumerable"); + assert.ok(dc.configurable, "constructor inherits configurability"); + + assert.ok(!db.writable, "a -> b is not writable"); + assert.ok(db.enumerable, "a -> b is enumerable"); + assert.ok(!db.configurable, "a -> b is not configurable"); + + assert.ok(da.writable, "a is writable"); + assert.ok(da.enumerable, "a is enumerable"); + assert.ok(da.configurable, "a is configurable"); +}; + +if (require.main == module) + require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/traits/object-tests.js b/tools/addon-sdk-1.4/packages/api-utils/tests/traits/object-tests.js new file mode 100644 index 0000000..afea3ce --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/traits/object-tests.js @@ -0,0 +1,317 @@ +"use strict"; + +var Trait = require("light-traits").Trait; +var utils = require("./utils"); +var Data = utils.Data; +var Method = utils.Method; +var Accessor = utils.Accessor; +var Required = utils.Required; +var Conflict = utils.Conflict; + +function method() {} + +exports.Assert = require("./assert").Assert; + +exports["test empty trait"] = function (assert) { + assert.equalTraits(Trait({}), {}); +}; + +exports["test simple trait"] = function (assert) { + var expected = { + a: Data(0, true, true, true), + b: Method(method, true, true, true) + }; + + assert.equalTraits(Trait({ a: 0, b: method }), expected); +}; + +exports["test simple trait with Trait.required property"] = function (assert) { + var actual = Trait({ a: Trait.required, b: 1 }); + var expected = { a: Required("a"), b: Data(1) }; + + assert.equalTraits(actual, expected); +}; + +exports["test ordering of trait properties is irrelevant"] = function (assert) { + var actual = Trait({ a: 0, b: 1, c: Trait.required }); + var expected = Trait({ b: 1, c: Trait.required, a: 0 }); + + assert.equalTraits(actual, expected); +}; + +exports["test trait with accessor property"] = function (assert) { + var record = { get a() {}, set a(v) {} }; + var get = Object.getOwnPropertyDescriptor(record, "a").get; + var set = Object.getOwnPropertyDescriptor(record, "a").set; + + assert.equalTraits(Trait(record), { a: Accessor(get, set) }); +}; + +exports["test simple composition"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1 }), Trait({ c: 2, d: method })); + var expected = { a: Data(0), b: Data(1), c: Data(2), d: Method(method) }; + + assert.equalTraits(actual, expected); +}; + +exports["test composition with conflict"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1 }), Trait({ a: 2, c: method })); + var expected = { a: Conflict("a"), b: Data(1), c: Method(method) }; + + assert.equalTraits(actual, expected); +}; + +exports["test composition of identical props does not cause conflict"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1 }), Trait({ a: 0, c: method })); + + assert.equalTraits(actual, { a: Data(0), b: Data(1), c: Method(method) }); +}; + +exports["test composition with identical Trait.required props"] = function (assert) { + var actual = Trait.compose(Trait({ a: Trait.required, b: 1 }), + Trait({ a: Trait.required, c: method })); + + assert.equalTraits(actual, { a: Required(), b: Data(1), c: Method(method) }); +}; + +exports["test composition satisfying a Trait.required prop"] = function (assert) { + var actual = Trait.compose(Trait({ a: Trait.required, b: 1 }), + Trait({ a: method })); + + assert.equalTraits(actual, { a: Method(method), b: Data(1) }); +}; + +exports["test compose is neutral wrt conflicts"] = function (assert) { + var actual = Trait.compose(Trait.compose(Trait({ a: 1 }), Trait({ a: 2 })), + Trait({ b: 0 })); + + assert.equalTraits(actual, { a: Conflict("a"), b: Data(0) }); +}; + +exports["test conflicting prop overrides Trait.required prop"] = function (assert) { + var actual = Trait.compose(Trait.compose(Trait({ a: 1 }), + Trait({ a: 2 })), + Trait({ a: Trait.required })); + + assert.equalTraits(actual, { a: Conflict("a") }); +}; + +exports["test compose is commutative"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1 }), Trait({ c: 2, d: method })); + var expected = Trait.compose(Trait({ c: 2, d: method }), + Trait({ a: 0, b: 1 })); + + assert.equalTraits(actual, expected); +}; + +exports["test compose is commutative, also for Trait.required/conflicting props"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1, c: 3, e: Trait.required }), + Trait({ c: 2, d: method })); + + var expected = Trait.compose(Trait({ c: 2, d: method }), + Trait({ a: 0, b: 1, c: 3, e: Trait.required })); + + assert.equalTraits(actual, expected); +}; + +exports["test compose is associative"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1, c: 3, d: Trait.required }), + Trait.compose(Trait({ c: 3, d: Trait.required }), + Trait({ c: 2, d: method, + e: "foo" }))); + + var expected = Trait.compose( + Trait.compose(Trait({ a: 0, b: 1, c: 3, d: Trait.required }), + Trait({ c: 3, d: Trait.required })), + Trait({ c: 2, d: method, e: "foo" })); + + assert.equalTraits(actual, expected); +}; + +exports["test diamond import of same prop does not generate conflict"] = function (assert) { + var actual = Trait.compose(Trait.compose(Trait({ b: 2 }), Trait({ a: 1 })), + Trait.compose(Trait({ c: 3 }), Trait({ a: 1 })), + Trait({ d: 4 })); + var expected = { a: Data(1), b: Data(2), c: Data(3), d: Data(4) }; + + assert.equalTraits(actual, expected); +}; + +exports["test resolve with empty resolutions has no effect"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: Trait.required, c: method }).resolve({}), + { a: Data(1), b: Required(), c: Method(method) }); +}; + +exports["test resolve: renaming"] = function (assert) { + var actual = Trait({ a: 1, b: Trait.required, c: method }); + + assert.equalTraits(actual.resolve({ a: "A", c: "C" }), + { A: Data(1), b: Required(), C: Method(method), + a: Required(), c: Required() }); +}; + +exports["test resolve: renaming to conflicting name causes conflict, order 1"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: "b" }), + { b: Conflict("b"), a: Required() }); +}; + +exports["test resolve: renaming to conflicting name causes conflict, order 2"] = function (assert) { + assert.equalTraits(Trait({ b: 2, a: 1 }).resolve({ a: "b" }), + { b: Conflict("b"), a: Required() }); +}; + +exports["test resolve: simple exclusion"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: undefined }), + { a: Required(), b: Data(2) }); +}; + +exports["test resolve: exclusion to empty trait"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: null, b: undefined }), + { a: Required(), b: Required() }); +}; + +exports["test resolve: exclusion and renaming of disjoint props"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: undefined, b: "c" }), + { a: Required(), c: Data(2), b: Required() }); +}; + +exports["test resolve: exclusion and renaming of overlapping props"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: undefined, b: "a" }), + { a: Data(2), b: Required() }); +}; + +exports["test resolve: renaming to a common alias causes conflict"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: "c", b: "c" }), + { c: Conflict("c"), a: Required(), b: Required() }); +}; + +exports["test resolve: renaming overrides Trait.required target"] = function (assert) { + assert.equalTraits(Trait({ a: Trait.required, b: 2 }).resolve({ b: "a" }), + { a: Data(2), b: Required() }); +}; + +exports["test resolve: renaming Trait.required properties has no effect"] = function (assert) { + assert.equalTraits(Trait({ a: 2, b: Trait.required }).resolve({ b: "a" }), + { a: Data(2), b: Required() }); +}; + +exports["test resolve: renaming of non-existent props has no effect"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: "c", d: "c" }), + { c: Data(1), b: Data(2), a: Required() }); +}; + +exports["test resolve: exclusion of non-existent props has no effect"] = function (assert) { + assert.equalTraits(Trait({ a: 1 }).resolve({ b: undefined }), { a: Data(1) }); +}; + +exports["test resolve is neutral w.r.t. Trait.required properties"] = function (assert) { + var actual = Trait({ a: Trait.required, b: Trait.required, c: "foo", d: 1 }); + var expected = { a: Required(), b: Required(), c: Data("foo"), d: Data(1) }; + assert.equalTraits(actual.resolve({ a: "c", b: undefined }), expected); +}; + +exports["test resolve supports swapping of property names, ordering 1"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: "b", b: "a" }), + { a: Data(2), b: Data(1) }); +}; + +exports["test resolve supports swapping of property names, ordering 2"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ b: "a", a: "b" }), + { a: Data(2), b: Data(1) }); +}; + +exports["test resolve supports swapping of property names, ordering 3"] = function (assert) { + assert.equalTraits(Trait({ b: 2, a: 1 }).resolve({ b: "a", a: "b" }), + { a: Data(2), b: Data(1) }); +}; + +exports["test resolve supports swapping of property names, ordering 4"] = function (assert) { + assert.equalTraits(Trait({ b: 2, a: 1 }).resolve({ a: "b", b: "a" }), + { a: Data(2), b: Data(1) }); +}; + +exports["test create simple"] = function (assert) { + var o1 = Trait({ + a: 1, + b: function () { + return this.a; + } + }).create(Object.prototype); + + assert.equal(Object.getPrototypeOf(o1), Object.prototype, "o1 prototype"); + assert.equal(1, o1.a, "o1.a"); + assert.equal(1, o1.b(), "o1.b()"); + assert.equal(Object.keys(o1).length, 2, "Object.keys(o1).length === 2"); +}; + +exports["test create with Array.prototype"] = function (assert) { + var o2 = Trait({}).create(Array.prototype); + assert.equal(Object.getPrototypeOf(o2), Array.prototype, "o2 prototype"); +}; + +exports["test exception for incomplete required properties"] = function (assert) { + assert.throws(function () { + Trait({ foo: Trait.required }).create(Object.prototype); + }, /Missing required property: `foo`/, "required prop error"); +}; + +exports["test exception for unresolved conflicts"] = function (assert) { + assert.throws(function () { + Trait.compose(Trait({ a: 0 }), Trait({ a: 1 })).create({}); + }, /Remaining conflicting property: `a`/, "conflicting prop error"); +}; + +exports["test verify that required properties are present but undefined"] = function (assert) { + var o4 = Object.create(Object.prototype, Trait({ foo: Trait.required })); + + assert.ok("foo" in o4, "required property present"); + assert.throws(function () { + o4.foo; + }, /Missing required property: `foo`/, "required prop error"); +}; + +exports["test verify that conflicting properties are present"] = function (assert) { + var o5 = Object.create(Object.prototype, Trait.compose(Trait({ a: 0 }), + Trait({ a: 1 }))); + + assert.ok("a" in o5, "conflicting property present"); + assert.throws(function () { + o5.a; + }, /Remaining conflicting property: `a`/, "conflicting prop access error"); +}; + +exports["test diamond with conflicts"] = function (assert) { + function makeT1(x) { + return Trait({ + m: function () { + return x + } + }) + }; + + function makeT2(x) { + return Trait.compose(Trait({ + t2: "foo" + }), makeT1(x)); + }; + + function makeT3(x) { + return Trait.compose(Trait({ + t3: "bar" + }), makeT1(x)); + }; + + var T4 = Trait.compose(makeT2(5), makeT3(5)); + + assert.throws(function () { + T4.create(Object.prototype); + }, /Remaining conflicting property: `m`/, "diamond prop conflict"); +}; + +exports["test providing requirements through proto"] = function (assert) { + var t = Trait({ required: Trait.required }).create({ required: "test" }); + assert.equal(t.required, "test", "property from proto is inherited"); +}; + +if (module == require.main) + require("test").run(exports); diff --git a/tools/addon-sdk-1.4/packages/api-utils/tests/traits/utils.js b/tools/addon-sdk-1.4/packages/api-utils/tests/traits/utils.js new file mode 100644 index 0000000..5647fb9 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/api-utils/tests/traits/utils.js @@ -0,0 +1,52 @@ +"use strict"; + +var ERR_CONFLICT = "Remaining conflicting property: "; +var ERR_REQUIRED = "Missing required property: "; + +exports.Data = function Data(value, enumerable, configurable, writable) { + return ({ + value: value, + enumerable: enumerable !== false, + configurable: configurable !== false, + writable: writable !== false + }); +}; + +exports.Method = function Method(method, enumerable, configurable, writable) { + return ({ + value: method, + enumerable: enumerable !== false, + configurable: configurable !== false, + writable: writable !== false + }); +}; + +exports.Accessor = function Accessor(get, set, enumerable, configurable) { + return ({ + get: get, + set: set, + enumerable: enumerable !== false, + configurable: configurable !== false + }); +}; + +exports.Required = function Required(name) { + function required() { throw new Error(ERR_REQUIRED + name) } + + return ({ + get: required, + set: required, + required: true + }); +}; + +exports.Conflict = function Conflict(name) { + function conflict() { throw new Error(ERR_CONFLICT + name) } + + return ({ + get: conflict, + set: conflict, + conflict: true + }); +}; + diff --git a/tools/addon-sdk-1.4/packages/development-mode/README.md b/tools/addon-sdk-1.4/packages/development-mode/README.md new file mode 100644 index 0000000..af674d1 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/development-mode/README.md @@ -0,0 +1,8 @@ +This package contains a program that pings a local server for +information about other programs to run. If the same program +is requested more than once, it is first unloaded before being run +again. This can be very useful for developing add-ons without +having to restart the parent application. + +In the future, functionality will be added that allows developers to +debug and profile their programs' behavior. diff --git a/tools/addon-sdk-1.4/packages/development-mode/docs/bootstrap.md b/tools/addon-sdk-1.4/packages/development-mode/docs/bootstrap.md new file mode 100644 index 0000000..2e0156a --- /dev/null +++ b/tools/addon-sdk-1.4/packages/development-mode/docs/bootstrap.md @@ -0,0 +1,6 @@ +This module contains functionality that allows a program to +"bootstrap" other programs: that is, set up a runtime +environment for them and execute them. + +At present, the functions defined by this module are +implementation-specific and subject to change. diff --git a/tools/addon-sdk-1.4/packages/development-mode/docs/main.md b/tools/addon-sdk-1.4/packages/development-mode/docs/main.md new file mode 100644 index 0000000..1669c49 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/development-mode/docs/main.md @@ -0,0 +1,5 @@ +This program simply pings a local "task queue" server using AJAX long +polling. When a program needs to be run, this program obtains +a JSON blob containing configuration information from the task queue +server and uses it to bootstrap the program. This goes on +indefinitely, until the application exits. diff --git a/tools/addon-sdk-1.4/packages/development-mode/lib/bootstrap.js b/tools/addon-sdk-1.4/packages/development-mode/lib/bootstrap.js new file mode 100644 index 0000000..6986c60 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/development-mode/lib/bootstrap.js @@ -0,0 +1,153 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack SDK. + * + * The Initial Developer of the Original Code is + * Atul Varma <atul@mozilla.com>. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +var {Cc,Cm,Ci,components} = require("chrome"); +var file = require("api-utils/file"); + +// A global registry of all Jetpack Programs that we're responsible +// for loading and unloading. Keys are XPCOM contract IDs of +// the harness services of Jetpack Programs, values are the +// wrappedJSObject of the harness services themselves. +var gServices = {}; + +var manager = Cm; +manager.QueryInterface(Ci.nsIComponentRegistrar); + +// Attempts to unload and then unregister the XPCOM component with the +// given contract ID, if it is managed by us. +function maybeUnload(contractID) { + if (contractID in gServices) { + try { + gServices[contractID].unload(); + } catch (e) { + console.exception(e); + } + var classID = gServices[contractID].classID; + delete gServices[contractID]; + maybeUnregister(contractID, classID); + } +} + +// Attempts to unregister the XPCOM component with the given +// contract ID and class ID, if it is managed by us. +function maybeUnregister(contractID, classID) { + try { + var factory = manager.getClassObjectByContractID(contractID, + Ci.nsIFactory); + manager.unregisterFactory(classID, factory); + } catch (e) { + console.exception(e); + } +} + +// A quit callable that is passed to the main() function of any +// Jetpack Program we manage. Whenever said program quits, we will +// automatically take care of unloading and unregistering it. +function makeQuit(contractID) { + return function quit(status) { + maybeUnload(contractID); + }; +} + +function logError(e) { + console.exception(e); +} + +function makeUnloader(contractID) { + return {unload: function unload() { maybeUnload(contractID); }}; +} + +// The main public function of this module; given a JSON harness options +// blob and a root directory of where the Jetpack Program +// is installed, takes care of loading the program, running it, and +// unloading its resources when they're no longer needed. +exports.run = function run(options, rootDirPath, dump) { + var harnessService; + var contractID = options.bootstrap.contractID; + var classID = components.ID(options.bootstrap.classID); + + maybeUnload(contractID); + options.runImmediately = true; + + var rootDir = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + rootDir.initWithPath(rootDirPath); + + // Note that we're reusing our own bootstrapping code here, rather + // than directly invoking the target Jetpack Program's bootstrapping + // infrastructure. If one works differently than the other, + // we could have problems, but for now we'll assume that this + // bootstrapping infrastructure is stable. + var HarnessService = packaging.buildHarnessService(rootDir, + dump, + logError, + makeQuit(contractID), + options); + var factory = HarnessService.prototype._xpcom_factory; + var proto = HarnessService.prototype; + manager.registerFactory(proto.classID, + proto.classDescription, + proto.contractID, + factory); + + try { + harnessService = factory.createInstance(null, Ci.nsISupports); + harnessService = harnessService.wrappedJSObject; + gServices[contractID] = harnessService; + harnessService.load(); + return makeUnloader(contractID, classID); + } catch (e) { + console.exception(e); + return null; + } +}; + +// When this module is unloaded, shut down all currently-running +// Jetpack Programs we manage and free their resources. +require("unload").when( + function() { + var argLists = []; + for (contractID in gServices) + argLists.push([contractID, gServices[contractID].classID]); + + argLists.forEach( + function(args) { + maybeUnload.apply(undefined, args); + }); + }); diff --git a/tools/addon-sdk-1.4/packages/development-mode/lib/main.js b/tools/addon-sdk-1.4/packages/development-mode/lib/main.js new file mode 100644 index 0000000..61854c9 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/development-mode/lib/main.js @@ -0,0 +1,95 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack SDK. + * + * The Initial Developer of the Original Code is + * Atul Varma <atul@mozilla.com>. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +var print; + +var {Cc,Ci} = require("chrome"); +var xhr = require("api-utils/xhr"); + +// TODO: Eventually we should be able to e.g. require("os").environ +// rather than access this XPCOM service directly. +var environ = Cc["@mozilla.org/process/environment;1"] + .getService(Ci.nsIEnvironment); + +function runTask(options) { + require("./bootstrap").run(options, packaging.root.path, print); + processNextTask(); +} + +function processNextTask() { + var req = new xhr.XMLHttpRequest(); + var port = environ.get("JETPACK_DEV_SERVER_PORT"); + var url = "http://localhost:" + port + "/api/task-queue/get"; + req.open("GET", url); + req.onreadystatechange = function() { + if (req.readyState == 4) { + if (req.status == 200) { + if (req.responseText) { + runTask(JSON.parse(req.responseText)); + } else + processNextTask(); + } else { + require("api-utils/timer").setTimeout(processNextTask, 1000); + } + } + }; + req.send(null); +} + +function makeMainWindow(quit) { + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Ci.nsIWindowWatcher); + var text = "Now in development mode. Close this window to exit."; + var window = ww.openWindow(null, "data:text/plain," + encodeURI(text), + "development-mode", "centerscreen", null); + + window.addEventListener("close", function() quit("OK"), false); +} + +exports.main = function(options, callbacks) { + var appInfo = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULAppInfo); + + print = callbacks.print; + if (appInfo.ID == "xulapp@toolness.com") + // We're running barebones XULRunner, open a default window. + makeMainWindow(callbacks.quit); + console.log("Starting."); + processNextTask(); +}; diff --git a/tools/addon-sdk-1.4/packages/development-mode/package.json b/tools/addon-sdk-1.4/packages/development-mode/package.json new file mode 100644 index 0000000..1dc71a3 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/development-mode/package.json @@ -0,0 +1,10 @@ +{ + "name": "development-mode", + "description": "Adds Add-on SDK development functionality to a program.", + "keywords": ["jetpack-low-level"], + "author": "Atul Varma (http://toolness.com/)", + "license": "MPL 1.1/GPL 2.0/LGPL 2.1", + "main": "main", + "dependencies": ["api-utils", "test-harness"], + "id": "anonid0-development-mode" +} diff --git a/tools/addon-sdk-1.4/packages/test-harness/README.md b/tools/addon-sdk-1.4/packages/test-harness/README.md new file mode 100644 index 0000000..a833d34 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/test-harness/README.md @@ -0,0 +1,8 @@ +<span class="aside"> +For more information on testing in the Add-on SDK, see the +[Reusable Modules](dev-guide/addon-development/implementing-reusable-module.html) +tutorial. +</span> + +This package contains a program that finds and runs tests. It is +automatically used whenever the `cfx test` command is executed. diff --git a/tools/addon-sdk-1.4/packages/test-harness/docs/harness.md b/tools/addon-sdk-1.4/packages/test-harness/docs/harness.md new file mode 100644 index 0000000..9f6cd7b --- /dev/null +++ b/tools/addon-sdk-1.4/packages/test-harness/docs/harness.md @@ -0,0 +1,2 @@ +This module contains the bulk of the test harness setup and execution +implementation. diff --git a/tools/addon-sdk-1.4/packages/test-harness/docs/run-tests.md b/tools/addon-sdk-1.4/packages/test-harness/docs/run-tests.md new file mode 100644 index 0000000..38f4569 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/test-harness/docs/run-tests.md @@ -0,0 +1,9 @@ +<span class="aside"> +For more information on testing in the Add-on SDK, see the +[Reusable Modules](dev-guide/addon-development/implementing-reusable-module.html) +tutorial. +</span> + +This module contains the package's main program, which does a +bit of high-level setup and then delegates test finding and running to +the `harness` module. diff --git a/tools/addon-sdk-1.4/packages/test-harness/lib/harness.js b/tools/addon-sdk-1.4/packages/test-harness/lib/harness.js new file mode 100644 index 0000000..a5a9284 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/test-harness/lib/harness.js @@ -0,0 +1,352 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * Irakli Gozalishvili <gozala@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const { Cc,Ci } = require("chrome"); +const { Loader } = require("@loader") + +var cService = Cc['@mozilla.org/consoleservice;1'].getService() + .QueryInterface(Ci.nsIConsoleService); + +// Cuddlefish loader for the sandbox in which we load and +// execute tests. +var sandbox; + +// Function to call when we're done running tests. +var onDone; + +// Function to print text to a console, w/o CR at the end. +var print; + +// How many more times to run all tests. +var iterationsLeft; + +// Only tests in files whose names match this regexp filter will be run. +var filter; + +// Whether to report memory profiling information. +var profileMemory; + +// Combined information from all test runs. +var results = { + passed: 0, + failed: 0, + testRuns: [] +}; + +// JSON serialization of last memory usage stats; we keep it stringified +// so we don't actually change the memory usage stats (in terms of objects) +// of the JSRuntime we're profiling. +var lastMemoryUsage; + +function analyzeRawProfilingData(data) { + var graph = data.graph; + var shapes = {}; + + // Convert keys in the graph from strings to ints. + // TODO: Can we get rid of this ridiculousness? + var newGraph = {}; + for (id in graph) { + newGraph[parseInt(id)] = graph[id]; + } + graph = newGraph; + + var modules = 0; + var moduleIds = []; + var moduleObjs = {UNKNOWN: 0}; + for (let name in data.namedObjects) { + moduleObjs[name] = 0; + moduleIds[data.namedObjects[name]] = name; + modules++; + } + + var count = 0; + for (id in graph) { + var parent = graph[id].parent; + while (parent) { + if (parent in moduleIds) { + var name = moduleIds[parent]; + moduleObjs[name]++; + break; + } + if (!(parent in graph)) { + moduleObjs.UNKNOWN++; + break; + } + parent = graph[parent].parent; + } + count++; + } + + print("\nobject count is " + count + " in " + modules + " modules" + + " (" + data.totalObjectCount + " across entire JS runtime)\n"); + if (lastMemoryUsage) { + var last = JSON.parse(lastMemoryUsage); + var diff = { + moduleObjs: dictDiff(last.moduleObjs, moduleObjs), + totalObjectClasses: dictDiff(last.totalObjectClasses, + data.totalObjectClasses) + }; + + for (let name in diff.moduleObjs) + print(" " + diff.moduleObjs[name] + " in " + name + "\n"); + for (let name in diff.totalObjectClasses) + print(" " + diff.totalObjectClasses[name] + " instances of " + + name + "\n"); + } + lastMemoryUsage = JSON.stringify( + {moduleObjs: moduleObjs, + totalObjectClasses: data.totalObjectClasses} + ); +} + +function dictDiff(last, curr) { + var diff = {}; + + for (let name in last) { + var result = (curr[name] || 0) - last[name]; + if (result) + diff[name] = (result > 0 ? "+" : "") + result; + } + for (let name in curr) { + var result = curr[name] - (last[name] || 0); + if (result) + diff[name] = (result > 0 ? "+" : "") + result; + } + return diff; +} + +function reportMemoryUsage() { + memory.gc(); + sandbox.memory.gc(); + + var mgr = Cc["@mozilla.org/memory-reporter-manager;1"] + .getService(Ci.nsIMemoryReporterManager); + var reporters = mgr.enumerateReporters(); + if (reporters.hasMoreElements()) + print("\n"); + while (reporters.hasMoreElements()) { + var reporter = reporters.getNext(); + reporter.QueryInterface(Ci.nsIMemoryReporter); + print(reporter.description + ": " + reporter.memoryUsed + "\n"); + } + + var weakrefs = [info.weakref.get() + for each (info in sandbox.memory.getObjects())]; + weakrefs = [weakref for each (weakref in weakrefs) if (weakref)]; + print("Tracked memory objects in testing sandbox: " + + weakrefs.length + "\n"); +} + +var gWeakrefInfo; + +function showResults() { + memory.gc(); + + if (gWeakrefInfo) { + gWeakrefInfo.forEach( + function(info) { + var ref = info.weakref.get(); + if (ref !== null) { + var data = ref.__url__ ? ref.__url__ : ref; + var warning = data == "[object Object]" + ? "[object " + data.constructor.name + "(" + + [p for (p in data)].join(", ") + ")]" + : data; + console.warn("LEAK", warning, info.bin); + } + } + ); + } + + print("\n"); + var total = results.passed + results.failed; + print(results.passed + " of " + total + " tests passed.\n"); + onDone(results); +} + +function cleanup() { + try { + for (let name in sandbox.modules) + sandbox.globals.memory.track(sandbox.modules[name], + "module global scope: " + name); + sandbox.globals.memory.track(sandbox, "Cuddlefish Loader"); + + if (profileMemory) { + gWeakrefInfo = [{ weakref: info.weakref, bin: info.bin } + for each (info in sandbox.globals.memory.getObjects())]; + } + + sandbox.unload(); + + if (sandbox.globals.console.errorsLogged && !results.failed) { + results.failed++; + console.error("warnings and/or errors were logged."); + } + + if (consoleListener.errorsLogged && !results.failed) { + console.warn(consoleListener.errorsLogged + " " + + "warnings or errors were logged to the " + + "platform's nsIConsoleService, which could " + + "be of no consequence; however, they could also " + + "be indicative of aberrant behavior."); + } + + consoleListener.errorsLogged = 0; + sandbox = null; + + memory.gc(); + } catch (e) { + results.failed++; + console.error("unload.send() threw an exception."); + console.exception(e); + }; + + require("api-utils/timer").setTimeout(showResults, 1); +} + +function nextIteration(tests) { + if (tests) { + results.passed += tests.passed; + results.failed += tests.failed; + + if (profileMemory) + reportMemoryUsage(); + + let testRun = []; + for each (let test in tests.testRunSummary) { + let testCopy = {}; + for (let info in test) { + testCopy[info] = test[info]; + } + testRun.push(testCopy); + } + + results.testRuns.push(testRun); + iterationsLeft--; + } + + if (iterationsLeft) { + let require = Loader.require.bind(sandbox, module.path); + require("api-utils/unit-test").findAndRunTests({ + testOutOfProcess: require('@packaging').enableE10s, + testInProcess: true, + filter: filter, + onDone: nextIteration + }); + } + else { + require("api-utils/timer").setTimeout(cleanup, 0); + } +} + +var POINTLESS_ERRORS = [ + "Invalid chrome URI:", + "OpenGL LayerManager Initialized Succesfully." +]; + +var consoleListener = { + errorsLogged: 0, + observe: function(object) { + if (!(object instanceof Ci.nsIScriptError)) + return; + this.errorsLogged++; + var message = object.QueryInterface(Ci.nsIConsoleMessage).message; + var pointless = [err for each (err in POINTLESS_ERRORS) + if (message.indexOf(err) == 0)]; + if (pointless.length == 0 && message) + print("console: " + message + "\n"); + } +}; + +function TestRunnerConsole(base, options) { + this.__proto__ = { + errorsLogged: 0, + warn: function warn() { + this.errorsLogged++; + base.warn.apply(base, arguments); + }, + error: function error() { + this.errorsLogged++; + base.error.apply(base, arguments); + }, + info: function info(first) { + if (options.verbose) + base.info.apply(base, arguments); + else + if (first == "pass:") + print("."); + }, + __proto__: base + }; +} + +var runTests = exports.runTests = function runTests(options) { + iterationsLeft = options.iterations; + filter = options.filter; + profileMemory = options.profileMemory; + onDone = options.onDone; + print = options.print; + + try { + cService.registerListener(consoleListener); + + var ptc = require("api-utils/plain-text-console"); + var url = require("api-utils/url"); + var system = require("api-utils/system"); + + print("Running tests on " + system.name + " " + system.version + + "/Gecko " + system.platformVersion + " (" + + system.id + ") under " + + system.platform + "/" + system.architecture + ".\n"); + + sandbox = Loader.new(require("@packaging")); + Object.defineProperty(sandbox.globals, 'console', { + value: new TestRunnerConsole(new ptc.PlainTextConsole(print), options) + }); + + nextIteration(); + } catch (e) { + print(require("api-utils/traceback").format(e) + "\n" + e + "\n"); + onDone({passed: 0, failed: 1}); + } +}; + +require("api-utils/unload").when(function() { + cService.unregisterListener(consoleListener); +}); diff --git a/tools/addon-sdk-1.4/packages/test-harness/lib/run-tests.js b/tools/addon-sdk-1.4/packages/test-harness/lib/run-tests.js new file mode 100644 index 0000000..4ea6e3f --- /dev/null +++ b/tools/addon-sdk-1.4/packages/test-harness/lib/run-tests.js @@ -0,0 +1,133 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma <atul@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +var obsvc = require("api-utils/observer-service"); +var system = require("api-utils/system"); +var options = require('@packaging'); +var {Cc,Ci} = require("chrome"); + +function runTests(iterations, filter, profileMemory, verbose, rootPaths, exit, print) { + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Ci.nsIWindowWatcher); + + let ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + let msg = 'Running tests...'; + let markup = '<?xml version="1.0"?><window xmlns="' + ns + + '" windowtype="test:runner"><label>' + msg + '</label></window>'; + let url = "data:application/vnd.mozilla.xul+xml," + escape(markup); + + + var window = ww.openWindow(null, url, "harness", "centerscreen", null); + + var harness = require("./harness"); + + function onDone(tests) { + window.close(); + if (tests.failed == 0) { + if (tests.passed === 0) + print("No tests were run\n"); + exit(0); + } else { + printFailedTests(tests, verbose, print); + exit(1); + } + }; + + // We have to wait for this window to be fully loaded *and* focused + // in order to avoid it to mess with our various window/focus tests. + // We are first waiting for our window to be fully loaded before ensuring + // that it will take the focus, and then we wait for it to be focused. + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + window.addEventListener("focus", function onfocus() { + window.removeEventListener("focus", onfocus, true); + // Finally, we have to run test on next cycle, otherwise XPCOM components + // are not correctly updated. + // For ex: nsIFocusManager.getFocusedElementForWindow may throw + // NS_ERROR_ILLEGAL_VALUE exception. + require("timer").setTimeout(function () { + harness.runTests({iterations: iterations, + filter: filter, + profileMemory: profileMemory, + verbose: verbose, + rootPaths: rootPaths, + print: print, + onDone: onDone}); + }, 0); + }, true); + window.focus(); + }, true); +} + +function printFailedTests(tests, verbose, print) { + if (!verbose) + return; + + let iterationNumber = 0; + let singleIteration = tests.testRuns.length == 1; + let padding = singleIteration ? "" : " "; + + print("\nThe following tests failed:\n"); + + for each (let testRun in tests.testRuns) { + iterationNumber++; + + if (!singleIteration) + print(" Iteration " + iterationNumber + ":\n"); + + for each (let test in testRun) { + if (test.failed > 0) { + print(padding + " " + test.name + ": " + test.errors +"\n"); + } + } + print("\n"); + } +} + +exports.main = function main() { + var testsStarted = false; + + if (!testsStarted) { + testsStarted = true; + runTests(options.iterations, options.filter, + options.profileMemory, options.verbose, + options.rootPaths, system.exit, + dump); + } +}; diff --git a/tools/addon-sdk-1.4/packages/test-harness/package.json b/tools/addon-sdk-1.4/packages/test-harness/package.json new file mode 100644 index 0000000..1ef10a5 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/test-harness/package.json @@ -0,0 +1,8 @@ +{ + "name": "test-harness", + "description": "A harness for running Jetpack tests.", + "author": "Atul Varma (http://toolness.com/)", + "keywords": ["jetpack-low-level"], + "license": "MPL 1.1/GPL 2.0/LGPL 2.1", + "dependencies": ["api-utils"] +} diff --git a/tools/addon-sdk-1.4/packages/test-harness/tests/test-packaging.js b/tools/addon-sdk-1.4/packages/test-harness/tests/test-packaging.js new file mode 100644 index 0000000..d030267 --- /dev/null +++ b/tools/addon-sdk-1.4/packages/test-harness/tests/test-packaging.js @@ -0,0 +1,14 @@ +var url = require("url"); +var file = require("file"); +var {Cm,Ci} = require("chrome"); +var options = require("@packaging"); + +exports.testPackaging = function(test) { + test.assertEqual(options.main, + 'test-harness/run-tests', + "main program should be the test harness"); + + test.assertEqual(options.metadata['test-harness'].author, + 'Atul Varma (http://toolness.com/)', + "packaging metadata should be available"); +}; |