diff options
Diffstat (limited to 'tools/addon-sdk-1.3/packages/addon-kit/lib')
16 files changed, 5202 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.3/packages/addon-kit/lib/clipboard.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/clipboard.js new file mode 100644 index 0000000..24f4641 --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/context-menu.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/context-menu.js new file mode 100644 index 0000000..3c43e35 --- /dev/null +++ b/tools/addon-sdk-1.3/packages/addon-kit/lib/context-menu.js @@ -0,0 +1,1527 @@ +/* -*- 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) { + let { opt } = apiUtils.validateOptions({ opt: opt }, { opt: rule }); + return 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.3/packages/addon-kit/lib/hotkeys.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/hotkeys.js new file mode 100644 index 0000000..f8c6434 --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/notifications.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/notifications.js new file mode 100644 index 0000000..f2a4ea2 --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/page-mod.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/page-mod.js new file mode 100644 index 0000000..d511464 --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/page-worker.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/page-worker.js new file mode 100644 index 0000000..93750a1 --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/panel.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/panel.js new file mode 100644 index 0000000..6fbe4fc --- /dev/null +++ b/tools/addon-sdk-1.3/packages/addon-kit/lib/panel.js @@ -0,0 +1,404 @@ +/* ***** 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 { 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"); + +require("api-utils/xpcom").utils.defineLazyServiceGetter( + this, + "windowMediator", + "@mozilla.org/appshell/window-mediator;1", + "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 + return this.on('inited', this._onShow.bind(this)); + 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.3/packages/addon-kit/lib/passwords.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/passwords.js new file mode 100644 index 0000000..8225da9 --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/private-browsing.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/private-browsing.js new file mode 100644 index 0000000..8336e7b --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/request.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/request.js new file mode 100644 index 0000000..a72b28c --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/selection.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/selection.js new file mode 100644 index 0000000..48db7fc --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/simple-storage.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/simple-storage.js new file mode 100644 index 0000000..b719e27 --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/tabs.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/tabs.js new file mode 100644 index 0000000..81681d6 --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/timers.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/timers.js new file mode 100644 index 0000000..9373dea --- /dev/null +++ b/tools/addon-sdk-1.3/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.3/packages/addon-kit/lib/widget.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/widget.js new file mode 100644 index 0000000..39f825a --- /dev/null +++ b/tools/addon-sdk-1.3/packages/addon-kit/lib/widget.js @@ -0,0 +1,938 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** 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 + }, +}; + +// Widgets attributes definition +let widgetAttributes = { + label: valid.label, + id: valid.id, + tooltip: valid.string, + width: valid.width, + content: valid.string, + panel: valid.panel +}; + +// 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.3/packages/addon-kit/lib/windows.js b/tools/addon-sdk-1.3/packages/addon-kit/lib/windows.js new file mode 100644 index 0000000..e9e097b --- /dev/null +++ b/tools/addon-sdk-1.3/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; + |