diff options
Diffstat (limited to 'ceee/firefox/content')
-rw-r--r-- | ceee/firefox/content/about.xul | 29 | ||||
-rw-r--r-- | ceee/firefox/content/bookmarks_api.js | 605 | ||||
-rw-r--r-- | ceee/firefox/content/ceee.png | bin | 0 -> 1062 bytes | |||
-rw-r--r-- | ceee/firefox/content/cf.js | 370 | ||||
-rw-r--r-- | ceee/firefox/content/cookie_api.js | 484 | ||||
-rw-r--r-- | ceee/firefox/content/externs.js | 91 | ||||
-rw-r--r-- | ceee/firefox/content/infobars_api.js | 218 | ||||
-rw-r--r-- | ceee/firefox/content/js-coverage.js | 630 | ||||
-rw-r--r-- | ceee/firefox/content/options.xul | 95 | ||||
-rw-r--r-- | ceee/firefox/content/overlay.js | 964 | ||||
-rw-r--r-- | ceee/firefox/content/overlay.xul | 84 | ||||
-rw-r--r-- | ceee/firefox/content/sidebar_api.js | 691 | ||||
-rw-r--r-- | ceee/firefox/content/tab_api.js | 480 | ||||
-rw-r--r-- | ceee/firefox/content/us/base.js | 1291 | ||||
-rw-r--r-- | ceee/firefox/content/us/ceee_bootstrap.js | 254 | ||||
-rw-r--r-- | ceee/firefox/content/us/json.js | 318 | ||||
-rw-r--r-- | ceee/firefox/content/userscript_api.js | 1077 | ||||
-rw-r--r-- | ceee/firefox/content/window_api.js | 355 |
18 files changed, 8036 insertions, 0 deletions
diff --git a/ceee/firefox/content/about.xul b/ceee/firefox/content/about.xul new file mode 100644 index 0000000..4d1792f6 --- /dev/null +++ b/ceee/firefox/content/about.xul @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. + + Defines the About dialog box. + --> + +<!DOCTYPE dialog SYSTEM "chrome://ceee/locale/about.dtd"> + +<dialog title="&about; Google Chrome Extensions Execution Environment" orient="vertical" autostretch="always" + onload="sizeToContent()" buttons="accept" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <groupbox align="center" orient="horizontal"> + <vbox> + <text value="Google Chrome Extensions Execution Environment" + style="font-weight: bold; font-size: x-large;"/> + <text value="&version; 0.2"/> + <separator class="thin"/> + <text value="&createdBy;" style="font-weight: bold;"/> + <text value="Google Inc."/> + <separator class="thin"/> + <text value="&homepage;" style="font-weight: bold;"/> + <text value="http://www.google.com" class="url" + onclick="window.open('http://www.google.com'); window.close();"/> + </vbox> + </groupbox> +</dialog> diff --git a/ceee/firefox/content/bookmarks_api.js b/ceee/firefox/content/bookmarks_api.js new file mode 100644 index 0000000..a95643e --- /dev/null +++ b/ceee/firefox/content/bookmarks_api.js @@ -0,0 +1,605 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file contains the implementation of the Chrome extension + * bookmarks APIs for the CEEE Firefox add-on. This file is loaded by + * the overlay.xul file, and requires that overlay.js has already been loaded. + * + * @supported Firefox 3.x + */ + +/** + * Place-holder namespace-object for helper functions used in this file. + * @private + */ +var CEEE_bookmarks_internal_ = { + /** Reference to the instance object for the ceee. @private */ + ceeeInstance_: null, + + /** + * Log an informational message to the Firefox error console. + * @private + */ + logInfo_: null, + + /** + * Log an error message to the Firefox error console. + * @private + */ + logError_: null +}; + +/** + * Iterates through all of the children of a given bookmark folder, invoking + * the callback for each child. + * + * @param {number} bookmarkId The id of the bookmark to find. + * @param {Function} callback Function to invoke for each child bookmark. If + * the function returns false, then the iteration breaks early. + * @private + */ +CEEE_bookmarks_internal_.forEachChild_ = function(bookmarkId, callback) { + try { + var index = 0; + // The Mozilla nsINavBookmarksService does not provide a means to directly + // access the number of bookmarks within a folder. Instead we iterate + // from 0 until an exception is thrown. + // TODO(twiz@chromium.org): Investigate if the bookmarks within a + // folder may be sparse, and so this loop will break too early. + do { + var childId = CEEE_bookmarks.getIdForItemAt(bookmarkId, index++); + // Using the !operator would coerce null values into false, which is + // not the break condition. We want to stop ONLY if false is returned. + if (false === callback(childId)) { + return; + } + } while(true); + } catch (e) { + // Catch the out-of-bounds exception thrown when accessing the element past + // the end of the set of children, otherwise pass the exception on. + if (e && e.result && + e.result == Components.results.NS_ERROR_ILLEGAL_VALUE) { + return; + } else { + throw e; + } + } +}; + +/** @typedef {{id: number, + * title: string, + * parentId: number, + * url: string, + * children: Array.<CEEE_bookmarks_internal_.BookmarkItem>}} + */ +CEEE_bookmarks_internal_.BookmarkItem; + +/** + * Packages an object containing the members specified by the Chrome + * bookmark extension BookmarkItem object. + * + * @param {number} bookmarkId The id of the bookmark to find. + * @param {string} title The title of the bookmark. + * @param {number} parentId The id of the parent of the bookmark. + * @param {Object} url A url object, as constructed by nsIIOService. If the + * bookmark is a folder, then this parameter should be null. + * @param {Array} children The array of children of the bookmark. Should be + * null if a url is provided. + * @return { {id: number, title: string, parentId: number, url: string, + * children: Array} } A BookmarkItem. + * @private + */ +CEEE_bookmarks_internal_.createChromeBookmark_ = function(bookmarkId, + title, parentId, url, children) { + return { + 'id': bookmarkId, + 'title': title, + 'parentId': parentId, + 'url': url ? url.spec : null, + 'children': children || []}; +}; + +/** + * Constructs and returns a BookmarkItem structure as defined by the + * Chrome extension bookmark model corresponding to the Mozilla places + * bookmark with the given id. + * + * @param {number} bookmarkId The id of the bookmark to find. + * @return {CEEE_bookmarks_internal_.BookmarkItem} + * If a bookmark with the given id is found, returns an object with + * the following properties defined by the Chrome bookmark API. + * @private + */ +CEEE_bookmarks_internal_.getBookmarkItemFromId_ = function(bookmarkId) { + var title = CEEE_bookmarks.getItemTitle(bookmarkId); + var parentId = CEEE_bookmarks.getFolderIdForItem(bookmarkId); + var url; + + try { + url = CEEE_bookmarks.getBookmarkURI(bookmarkId); + } catch (e) { + // If a bookmark does not exist for this id, getBookmarkURI will throw + // NS_ERROR_INVALID_ARG. We catch that here and return null instead. + if (!(e && e.result && + e.result == Components.results.NS_ERROR_INVALID_ARG)) { + throw e; + } + } + + var item = this.createChromeBookmark_(bookmarkId, title, parentId, url); + + this.forEachChild_(bookmarkId, function(childId) { + item.children.push(childId); + }); + return item; +}; + +/** + * Returns an array of BookmarkItem objects, as defined by the Chrome bookmark + * extension API for the children of the given folder. + * + * @param {number} folderId The id of the folder whose children are to + * be returned. + * @return {Array} An array of BookmarItem objects representing the children + * of the folder with id passed as folderId. + * @private + */ +CEEE_bookmarks_internal_.getChildren_ = function(folderId) { + var itemType = CEEE_bookmarks.getItemType(folderId); + var children = []; + + // Bookmarks and separators do not have children, only folders have children. + if (CEEE_bookmarks.TYPE_BOOKMARK == itemType || + CEEE_bookmarks.TYPE_SEPARATOR == itemType) { + return children; + } + + var impl = this; + this.forEachChild_(folderId, function(childId) { + var treeNode = impl.createChromeBookmark_( + childId, CEEE_bookmarks.getItemTitle(childId), folderId); + + var itemType = CEEE_bookmarks.getItemType(childId); + if (CEEE_bookmarks.TYPE_BOOKMARK == itemType) { + var url = CEEE_bookmarks.getBookmarkURI(childId); + if (url) { + treeNode.url = url.spec; + } + } else if (CEEE_bookmarks.TYPE_SEPARATOR != itemType) { + treeNode.children = impl.getChildren_(childId); + } + children.push(treeNode); + }); + + return children; +}; + +/** + * Helper function for searching for a set of search terms in a given text. + * + * @param {!Array.<string>} querySet An array of strings, for which we are + * searching. + * @param {string} text A string in which we are looking for all of the + * elements of querySet. + * @return {boolean} Whether all elements in querySet are present in text. + * @private + */ +CEEE_bookmarks_internal_.textInQuerySet_ = function(querySet, text) { + if (!text) { + return false; + } + + for (var wordIndex = 0; wordIndex < querySet.length; ++wordIndex) { + if (-1 == text.indexOf(querySet[wordIndex])) { + return false; + } + } + + return true; +}; + +/** + * Searches for the set of bookmarks containing all elements of the strings + * in querySet. The search starts at item bookmarkId, and recursively visits + * all children items. + * When a bookmark item is found, onMatch is called to register the match. + * If onMatch returns false, the search terminates. + * + * @param {!Array.<string>} querySet Array of strings to find in the title/url + * of the bookmarks contained in the tree rooted at bookmarkId. + * @param {number} bookmarkId The id of the folder/item to search. + * @param {function(number):boolean} onMatch Function to call when a match is + * found. + * @return {boolean} true if the search was exhaustive, and all bookmark + * children were examined. + * @private + */ +CEEE_bookmarks_internal_.getBookmarksContainingText_ = + function(querySet, bookmarkId, onMatch) { + var itemType = CEEE_bookmarks.getItemType(bookmarkId); + if (CEEE_bookmarks.TYPE_FOLDER == itemType || + CEEE_bookmarks.TYPE_DYNAMIC_CONTAINER == itemType) { + var impl = this; + // Recurse on each of the children of bookmark with id bookmarkId. + this.forEachChild_(bookmarkId, function(childId) { + if (!impl.getBookmarksContainingText_(querySet, childId, onMatch)) { + return false; + } + }); + } else { + var title = CEEE_bookmarks.getItemTitle(bookmarkId); + var url = CEEE_bookmarks.getBookmarkURI(bookmarkId); + if (url) { + url = url.spec; + } + + if (this.textInQuerySet_(querySet, title) || + this.textInQuerySet_(querySet, url)) { + return onMatch(bookmarkId); + } + } + + return true; +}; + + +/** + * Command name for Chrome extension bookmarks API function, + * chrome.bookmarks.getChildren. + */ +CEEE_bookmarks_internal_.CMD_GET_BOOKMARK_CHILDREN = + 'bookmarks.getChildren'; + +/** + * Implementation of Chrome extension bookmarks API function, + * chrome.bookmarks.getTree. + * See + * http://dev.chromium.org/developers/design-documents/extensions/bookmarks-api + */ +CEEE_bookmarks_internal_.getBookmarkChildren_ = function(cmd, data) { + if (!data || !data.args) { + return; + } + + var bookmarkId = data.args; + var children = []; + var impl = this; + this.forEachChild_(bookmarkId, function(childId) { + children.push(impl.getBookmarkItemFromId_(childId)); + }); + return children; +}; + +/** + * Command name for Chrome extension bookmarks API function, + * chrome.bookmarks.getTree. + */ +CEEE_bookmarks_internal_.CMD_GET_BOOKMARK_TREE = 'bookmarks.getTree'; + +/** + * Implementation of Chrome extension bookmarks API function, + * chrome.bookmarks.getTree. + * See + * http://dev.chromium.org/developers/design-documents/extensions/bookmarks-api + */ +CEEE_bookmarks_internal_.getBookmarkTree_ = function(cmd, data) { + var treeRoot = this.createChromeBookmark_( + CEEE_bookmarks.placesRoot, + CEEE_bookmarks.getItemTitle(CEEE_bookmarks.placesRoot)); + // TODO(twiz@chromium.org): This routine presently returns the root + // of places, which includes elements outside of the normal + // 'bookmarks scope', such as the recent browsing histroy, etc. It + // should be determined if bookmarksMenuFolder or + // unfiledBookmarksFolder is more appropriate. + treeRoot.children = this.getChildren_(CEEE_bookmarks.placesRoot); + return treeRoot; +}; + +/** + * Command name for Chrome extension bookmarks API function, + * chrome.bookmarks.search. + */ +CEEE_bookmarks_internal_.CMD_SEARCH_BOOKMARKS = 'bookmarks.search'; + +/** + * Implementation of Chrome extension bookmarks API function, + * chrome.bookmarks.search. + * See + * http://dev.chromium.org/developers/design-documents/extensions/bookmarks-api + */ +CEEE_bookmarks_internal_.searchBookmarks_ = function(cmd, data) { + if (!data || !data.args) { + return; + } + + var results = []; + var query = data.args; + // Because FireFox's JSON parsing engine does not support JSON encoding of + // primitive types (such as strings), we explicitly check for a 'null' + // string here to indicate that no arguments were given, and return + // an empty set. + // NOTE: The check against 3 is because encoded strings will be bracketed + // with ". ie "a". + if (!query || query == 'null' || query.length < 3) { + return results; + } + + var impl = this; + // The query, as performed by the chrome implementation is case-insensitive. + query = query.toLowerCase(); + query = query.substring(1, query.length - 1); + var querySet = query.split(' '); + this.getBookmarksContainingText_( + querySet, + CEEE_bookmarks.placesRoot, + function(bookmarkId) { + + results.push(impl.getBookmarkItemFromId_(bookmarkId)); + + // Restrict the set of returned results to no greater than 50. + // This mimics the behaviour of the API as implemented in Chrome. + return results.length <= 50; + }); + + return results; +}; + +/** + * Command name for Chrome extension bookmarks API function, + * chrome.bookmarks.remove. + */ +CEEE_bookmarks_internal_.CMD_REMOVE_BOOKMARK = 'bookmarks.remove'; + +/** + * Implementation of Chrome extension bookmarks API function, + * chrome.bookmarks.remove. + * See + * http://dev.chromium.org/developers/design-documents/extensions/bookmarks-api + */ +CEEE_bookmarks_internal_.removeBookmark_ = function(cmd, data) { + if (!data || !data.args) { + return; + } + + var args = CEEE_json.decode(data.args); + if (!args || !args.id) { + return; + } + + var recurse = cmd == this.CMD_REMOVE_BOOKMARK_TREE; + var itemType = CEEE_bookmarks.getItemType(args.id); + if (CEEE_bookmarks.TYPE_FOLDER == itemType || + CEEE_bookmarks.TYPE_DYNAMIC_CONTAINER == itemType) { + // Disallow removal of a non-empty folder unless the recursive argument + // is set. + var children = this.getChildren_(args.id); + if (!(children && children.length > 0 && !recurse)) { + CEEE_bookmarks.removeFolder(args.id); + } + } else { + CEEE_bookmarks.removeItem(args.id); + } +} + +/** + * Command name for Chrome extension bookmarks API function, + * chrome.bookmarks.removeTree. + */ +CEEE_bookmarks_internal_.CMD_REMOVE_BOOKMARK_TREE = 'bookmarks.removeTree'; + +/** + * Implementation of Chrome extension bookmarks API function, + * chrome.bookmarks.removeTree. + * Note: This routine aliases to removeBookmark_, which specializes its + * behaviour based on the contents of the command string argument. + * See + * http://dev.chromium.org/developers/design-documents/extensions/bookmarks-api + */ +CEEE_bookmarks_internal_.removeBookmarkTree_ = + CEEE_bookmarks_internal_.removeBookmark_; + + +/** + * Command name for Chrome extension bookmarks API function, + * chrome.bookmarks.create. + */ +CEEE_bookmarks_internal_.CMD_CREATE_BOOKMARK = 'bookmarks.create'; + +/** + * Implementation of Chrome extension bookmarks API function, + * chrome.bookmarks.create. + * See + * http://dev.chromium.org/developers/design-documents/extensions/bookmarks-api + */ +CEEE_bookmarks_internal_.createBookmark_ = function(cmd, data) { + if (!data || !data.args) { + return; + } + + var args = CEEE_json.decode(data.args); + if (!args) { + return; + } + + // If no parent id is provided, default to the unfiledBookmarksFolder. + // Explicitly test for null, as 0 may be a valid bookmark id. + var parentId = args.parentId; + if (parentId == null) { + parentId = CEEE_bookmarks.unfiledBookmarksFolder; + } + + // If no index is provided, then insert at the end of the child-list. + var index = args.index; + if (index == null) { + index = CEEE_bookmarks.DEFAULT_INDEX; + } + + var title = args.title; + if (!title) { + return; + } + + var url = args.url; + var newBookmarkId; + try { + // If no url is provided, then create a folder. + if (!url) { + newBookmarkId = CEEE_bookmarks.createFolder(parentId, + title, + index); + } else { + var uri = CEEE_ioService.newURI(url, null, null); + newBookmarkId = CEEE_bookmarks.insertBookmark(parentId, + uri, + index, + title); + } + + return this.getBookmarkItemFromId_(newBookmarkId); + } catch (e) { + // If the provided url is not formatted correctly, then newURI will throw. + // Return null to indicate failure. + return; + } +}; + +/** + * Command name for Chrome extension bookmarks API function, + * chrome.bookmarks.move. + */ +CEEE_bookmarks_internal_.CMD_MOVE_BOOKMARK = 'bookmarks.move'; + +/** + * Implementation of Chrome extension bookmarks API function, + * chrome.bookmarks.move. + * See + * http://dev.chromium.org/developers/design-documents/extensions/bookmarks-api + */ +CEEE_bookmarks_internal_.moveBookmark_ = function(cmd, data) { + if (!data || !data.args) { + return; + } + + var args = CEEE_json.decode(data.args); + var id = args.id; + var parentId = args.parentId; + + if (parentId == null) { + parentId = CEEE_bookmarks.getFolderIdForItem(id); + } + + if (parentId == null) { + return; + } + + var index = args.index; + if (!index) { + index = CEEE_bookmarks.DEFAULT_INDEX; + } + + CEEE_bookmarks.moveItem(id, parentId, index); +} + +/** + * Command name for Chrome extension bookmarks API function, + * chrome.bookmarks.update. + */ +CEEE_bookmarks_internal_.CMD_SET_BOOKMARK_TITLE = 'bookmarks.update'; + +/** + * Implementation of Chrome extension bookmarks API function, + * chrome.bookmarks.update. + * See + * http://dev.chromium.org/developers/design-documents/extensions/bookmarks-api + */ +CEEE_bookmarks_internal_.setBookmarkTitle_ = function(cmd, data) { + if (!data || !data.args) { + return; + } + + var args = CEEE_json.decode(data.args); + if (!args.id) { + return; + } + + CEEE_bookmarks.setItemTitle(args.id, args.title); + return this.getBookmarkItemFromId_(args.id); +} + +/** + * Command name for Chrome extension bookmarks API function, + * chrome.bookmarks.get. + */ +CEEE_bookmarks_internal_.CMD_GET_BOOKMARKS = 'bookmarks.get'; + +/** + * Implementation of Chrome extension bookmarks API function, + * chrome.bookmarks.get. + * See + * http://dev.chromium.org/developers/design-documents/extensions/bookmarks-api + */ +CEEE_bookmarks_internal_.getBookmarks_ = function(cmd, data) { + if (!data || !data.args) { + return; + } + + var args = CEEE_json.decode(data.args); + var bookmarks = []; + if (!args) { + bookmarks.push(this.getBookmarkItemFromId_( + CEEE_bookmarks.placesRoot)); + } else { + for (var bookmarkId = 0; bookmarkId < args.length; ++bookmarkId) { + bookmarks.push(this.getBookmarkItemFromId_( + args[bookmarkId])); + } + } + + return bookmarks; +} + +/** + * Initialization routine for the CEEE Bookmark API module. + * @param {!Object} ceeeInstance Reference to the global ceee instance. + * @return {Object} Reference to the bookmark module. + * @public + */ +function CEEE_initialize_bookmarks(ceeeInstance) { + CEEE_bookmarks_internal_.ceeeInstance_ = ceeeInstance; + var bookmarks = CEEE_bookmarks_internal_; + + ceeeInstance.registerExtensionHandler(bookmarks.CMD_GET_BOOKMARK_CHILDREN, + bookmarks, + bookmarks.getBookmarkChildren_); + ceeeInstance.registerExtensionHandler(bookmarks.CMD_GET_BOOKMARK_TREE, + bookmarks, + bookmarks.getBookmarkTree_); + ceeeInstance.registerExtensionHandler(bookmarks.CMD_SEARCH_BOOKMARKS, + bookmarks, + bookmarks.searchBookmarks_); + ceeeInstance.registerExtensionHandler(bookmarks.CMD_REMOVE_BOOKMARK, + bookmarks, + bookmarks.removeBookmark_); + ceeeInstance.registerExtensionHandler(bookmarks.CMD_REMOVE_BOOKMARK_TREE, + bookmarks, + bookmarks.removeBookmarkTree_); + ceeeInstance.registerExtensionHandler(bookmarks.CMD_CREATE_BOOKMARK, + bookmarks, + bookmarks.createBookmark_); + ceeeInstance.registerExtensionHandler(bookmarks.CMD_MOVE_BOOKMARK, + bookmarks, + bookmarks.moveBookmark_); + ceeeInstance.registerExtensionHandler(bookmarks.CMD_SET_BOOKMARK_TITLE, + bookmarks, + bookmarks.setBookmarkTitle_); + ceeeInstance.registerExtensionHandler(bookmarks.CMD_GET_BOOKMARKS, + bookmarks, + bookmarks.getBookmarks_); + + bookmarks.logInfo_ = ceeeInstance.logInfo; + bookmarks.logError_ = ceeeInstance.logError; + + return bookmarks; +}; diff --git a/ceee/firefox/content/ceee.png b/ceee/firefox/content/ceee.png Binary files differnew file mode 100644 index 0000000..f375ec4 --- /dev/null +++ b/ceee/firefox/content/ceee.png diff --git a/ceee/firefox/content/cf.js b/ceee/firefox/content/cf.js new file mode 100644 index 0000000..48c0328 --- /dev/null +++ b/ceee/firefox/content/cf.js @@ -0,0 +1,370 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file contains a helper object to wrap an instance of + * ChromeFrame for a given top-level window in Firefox. The main functions of + * this object are to initialize ChromeFrame and properly handle queueing of + * messages sent to. + * + * @supported Firefox 3.x + */ + +var CEEE_CfHelper; + +(function() { + +/** + * Constructor for ChromeFrame helper object. + * @param {!Object} instance A reference the the main ceee instance for + * this top-level window. + * @constructor + */ +function CfHelper(instance) { + // The caller must specify the ceee instance object. + if (!instance) + throw 'Must specify instance'; + + /** A reference to the main ceee instance for this top-level window. */ + this.ceee_ = instance; + + /** Reference to the ChromeFrame <embed> element in the DOM. */ + this.cf_ = null; + + /** True when ChromeFrame is ready for use. */ + this.isReady_ = false; + + /** True if we found no enabled extensions and tried installing one. */ + this.alreadyTriedInstalling_ = false; + + /** + * Queue of pending messages to be sent to ChromeFrame once it becomes ready. + */ + this.queue_ = []; + + /** + * A reference to the parent element (the toolbar) of the cf_ node. + */ + this.parent_ = null; +} + +/** + * Value used for id attribute of the ChromeFrame <embed> element. + * @const + */ +CfHelper.prototype.CHROME_FRAME_ID = 'ceee-browser'; + +/** Origin for use with postPrivateMessage. @const */ +CfHelper.prototype.ORIGIN_EXTENSION = '__priv_xtapi'; + +/** Target for event notifications. @const */ +CfHelper.prototype.TARGET_EVENT_REQUEST = '__priv_evtreq'; + +/** Target for responses to Chrome Extension API requests. @const */ +CfHelper.prototype.TARGET_API_RESPONSE = '__priv_xtres'; + +/** Target for message port requests. @const */ +CfHelper.prototype.TARGET_PORT_REQUEST = '__priv_prtreq'; + +/** Target for message port requests. @const */ +CfHelper.prototype.TARGET_PORT_RESPONSE = '__priv_prtres'; + +/** + * Preference that holds the path of the CRX file that was last installed + * by the CEEE. @const + */ +CfHelper.prototype.LAST_INSTALL_PATH = 'last_install_path'; + +/** + * Preference that holds the modified time of the CRX file that was last + * installed by the CEEE. @const + */ +CfHelper.prototype.LAST_INSTALL_TIME = 'last_install_time'; + +/** @return {Object} The ChromeFrame <embed> element. */ +CfHelper.prototype.get = function() { + return this.cf_; +}; + +/** @return {boolean} True if ChromeFrame is ready for use. */ +CfHelper.prototype.isReady = function() { + return this.isReady_; +}; + +/** Post all pending messages we have queued up to ChromeFrame. + * @private + */ +CfHelper.prototype.postPendingMessages_ = function() { + this.ceee_.logInfo('CfHelper.postPendingMessages'); + + while (this.queue_.length > 0) { + var pair = this.queue_.shift(); + this.postMessage(pair[0], pair[1]); + } +}; + +/** + * Posts a message to ChromeFrame. + * + * @param {!string} message The message to be sent. + * @param {!string} target The target of the message. + */ +CfHelper.prototype.postMessage = function(message, target) { + if (typeof message != 'string' || typeof target != 'string') { + this.ceee_.logError( + 'CfHelper.postMessage: message or target is not a string'); + return; + } + + if (!this.isReady_) { + this.ceee_.logInfo('CfHelper.postMessage defering msg=' + message); + // Queue the message and target for later. + this.queue_.push([message, target]); + return; + } + + this.ceee_.logInfo('CfHelper.postMessage msg=' + message); + + // We use private messages so that we can specify the origin. + // + // Messages must flow through the master CF instance. + CEEE_globals.masterCf.get().postPrivateMessage(message, + this.ORIGIN_EXTENSION, + target); +}; + +/** + * Create an <embed> element to hold the ChromeFrame and return it. + * + * @param {!HTMLElement} parent The DOM element that will hold the 'embed' + * element. + * @param {string} idValue Value of id attribute for created element. + * @param {!function()} onCfReady A callback for ChromeFrame becomes ready. + * @param {!function(Object, Object)} onCfMessage A callback to handle messages + * posted from the ChromeFrame guest page to the ChromeFrame host page. + * @return {HTMLElement} The <embed> element holding the ChromeFrame. + */ +CfHelper.prototype.create = function(parent, idValue, onCfReady, onCfMessage) { + // Create the ChromeFrame instance using the correct extension path and + // add it to the XUL DOM. + this.parent_ = parent; + var doc = parent.ownerDocument; + this.cf_ = doc.createElementNS('http://www.w3.org/1999/xhtml', 'embed'); + if (!this.cf_) { + this.ceee_.logError('Unable to create ChromeFrame'); + return null; + } + + this.cf_.id = idValue; + this.cf_.setAttribute('type', 'application/chromeframe'); + // TODO(rogerta@chromium.org): the height here is arbitrary. We + // will need to size appropriately when we have more info. + this.cf_.setAttribute('width', '1440'); + this.cf_.setAttribute('height', '25'); + this.cf_.setAttribute('flex', '1'); + this.cf_.setAttribute('privileged_mode', '1'); + this.cf_.setAttribute('chrome_extra_arguments', + '--enable-experimental-extension-apis'); + // TODO(joi@chromium.org) Remove this once the "*" default is removed from CF. + this.cf_.setAttribute('chrome_functions_automated', ''); + + // Add ChromeFrame to the DOM. This *must* be done before setting the + // event listeners or things will just silently fail. + parent.insertBefore(this.cf_, parent.firstChild); + + // Setup all event handlers. + this.cf_.onprivatemessage = onCfMessage; + + // OnMessage handles messages via window.externalHost. We currently only + // need this to match the IE workaround for windows.getCurrent. + var originCF = this.cf_; + this.cf_.onmessage = function(event) { + if (!event || !event.origin || !event.data) { + return; + } + if (event.data == 'ceee_getCurrentWindowId') { + var windowId = CEEE_mozilla_windows.getWindowId(window); + originCF.postMessage('ceee_getCurrentWindowId ' + windowId, + event.origin); + } + }; + + var impl = this; + this.cf_.addEventListener('readystatechanged', + function() {impl.onReadyStateChange_(onCfReady);}, false); + + return this.cf_; +}; + +// HTTP request states +/** + * HTTP request state. open() has not been called yet. + * @const + */ +var READY_STATE_UNINITIALIZED = 0; + +/** + * HTTP request state. send() has not been called yet. + * @const + */ +var READY_STATE_LOADING = 1; + +/** + * HTTP request state. headers and status are available. + * @const + */ +var READY_STATE_LOADED = 2; + +/** + * HTTP request state. responseText holds partial data. + * @const + */ +var READY_STATE_INTERACTIVE = 3; + +/** + * HTTP request state. The operation is complete. + * @const + */ +var READY_STATE_COMPLETED = 4; + +/** + * Handle 'embed' element ready state events. + * @param {!function()} onCfReady A callback for ChromeFrame becomes ready. + * @private + */ +CfHelper.prototype.onReadyStateChange_ = function(onCfReady) { + this.ceee_.logInfo('CfHelper.readystatechange: state=' + + this.cf_.readystate); + if (this.cf_.readystate == READY_STATE_COMPLETED) { + // Do this before we even load the extension at the other end so + // that extension automation is set up before any background pages + // in the extension load. + CEEE_globals.masterCf.onChromeFrameReady(this.cf_, + this.ceee_.logInfo); + + // TODO(ibazarny@google.com): Not every chrome frame needs to load + // extensions. However, I don't know how to make it + // work. Should be something like: + //if (!loadExtensions) { + // this.isReady_ = true; + // this.postPendingMessages_(); + // onCfReady(); + // return; + //} + var lastInstallPath = + CEEE_globals.getCharPreference(this.LAST_INSTALL_PATH); + var lastInstallTime = + CEEE_globals.getCharPreference(this.LAST_INSTALL_TIME); + + // If the extension to load is a CRX file (and not a directory) and + // its a new path or its an old path with a later modification time, + // install it now. + var extensionFile = this.ceee_.getCrxToInstall(); + var extensionPath = extensionFile && extensionFile.path; + var impl = this; + if (this.ceee_.isCrx(extensionPath) && + (lastInstallPath != extensionPath || + lastInstallTime < extensionFile.lastModifiedTime)) { + this.ceee_.logInfo('Installing extension ' + extensionPath); + // Install and save the path and file time. + this.cf_.installExtension(extensionPath, function(res) { + impl.cf_.getEnabledExtensions(function(extensionDirs) { + impl.onGetEnabledExtensionsComplete_(extensionDirs, onCfReady); + }); + }); + } else { + // We are not installing a CRX file. Before deciding what we should do, + // we need to find out what extension may already be installed. + this.cf_.getEnabledExtensions(function(extensionDirs) { + impl.onGetEnabledExtensionsComplete_(extensionDirs, onCfReady); + }); + } + } +}; + +/** + * Process response from 'getEnabledExtensions'. + * @param {string} extensionDirs tab-separate list of extension dirs. Only first + * entry is handled. + * @param {function()} onCfReady A callback for ChromeFrame becomes ready. + * @private + */ +CfHelper.prototype.onGetEnabledExtensionsComplete_ = function (extensionDirs, + onCfReady) { + this.ceee_.logInfo('OnGetEnabledExtensions: ' + extensionDirs); + + var extensions = extensionDirs.split('\t'); + var extensionFile = this.ceee_.getCrxToInstall(); + var impl = this; + if (extensions.length > 0 && extensions[0] != '') { + this.ceee_.logInfo('Got enabled extension: ' + extensions[0]); + if (extensionFile) { + CEEE_globals.setCharPreference( + this.LAST_INSTALL_PATH, extensionFile.path); + CEEE_globals.setCharPreference( + this.LAST_INSTALL_TIME, + extensionFile.lastModifiedTime.toString()); + } + this.onLoadExtension_(extensions[0], onCfReady); + } else if (extensionFile && !this.ceee_.isCrx(extensionFile.path)) { + this.ceee_.logInfo('Loading exploded path:' + extensionFile.path); + this.cf_.loadExtension(extensionFile.path, function() { + impl.onLoadExtension_(extensionFile.path, onCfReady); + }); + } else if (!this.alreadyTriedInstalling_ && extensionFile) { + this.ceee_.logInfo('Attempting to do first-time .crx install.'); + this.alreadyTriedInstalling_ = true; + this.cf_.installExtension(extensionFile.path, function(res) { + impl.cf_.getEnabledExtensions(function(extensionDirs) { + impl.onGetEnabledExtensionsComplete_(extensionDirs, onCfReady); + }); + }); + } else { + // Remove the toolbar entirely. The direct parent is the toolbaritem + // (check overlay.xul for more information), so we to remove our parentNode. + var toolbar = this.parent_.parentNode.parentNode; + toolbar.parentNode.removeChild(toolbar); + this.ceee_.logInfo('No extension installed.'); + } +}; + +/** + * Process response from 'loadExtension' call. + * @param {string} baseDir Extension home dir. + * @param {!function()} onCfReady A callback for ChromeFrame becomes ready. + * @private + */ +CfHelper.prototype.onLoadExtension_ = function (baseDir, onCfReady) { + var extensions = [baseDir]; + this.ceee_.logInfo('OnLoadExtension: ' + baseDir); + this.ceee_.initExtensions(extensions); + + var url = this.ceee_.getToolstripUrl(); + if (url) { + // Navigate ChromeFrame to the URL we want for the toolstrip. + // NOTE: CF expects a full URL. + this.cf_.src = url; + this.ceee_.logInfo('OnLoadExtension: extension URL=' + url); + } else { + // Remove the toolbar entirely. The direct parent is the toolbaritem + // (check overlay.xul for more information), so we to remove our parentNode. + var toolbar = this.parent_.parentNode.parentNode; + toolbar.parentNode.removeChild(toolbar); + this.ceee_.logInfo('No extension URL'); + } + + this.isReady_ = true; + this.postPendingMessages_(); + onCfReady(); + var parent = this.parent_; + setTimeout(function(){ + parent.selectedIndex = 0; + parent.removeChild(parent.lastChild); + }, 0); +}; + +// Make the constructor visible outside this anonymous block. +CEEE_CfHelper = CfHelper; + +})(); diff --git a/ceee/firefox/content/cookie_api.js b/ceee/firefox/content/cookie_api.js new file mode 100644 index 0000000..fda3296 --- /dev/null +++ b/ceee/firefox/content/cookie_api.js @@ -0,0 +1,484 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file contains the implementation of the Chrome extension + * cookies APIs for the CEEE Firefox add-on. This file is loaded by the + * overlay.xul file, and requires that overlay.js has already been loaded. + * + * @supported Firefox 3.x + */ + + +/** + * "cookie-changed" Mozilla event observer. + * @param {function()} handler Handle event function. + * @constructor + */ +function CEEE_CookieChangeObserver(handler) { + this.observe = handler; +} + +/** + * XPCOM interface function. + * @param {nsIIDRef} iid Interface ID. + * @return this object when the queried interface is nsIObserver. + */ +CEEE_CookieChangeObserver.prototype.QueryInterface = function(iid) { + if (iid.equals(Components.interfaces.nsIObserver) || + iid.equals(Components.interfaces.nsISupports)) { + return this; + } + throw Components.results.NS_NOINTERFACE; +}; + +/** + * Registers the observer in the Mozilla Observer service. + * @private + */ +CEEE_CookieChangeObserver.prototype.register_ = function() { + var observerService = Components.classes['@mozilla.org/observer-service;1'] + .getService(Components.interfaces.nsIObserverService); + observerService.addObserver(this, 'cookie-changed', false); +}; + +/** + * Unregisters the observer in the Mozilla Observer service. + * @private + */ +CEEE_CookieChangeObserver.prototype.unregister_ = function() { + var observerService = Components.classes['@mozilla.org/observer-service;1'] + .getService(Components.interfaces.nsIObserverService); + observerService.removeObserver(this, 'cookie-changed'); +}; + + +/** + * chrome.cookies interface implementation. + * @type Object + */ +var CEEE_cookie_internal_ = CEEE_cookie_internal_ || { + /** + * Reference to the instance object for the ceee. + * @private + */ + ceeeInstance_: null, + + // Cookie store IDs. + /** @const */ STORE_PRIVATE: 'private', + /** @const */ STORE_NORMAL: 'normal' +}; + +/** + * Initializes the cookies module during onLoad message from the browser. + * @public + */ +CEEE_cookie_internal_.onLoad = function() { + var cookies = this; + this.cookieChangeObserver_ = new CEEE_CookieChangeObserver( + function(subject, topic, data) { + if (topic != 'cookie-changed') { + return; + } + if (data == 'deleted' || data == 'added' || data == 'changed') { + cookies.onCookieChanged_( + subject.QueryInterface(Components.interfaces.nsICookie2), data); + } + }); + this.cookieChangeObserver_.register_(); +} + +/** + * Unloads the cookies module during onUnload message from the browser. + * @public + */ +CEEE_cookie_internal_.onUnload = function() { + this.cookieChangeObserver_.unregister_(); +}; + +/** + * Returns a cookie store ID according to the current private browsing state. + * @return Current cookie store ID. + * @private + */ +CEEE_cookie_internal_.getCurrentStoreId_ = function() { + return CEEE_globals.privateBrowsingService.isInPrivateBrowsing ? + this.STORE_PRIVATE : + this.STORE_NORMAL; +}; + + +/** + * @typedef {{ + * name : string, + * value : string, + * domain : string, + * hostOnly : boolean, + * path : string, + * secure : boolean, + * httpOnly : boolean, + * session : boolean, + * expirationDate: ?number, + * storeId : string + * }} + */ +CEEE_cookie_internal_.Cookie; + +/** + * Builds an object that represents a "cookie", as defined by the Google Chrome + * extension API. + * @param {nsICookie2} cookie Mozilla cookie info. + * @return {CEEE_cookie_internal_.Cookie} chrome.cookies.Cookie object. + * @private + */ +CEEE_cookie_internal_.buildCookieValue_ = function(cookie) { + var host = cookie.host; + if (host && host[0] == '.') host = host.substr(1); + var info = { + name : cookie.name, + value : cookie.value, + domain : host, + hostOnly : !cookie.isDomain, + path : cookie.path, + secure : cookie.isSecure, + httpOnly : cookie.isHttpOnly, + session : cookie.isSession, + storeId : this.getCurrentStoreId_() + }; + if (!cookie.isSession) { + info.expirationDate = cookie.expires; + } + + return info; +}; + +/** + * @typedef {{ + * id: string, + * tabIds: Array.<string> + * }} + */ +CEEE_cookie_internal_.CookieStore; + +/** + * Builds an object that represents a "cookie store", as defined + * by the Google Chrome extension API. + * @return {CEEE_cookie_internal_.CookieStore} + * chrome.cookies.CookieStore object. + * @private + */ +CEEE_cookie_internal_.buildCookieStoreValue_ = function() { + var info = {}; + info.id = this.getCurrentStoreId_(); + info.tabIds = []; + + return info; +}; + +/** + * Sends an event message to Chrome API when a cookie is set or removed. + * @param {nsICookie2} cookie Changed/removed cookie. + * @param {string} action One of the "deleted", "added" or "changed" strings. + * @private + */ +CEEE_cookie_internal_.onCookieChanged_ = function(cookie, action) { + // Send the event notification to ChromeFrame. + var info = [{ + removed: action == 'deleted', + cookie: this.buildCookieValue_(cookie) + }]; + var msg = ['cookies.onChanged', CEEE_json.encode(info)]; + this.ceeeInstance_.getCfHelper().postMessage( + CEEE_json.encode(msg), + this.ceeeInstance_.getCfHelper().TARGET_EVENT_REQUEST); +}; + + +/** + * Implements the 'cookies.get' function in the Chrome extension API. + * @param cmd Command. + * @param data Arguments. + * @return {?CEEE_cookie_internal_.Cookie} Cookie found or null. + * @private + */ +CEEE_cookie_internal_.CMD_GET_COOKIE = 'cookies.get'; +CEEE_cookie_internal_.getCookie_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var details = args[0]; + var cookie = this.getCookieInternal_(details); + if (cookie) { + return this.buildCookieValue_(cookie); + } +}; + +/** + * Implements the main (internal) part of the 'cookies.get' function. + * @param {{url:string, name:string, storeId:?string}} details + * Cookie filter to get. + * @return {?nsICookie2} Mozilla cookie object or null if cookie doesn't exist. + * @private + */ +CEEE_cookie_internal_.getCookieInternal_ = function(details) { + var newDetails = { url: details.url, name: details.name }; + if (typeof(details.storeId) == 'string') { + newDetails.storeId = details.storeId; + } + var cookies = this.getAllCookiesInternal_(newDetails); + if (cookies.length == 0) { + return null; + } else { + var signum = function(n) { + return n > 0 ? 1 : n < 0 ? -1 : 0; + }; + cookies.sort(function(a, b) { + return 4 * signum(b.host.length - a.host.length) + + 2 * signum(b.path.length - a.path.length) + + signum((a.creationTime || 0) - (b.creationTime || 0)); + }); + return cookies[0]; + }; +}; + +/** + * Implements the 'cookies.getAll' function in the Chrome extension API. + * @param cmd Command. + * @param data Arguments. + * @return {Array.<CEEE_cookie_internal_.Cookie>} Array of the cookies found + * against parameters. + * @private + */ +CEEE_cookie_internal_.CMD_GET_ALL_COOKIES = 'cookies.getAll'; +CEEE_cookie_internal_.getAllCookies_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var details = args[0]; + var self = this; + return this.getAllCookiesInternal_(details).map(function(cookie, i, a) { + return self.buildCookieValue_(cookie); + }); +}; + +/** + * Implements the main (internal) part of the 'cookies.getAll' function. + * @param {Object} details Search parameters. + * @return {Array.<nsICookie2>} Array of the cookies found against parameters. + * @private + */ +CEEE_cookie_internal_.getAllCookiesInternal_ = function(details) { + var storeId = this.getCurrentStoreId_(); + if (typeof(details.storeId) == 'string' && details.storeId != storeId) { + // We do have access to current cookie store only. + return []; + } + var cme = CEEE_cookieManager.enumerator; + var cookies = []; + while (cme.hasMoreElements()) { + var cookie = cme.getNext().QueryInterface(Components.interfaces.nsICookie2); + cookies.push(cookie); + } + + + /** + * Filters cookies array with a filtering function. + * @param {string} prop Property to filter on. + * @param {string} type Property type. + * @param {function(Object, number, Array.<Object>)} func Filtering function. + */ + var filterCookies = function(prop, type, func) { + if (typeof(details[prop]) == type) { + cookies = cookies.filter(func); + } + }; + + /** + * Checks whether a given domain is a subdomain of another one. + * @param {string} domain Domain name. + * @param {string} subdomain Subdomain name. + * @return {boolean} True iff subdomain is a sub-domain of the domain. + */ + var isSubDomain = function(domain, subdomain) { + var domLen = domain.length; + var subdomLen = subdomain.length; + return domLen <= subdomLen && + subdomain.substr(subdomLen - domLen) == domain && + (domLen == subdomLen || + domain[0] == '.' || + subdomain[subdomLen - domLen - 1] == '.'); + }; + + /** + * Filters cookies array by a domain name. + * @param {string} domain Domain name. + */ + var filterCookiesByDomain = function(domain) { + cookies = cookies.filter(function(cookie, i, a) { + // Check whether the given string is equal or a subdomain + // of the cookie domain. + return isSubDomain(domain, cookie.host); + }); + }; + + /** + * Filters cookies array by the URL host. + * @param {string} host Host name. + */ + var filterCookiesByHost = function(host) { + cookies = cookies.filter(function(cookie, i, a) { + // Check whether the cookie domain is equal or a subdomain + // of the given URL host. + return isSubDomain(cookie.host, host); + }); + }; + + // Filtering by argument details. + filterCookies('name', 'string', function(cookie, i, a) { + return cookie.name == details.name; + }); + filterCookies('path', 'string', function(cookie, i, a) { + return cookie.path == details.path; + }); + filterCookies('secure', 'boolean', function(cookie, i, a) { + return cookie.isSecure == details.secure; + }); + filterCookies('session', 'boolean', function(cookie, i, a) { + return cookie.isSession == details.session; + }); + if (details.domain) { + filterCookiesByDomain(details.domain); + } + if (typeof(details.url) == 'string') { + var uri = null; + try { + uri = CEEE_ioService.newURI(details.url, null, null); + } catch (e) { + // uri left null if something happens. + } + if (uri) { + filterCookiesByHost(uri.host); + var path = uri.path || '/'; + cookies = cookies.filter(function(cookie, i, a) { + // Any valid subpath should pass the filter. + return path.indexOf(cookie.path) == 0 && + (path.length == cookie.path.length || + path.charAt(cookie.path.length) == '/'); + }); + } else { + // not valid URL; no cookie can match it. + cookies = []; + } + } + + return cookies; +}; + +/** + * Implements the 'cookies.remove' function in the Chrome extension API. + * @param cmd Command. + * @param data Arguments. + * @private + */ +CEEE_cookie_internal_.CMD_REMOVE_COOKIE = 'cookies.remove'; +CEEE_cookie_internal_.removeCookie_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var details = args[0]; + var newDetails = { url: details.url, name: details.name }; + if (typeof(details.storeId) == 'string') { + newDetails.storeId = details.storeId; + } + var cookie = this.getCookieInternal_(newDetails); + if (cookie) { + CEEE_cookieManager.remove(cookie.host, cookie.name, cookie.path, false); + } +}; + +/** + * Implements the 'cookies.set' function in the Chrome extension API. + * @param cmd Command. + * @param data Arguments. + * @private + */ +CEEE_cookie_internal_.CMD_SET_COOKIE = 'cookies.set'; +CEEE_cookie_internal_.setCookie_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var details = args[0]; + var uri = null; + try { + uri = CEEE_ioService.newURI(details.url, null, null); + } catch (e) { + // Invalid URL. + return; + } + var DOOMSDAY = (new Date(2222, 0, 1)).getTime() / 1000; + var domain = details.domain ? '.' + details.domain : uri.host; + var path = details.path || uri.path || '/'; + var name = details.name || ''; + var value = details.value || ''; + var secure = details.secure || false; + var isHttpOnly = !details.domain; + var isSession = typeof(details.expirationDate) != 'number'; + // Expiration date should be set even for a session cookie! + // Otherwise the cookie never appears even up to the end of the session. + // Possibly Mozilla bug? + var expire = isSession ? DOOMSDAY : details.expirationDate; + CEEE_cookieManager.add( + domain, path, name, value, secure, isHttpOnly, isSession, expire); +}; + +/** + * Implements the 'cookies.getAllCookieStores' function in + * the Chrome extension API. + * The Firefox browser always contains exactly one cookie store. + * @param cmd Command. + * @param data Arguments. + * @return {Array.<CEEE_cookie_internal_.CookieStore>} Array of the + * CookieStore objects. + * @private + */ +CEEE_cookie_internal_.CMD_GET_ALL_COOKIE_STORES = + 'cookies.getAllCookieStores'; +CEEE_cookie_internal_.getAllCookieStores_ = function(cmd, data) { + var storeId = this.getCurrentStoreId_(); + var wm = CEEE_mozilla_windows.service; + var be = wm.getEnumerator(CEEE_mozilla_windows.WINDOW_TYPE); + var stores = [this.buildCookieStoreValue_()]; + var nTab = 0; + while (be.hasMoreElements()) { + var win = be.getNext(); + var tabs = win.gBrowser.tabContainer.childNodes; + for (var iTab = 0; iTab < tabs.length; iTab++) { + var tabId = CEEE_mozilla_tabs.getTabId(tabs[iTab]); + stores[0].tabIds.push(tabId); + } + } + return stores; +}; + +/** + * Initialization routine for the CEEE Cookies API module. + * @param {!Object} ceeeInstance Reference to the global ceee instance. + * @return {Object} Reference to the cookies module. + * @public + */ +function CEEE_initialize_cookies(ceeeInstance) { + CEEE_cookie_internal_.ceeeInstance_ = ceeeInstance; + var cookies = CEEE_cookie_internal_; + + // Register the extension handling functions with the ceee instance. + ceeeInstance.registerExtensionHandler(cookies.CMD_GET_COOKIE, + cookies, + cookies.getCookie_); + ceeeInstance.registerExtensionHandler(cookies.CMD_GET_ALL_COOKIES, + cookies, + cookies.getAllCookies_); + ceeeInstance.registerExtensionHandler(cookies.CMD_REMOVE_COOKIE, + cookies, + cookies.removeCookie_); + ceeeInstance.registerExtensionHandler(cookies.CMD_SET_COOKIE, + cookies, + cookies.setCookie_); + ceeeInstance.registerExtensionHandler(cookies.CMD_GET_ALL_COOKIE_STORES, + cookies, + cookies.getAllCookieStores_); + + return cookies; +} diff --git a/ceee/firefox/content/externs.js b/ceee/firefox/content/externs.js new file mode 100644 index 0000000..8170797 --- /dev/null +++ b/ceee/firefox/content/externs.js @@ -0,0 +1,91 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview 'externs' file for JSCompiler use with Firefox CEEE javascript + * code loaded from overlay.xul. + */ + +// Defined in CEEE global module +var CEEE_bookmarks +var CEEE_globals +var CEEE_ioService +var CEEE_json +var CEEE_mozilla_tabs +var CEEE_mozilla_windows + +// Javascript predefined variables +var arguments; +var undefined; + +// DOM predefined variables +var document; +var window; + +// Javascript/DOM builtin function +function parseInt(str, radix) {} +function open(utl, target, features) {} + + +// Javasript runtime classes +/** + * @returns string + * @constructor + */ +function Date(opt_a, opt_b, opt_c, opt_d, opt_e, opt_f, opt_g) {} +/** + * @param {*} opt_a + * @param {*} opt_b + * @param {*} opt_c + * @returns !Error + * @constructor + */ +function Error(opt_a, opt_b, opt_c) {} +/** + * @param {*} opt_a + * @param {*} opt_b + * @returns !RegExp + * @constructor + */ +function RegExp(opt_a, opt_b) {} +/** + * @param {*} opt_a + * @returns string + * @constructor + */ +function String(opt_a) {} + + +// Predefined XPCOM runtime entities. +/** @constructor */ +function XPCNativeWrapper(a) {} + +var Components; +Components.classes; +Components.interfaces; +Components.utils; + +var Application; +Application.console; +Application.console.log = function(arg) {}; + +function dump(text) {} + +/** @constructor */ +function nsICookie2(){} +/** @constructor */ +function nsIFile(){} +/** @constructor */ +function nsIIDRef(){} +/** @constructor */ +function nsILocalFile(){} +/** @constructor */ +function nsIDOMWindow(){} + +/** + * window.location pseudoclass, see + * https://developer.mozilla.org/en/window.location#Location_object + * @constructor + */ +function Location(){} diff --git a/ceee/firefox/content/infobars_api.js b/ceee/firefox/content/infobars_api.js new file mode 100644 index 0000000..44963a3 --- /dev/null +++ b/ceee/firefox/content/infobars_api.js @@ -0,0 +1,218 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file contains the implementation of the Chrome extension + * infobars API for the CEEE Firefox add-on. This file is loaded by the + * overlay.xul file, and requires that overlay.js has already been loaded. + * + * @supported Firefox 3.x + */ + +/** + * Place-holder namespace-object for helper functions used in this file. + * @private + */ +var CEEE_infobars_internal_ = CEEE_infobars_internal_ || { + /** Reference to the instance object for the ceee. @private */ + ceeeInstance_: null +}; + +/** + * @const + */ +CEEE_infobars_internal_.NOTIFICATION_ID = 'ceeeNotification'; + +/** + * @const + */ +CEEE_infobars_internal_.ANONID = 'anonid'; + +/** + * @const + */ +CEEE_infobars_internal_.PARENT_ANONID = 'details'; + +/** + * @const + */ +CEEE_infobars_internal_.ID_PREFIX = 'ceee-infobar-'; + +/** + * Get a File object for the given user script. Its assumed that the user + * script file name is relative to the Chrome Extension root. + * + * @param {!string} path Relative file name of the user script. + * @return {string} Absolute URL for the user script. + */ +function CEEE_infobars_getUserScriptUrl(path) { + var ceee = CEEE_infobars_internal_.ceeeInstance_; + var id = ceee.getToolstripExtensionId(); + return 'chrome-extension://' + id + '/' + path; +} + +/** + * Implementation of experimental.infobars.show method. + * @param {string} cmd Command name, 'experimenta.infobars.show' expected + * @param {Object} data Additional call info, includes args field with + * json-encoded arguments. + */ +CEEE_infobars_internal_.show = function(cmd, data){ + var ceee = CEEE_infobars_internal_.ceeeInstance_; + var args = CEEE_json.decode(data.args)[0]; + var tabId = args.tabId; + var path = args.path; + var url = CEEE_infobars_getUserScriptUrl(path); + var tabbrowser = null; + var browser = null; + if ('number' != typeof tabId || tabId < 0) { + tabbrowser = document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + browser = tabbrowser.selectedBrowser; + } else { + var tabInfo = CEEE_mozilla_tabs.findTab(tabId); + tabbrowser = tabInfo.tabbrowser; + browser = tabbrowser.getBrowserAtIndex(tabInfo.index); + } + var impl = CEEE_infobars_internal_; + var notificationBox = tabbrowser.getNotificationBox(browser); + var notification = notificationBox.appendNotification( + '', // message + impl.NOTIFICATION_ID, // identifier + null, // icon + notificationBox.PRIORITY_INFO_HIGH, + null // buttons + ); + var parent = document.getAnonymousElementByAttribute(notification, + impl.ANONID, impl.PARENT_ANONID); + // We don't need the default content there. Removing icon, message text etc. + while (parent.firstChild) { + parent.removeChild(parent.lastChild); + } + + // Let's assign window id first and use it as base for chrome frame id. + var result = ceee.getWindowModule().buildWindowValue( + new NotificationAsWindow(notification, parent)); + var id = impl.ID_PREFIX + result.id; + var cf = CEEE_infobars_createChromeFrame_(parent, id, url); + var infobars = + notification.ownerDocument.defaultView.CEEE_infobars_internal_; + infobars.lookupMap_ = infobars.lookupMap_ || {}; + infobars.lookupMap_[id] = notificationBox.id; + return result; +}; +CEEE_infobars_internal_.show.CMD = 'experimental.infobars.show'; + +CEEE_infobars_internal_.lookup = function(id) { + var ceee = CEEE_infobars_internal_.ceeeInstance_; + var impl = CEEE_infobars_internal_; + var e = CEEE_mozilla_windows.service.getEnumerator( + CEEE_mozilla_windows.WINDOW_TYPE); + while (e.hasMoreElements()) { + var win = e.getNext(); + var infobars = win.CEEE_infobars_internal_; + var notificationBoxId = infobars.lookupMap_ && + infobars.lookupMap_[impl.ID_PREFIX + id]; + if (!notificationBoxId) { + continue; + } + var doc = win.document; + var notificationBox = doc.getElementById(notificationBoxId); + var notifications = notificationBox.allNotifications; + for (var i = 0, curr; curr = notifications[i]; ++i) { + var parent = document.getAnonymousElementByAttribute(curr, + impl.ANONID, impl.PARENT_ANONID); + if (parent.firstChild.id == impl.ID_PREFIX + id) { + return new NotificationAsWindow(curr, parent); + } + } + // Notification not found. Remove from lookup cache + delete infobars.lookupMap_[impl.ID_PREFIX + id]; + // Once we had it in lookup map, no point to continue search + break; + } + return null; +}; + +/** + * Create the ChromeFrame element for infobar content. + * @private + */ +function CEEE_infobars_createChromeFrame_(parent, id, url) { + var ceee = CEEE_infobars_internal_.ceeeInstance_; + var cf = parent.ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', + 'embed'); + cf.id = id; + // TODO(ibazarny@google.com): the height here is arbitrary. We will + // need to size appropriately when we have more info. + cf.setAttribute('height', '35'); + cf.setAttribute('flex', '1'); + cf.setAttribute('type', 'application/chromeframe'); + cf.setAttribute('privileged_mode', '1'); + cf.setAttribute('chrome_extra_arguments', + '--enable-experimental-extension-apis'); + cf.setAttribute('src', url); + parent.appendChild(cf); + return cf; +} + +/** + * Initialization routine for the CEEE infobars API module. + * @param {!Object} ceeeInstance Reference to the global ceee instance. + * @param {!Object} windowModule Window management module, used to register + * fake window lookup so we can turn notifications into windows. + * @return {Object} Reference to the infobars module. + * @public + */ +function CEEE_initialize_infobars(ceeeInstance, windowModule) { + CEEE_infobars_internal_.ceeeInstance_ = ceeeInstance; + var infobars = CEEE_infobars_internal_; + ceeeInstance.registerExtensionHandler(infobars.show.CMD, + infobars, + infobars.show); + windowModule.registerLookup(infobars.lookup); + return infobars; +} + +/** + * Make notification look like Window, so that window api module can work with + * it. + * @constructor + */ +function NotificationAsWindow(notification, cfContainer){ + this.isAdapter = true; + this.notification_ = notification; + this.cfContainer_ = cfContainer; +} + +NotificationAsWindow.prototype.__defineGetter__('screenX', function(){ + return this.cfContainer_.boxObject.screenX; +}); + +NotificationAsWindow.prototype.__defineGetter__('screenY', function(){ + return this.cfContainer_.boxObject.screenY; +}); + +NotificationAsWindow.prototype.__defineGetter__('outerWidth', function(){ + return this.cfContainer_.boxObject.width; +}); + +NotificationAsWindow.prototype.__defineGetter__('outerHeight', function(){ + return this.cfContainer_.boxObject.height; +}); + +NotificationAsWindow.prototype.__defineGetter__( + CEEE_mozilla_windows.WINDOW_ID, + function(){ + return this.notification_[CEEE_mozilla_windows.WINDOW_ID]; + }); + +NotificationAsWindow.prototype.__defineSetter__( + CEEE_mozilla_windows.WINDOW_ID, + function(value){ + this.notification_[CEEE_mozilla_windows.WINDOW_ID] = value; + }); + +NotificationAsWindow.prototype.close = function() { + this.notification_.close(); +}; diff --git a/ceee/firefox/content/js-coverage.js b/ceee/firefox/content/js-coverage.js new file mode 100644 index 0000000..eca2227 --- /dev/null +++ b/ceee/firefox/content/js-coverage.js @@ -0,0 +1,630 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview JavaScript code-coverage utility. + * This module powers a FireFox extension that gathers code coverage results. + * It depends on the Venkman JavaScript Debugger extension. + * <p> + * To enable coverage tracking, set the JS_COVERAGE environment variable to 1. + * Then, optionally, set the JS_COVERAGE_FILE environment variable to the full + * path where the results LCOV data should be stored. The default path is + * "/tmp/jscov.dat". Finally, set the JS_COVERAGE_URL_PATTERN environment + * variable to a regex of the files you want to instrument. The default pattern + * is "^http://.*js". + * <p> + * The coverage seems to be a little flaky, in the sense that it does not + * always seem to catch all the javascript files being loaded into Firefox. + * From what I have observed, it seems that the files of extensions might be + * cached, and when loaded from this cache the onScriptCreated() callback is + * not always called. If I save any one file in the extension, then all files + * of that extension are caught correctly. Still investigating. + */ + + +/** Namespace for everything in this extension. */ +var LCOV = {}; + + +/** + * This helper method returns a given class based on a class name from the + * global Components repository. + * @param {String} className: The name of the class to return + * @return {Object}: The requested class (not an instance) + */ +LCOV.CC = function(className) { + return Components.classes[className]; +}; + + +/** + * This helper method returns a given interface based on an interface name + * from the global Components repository. + * @param {String} interfaceName The name of the interface to return + * @return {Object} The requested interface + */ +LCOV.CI = function(interfaceName) { + return Components.interfaces[interfaceName]; +}; + + +/** Debugger service component class */ +LCOV.debuggerService = LCOV.CC('@mozilla.org/js/jsd/debugger-service;1'); + + +/** Environment component class */ +LCOV.environment = LCOV.CC("@mozilla.org/process/environment;1"); + + +/** Local file component class */ +LCOV.localFile = LCOV.CC('@mozilla.org/file/local;1'); + + +/** File output stream component class */ +LCOV.fileOutputStream = LCOV.CC('@mozilla.org/network/file-output-stream;1'); + + +/** Debugger service component interface */ +LCOV.jsdIDebuggerService = LCOV.CI('jsdIDebuggerService'); + + +/** Script component interface */ +LCOV.jsdIScript = LCOV.CI('jsdIScript'); + + +/** Execution hook component interface */ +LCOV.jsdIExecutionHook = LCOV.CI("jsdIExecutionHook"); + + +/** Environment component interface */ +LCOV.nsIEnvironment = LCOV.CI('nsIEnvironment'); + + +/** Local file component interface */ +LCOV.nsILocalFile = LCOV.CI('nsILocalFile'); + + +/** File output stream component interface */ +LCOV.nsIFileOutputStream = LCOV.CI('nsIFileOutputStream'); + + +/** Log an informational message to the Firefox error console. */ +LCOV.logInfo = function(msg) { + Application.console.log('LCOV: ' + msg); +}; + + +/** Log a error message to the Firefox error console. */ +LCOV.logError = function(msg) { + Components.utils.reportError('LCOV: *** ' + msg); +}; + + +/** + * Useful function for dumping the properties of an object, if for some reason + * the JSON encoding does not work. + * @param {string} message String to prepend to dumped message. + * @param {Object} o The object to dump. + */ +LCOV.dumpit = function(message, o) { + var buf = 'dumpit:' + message + ': '; + + if (o && o.wrappedJSObject) + o = o.wrappedJSObject; + + if (o) { + for (pn in o) { + try { + if (o[pn] && typeof o[pn] != 'function') + buf += ' ' + pn + '=' + o[pn]; + } catch (ex) { + buf += ' ' + pn + ':ex=' + ex; + } + } + } else { + buf += 'object is null or not defined'; + } + + LCOV.logInfo(buf); +}; + + +/** + * The coverage class is responsible for creating and setting up a JavaScript + * debugger. It then tracks every line of javascript executed so that coverage + * data can be reported after a test run. + * @constructor + */ +LCOV.CoverageTracker = function() { + + /** + * The Venkman JavaScript Debugger service + * @type jsdIDebuggerService + * @private + */ + this.javascriptDebugger_ = LCOV.debuggerService.getService( + LCOV.jsdIDebuggerService); + if (!this.javascriptDebugger_) { + LCOV.logError('Coult not get debugger service.'); + } + + /** + * Whether coverage is enabled. + * @type boolean + * @private + */ + this.isCoverageEnabled_ = this.getIsCoverageEnabled_(); + + if (this.isCoverageEnabled() && this.javascriptDebugger_) { + LCOV.logInfo('is enabled'); + + var pattern = this.getUrlPattern_(); + LCOV.logInfo('Will instrument URLs macthing: ' + pattern); + + /** + * Regular expression for matching page URLs to instrument. + * @type RegExp + * @private + */ + this.reUrlPattern_ = new RegExp(pattern, 'i'); + + this.startCoverage_(); + } else { + LCOV.logInfo('not enabled'); + } +}; + + +/** + * Environment variable name for whether coverage is requested + * @type String + * @private + */ +LCOV.CoverageTracker.JS_COVERAGE_ENV_ = 'JS_COVERAGE'; + + +/** + * Environment variable name for the output file path + * @type String + * @private + */ +LCOV.CoverageTracker.JS_COVERAGE_FILE_ENV_ = 'JS_COVERAGE_FILE'; + + +/** + * Environment variable name for regular expression used for matching page + * URLs to instrument + * @type String + * @private + */ +LCOV.CoverageTracker.JS_COVERAGE_URL_PATTERN_ENV_ = 'JS_COVERAGE_URL_PATTERN'; + + +/** + * Default output file path + * @type String + * @private + */ +LCOV.CoverageTracker.DEFAULT_FILE_ = '/tmp/jscov.dat'; + + +/** + * Interval between flushes in milliseconds + * @type Number + * @private + */ +LCOV.CoverageTracker.FLUSH_INTERVAL_MS_ = 2000; + + +/** + * Maximum number of lines stored at a time before a flush is triggered + * @type Number + * @private + */ +LCOV.CoverageTracker.HITS_SIZE_THRESHOLD_ = 10000; + + +/** + * Singleton instance of the coverage tracker + * @type LCOV.CoverageTracker + * @private + */ +LCOV.CoverageTracker.instance_ = null; + + +/** + * All the recorded line hit data + * @type Object + * @private + */ +LCOV.CoverageTracker.prototype.hits_ = null; + + +/** + * Number of lines stored in the {@code hits_} field + * @type Number + * @private + */ +LCOV.CoverageTracker.prototype.hitsSize_ = 0; + + +/** + * Handle to an event for flushing the data regularly + * @type Number + * @private + */ +LCOV.CoverageTracker.prototype.flushEvent_ = null; + + +/** + * Output file stream + * @type nsIFileOutputStream + * @private + */ +LCOV.CoverageTracker.prototype.stream_ = null; + + +/** + * Gets a singleton instance of the coverage tracker. This method is + * idempotent. + * @return {LCOV.CoverageTracker} coverage tracker instance + */ +LCOV.CoverageTracker.getInstance = function() { + if (!LCOV.CoverageTracker.instance_) { + LCOV.CoverageTracker.instance_ = new LCOV.CoverageTracker(); + } + return LCOV.CoverageTracker.instance_; +}; + + +/** + * Returns a cached value for whether or not coverage is enabled for this test + * run. + * @return {boolean} Whether or not coverage is enabled + */ +LCOV.CoverageTracker.prototype.isCoverageEnabled = function() { + return this.isCoverageEnabled_; +}; + + +/** + * Set of scripts that have been been created. This is really only used for + * logging and debuggin purposes. + * @private + */ +LCOV.scripts_ = {}; + + +/** + * Setup method that gets invoked from the extension's overlay. This must + * be called once from each top-level Firefox window. + */ +LCOV.CoverageTracker.prototype.setupCurrentWindow = function() { + if (this.isCoverageEnabled()) { + // The script hooks have to be registered in each top-level Firefox window. + var coverageTracker = this; + this.javascriptDebugger_.scriptHook = { + 'onScriptCreated': + function(script) { + // This function seems to be called once for each javascript + // function defined in the source file. The script will also + // be destroyed once when each function returns. + coverageTracker.instrumentScript_(script); + }, + 'onScriptDestroyed': + function(script) { + if (typeof LVOC != 'undefined') { + var fileName = script.fileName.toString(); + LCOV.scripts_[fileName] -= 1; + if (0 == LCOV.scripts_[fileName]) { + //LCOV.logInfo('onScriptDestroyed ' + fileName); + delete LCOV.scripts_[fileName]; + } + } + coverageTracker.flushHits_(); + } + }; + } +}; + + +/** + * Disposes of the object. Note that the singleton currently lives for as long + * as the browser is open, so there are no callers of this method. + */ +LCOV.CoverageTracker.prototype.dispose = function() { + this.endCoverage_(); +}; + + +/** + * Begin the coverage cycle. First, clear out any existing coverage data. + * Second, attach to the interrupt hook within the javascript debugger. Please + * note that this is a very expensive operation so as few scripts as possible + * should be evaluated. + * @private + */ +LCOV.CoverageTracker.prototype.startCoverage_ = function() { + LCOV.logInfo('starting'); + this.openOutputStream_(); + this.clearCoverageData_(); + + var coverageTracker = this; + + this.javascriptDebugger_.on(); + + // Enum all scripts there were loaded prior to the debugger turned on. + this.javascriptDebugger_.enumerateScripts({ + enumerateScript: function(script) { + //var fileName = script.fileName.toString(); + //LCOV.logInfo('Script already loaded: ' + fileName); + coverageTracker.instrumentScript_(script); + }}); + + var flags = LCOV.jsdIDebuggerService.DISABLE_OBJECT_TRACE | + LCOV.jsdIDebuggerService.DEBUG_WHEN_SET | + LCOV.jsdIDebuggerService.MASK_TOP_FRAME_ONLY; + //flags |= this.javascriptDebugger_.flags; + this.javascriptDebugger_.flags |= flags; + + this.javascriptDebugger_.interruptHook = { + 'onExecute': + function(frame, type, val) { + coverageTracker.recordExecution_(frame); + return LCOV.jsdIExecutionHook.RETURN_CONTINUE; + } + }; + this.flushEvent_ = window.setInterval( + function() { + if (coverageTracker.hitsSize_ > 0) + LCOV.logInfo('timed out, flushing'); + + coverageTracker.flushHits_(); + }, + LCOV.CoverageTracker.FLUSH_INTERVAL_MS_); +}; + + +/** + * End the coverage cycle by killing the interrupt hook attached to the + * javascript debugger. Additionally, turn the javascript debugger off. + * @private + */ +LCOV.CoverageTracker.prototype.endCoverage_ = function() { + LCOV.logInfo('ending'); + window.clearInterval(this.flushEvent_); + this.flushEvent_ = null; + this.javascriptDebugger_.interruptHook = null; + this.javascriptDebugger_.scriptHook = null; + this.javascriptDebugger_.off(); + this.clearCoverageData_(); + this.closeOutputStream_(); +}; + + +/** + * Determines whether or not the script should be considered for coverage, and + * if so then begins instrumentation. Currently, the following rules are + * enforced: + * <ol> + * <li>The script cannot be a chrome script, (i.e. a browser or FF extension + * script)</li> + * <li>The script cannot be on the local filesystem, (i.e. a FF + * component)</li> + * <li>The script cannot be inline javascript</li> + * <li>The script must resemble a URL, (i.e. not "XStringBundle")</li> + * </ol> + * @param {jsdIScript} script The script to instrument + * @private + */ +LCOV.CoverageTracker.prototype.instrumentScript_ = function(script) { + var fileName = script.fileName.toString(); + var doInstrumentation = this.reUrlPattern_.test(fileName) && + !(fileName.lastIndexOf('js-coverage.js') == fileName.length - 14); + + // Log whether the script will be instrumented or not. + if (!LCOV.scripts_[fileName]) { + LCOV.scripts_[fileName] = 1; + var prefix = doInstrumentation ? 'Instrumenting ' : 'Not Instrumenting '; + //LCOV.logInfo(prefix + fileName); + } else { + LCOV.scripts_[fileName] += 1; + } + + if (doInstrumentation) { + // Turns on debugging for this script. + script.flags |= LCOV.jsdIScript.FLAG_DEBUG; + + // Records all instrumentable lines in the script as not yet hit. + this.writeOutput_('SF:' + fileName + '\n'); + var lineBegin = script.baseLineNumber; + var lineEnd = lineBegin + script.lineExtent + 1; + for (var lineIdx = lineBegin; lineIdx < lineEnd; lineIdx++) { + if (script.isLineExecutable(lineIdx, + LCOV.jsdIScript.PCMAP_SOURCETEXT)) { + this.writeOutput_('DA:' + lineIdx + ',0\n'); + } + } + this.writeOutput_('end_of_record\n'); + this.flushOutputStream_(); + } else { + // Turns off debugging for this script. + script.flags &= ~LCOV.jsdIScript.FLAG_DEBUG; + } +}; + + +/** + * Clears any stored coverage data. + * @private + */ +LCOV.CoverageTracker.prototype.clearCoverageData_ = function() { + //LCOV.logInfo('clearing data'); + this.hits_ = {}; + this.hitsSize_ = 0; +}; + + +/** + * Checks to see whether or not coverage is enabled for this test run. + * @return {Boolean} Whether or not coverage is enabled + * @private + */ +LCOV.CoverageTracker.prototype.getIsCoverageEnabled_ = function() { + var userEnvironment = LCOV.environment.getService(LCOV.nsIEnvironment); + var isEnabled; + return userEnvironment.exists(LCOV.CoverageTracker.JS_COVERAGE_ENV_) && + !!userEnvironment.get(LCOV.CoverageTracker.JS_COVERAGE_ENV_); +}; + + +/** + * Gets the regular expression used for matching page URLs to instrument. + * Only pages that match this pattern will be instrumented. + */ +LCOV.CoverageTracker.prototype.getUrlPattern_ = function() { + var userEnvironment = LCOV.environment.getService(LCOV.nsIEnvironment); + var re = userEnvironment.exists( + LCOV.CoverageTracker.JS_COVERAGE_URL_PATTERN_ENV_) && + userEnvironment.get(LCOV.CoverageTracker.JS_COVERAGE_URL_PATTERN_ENV_); + if (!re) + re = '^http://.*js$'; + + return re; +}; + + +/** + * Gets the output file for coverage data. + * @return {String} Full path to the file + * @private + */ +LCOV.CoverageTracker.prototype.getCoverageFile_ = function() { + var userEnvironment = LCOV.environment.getService(LCOV.nsIEnvironment); + var file; + if (userEnvironment.exists(LCOV.CoverageTracker.JS_COVERAGE_FILE_ENV_)) { + file = userEnvironment.get(LCOV.CoverageTracker.JS_COVERAGE_FILE_ENV_); + if (file.length == 0) { + file = LCOV.CoverageTracker.DEFAULT_FILE_; + } + } else { + file = LCOV.CoverageTracker.DEFAULT_FILE_; + } + + LCOV.logInfo('data will be written to ' + file); + return file; +}; + + +/** + * Records a line-hit entry to the current collection data set. + * @param {jsdIStackFrame} frame A jsdIStackFrame object representing the + * bottom stack frame + * @private + */ +LCOV.CoverageTracker.prototype.recordExecution_ = function(frame) { + var lineNumber = frame.line; + var fileName = frame.script.fileName.toString(); + var fileData; + if (fileName in this.hits_) { + fileData = this.hits_[fileName]; + } else { + // Add this file to the data set. + fileData = this.hits_[fileName] = {}; + } + + if (lineNumber in fileData) { + // Increment the hit value at this line. + fileData[lineNumber]++; + } else { + // Make sure the working set doesn't grow too large. + if (this.hitsSize_ >= LCOV.CoverageTracker.HITS_SIZE_THRESHOLD_) { + LCOV.logInfo('threshold reached, flushing'); + this.flushHits_(); + } + // Since this line has not been tracked yet, set the value to 1 hit. + fileData[lineNumber] = 1; + this.hitsSize_++; + } +}; + + +/** + * Appends to the output file in the "lcov" format which is outlined below. + * <pre> + * SF:<em>source file name 1</em> + * DA:<em>line_no</em>,<em>hit</em> + * DA:<em>line_no</em>,<em>hit</em> + * ... + * end_of_record + * SF:<em>source file name 2</em> + * DA:<em>line_no</em>,<em>hit</em> + * DA:<em>line_no</em>,<em>hit</em> + * ... + * end_of_record + * </pre> + * @private + */ +LCOV.CoverageTracker.prototype.flushHits_ = function() { + //LCOV.logInfo('flushing'); + for (var file in this.hits_) { + var fileData = this.hits_[file]; + this.writeOutput_('SF:' + file + '\n'); + + for (var lineHit in fileData) { + var lineHitCnt = fileData[lineHit]; + this.writeOutput_('DA:' + lineHit + ',' + lineHitCnt + '\n'); + } + + this.writeOutput_('end_of_record\n'); + } + this.clearCoverageData_(); + this.flushOutputStream_(); +}; + + +/** + * Opens a file stream suitable for outputting coverage results. + * @private + */ +LCOV.CoverageTracker.prototype.openOutputStream_ = function() { + // We're outside of the sandbox so we can write directly to disk. + var outputFile = LCOV.localFile.createInstance(LCOV.nsILocalFile); + outputFile.initWithPath(this.getCoverageFile_()); + this.stream_ = LCOV.fileOutputStream.createInstance(LCOV.nsIFileOutputStream); + this.stream_.init(outputFile, 0x04 | 0x08 | 0x10, 424, 0); +}; + + +/** + * Writes a string to the output file stream. + * @param {String} str The string to write to the stream + * @private + */ +LCOV.CoverageTracker.prototype.writeOutput_ = function(str) { + this.stream_.write(str, str.length); +}; + + +/** + * Closes the output stream. + * @private + */ +LCOV.CoverageTracker.prototype.closeOutputStream_ = function() { + this.flushOutputStream_(); + this.stream_.close(); +}; + + +/** + * Closes the output stream. + * @private + */ +LCOV.CoverageTracker.prototype.flushOutputStream_ = function() { + this.stream_.flush(); +}; + +// Setup the current top-level Firefox window for coverage. +LCOV.CoverageTracker.getInstance().setupCurrentWindow(); +LCOV.logInfo('add-on loaded'); diff --git a/ceee/firefox/content/options.xul b/ceee/firefox/content/options.xul new file mode 100644 index 0000000..655c6f8 --- /dev/null +++ b/ceee/firefox/content/options.xul @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. + + Defines the options dialog. + --> + +<!DOCTYPE prefwindow SYSTEM "chrome://ceee/locale/options.dtd"> +<prefwindow + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + id="ceee-options" + title="&options.title;" + ondialogaccept="onDialogAccept();"> + <script> + <![CDATA[ + function onDialogAccept() { + var prefs = Components.classes['@mozilla.org/preferences-service;1'] + .getService(Components.interfaces.nsIPrefService) + .getBranch('capability.policy.default.checkloaduri.'); + if (prefs) { + var debugging = document.getElementById('ceee-options-debug-bool'); + var enableDebugging = debugging && debugging.checked; + if (enableDebugging) { + // Allowing all web pages to load file:/// URLs is a bad idea, and + // this state should not be left on all the time. This is a necessary + // evil for now because of the way content scripts are injected into + // the page (for debugging) via file:/// URL <script> elements, but + // we may need to look at this again. + prefs.setCharPref('enabled', 'allAccess'); + } else { + prefs.setCharPref('enabled', 'noAccess'); + } + } + return true; + }; + ]]> + </script> + <prefpane id="pane1" label="&options.title;"> + <!-- This section maps preferences to their type and name in the + about:config page. It also assigns an id so that the preference can + be associated with a UI element below. Note that nothing inside + the preferences element defines any UI at all. + + If a preference should be a unicode string, use type="unichar" instead + --> + <preferences> + <preference + id="ceee-options-url" + name="extensions.ceee.url" + type="string"/> + <preference + id="ceee-options-path" + name="extensions.ceee.path" + type="string"/> + <preference + id="ceee-options-debug" + name="extensions.ceee.debug" + type="bool"/> + </preferences> + + <!-- This section defines the actual UI for the options dialog. --> + <label + accesskey="&options.path.accesskey;" + control="ceee-options-path-text"> + &options.path.label; + </label> + <textbox + id="ceee-options-path-text" + preference="ceee-options-path" + size="80"/> + + <separator/> + <label + accesskey="&options.url.accesskey;" + control="ceee-options-url-text"> + &options.url.label; + </label> + <textbox + id="ceee-options-url-text" + preference="ceee-options-url" + size="80"/> + + <separator/> + <checkbox + id="ceee-options-debug-bool" + label="&options.debug.label;" + preference="ceee-options-debug"/> + + <separator/> + <description><html:b>&options.restart.text;</html:b></description> + </prefpane> +</prefwindow> diff --git a/ceee/firefox/content/overlay.js b/ceee/firefox/content/overlay.js new file mode 100644 index 0000000..482f99d --- /dev/null +++ b/ceee/firefox/content/overlay.js @@ -0,0 +1,964 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file contains the main code for the CEEE Firefox + * add-on. This add-on mostly implements the Google Chrome extension APIs in + * Firefox. + * + * The CEEE extension is loaded into a ChromeFrame object (see the + * html:embed element in overlay.xul) and catches Chrome extension API requests + * using the ChromeFrame postMessage support. + * + * This add-on runs with privilege in Firefox, but the extension running inside + * the ChromeFrame is considered 'unsafe' code. Precautions are taken in the + * code to make sure that unsafe code is never executed with the privilege + * of the extension. In practice though, the extension running inside Chrome + * Frame is trusted code since there are mechanisms outside of this file that + * will ensure this. + * + * @supported Firefox 3.x + */ + +// Commenting out anonymous function wrapper for this whole file to allow for +// unit testing this code. +// (function(){ + +/** + * Imports the symbol CEEE_globals into this scope. + */ +Components.utils['import']('resource://modules/global.js', window); +if (!CEEE_globals) + Components.utils.reportError('CEEE: *** globals module not imported'); +if (!CEEE_mozilla_windows) + Components.utils.reportError('CEEE: *** mozilla windows module not imported'); +if (!CEEE_mozilla_tabs) + Components.utils.reportError('CEEE: *** mozilla tabs module not imported'); + +// Log from where we are running. +var ffCeeeLoc = Components.classes['@mozilla.org/extensions/manager;1'] + .getService(Components.interfaces.nsIExtensionManager) + .getInstallLocation(CEEE_globals.ADDON_ID) + .getItemLocation(CEEE_globals.ADDON_ID); +Application.console.log('CEEE: running from ' + ffCeeeLoc.path); + +/** + * Constructor for the object that implements the CEEE API. + * @constructor + */ +function CEEE_Class() { + /** @const */ this.TAB_STATUS_LOADING = 'loading'; + /** @const */ this.TAB_STATUS_COMPLETE = 'complete'; + + // Internal constants used by implementation. + /** @const */ this.API_EVENT_NAME_ = 'ceee-dom-api'; + /** @const */ this.API_ELEMENT_NAME_ = 'ceee-api-element'; + /** @const */ this.DATA_ATTRIBUTE_NAME_ = 'ceee-event-data'; + /** @const */ this.RETURN_ATTRIBUTE_NAME_ = 'ceee-event-return'; + /** @const */ this.ORIGIN_EXTENSION_ = '__priv_xtapi'; + + /** + * When true, this allows content scripts to be debugged using Firebug. + */ + this.contentScriptDebugging = CEEE_globals.getBoolPreference('debug'); + + /** + * The dispatch table for the UI/user context API. + * @type {Object} + * @private + */ + this.dispatch_ = {}; + + /** + * The dispatch table for the DOM context API. + * @type {Object} + * @private + */ + this.dom_dispatch_ = {}; + + /** + * Reference to the API module implementing the CEEE windows API. + * @type {Object} + * @private + */ + this.windowModule_ = null; + + /** + * Reference to the API module implementing the CEEE tabs API. + * @type {Object} + * @private + */ + this.tabsModule_ = null; + + /** + * Reference to the API module implementing the CEEE cookies API. + * @type {Object} + * @private + */ + this.cookiesModule_ = null; + + /** + * Reference to the API module implementing the CEEE sidebar API. + * @type {Object} + * @private + */ + this.sidebarModule_ = null; + + /** + * Reference to the API module implementing the CEEE user scripts API. + * @type {Object} + * @private + */ + this.userscriptsModule_ = null; + + /** + * Collection of functions to run once the extensions have been fully + * initialized. Maps a content window to a map of description-to-function. + * @type {Object} + * @private + */ + this.runWhenExtensionsInited_ = {}; +}; + +/** + * Log an informational message to the Firefox error console. + * @public + */ +CEEE_Class.prototype.logInfo = function(msg) { + dump('[CEEE] ' + msg + '\n'); + Application.console.log('CEEE: ' + msg); +}; + +/** + * Log an error message to the Firefox error console. + * @public + */ +CEEE_Class.prototype.logError = function(msg) { + var functionName = CEEE_Class.prototype.logError.caller.name + ':: '; + dump('[CEEE] *** ' + functionName + msg + '\n'); + Components.utils.reportError('CEEE: *** ' + functionName + msg); +}; + +/** + * Called once the CEEE host document is fully loaded in a top-level + * firefox window. This will be called for each top-level window. + * @private + */ +CEEE_Class.prototype.onLoad_ = function() { + // Initialize all of the imported API modules. + this.windowModule_ = CEEE_initialize_windows(this); + this.tabsModule_ = CEEE_initialize_tabs(this); + this.cookiesModule_ = CEEE_initialize_cookies(this); + + // TODO(twiz@chromium.org) : Re-enable bookmarks support once the + // Chrome Frame message passing system is more stable when a pop-up + // is present. bb2279154. + //CEEE_initialize_bookmarks(this); + + this.userscriptsModule_ = new CEEE_UserScriptManager(this); + this.infobarsModule_ = CEEE_initialize_infobars(this, + (/**@type {!Object}*/this.windowModule_)); + + this.createChromeFrame_(); + + // NOTE: this function must be called after createChromeFrame_() in order to + // make sure that the chrome process started uses the correct command line + // arguments. + this.sidebarModule_ = CEEE_initialize_sidebar(this); + + // TODO(joi@chromium.org) We used to have functionality here to show + // an error in case no extension was specified; this needs to be + // done by putting some HTML into CF now. + + // Make a list of all registered functions - those are the ones we + // wish to overtake in case our CF becomes the new "master" CF, the others + // we will leave with Chrome. + var functions = []; + for (var key in this.dispatch_) { + functions.push(key); + } + var csFunctions = functions.join(','); + CEEE_globals.masterCf.init(csFunctions, this.logInfo); + + var impl = this; + + window.addEventListener('focus', function() {impl.onFocus_();}, + false); + window.addEventListener('unload', function() {impl.onUnload_();}, + false); + + // TODO(rogerta@chromium.org): after setting the src property of + // ChromeFrame, it was asked in a code review whether the "load" + // event could race with the javascript below which sets the + // onCfReady_ function, or any functions that it calls directly or + // indirectly. For example, is it possible for ChromeFrame to fire + // the "load" event before userscripts_api.js runs, which sets the + // openPendingChannels property? + // + // From my experience with Firefox 3.0.x, this has not happened. The xul + // file loads the javascript files in the order specified, and + // userscripts_api.js always loads after this file. If this does end up + // being a problem, the solution would be create a new javascript file that + // is called *after* all the other javascript files that set all properties, + // that simply adds onLoad_ as a listener to the chrome window (i.e. the + // last executable line in this file). + + // Listen for DOMContentLoaded in order to hook the windows when needed. + // No need to listen for DOMFrameContentLoaded, because this is generated + // for the <iframe> element in the parent document. We still get + // DOMContentLoaded for the actual document object even when a frame is + // embedded. + var ac = document.getElementById('appcontent'); + ac.addEventListener('DOMContentLoaded', function (evt) { + impl.onDomContentLoaded_(evt.target); + }, false); + + // Listen for tab events. + var mainBrowser = document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + mainBrowser.addEventListener('load', function(evt) { + impl.onContentLoaded_(evt.target); + }, true); + + // For each tab already created, make to inject all required user scripts + // once the extension get loaded. + var tabCount = mainBrowser.tabContainer.itemCount; + for (var i = 0; i < tabCount; ++i) { + var doc = mainBrowser.getBrowserAtIndex(i).contentDocument; + impl.onDomContentLoaded_(doc); + impl.onContentLoaded_(doc); + } + + // Notify the tabs module that the page has loaded. + this.tabsModule_.onLoad(); + + this.cookiesModule_.onLoad(); + this.sidebarModule_.onLoad(); + + this.windowModule_.onWindowOpened_(window); + this.logInfo('onLoad_: done'); +}; + +/** + * Adds a function to a collection of functions to execute once extensions + * have been initialized. + * @param w {Object} Content window to run in. + * @param desc {string} Description of function. + * @param f {function()} Function to defer. + */ +CEEE_Class.prototype.runWhenExtensionsInited = function(w, desc, f) { + this.logInfo('Deferring "' + desc + '" for window=' + w.location.href); + var map = this.runWhenExtensionsInited_[w]; + if (!map) { + map = {}; + this.runWhenExtensionsInited_[w] = map; + } + map[desc] = f; +}; + +/** + * Called once we know which extensions are loaded. + * @param extensions {Array.<string>} Base directories (paths, not + * File objects) of extensions loaded. + */ +CEEE_Class.prototype.initExtensions = function(extensions) { + // Initialize only once. + if (!this.toolstripUrl_ && extensions.length > 0) { + var baseDir = Components.classes['@mozilla.org/file/local;1'] + .getService(Components.interfaces.nsILocalFile); + baseDir.initWithPath(extensions[0]); + + // Will set attributes read from the manifest file. + // TODO(joi@chromium.org) Generalize to multiple extensions. + this.loadToolstripManifest(baseDir); + if (!this.toolstripUrl_) { + this.logInfo('Apparently, no extensions are loaded.'); + } else { + this.toolstripDir_ = baseDir; + + // Inject user scripts for pages loaded before we knew which + // extensions were installed. Make sure to delete all array elements + // and clear the array so that we don't leak references that may be + // held by the closures in the array. + for (var w in this.runWhenExtensionsInited_) { + var map = this.runWhenExtensionsInited_[w]; + for (var desc in map) { + var windowName = (w && w.location && w.location.href) || '?'; + this.logInfo('Running "' + desc + '" in window=' + windowName); + map[desc](); + delete map[desc]; + } + delete this.runWhenExtensionsInited_[w]; + } + + this.runWhenExtensionsInited_ = {}; + } + } +} + +/** + * Create the ChromeFrame element that goes into the toolstrip. + * @private + */ +CEEE_Class.prototype.createChromeFrame_ = function() { + var impl = this; + var onCfReady = function() {impl.onCfReady_();}; + var onCfMessage = function(evt, target) {impl.onCfMessage_(evt, target);}; + + this.cfHelper_ = new CEEE_CfHelper(this); + var parent = document.getElementById('ceee-frame-deck'); + var cf = this.cfHelper_.create(parent, this.cfHelper_.CHROME_FRAME_ID, + onCfReady, onCfMessage); +}; + +/** Returns a helper object for working with ChromeFrame. */ +CEEE_Class.prototype.getCfHelper = function() { + return this.cfHelper_; +}; + +/** Returns a helper object for working with windows. */ +CEEE_Class.prototype.getWindowModule = function() { + return this.windowModule_; +}; + +/** Returns a helper object for working with tabs. */ +CEEE_Class.prototype.getTabModule = function() { + return this.tabsModule_; +}; + +/** Returns a helper object for working with cookies. */ +CEEE_Class.prototype.getCookieModule = function() { + return this.cookiesModule_; +}; + +/** Returns a helper object for working with sidebar. */ +CEEE_Class.prototype.getSidebarModule = function() { + return this.sidebarModule_; +}; + +/** Returns a helper object for working with user scripts. */ +CEEE_Class.prototype.getUserScriptsModule = function() { + return this.userscriptsModule_; +}; + +/** + * Called when the CEEE host window gets focus. + * @private + */ +CEEE_Class.prototype.onFocus_ = function() { + this.windowModule_.onWindowFocused_(window); +}; + +/** + * Called when the CEEE host document is unloaded. This will be called for + * each top-level window. + * @private + */ +CEEE_Class.prototype.onUnload_ = function() { + CEEE_globals.masterCf.onChromeFrameGoing(this.cfHelper_.get(), + this.logInfo); + this.windowModule_.onWindowRemoved_(window); + this.cookiesModule_.onUnload(); + this.sidebarModule_.onUnload(); + this.logInfo('onUnload_: done'); +}; + +/** + * Called when the page inside ChromeFrame is loaded and ready. Before this + * call, we cannot assume that ChromeFrame is ready to accept any postMessage + * calls. + * @private + */ +CEEE_Class.prototype.onCfReady_ = function() { + this.logInfo('onCfReady_: done'); +}; + +/** + * Called when the main html content of a document is loaded, but before any + * referenced resources, like images and so on, are loaded. This corresponds + * to the DOMContentLoaded event in Firefox. + * + * @param doc The document of the page that fired the event. + * @private + */ +CEEE_Class.prototype.onDomContentLoaded_ = function(doc) { + var impl = this; + + // The target of the event is the document being loaded. The defaultView + // points to the window of this document. Remember that this window has + // been wrapped to make it safe. + var w = doc.defaultView; + if (w) { + w.addEventListener(this.API_EVENT_NAME_, function(evt2) { + impl.onDomApiCall_(w, evt2); + }, false); + + // Attach a handler for onHashChange so that we can fire tab status changes + // when that particular event occurs. + w.addEventListener('hashchange', function() { + impl.onHashChange_(doc); + }, false); + + // TODO(rogerta@chromium.org): I hook unload in order to perform + // cleanup for this content window, specifically to cleanup data + // for the ports. However, chrome extensions allow user scripts + // to post messages during unload, and this cleanup causes those + // messages to be lost. I need to find another event that + // guarantees to fire after all unload listeners to perform the + // cleanup. + w.addEventListener('unload', function() { + impl.userscriptsModule_.onContentUnloaded(w); + impl.logInfo('onContentUnloaded_: done'); + }, false); + + if (this.toolstripDir_) { + // Run all user scripts that match this page. + this.userscriptsModule_.runUserScripts(w); + } else { + this.runWhenExtensionsInited(w, 'runUserScripts', function() { + impl.userscriptsModule_.runUserScripts(w); + }); + } + + // TODO(rogerta@chromium.org): It would be cool to be able detect + // if the content is for an embedded frame, but I have not figured + // out how to do that. It seems that in some cases we get + // DOMContentLoaded even for embedded frames. Not a big deal + // since onTabStatusChanged_() handles it correctly. + this.tabsModule_.onTabStatusChanged(doc, impl.TAB_STATUS_LOADING); + } else { + this.logError('onDomContentLoaded_: no default view for document'); + } + + this.logInfo('onDomContentLoaded_: done loc=' + doc.location); +}; + +/** + * Called when the main html content of a document *and* all its referenced + * resources, like images and so on, are loaded. This corresponds + * to the "onload" event in Firefox, and always happens after the + * DOMContentLoaded event. + * + * @param doc The document of the page that fired the event. + * @private + */ +CEEE_Class.prototype.onContentLoaded_ = function(doc) { + // This event is also fired for images, but we don't want those. Filter out + // the document events as described in + // https://developer.mozilla.org/en/Code_snippets/On_page_load. + if (doc.nodeName == '#document') { + // We only want to generate tab notifications for top-level documents, + // not internal frames. + if (doc.defaultView.frameElement) { + this.logInfo('onContentLoaded_(frame): done loc=' + doc.location); + } else { + this.tabsModule_.onTabStatusChanged(doc, this.TAB_STATUS_COMPLETE); + this.logInfo('onContentLoaded_: done loc=' + doc.location); + } + } +}; + +/** + * Called when a hash change occurs in a given document. + * + * @param doc The document of the page that fired the event. + */ +CEEE_Class.prototype.onHashChange_ = function(doc) { + this.tabsModule_.onTabStatusChanged(doc, this.TAB_STATUS_LOADING); + this.tabsModule_.onTabStatusChanged(doc, this.TAB_STATUS_COMPLETE); +}; + +/** + * @param path {string} A filesystem path or the empty string. + * @return True if the specified path is a .crx (or empty). + */ +CEEE_Class.prototype.isCrx = function(path) { + return (path == '' || /\.crx$/.test(path)); +} + +/** + * Get the path of the .crx file to install. + * + * @return A File object that represents a .crx file to install with Chrome. + */ +CEEE_Class.prototype.getCrxToInstall = function() { + if (!this.crxToInstall_) { + // See if the user is overriding the location of the .crx file. + var crx; + var path = CEEE_globals.getCharPreference('path'); + if (path) { + this.logInfo('getCrxToInstall: path preference exists, try it'); + crx = Components.classes['@mozilla.org/file/local;1'] + .getService(Components.interfaces.nsILocalFile); + try { + crx.initWithPath(path); + + // Make sure the file we read is valid, so that otherwise we + // can use the first file found in the default directory. We + // allow paths from preferences to be paths to either a .crx + // file or an exploded directory, but only search for .crx + // files when scanning the directory below. + if (!crx.exists() || (this.isCrx(path) && crx.isDirectory()) || + (!this.isCrx(path) && !crx.isDirectory())) { + this.logError('getCrxToInstall: "' + path + + '" does not exist or is of the wrong type'); + crx = null; + } + } catch(e) { + this.logError('getCrxToInstall: ' + + 'extensions.ceee.path must be a valid absolute path'); + crx = null; + } + } + + // The user did not override the path to an extension to install. + // Get the default from a well-known directory. + if (!crx) { + var env = Components.classes['@mozilla.org/process/environment;1']; + var userEnv = env.getService(Components.interfaces['nsIEnvironment']); + var PFILES32 = 'ProgramFiles(x86)'; + var PFILES = 'ProgramFiles'; + var sysPfiles = (userEnv.exists(PFILES32) && userEnv.get(PFILES32)) || + (userEnv.exists(PFILES) && userEnv.get(PFILES)) || + 'C:\Program Files'; + path = sysPfiles + '\\Google\\CEEE\\Extensions'; + this.logInfo('getCrxToInstall: trying: ' + path); + + var dir = Components.classes['@mozilla.org/file/local;1'] + .getService(Components.interfaces.nsILocalFile); + dir.initWithPath(path); + + // Find the first file with a .crx extension. Failing that, find the + // first directory. + // TODO(rogerta@chromium.org): eventually we want to support + // loading any number of extensions. + if (dir && dir.exists() && dir.isDirectory()) { + var e = dir.directoryEntries; + while (e.hasMoreElements()) { + var temp = e.getNext().QueryInterface(Components.interfaces.nsIFile); + if (temp && !temp.isDirectory() && /\.crx$/.test(temp.path)) { + crx = temp; + break; + } + } + + // Fall back to using the first directory. + if (!crx) { + e = dir.directoryEntries; + while (e.hasMoreElements()) { + var temp = e.getNext().QueryInterface( + Components.interfaces.nsIFile); + if (temp && temp.isDirectory()) { + crx = temp; + break; + } + } + } + + if (!crx) + crx = null; + } else { + this.logError('Not a directory: ' + path); + } + } + + // Make sure the .crx or exploded directory is valid. + if (!crx) { + this.logInfo('getCrxToInstall: crx is undefined'); + } else if (!crx.exists()) { + this.logError('getCrxToInstall: "' + crx.path + + '" does not exist'); + crx = null; + } else { + this.logInfo('getCrxToInstall: extension found at: ' + crx.path); + } + + this.crxToInstall_ = crx; + } + + // NOTE: its important to return a clone in case the caller modifies the + // file object it receives. + return this.crxToInstall_ ? this.crxToInstall_.clone() : null; +}; + +/** + * Get a File object for the manifest of the Chrome extension registered. + * + * @param baseDir {File} A File object representing the base dir of + * the extension. + * @return A File object that represents the Chrome extension manifest. + * @private + */ +CEEE_Class.prototype.getToolstripManifestFile_ = function(baseDir) { + if (baseDir) { + baseDir = baseDir.clone(); + baseDir.append('manifest.json'); + if (!baseDir.exists() || !baseDir.isFile()) + baseDir = null; + } + + return baseDir; +}; + +/** + * Computes the extension id from the given key. + * + * @private + */ +CEEE_Class.prototype.computeExtensionId_ = function(key) { + if (key.length == 0) + return null; + + // nsICryptoHash expects an array of base64 encoded bytes + var encodedKey = window.atob(key); + var encodedKeyArray = []; + for (var i = 0; i < encodedKey.length; ++i) + encodedKeyArray.push(encodedKey.charCodeAt(i)); + + var cryptoHash = Components.classes['@mozilla.org/security/hash;1'] + .createInstance(Components.interfaces.nsICryptoHash); + cryptoHash.init(cryptoHash.SHA256); + cryptoHash.update(encodedKeyArray, encodedKeyArray.length); + var hashedKey = cryptoHash.finish(false); + + // Then the Id is constructed by offsetting the char 'a' with the nibble + // values of the first 16 bytes of the Hex decoding of the hashed key. + var base = 'a'.charCodeAt(0); + var extensionId = ''; + for (var i = 0; i < 16; ++i) { + var hexValue = ('0' + hashedKey.charCodeAt(i).toString(16)).slice(-2); + extensionId += String.fromCharCode(base + parseInt(hexValue[0], 16)); + extensionId += String.fromCharCode(base + parseInt(hexValue[1], 16)); + } + return extensionId; +}; + +/** + * Load the toolstrip manifest file info and set it as attributes. + * + * @public + */ +CEEE_Class.prototype.loadToolstripManifest = function(baseDir) { + if (!CEEE_json) + return; + + var manifestFile = this.getToolstripManifestFile_(baseDir); + if (!manifestFile) { + this.logError('loadToolstripManifest: no manifest'); + return; + } + + this.logInfo( + 'loadToolstripManifest: loading manifest: ' + manifestFile.path); + + try { + var stream = Components.classes['@mozilla.org/network/file-input-stream;1']. + createInstance(Components.interfaces.nsIFileInputStream); + stream.init(manifestFile, -1, 0, 0); + var manifestData = CEEE_json.decodeFromStream(stream, -1); + } catch (ex) { + this.logError('loadToolstripManifest: error reading manifest: ' + ex); + return; + } finally { + if (stream) + stream.close(); + } + + if (!manifestData) { + this.logError('loadToolstripManifest: no manifest data'); + return; + } + + if (!manifestData.key) { + this.logError('loadToolstripManifest: key missing from manifest'); + return; + } + + if (manifestData.toolstrips && manifestData.toolstrips.length > 0) { + this.extensionId_ = this.computeExtensionId_(manifestData.key); + if (this.extensionId_) { + // TODO(mad@chromium.org): For now we only load the first one we find, + // we may want to stack them at one point... + this.toolstripUrl_ = 'chrome-extension://' + this.extensionId_ + '/' + + manifestData.toolstrips[0]; + } + } + + // Extract all content scripts from the manifest. This include javascript + // code to run as well as CSS scripts to load. + this.userscriptsModule_.loadUserScripts(manifestData); + + // Add more code here to extract more stuff from the manifest, + // user scripts for example. +}; + +/** + * Get the Chrome extension Id of the toolstrip. + * + * @return A string representing the Chrome extension Id of the toolstrip. + * @public + */ +CEEE_Class.prototype.getToolstripExtensionId = function() { + return this.extensionId_; +}; + +/** + * Get the base directory of our one and only extension. Valid only + * after the extensions list has been loaded. + * + * @return A File object for the base directory of the extension. + */ +CEEE_Class.prototype.getToolstripDir = function() { + return this.toolstripDir_.clone(); +}; + +/** + * Get the URL of the Toolstrip. + * + * @return A string that represents the URL of the HTML file to use for the + * toolstrip. + */ +CEEE_Class.prototype.getToolstripUrl = function() { + // If the URL is overridden in the prefences, then use that. + var url = CEEE_globals.getCharPreference('url'); + + // If no override found, use the toolstrip's default URL, if any. + if ((!url || 0 == url.length) && this.toolstripUrl_) { + url = this.toolstripUrl_; + } + + return url; +}; + +/** + * Open a stream for reading from the given file. + * + * @param {!nsILocalFile} file The file to open for reading. + * @return An nsIConverterInputStream that can be read from. + * @public + */ +CEEE_Class.prototype.openFileStream = function(file) { + var fstream = Components.classes['@mozilla.org/network/file-input-stream;1']. + createInstance(Components.interfaces.nsIFileInputStream); + fstream.init(file, -1, 0, 0); + + // Make sure to specify the size of the file, or the read will be truncated + // the to default size, which is 8kb as of this writing. + var stream = + Components.classes['@mozilla.org/intl/converter-input-stream;1']. + createInstance(Components.interfaces.nsIConverterInputStream); + stream.init(fstream, 'UTF-8', file.fileSize, 0); + + return stream; +}; + +/** + * Called in the following cases: + * - the extension running inside ChromeFrame makes a Google Chrome extension + * API call. In this case, the origin is '__priv_xtapi' and the target is + * '__priv_xtreq' + * - a post message request to a given port from the extenstion. In this case, + * the origin is '__priv_xtapi' and the target is '__priv_prtreq'. + * - a response from a post message request to the extenstion. In this case, + * the origin is '__priv_xtapi' and the target is '__priv_prtres'. + * + * Google Chrome extension API calls + * --------------------------------- + * This function is passed an object whose data property is a json-encoded + * string describing the API call that was made. The json encoding is defined + * as follows: + * + * { + * 'name': '<api-function-name>', + * 'args': '[<argument-0>, <argument-1>, ...]', + * 'rqid': <id> + * } + * + * Note that the args property is itself a json-encoded array of the APIs + * arguments. If the API has no arguments then args is 'null'. + * + * If the caller expects a response, then the rqid value must be + * present in that response. A response to an API call is done by + * json-encoding a response object and calling postMesasge() on the ChromeFrame + * instance. The response object is defined as follows: + * + * { + * 'res': '<return-value>', + * 'rqid': <id> + * } + * + * The <return-value> is itself a json-encoded string of whatever the API + * is supposed to return. + * + * Port Request and Response + * ------------------------- + * For a description of the format of the evt argument refer to the docs for + * onPortEvent(). + * + * @param {!Object} evt API event fired from the extension. + * @param {!string} target The target of the message. + * @private + */ +CEEE_Class.prototype.onCfMessage_ = function(evt, target) { + // Do some sanity checking on the event that is passed to us. + if (!evt || !CEEE_json) + return; + + // if this is a response to a port request, give it special handling. + if (target == this.cfHelper_.TARGET_PORT_REQUEST || + target == this.cfHelper_.TARGET_PORT_RESPONSE) { + this.userscriptsModule_.onPortEvent(evt); + return; + } + + var data = CEEE_json.decode(evt.data); + if (!data) { + this.logError('onCfMessage_: no data'); + return; + } + + var cmd = data.name; + if (!cmd) { + this.logError('onCfMessage_: no name'); + return; + } + + if ('rqid' in data) { + this.logInfo('onCfMessage_: cmd=' + cmd + ' rqid=' + data.rqid); + } + + // All is good so far, so try to make the appropriate call. + var value; + var err; + var handler = this.dispatch_[cmd]; + if (typeof handler == 'function') { + try { + value = handler.call(this, cmd, data); + } catch(ex) { + this.logError('onCfMessage_: exception thrown by handler cmd=' + cmd + + ' err=' + ex); + err = ex.message; + value = null; + } + } else { + this.logError('onCfMessage_: dispatch entry is not a function'); + } + + // If the caller expects a response, then send it now. + if ('rqid' in data) { + var r = {}; + r.rqid = data.rqid; + r.res = typeof value != 'undefined' ? CEEE_json.encode(value) : ''; + if (err) + r.err = err; + + this.cfHelper_.postMessage(CEEE_json.encode(r), + this.cfHelper_.TARGET_API_RESPONSE); + } +}; + +/** + * Called when code in the DOM context makes a CEEE API call. DOM context + * refers to code running in the context of a regular web page loaded into one + * of the Firefox main tabs. + * + * @param {!Object} w Window of the DOM context. + * @param {!Object} evt A DOM event fired from plugin. + * @private + */ +CEEE_Class.prototype.onDomApiCall_ = function(w, evt) { + // Do some sanity checking on the event that is passed to us. + if (!evt || !evt.target || !CEEE_json) + return; + + var s = evt.target.getAttribute(this.DATA_ATTRIBUTE_NAME_); + var data = CEEE_json.decode(s); + if (!data) { + this.logError('onDomApiCall_: no data'); + return; + } + + var cmd = data.name; + if (!cmd) { + this.logError('onDomApiCall_: no name'); + return; + } + + var handler = this.dom_dispatch_[cmd]; + var value; + if (typeof handler == 'function') { + try { + value = handler.call(this, w, cmd, data); + } catch(ex) { + this.logError('onDomApiCall_: exception thrown by handler cmd=' + cmd + + ' err=' + ex); + value = ''; + } + } else { + this.logError('onDomApiCall_: no dom_dispatch function for cmd=' + cmd); + } + + if (value) { + // The built-in Firefox json encoding does not work with primitive types, + // so check for that explicitly and handle correctly. + var s = + typeof value == 'object' ? CEEE_json.encode(value) : value; + evt.target.setAttribute(this.RETURN_ATTRIBUTE_NAME_, s); + } +}; + +/** + * Assign a handler associated with the string given in command. + * @param {string} command + * @param {Object} object + * @param {function(Object, Object):Object} handler + * @public + */ +CEEE_Class.prototype.registerExtensionHandler = function(command, + object, + handler) { + this.dispatch_[command] = + function() {return handler.apply(object, arguments);}; +} + +/** + * Assign a handler associated with the DOM event corresponding to the string + * given in command argument. + * @param {string} command + * @param {Object} object + * @param {function(Object, Object):Object} handler + * @public + */ +CEEE_Class.prototype.registerDomHandler = function(command, object, handler) { + this.dom_dispatch_[command] = + function() {return handler.apply(object, arguments);}; +} + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// + +/** + * Global CEEE initialization routine. + * @return {Object} A reference to the initialized CEEE instance. + * @public + */ +function CEEE_initialize() { + var instance = new CEEE_Class(); + + window.addEventListener('load', function() {instance.onLoad_();}, + false); + instance.logInfo('Started'); + + if (instance.contentScriptDebugging) + instance.logInfo('Content script debugging is enabled'); + + return instance; +} + +/** +* Global reference to the CEEE instance. +* @private +*/ +var CEEE_instance_ = CEEE_initialize(); + +// })(); diff --git a/ceee/firefox/content/overlay.xul b/ceee/firefox/content/overlay.xul new file mode 100644 index 0000000..883d5ba --- /dev/null +++ b/ceee/firefox/content/overlay.xul @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="chrome://ceee/skin/overlay.css" type="text/css"?> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. + + Defines how the toolstrip is inserted into the firefox UI. + --> + +<!DOCTYPE overlay SYSTEM "chrome://ceee/locale/ceee.dtd"> + +<overlay id="ceee-overlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <!-- TODO(rogerta@chromium.org): js-coverage.js is only needed for + coverage, should be removed before shipping. --> + <script id="ceee-coverage-js" src="js-coverage.js"/> + <script id="ceee-js" src="overlay.js"/> + <script id="cf-helper-js" src="cf.js"/> + <script id="ceee-bookmarks-api-js" src="bookmarks_api.js"/> + <script id="ceee-infobars-api-js" src="infobars_api.js"/> + <script id="ceee-window-api-js" src="window_api.js"/> + <script id="ceee-tab-api-js" src="tab_api.js"/> + <script id="ceee-userscript-api-js" src="userscript_api.js"/> + <script id="ceee-cookie-api-js" src="cookie_api.js"/> + <script id="ceee-sidebar-api-js" src="sidebar_api.js"/> + + <stringbundleset id="stringbundleset"> + <stringbundle id="ceee-strings" + src="chrome://ceee/locale/ceee.properties"/> + </stringbundleset> + + <!-- all toolstrips should be overlayed inside the toolbox element for + best compatibility. --> + <toolbox id="navigator-toolbox"> + <!-- the hidden=false attribute makes sure that the toolbat will be visible + right after the extension is installed. The persist attribute lists + the names of all the attributes whose value should be persisted when + firefox is stopped and restarted. In this case, we want the hidden + attribute of the toolbar to persist according to the user's choice. + --> + <toolbar id="ceee-toolstrip" toolbarname="&ceee.toolstrip.label;" + accesskey="&ceee.toolstrip.accesskey;" class="chromeclass-toolstrip" + context="toolstrip-context-menu" hidden="false" persist="hidden"> + <!-- the only type of UI control that can be directly a child of a + toolbar is a toolbarbutton. Any other type of control, like the + browser control I am using here, must be wrapped inside a + toolbaritem to appear correctly. --> + <toolbaritem id="ceee-browser-item" title="&ceee.browser.label;" + flex="1" tooltiptext="&ceee.browser.tooltip;"> + <deck id="ceee-frame-deck" flex="1" selectedIndex="1"> + <!-- The ChromeFrame will be placed as a child of this element + just before the following div element in the overlay.js file. --> + <div xmlns="http://www.w3.org/1999/xhtml" + style="background-color:white"/> + </deck> + </toolbaritem> + </toolbar> + </toolbox> + + <hbox id="browser"> + <splitter id="ceee-sidebar-splitter" + resizeafter="flex" + resizebefore="flex" + collapsed="true"/> + <vbox id="ceee-sidebar-container" + flex="1" + collapsed="true" + minwidth="150" width="200"> + <label id="ceee-sidebar-label" + value="My title" + style="font-size:14pt;"/> + <!-- The ChromeFrame will be placed as a child of this element + just before the following div element in the overlay.js file. --> + </vbox> + </hbox> + + <window id="main-window"> + <vbox id="ceee-sidebar-icon-box"> + <!-- The ChromeFrame will be placed as a child of this element + just before the following div element in the overlay.js file. --> + </vbox> + </window> + +</overlay> diff --git a/ceee/firefox/content/sidebar_api.js b/ceee/firefox/content/sidebar_api.js new file mode 100644 index 0000000..fa19eba --- /dev/null +++ b/ceee/firefox/content/sidebar_api.js @@ -0,0 +1,691 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file contains the implementation of the Chrome extension + * sidebars API for the CEEE Firefox add-on. This file is loaded by the + * overlay.xul file, and requires that overlay.js has already been loaded. + * The overlay.xul file contains the necessary DOM structure for the sidebar + * implementation. + * + * This is a preliminary version, not all the functions implemented properly. + * - sidebar minitab icon is fixed; + * - badgeText is not used; + * - others... + * TODO(akoub@google.com): implement all the features. + * + * @supported Firefox 3.x + */ + +/** + * Place-holder namespace-object for helper functions used in this file. + * @private + */ +var CEEE_sidebar_internal_ = CEEE_sidebar_internal_ || { + /** Reference to the instance object for the ceee toolstrip. @private */ + ceeeInstance_: null, + + /** Commands. */ + /** @const */ CMD_SHOW: 'experimental.sidebar.show', + /** @const */ CMD_HIDE: 'experimental.sidebar.hide', + /** @const */ CMD_EXPAND: 'experimental.sidebar.expand', + /** @const */ CMD_COLLAPSE: 'experimental.sidebar.collapse', + /** @const */ CMD_NAVIGATE: 'experimental.sidebar.navigate', + /** @const */ CMD_GET_STATE: 'experimental.sidebar.getState', + /** @const */ CMD_SET_ICON: 'experimental.sidebar.setIcon', + /** @const */ CMD_SET_TITLE: 'experimental.sidebar.setTitle', + /** @const */ CMD_SET_BADGE_TEXT: 'experimental.sidebar.setBadgeText', + + /** Tab attribute that contains all the information about sidebar state. + * @type {string} + * @private + */ + /** @const */ SIDEBAR_INFO: 'ceee_sidebar', + /** @const */ TAB_ID: 'ceee_tabid', + + /** Overlay elements IDs that represent the sidebar. + * None of the elements are visible in the 'hidden' state. + * Only ICON_BOX_ID and enclosed ICON_ID elements are visible in the 'shown' + * and 'active' states. SPLITTER_ID and CONTAINER_ID (with the enclosed + * BROWSER_ID and LABEL_ID) are visible in the 'active' state only. + * @type {string} + * @private + */ + /** @const */ ICON_BOX_ID: 'ceee-sidebar-icon-box', + /** @const */ SPLITTER_ID: 'ceee-sidebar-splitter', + /** @const */ CONTAINER_ID: 'ceee-sidebar-container', + /** @const */ BROWSER_ID: 'ceee-sidebar-browser', + /** @const */ ICON_ID: 'ceee-sidebar-icon', + /** @const */ LABEL_ID: 'ceee-sidebar-label', + + /** Sidebar states. + * No sidebar elements are visible in the 'hidden' state. + * Icon box and the icon are visible in the 'shown' state. + * Icon box, icon, and the sidebar content are visible in 'active' state. + * @type {string} + * @private + */ + /** @const */ STATE_HIDDEN: 'hidden', + /** @const */ STATE_SHOWN: 'shown', + /** @const */ STATE_ACTIVE: 'active', + + /** scrollbars widths. + * @type {number} + * @private + */ + scrollWidth_: 17 +}; + +/** + * Provides a CEEE Toolstrip extension Id. + * @return {string} CEEE Toolstrip extension Id. + * @private + */ +CEEE_sidebar_internal_.getExtensionId_ = function() { + return this.ceeeInstance_.getToolstripExtensionId(); +}; + +/** + * Get a full URL for the given relative path. Assumed that the user + * path is relative to the Chrome Extension root. + * + * @param {!string} path Relative path of the user resource. + * @return {string} Absolute URL for the user resource. + * @private + */ +CEEE_sidebar_internal_.getResourceUrl_ = function(path) { + return 'chrome-extension://' + this.getExtensionId_() + '/' + path; +}; + +/** + * Creates a sidebar object that is linked to a tab and reflects its state. + * @private + */ +CEEE_sidebar_internal_.createNewSidebar_ = function() { + var sidebar = { + state: this.STATE_HIDDEN, + title: '', + badgeText: '', + url: 'about:blank', + icon: '' + }; + return sidebar; +}; + +/** + * Shows the sidebar in its current state. Normally this function is called + * on a TabSelect event, but it is also called whenever the sidebar status + * is changed. + * @param {nsIDOMXULElement} tab XUL tab element to show/hide the sidebar with. + * @private + */ +CEEE_sidebar_internal_.showSidebar_ = function(tab) { + var ceee = this.ceeeInstance_; + + var sidebar = tab[this.SIDEBAR_INFO]; + if (!sidebar) { + ceee.logError('NO SIDEBAR INFO linked to the tab'); + return; + } + + ceee.logInfo('sidebar.showSidebar_ state = ' + sidebar.state + + '; tabId = ' + tab[this.TAB_ID] + + '; url = ' + sidebar.url + + '; title = ' + sidebar.title); + + // Show/hide the sidebar elements and possibly fill in the contents. + var sidebarIconBox = document.getElementById(this.ICON_BOX_ID); + var sidebarSplitter = document.getElementById(this.SPLITTER_ID); + var sidebarContainer = document.getElementById(this.CONTAINER_ID); + var sidebarBrowser = document.getElementById(this.BROWSER_ID); + var sidebarIcon = document.getElementById(this.ICON_ID); + var sidebarLabel = document.getElementById(this.LABEL_ID); + if (this.STATE_SHOWN == sidebar.state) { + sidebarIconBox.style.display = 'block'; + sidebarSplitter.setAttribute('collapsed', 'true'); + sidebarContainer.setAttribute('collapsed', 'true'); + } else if (this.STATE_ACTIVE == sidebar.state) { + sidebarIconBox.style.display = 'block'; + sidebarSplitter.setAttribute('collapsed', 'false'); + sidebarContainer.setAttribute('collapsed', 'false'); + if (sidebarBrowser.src != sidebar.url) { + sidebarBrowser.src = sidebar.url; + } + sidebarLabel.setAttribute('value', sidebar.title); + } else { + // STATE_HIDDEN: + sidebarIconBox.style.display = 'none'; + sidebarSplitter.setAttribute('collapsed', 'true'); + sidebarContainer.setAttribute('collapsed', 'true'); + } + this.onWindowResize_(null); +}; + +/** + * Routine initializaing the sidebar module during onLoad message from the + * browser. Performed for every browser window. + * @public + */ +CEEE_sidebar_internal_.onLoad = function() { + var ceee = this.ceeeInstance_; + ceee.logInfo('sidebar.onLoad'); + + // Add listeners to Tabs events. + var mainBrowser = document.getElementById( + CEEE_globals.MAIN_BROWSER_ID); + var tabContainer = mainBrowser.tabContainer; + var sidebar = this; + tabContainer.addEventListener('TabOpen', + function(evt) { sidebar.onTabOpen_(evt); }, + false); + tabContainer.addEventListener('TabMove', + function(evt) { sidebar.onTabMove_(evt); }, + false); + tabContainer.addEventListener('TabSelect', + function(evt) { sidebar.onTabSelect_(evt); }, + false); + tabContainer.addEventListener('TabClose', + function(evt) { sidebar.onTabClose_(evt); }, + false); + + // Create a sidebar object for each open tab. + for (var i = 0; i < tabContainer.itemCount; ++i) { + var tab = tabContainer.getItemAtIndex(i); + tab[this.SIDEBAR_INFO] = this.createNewSidebar_(); + if (tab.selected) { + this.showSidebar_(tab); + } + } + + this.scrollWidth_ = this.getScrollbarsWidth_(); + + window.addEventListener( + 'resize', CEEE_globals.hitch(this.onWindowResize_, this), false); +}; + +/** + * Calculates current width of a scroll bar. The function makes it with + * direct modelling of an element in the browser's document. + * Does anybody know a better way? + * @return {number} Scrollbars width. + * @private + */ +CEEE_sidebar_internal_.getScrollbarsWidth_ = function() { + var doc = gBrowser.getBrowserForTab(gBrowser.selectedTab).contentDocument; + + if (doc && doc.body) { + var outer = doc.createElement('div'); + outer.style.position = 'fixed'; + outer.style.left = '-1000px'; + outer.style.top = '-1000px'; + outer.style.width = '100px'; + outer.style.height = '100px'; + outer.style.overflowY = 'hidden'; + + var inner = doc.createElement('div'); + inner.width = '100%'; + inner.height = '200px'; + outer.appendChild(inner); + doc.body.appendChild(outer); + + var widthNoBar = inner.offsetWidth; + outer.style.overflowY = 'scroll'; + var widthWithBar = inner.offsetWidth; + doc.body.removeChild(outer); + + return widthNoBar - widthWithBar; + } else { + return 17; + } +}; + +/** + * Positioning sidebar icon on window resize. + * TODO(akoub@google.com): meet RTL languages layout. + * @param {nsIDOMEvent} evt Resize event. + */ +CEEE_sidebar_internal_.onWindowResize_ = function(evt) { + var iconbox = document.getElementById(this.ICON_BOX_ID); + if (iconbox.style.display == 'none') { + return; + } + var right = 0; + var bottom = 0; + + // Place the icon over status-bar if visible + var statusBar = document.getElementById('status-bar'); + var statusBarStyles = window.getComputedStyle(statusBar, ''); + var statusBarVisible = statusBarStyles.display != 'none'; + if (statusBarVisible) { + bottom += parseInt(statusBarStyles.height, 10); + } + + // Place icon on the left of the sidebar if visible. + var splitter = document.getElementById(this.SPLITTER_ID); + var splitterVisible = splitter.getAttribute('collapsed') == 'false'; + if (splitterVisible) { + var splitterWidth = + parseInt(window.getComputedStyle(splitter, '').width, 10) + + parseInt(window.getComputedStyle(splitter, '').borderLeftWidth, 10) + + parseInt(window.getComputedStyle(splitter, '').borderRightWidth, 10); + var browser = document.getElementById(this.CONTAINER_ID); + var browserWidth = + parseInt(window.getComputedStyle(browser, '').width, 10); + right += splitterWidth + browserWidth; + } + + // Shift more to the left/top when scrollbars are visible. + var innerDoc = + gBrowser.getBrowserForTab(gBrowser.selectedTab).contentDocument; + var innerWindow = innerDoc && innerDoc.defaultView; + if (innerDoc.body) { + var innerStyles = innerWindow.getComputedStyle(innerDoc.body, ''); + if (innerStyles.overflow == 'scroll' || + innerStyles.overflowY == 'scroll' || + innerStyles.overflow != 'hidden' && + innerStyles.overflowY != 'hidden' && + innerDoc.body.clientHeight < innerDoc.body.scrollHeight) { + right += this.scrollWidth_; + } + if (innerStyles.overflow == 'scroll' || + innerStyles.overflowX == 'scroll' || + innerStyles.overflow != 'hidden' && + innerStyles.overflowX != 'hidden' && + innerDoc.body.clientWidth < innerDoc.body.scrollWidth) { + bottom += this.scrollWidth_; + } + } + + iconbox.style.right = right + 'px'; + iconbox.style.bottom = bottom + 'px'; + iconbox.style.zIndex = '25'; +}; + +/** + * TabOpen event handler. Creates a sidebar status object and link it + * to a newly created tab. + * @param {nsIDOMEvent} evt Event object. + * @private + */ +CEEE_sidebar_internal_.onTabOpen_ = function(evt) { + var ceee = this.ceeeInstance_; + ceee.logInfo('sidebar.TabOpen'); + var tab = evt.target; + tab[this.SIDEBAR_INFO] = this.createNewSidebar_(); +}; + +/** + * TabClose event handler. NOTHING TO DO. + * @param {nsIDOMEvent} evt Event object. + * @private + */ +CEEE_sidebar_internal_.onTabClose_ = function(evt) { + var ceee = this.ceeeInstance_; + ceee.logInfo('sidebar.TabClose'); +}; + +/** + * TabSelect event handler. Updates the sidebar elements state according to + * the selected tab. + * @param {nsIDOMEvent} evt Event object. + * @private + */ +CEEE_sidebar_internal_.onTabSelect_ = function(evt) { + var ceee = this.ceeeInstance_; + ceee.logInfo('sidebar.TabSelect'); + this.showSidebar_(evt.target); +}; + +/** + * TabMove event handler. NOTHING TO DO. + * @param {nsIDOMEvent} evt Event object. + * @private + */ +CEEE_sidebar_internal_.onTabMove_ = function(evt) { + var ceee = this.ceeeInstance_; + ceee.logInfo('sidebar.TabMove'); +}; + +/** + * Deinitialization of the module. NOTHING TO DO. + * @public + */ +CEEE_sidebar_internal_.onUnload = function() { + var ceee = this.ceeeInstance_; + ceee.logInfo('sidebar.onUnload'); +}; + +/** + * Finds a tab associated with the given tabId. + * Returns current tab if the tabId is undefined. + * @param {?number} tabId Tab identifier. + * @return {?nsIDOMXULElement} Associated tab XUL DOM element. + */ +CEEE_sidebar_internal_.findTab_ = function(tabId) { + if ('undefined' == typeof tabId) { + var tabbrowser = document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + return tabbrowser.selectedTab; + } else if ('number' == typeof tabId) { + var tabObject = CEEE_mozilla_tabs.findTab(tabId); + if (!tabObject) { + this.ceeeInstance_.logError('Tab with tabId=' + tabId + ' not found'); + } else { + return tabObject.tab; + } + } else { + this.ceeeInstance_.logError('Not valid tabId value: ' + tabId); + } +}; + +/** + * Sets a new state to the sidebar. + * @param {nsIDOMXULElement} tab Tab associated with this sidebar. + * @param {string} newState New state to assign to the sidebar. + */ +CEEE_sidebar_internal_.setState_ = function(tab, newState) { + var sidebarInfo = tab[this.SIDEBAR_INFO]; + if (sidebarInfo.state != newState) { + sidebarInfo.state = newState; + this.onStateChanged_(tab); + if (tab.selected) { + this.showSidebar_(tab); + } + } +}; + +/** + * Implementation of experimental.sidebar.show method. + * It performs HIDDEN -> SHOWN sidebar's state transition. + * Has no effect if the state is already SHOWN or ACTIVE. + * @param {string} cmd Command name, 'experimental.sidebar.show' expected. + * @param {Object} data Additional call info, includes args field with + * json-encoded arguments. + */ +CEEE_sidebar_internal_.show_ = function(cmd, data) { + var ceee = this.ceeeInstance_; + + var args = CEEE_json.decode(data.args); + var tabId = args.tabId; + + var tab = this.findTab_(tabId); + var sidebarInfo = tab[this.SIDEBAR_INFO]; + + // We don't wanna change the state if it is already ACTIVE. + if (this.STATE_HIDDEN == sidebarInfo.state) { + this.setState_(tab, this.STATE_SHOWN); + } + + ceee.logInfo('sidebar.show done'); +}; + +/** + * Implementation of experimental.sidebar.hide method. + * It effectively hides the sidebar changing its state to HIDDEN. + * Has no effect if the state is already HIDDEN. + * @param {string} cmd Command name, 'experimental.sidebar.hide' expected. + * @param {Object} data Additional call info, includes args field with + * json-encoded arguments. + */ +CEEE_sidebar_internal_.hide_ = function(cmd, data) { + var ceee = this.ceeeInstance_; + + var args = CEEE_json.decode(data.args); + var tabId = args.tabId; + + var tab = this.findTab_(tabId); + this.setState_(tab, this.STATE_HIDDEN); + + ceee.logInfo('sidebar.hide done'); +}; + +/** + * Implementation of experimental.sidebar.expand method. + * It performs SHOWN -> ACTIVE sidebar's state transition. + * It's not permitted to expand a hidden sidebar, the 'show' function should + * have been called before. + * @param {string} cmd Command name, 'experimental.sidebar.expand' expected. + * @param {Object} data Additional call info, includes args field with + * json-encoded arguments. + */ +CEEE_sidebar_internal_.expand_ = function(cmd, data) { + var ceee = this.ceeeInstance_; + + var args = CEEE_json.decode(data.args); + var tabId = args.tabId; + + var tab = this.findTab_(tabId); + var sidebarInfo = tab[this.SIDEBAR_INFO]; + + // The function cannot be invoked when the sidebar in its 'hidden' state. + if (this.STATE_HIDDEN == sidebarInfo.state) { + ceee.logError('HIDDEN -> ACTIVE state transition not permitted.'); + } else { + this.setState_(tab, this.STATE_ACTIVE); + } + + ceee.logInfo('sidebar.expand done'); +}; + +/** + * Implementation of experimental.sidebar.collapse method. + * It performs ACTIVE -> SHOWN sidebar's state transition. + * Has no effect if the state is already SHOWN or HIDDEN. + * @param {string} cmd Command name, 'experimental.sidebar.collapse' expected. + * @param {Object} data Additional call info, includes args field with + * json-encoded arguments. + */ +CEEE_sidebar_internal_.collapse_ = function(cmd, data) { + var ceee = this.ceeeInstance_; + + var args = CEEE_json.decode(data.args); + var tabId = args.tabId; + + var tab = this.findTab_(tabId); + var sidebarInfo = tab[this.SIDEBAR_INFO]; + + // No effect if the sidebar in its 'hidden' state. + if (this.STATE_ACTIVE == sidebarInfo.state) { + this.setState_(tab, this.STATE_SHOWN); + } + + ceee.logInfo('sidebar.collapse done'); +}; + +/** + * Implementation of experimental.sidebar.navigateTo method. + * @param {string} cmd Command name, 'experimental.sidebar.navigate' expected. + * @param {Object} data Additional call info, includes args field with + * json-encoded arguments. + */ +CEEE_sidebar_internal_.navigate_ = function(cmd, data) { + var ceee = this.ceeeInstance_; + + var args = CEEE_json.decode(data.args); + var tabId = args.tabId; + var url = args.url; + + var tab = this.findTab_(tabId); + var sidebarInfo = tab[this.SIDEBAR_INFO]; + sidebarInfo.url = url; + + if (this.STATE_ACTIVE == sidebarInfo.state && tab.selected) { + this.showSidebar_(tab); + } + + ceee.logInfo('sidebar.navigate done'); +}; + +/** + * Implementation of experimental.sidebar.setIcon method. + * @param {string} cmd Command name, 'experimental.sidebar.setIcon' expected. + * @param {Object} data Additional call info, includes args field with + * json-encoded arguments. + */ +CEEE_sidebar_internal_.setIcon_ = function(cmd, data) { + var ceee = this.ceeeInstance_; + + var args = CEEE_json.decode(data.args); + var tabId = args.tabId; + var imagePath = args.imagePath; + var imageData = args.imageData; + + var tab = this.findTab_(tabId); + var sidebarInfo = tab[this.SIDEBAR_INFO]; + if ('string' == typeof imagePath) { + sidebarInfo.icon = CEEE_sidebar_getUserScriptUrl(imagePath); + } + + if (this.STATE_HIDDEN != sidebarInfo.state && tab.selected) { + this.showSidebar_(tab); + } + + ceee.logInfo('sidebar.setIcon done'); +}; + +/** + * Implementation of experimental.sidebar.setTitle method. + * @param {string} cmd Command name, 'experimental.sidebar.setTitle' expected. + * @param {Object} data Additional call info, includes args field with + * json-encoded arguments. + */ +CEEE_sidebar_internal_.setTitle_ = function(cmd, data) { + var ceee = this.ceeeInstance_; + + var args = CEEE_json.decode(data.args); + var tabId = args.tabId; + var title = args.title; + + var tab = this.findTab_(tabId); + var sidebarInfo = tab[this.SIDEBAR_INFO]; + sidebarInfo.title = title; + + if (this.STATE_HIDDEN != sidebarInfo.state && tab.selected) { + this.showSidebar_(tab); + } + + ceee.logInfo('sidebar.setTitle done'); +}; + +/** + * Implementation of experimental.sidebar.setBadgeText method. + * @param {string} cmd Command name, 'experimental.sidebar.setBadgeText' + * expected. + * @param {Object} data Additional call info, includes args field with + * json-encoded arguments. + */ +CEEE_sidebar_internal_.setBadgeText_ = function(cmd, data) { + var ceee = this.ceeeInstance_; + + var args = CEEE_json.decode(data.args); + var tabId = args.tabId; + var badgeText = args.badgeText; + + var tab = this.findTab_(tabId); + var sidebarInfo = tab[this.SIDEBAR_INFO]; + sidebarInfo.badgeText = badgeText; + + if (this.STATE_HIDDEN != sidebarInfo.state && tab.selected) { + this.showSidebar_(tab); + } + + ceee.logInfo('sidebar.setAttributes done'); +}; + +/** + * Implementation of experimental.sidebar.getState method. + * @param {string} cmd Command name, 'experimental.sidebar.getState' expected. + * @param {Object} data Additional call info, includes args field with + * json-encoded arguments. + */ +CEEE_sidebar_internal_.getState_ = function(cmd, data) { + var ceee = this.ceeeInstance_; + + var args = CEEE_json.decode(data.args); + var tabId = args.tabId; + + var tab = this.findTab_(tabId); + var sidebarInfo = tab[this.SIDEBAR_INFO]; + + ceee.logInfo('sidebar.getState(' + tabId + ') = ' + sidebarInfo.state); + return sidebarInfo.state; +}; + +/** + * Sends an event message to Chrome API when a sidebar state has changed. + * @param {nsIDOMXULElement} tab Where the associated sidebar state has changed. + * @private + */ +CEEE_sidebar_internal_.onStateChanged_ = function(tab) { + // Send the event notification to ChromeFrame. + var sidebarInfo = tab[this.SIDEBAR_INFO]; + var info = [{ + tabId: tab[this.TAB_ID], + state: sidebarInfo.state + }]; + var msg = ['experimental.sidebar.onStateChanged', CEEE_json.encode(info)]; + this.ceeeInstance_.getCfHelper().postMessage( + CEEE_json.encode(msg), + this.ceeeInstance_.getCfHelper().TARGET_EVENT_REQUEST); +}; + +/** + * Initialization routine for the CEEE sidebar API module. + * @param {!Object} ceeeInstance Reference to the global CEEE toolstrip object. + * @return {Object} Reference to the sidebar module. + * @public + */ +function CEEE_initialize_sidebar(ceeeInstance) { + // NOTE: this function must be called after the CF instance used for the + // toolbar is created, in order to make sure that the chrome process started + // uses the correct command line arguments. + + // Create ChromeFrame instances to host the sidebar content. + var cf = document.createElementNS('http://www.w3.org/1999/xhtml', 'embed'); + cf.id = 'ceee-sidebar-browser'; + cf.setAttribute('flex', '1'); + cf.setAttribute('type', 'application/chromeframe'); + document.getElementById('ceee-sidebar-container').appendChild(cf); + + // Create ChromeFrame instances to host the sidebar icon. + cf = document.createElementNS('http://www.w3.org/1999/xhtml', 'embed'); + cf.id = 'ceee-sidebar-icon'; + cf.setAttribute('width', '16'); + cf.setAttribute('height', '16'); + cf.setAttribute('type', 'application/chromeframe'); + cf.setAttribute('src', 'http://www.google.com/favicon.ico'); + document.getElementById('ceee-sidebar-icon-box').appendChild(cf); + + var sidebar = CEEE_sidebar_internal_; + sidebar.ceeeInstance_ = ceeeInstance; + ceeeInstance.registerExtensionHandler(sidebar.CMD_SHOW, + sidebar, + sidebar.show_); + ceeeInstance.registerExtensionHandler(sidebar.CMD_HIDE, + sidebar, + sidebar.hide_); + ceeeInstance.registerExtensionHandler(sidebar.CMD_EXPAND, + sidebar, + sidebar.expand_); + ceeeInstance.registerExtensionHandler(sidebar.CMD_COLLAPSE, + sidebar, + sidebar.collapse_); + ceeeInstance.registerExtensionHandler(sidebar.CMD_NAVIGATE, + sidebar, + sidebar.navigate_); + ceeeInstance.registerExtensionHandler(sidebar.CMD_SET_ICON, + sidebar, + sidebar.setIcon_); + ceeeInstance.registerExtensionHandler(sidebar.CMD_SET_TITLE, + sidebar, + sidebar.setTitle_); + ceeeInstance.registerExtensionHandler(sidebar.CMD_SET_BADGE_TEXT, + sidebar, + sidebar.setBadgeText_); + ceeeInstance.registerExtensionHandler(sidebar.CMD_GET_STATE, + sidebar, + sidebar.getState_); + + ceeeInstance.logInfo('CEEE_initialize_sidebar done'); + return sidebar; +} diff --git a/ceee/firefox/content/tab_api.js b/ceee/firefox/content/tab_api.js new file mode 100644 index 0000000..35470be --- /dev/null +++ b/ceee/firefox/content/tab_api.js @@ -0,0 +1,480 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file contains the implementation of the Chrome extension + * tab APIs for the CEEE Firefox add-on. This file is loaded by the + * overlay.xul file, and requires that overlay.js has already been loaded. + * + * @supported Firefox 3.x + */ + +/** + * Place-holder namespace-object for helper functions used in this file. + * @private + */ +var CEEE_tabs_internal_ = CEEE_tabs_internal_ || { + /** Reference to the instance object for the CEEE. @private */ + ceeeInstance_: null +}; + +/** + * Routine initializaing the tabs module during onLoad message from the + * browser. + * @public + */ +CEEE_tabs_internal_.onLoad = function() { + var mainBrowser = document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + var tabContainer = mainBrowser.tabContainer; + var tabs = this; + tabContainer.addEventListener('TabOpen', + function(evt) {tabs.onTabOpened_(evt);}, + false); + tabContainer.addEventListener('TabMove', + function(evt) {tabs.onTabMoved_(evt);}, + false); + tabContainer.addEventListener('TabSelect', + function(evt) {tabs.onTabSelected_(evt);}, + false); + tabContainer.addEventListener('TabClose', + function(evt) {tabs.onTabClosed_(evt);}, + false); +}; + +/** + * Build an object that represents a "tab", as defined by the Google Chrome + * extension API. For newly created tabs, the optional URL parameter needs to + * be specified since some part of tab creation are asynchronous. Note also + * that the title property of the returned "tab" may be '' for newly created + * tabs too. + * + * @param {!Object} mainBrowser Firefox's main tabbed browser. + * @param {!Object} tab The Firefox tab from which to build the description. + * @param {string} url_opt Optional string contain URL. IF not specified, the + * URL is taken from the tab's associated browser. + * @return A tab description as defined by Google Chrome extension API. + */ +CEEE_tabs_internal_.buildTabValue = function(mainBrowser, tab, url_opt) { + var browser = mainBrowser.getBrowserForTab(tab); + var container = mainBrowser.tabContainer; + var index = container.getIndexOfItem(tab); + + var ret = {}; + ret.id = CEEE_mozilla_tabs.getTabId(tab); + ret.index = index; + ret.windowId = CEEE_mozilla_windows.getWindowId(window); + ret.url = url_opt || browser.currentURI.asciiSpec; + ret.title = browser.contentTitle; + ret.selected = (tab == mainBrowser.selectedTab); + ret.incognito = CEEE_globals.privateBrowsingService.isInPrivateBrowsing; + + return ret; +}; + +/** + * Called when a new tab is created in this top-level window. + * + * @param evt DOM event object. + * @private + */ +CEEE_tabs_internal_.onTabOpened_ = function(evt) { + var mainBrowser = document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + var container = mainBrowser.tabContainer; + var browser = evt.target && evt.target.linkedBrowser; + if (!mainBrowser || !container || !browser) + return; + + // Build a tab info object that corresponds to the new tab. + var index = mainBrowser.getBrowserIndexForDocument(browser.contentDocument); + var tab = container.getItemAtIndex(index); + var info = [this.buildTabValue(mainBrowser, tab)]; + + // Send the event notification to ChromeFrame. + var msg = ['tabs.onCreated', CEEE_json.encode(info)]; + this.ceeeInstance_.getCfHelper().postMessage( + CEEE_json.encode(msg), + this.ceeeInstance_.getCfHelper().TARGET_EVENT_REQUEST); +}; + +/** + * Called when a tab is moved in this top-level window. + * + * @param evt DOM event object. + * @private + */ +CEEE_tabs_internal_.onTabMoved_ = function(evt) { + var mainBrowser = document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + var container = mainBrowser.tabContainer; + var browser = evt.target && evt.target.linkedBrowser; + if (!mainBrowser || !container || !browser) + return; + + // Build info corresponding to moved tab. + // + // Through experimentation, I've discovered that the detail property of the + // evt object contains the old index of the tab. This was discovered with + // Firefox 3.0.11. According to the docs at + // https://developer.mozilla.org/en/DOM/event.detail, this property + // specifies extra details about the event, and its value is event-specific. + var oldIndex = evt.detail; + var index = mainBrowser.getBrowserIndexForDocument(browser.contentDocument); + if (oldIndex == index) + return; + + var tab = container.getItemAtIndex(index); + + var info = [CEEE_mozilla_tabs.getTabId(tab), { + windowId: CEEE_mozilla_windows.getWindowId(window), + fromIndex: oldIndex, + toIndex: index + }]; + + // Send the event notification to ChromeFrame. + var msg = ['tabs.onMoved', CEEE_json.encode(info)]; + this.ceeeInstance_.getCfHelper().postMessage( + CEEE_json.encode(msg), + this.ceeeInstance_.getCfHelper().TARGET_EVENT_REQUEST); +}; + +/** + * Called when a tab is selected. + * + * @param evt DOM event object. + * @private + */ +CEEE_tabs_internal_.onTabSelected_ = function(evt) { + var mainBrowser = document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + var container = mainBrowser.tabContainer; + var browser = evt.target && evt.target.linkedBrowser; + if (!mainBrowser || !container || !browser) + return; + + // Build info corresponding to selected tab. + var index = mainBrowser.getBrowserIndexForDocument(browser.contentDocument); + var tab = container.getItemAtIndex(index); + var info = [CEEE_mozilla_tabs.getTabId(tab), + {windowId: CEEE_mozilla_windows.getWindowId(window)}]; + + // Send the event notification to ChromeFrame. + var msg = ['tabs.onSelectionChanged', CEEE_json.encode(info)]; + this.ceeeInstance_.getCfHelper().postMessage( + CEEE_json.encode(msg), + this.ceeeInstance_.getCfHelper().TARGET_EVENT_REQUEST); +}; + +/** + * Called when a tab is closed in this top-level window. + * + * @param evt DOM event object. + * @private + */ +CEEE_tabs_internal_.onTabClosed_ = function(evt) { + // This function is called *before* the tab is actually removed from the + // tab container. Therefore, we can call methods like + // getBrowserIndexForDocument() and still expect them to find the tab that + // has been closed. + var mainBrowser = document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + var container = mainBrowser.tabContainer; + var browser = evt.target && evt.target.linkedBrowser; + if (!mainBrowser || !container || !browser) + return; + + // Send the event notification to ChromeFrame. + var index = mainBrowser.getBrowserIndexForDocument(browser.contentDocument); + var tab = container.getItemAtIndex(index); + var info = [CEEE_mozilla_tabs.getTabId(tab)]; + var msg = ['tabs.onRemoved', CEEE_json.encode(info)]; + this.ceeeInstance_.getCfHelper().postMessage( + CEEE_json.encode(msg), + this.ceeeInstance_.getCfHelper().TARGET_EVENT_REQUEST); +}; + +/** + * Send a status change notification to the extension. + * + * @param doc The content document object for the tab. + * @param status New status of the tab. + * @public + */ +CEEE_tabs_internal_.onTabStatusChanged = function(doc, status) { + var mainBrowser = document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + var container = mainBrowser.tabContainer; + if (!mainBrowser || !container) + return; + + // Build a tab info object that corresponds to the new status. If we can't + // find the document, then this means its an embedded frame. We don't want + // to generate a status change in this case anyway. + var index = mainBrowser.getBrowserIndexForDocument(doc); + if (-1 == index) + return; + + var tab = container.getItemAtIndex(index); + var info = [CEEE_mozilla_tabs.getTabId(tab), + {status: status}, + this.buildTabValue(mainBrowser, tab) + ]; + + // Send the event notification to ChromeFrame. + var msg = ['tabs.onUpdated', CEEE_json.encode(info)]; + this.ceeeInstance_.getCfHelper().postMessage( + CEEE_json.encode(msg), + this.ceeeInstance_.getCfHelper().TARGET_EVENT_REQUEST); +}; + +CEEE_tabs_internal_.CMD_GET_TAB = 'tabs.get'; +CEEE_tabs_internal_.getTab_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var id = args[0]; + var r = CEEE_mozilla_tabs.findTab(id); + if (!r) { + throw(new Error(CEEE_tabs_internal_.CMD_GET_TAB + + ': invalid tab id=' + id)); + } + + return this.buildTabValue(r.tabBrowser, r.tab); +}; + +CEEE_tabs_internal_.CMD_GET_SELECTED_TAB = 'tabs.getSelected'; +CEEE_tabs_internal_.getSelectedTab_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var id = args[0]; + var win = CEEE_mozilla_windows.findWindow(id); + if (!win) { + throw(new Error(CEEE_tabs_internal_.CMD_GET_SELECTED_TAB + + ': invalid window id=' + id)); + } + + var mainBrowser = win.document.getElementById( + CEEE_globals.MAIN_BROWSER_ID); + if (!mainBrowser) { + throw(new Error(CEEE_tabs_internal_.CMD_GET_SELECTED_TAB + + ': cannot find main browser win id=' + id)); + } + + var tab = mainBrowser.selectedTab; + return this.buildTabValue(mainBrowser, tab); +}; + +CEEE_tabs_internal_.CMD_GET_ALL_TABS_IN_WINDOW = 'tabs.getAllInWindow'; +CEEE_tabs_internal_.getAllTabsInWindow_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var id = args[0]; + var win = CEEE_mozilla_windows.findWindow(id); + if (!win || !win.document) { + throw(new Error(CEEE_tabs_internal_.CMD_GET_ALL_TABS_IN_WINDOW + + ': invalid window id=' + id)); + } + + var mainBrowser = win.document.getElementById( + CEEE_globals.MAIN_BROWSER_ID); + if (!mainBrowser) { + throw(new Error(CEEE_tabs_internal_.CMD_GET_ALL_TABS_IN_WINDOW + + ': cannot find main browser win id=' + id)); + } + + var ret = []; + var tabs = mainBrowser.tabContainer; + var count = tabs.itemCount; + var selectedTab = mainBrowser.selectedTab; + for (var i = 0; i < count; ++i) { + var tab = tabs.getItemAtIndex(i); + var t = this.buildTabValue(mainBrowser, tab); + ret.push(t); + } + + return ret; +}; + +CEEE_tabs_internal_.CMD_CREATE_TAB = 'tabs.create'; +CEEE_tabs_internal_.createTab_ = function(cmd, data) { + var args_list = CEEE_json.decode(data.args); + var args = args_list[0]; + var win = CEEE_mozilla_windows.findWindow(args.windowId); + if (!win) { + throw(new Error(CEEE_tabs_internal_.CMD_CREATE_TAB + + ': invalid window id=' + args.windowId)); + } + + var mainBrowser = win.document.getElementById( + CEEE_globals.MAIN_BROWSER_ID); + if (!mainBrowser) { + throw(new Error(CEEE_tabs_internal_.CMD_CREATE_TAB + + ': cannot find main browser win id=' + args.windowId)); + } + + var tab = mainBrowser.addTab(args.url); + + if (args.index) + mainBrowser.moveTabTo(tab, args.index); + + if (!('selected' in args) || args.selected) + mainBrowser.selectedTab = tab; + + return this.buildTabValue(mainBrowser, tab, args.url); +}; + +CEEE_tabs_internal_.CMD_UPDATE_TAB = 'tabs.update'; +CEEE_tabs_internal_.updateTab_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var id = args[0]; + var t = CEEE_mozilla_tabs.findTab(id); + if (!t) { + throw(new Error(CEEE_tabs_internal_.CMD_UPDATE_TAB + + ': invalid tab id=' + id)); + } + + var info = args[1]; + if (info && info.url) + t.tabBrowser.getBrowserForTab(t.tab).loadURI(info.url); + + if (info && info.selected) + t.tabBrowser.selectedTab = t.tab; + + return this.buildTabValue(t.tabBrowser, t.tab, info.url); +}; + +CEEE_tabs_internal_.CMD_MOVE_TAB = 'tabs.move'; +CEEE_tabs_internal_.moveTab_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var id = args[0]; + var t = CEEE_mozilla_tabs.findTab(id); + if (!t) { + throw(new Error(CEEE_tabs_internal_.CMD_MOVE_TAB + + ': invalid tab id=' + id)); + } + + var newIndex = args[1].index; + var oldIndex = t.index; + if (oldIndex != newIndex) + t.tabBrowser.moveTabTo(t.tab, newIndex); + + return this.buildTabValue(t.tabBrowser, t.tab); +}; + +CEEE_tabs_internal_.CMD_REMOVE_TAB = 'tabs.remove'; +CEEE_tabs_internal_.removeTab_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var id = args[0]; + var r = CEEE_mozilla_tabs.findTab(id); + if (!r) { + throw(new Error(CEEE_tabs_internal_.CMD_REMOVE_TAB + + ': invalid tab id=' + id)); + } + + r.tabBrowser.removeTab(r.tab); +}; + +CEEE_tabs_internal_.CMD_EXECUTE_SCRIPT = 'tabs.executeScript'; +CEEE_tabs_internal_.executeScript_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var id = args[0]; + var scriptDef = args[1]; + var t; + + if (id) { + t = CEEE_mozilla_tabs.findTab(id); + if (!t) { + throw(new Error(CEEE_tabs_internal_.CMD_EXECUTE_SCRIPT + + ': invalid tab id=' + id)); + } + } else { + var win = CEEE_mozilla_windows.findWindow(null); + if (!win) { + throw(new Error(CEEE_tabs_internal_.CMD_EXECUTE_SCRIPT + + ': no window')); + } + + var mainBrowser = win.document.getElementById( + CEEE_globals.MAIN_BROWSER_ID); + if (!mainBrowser) { + throw(new Error(CEEE_tabs_internal_.CMD_EXECUTE_SCRIPT + + ': cannot find main browser')); + } + + t = { + 'tab': mainBrowser.selectedTab, + 'tabBrowser': mainBrowser + }; + } + + var w = t.tabBrowser.getBrowserForTab(t.tab).contentWindow; + return this.ceeeInstance_.getUserScriptsModule().executeScript(w, scriptDef); +}; + +CEEE_tabs_internal_.CMD_INSERT_CSS = 'tabs.insertCSS'; +CEEE_tabs_internal_.insertCss_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var id = args[0]; + var details = args[1]; + var t; + + if (id) { + t = CEEE_mozilla_tabs.findTab(id); + if (!t) { + throw new Error(CEEE_tabs_internal_.CMD_INSERT_CSS + + ': invalid tab id=' + id); + } + } else { + var win = CEEE_mozilla_windows.findWindow(null); + if (!win) { + throw new Error(CEEE_tabs_internal_.CMD_INSERT_CSS + ': no window'); + } + + var mainBrowser = win.document.getElementById( + CEEE_globals.MAIN_BROWSER_ID); + if (!mainBrowser) { + throw new Error(CEEE_tabs_internal_.CMD_INSERT_CSS + + ': cannot find main browser'); + } + + t = { + tab: mainBrowser.selectedTab, + tabBrowser: mainBrowser + }; + } + + var w = t.tabBrowser.getBrowserForTab(t.tab).contentWindow; + return this.ceeeInstance_.getUserScriptsModule().insertCss(w, details); +}; + +/** + * Initialization routine for the CEEE tabs API module. + * @param {!Object} ceeeInstance Reference to the global ceee instance. + * @return {Object} Reference to the tabs module. + * @public + */ +function CEEE_initialize_tabs(ceeeInstance) { + CEEE_tabs_internal_.ceeeInstance_ = ceeeInstance; + var tabs = CEEE_tabs_internal_; + ceeeInstance.registerExtensionHandler(tabs.CMD_GET_TAB, + tabs, + tabs.getTab_); + ceeeInstance.registerExtensionHandler(tabs.CMD_GET_SELECTED_TAB, + tabs, + tabs.getSelectedTab_); + ceeeInstance.registerExtensionHandler(tabs.CMD_GET_ALL_TABS_IN_WINDOW, + tabs, + tabs.getAllTabsInWindow_); + ceeeInstance.registerExtensionHandler(tabs.CMD_CREATE_TAB, + tabs, + tabs.createTab_); + ceeeInstance.registerExtensionHandler(tabs.CMD_UPDATE_TAB, + tabs, + tabs.updateTab_); + ceeeInstance.registerExtensionHandler(tabs.CMD_MOVE_TAB, + tabs, + tabs.moveTab_); + ceeeInstance.registerExtensionHandler(tabs.CMD_REMOVE_TAB, + tabs, + tabs.removeTab_); + ceeeInstance.registerExtensionHandler(tabs.CMD_EXECUTE_SCRIPT, + tabs, + tabs.executeScript_); + ceeeInstance.registerExtensionHandler(tabs.CMD_INSERT_CSS, + tabs, + tabs.insertCss_); + + return tabs; +} diff --git a/ceee/firefox/content/us/base.js b/ceee/firefox/content/us/base.js new file mode 100644 index 0000000..58401de --- /dev/null +++ b/ceee/firefox/content/us/base.js @@ -0,0 +1,1291 @@ +// Copyright 2006 Google Inc. +// All Rights Reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in +// the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +/** + * @fileoverview Bootstrap for the Google JS Library (Closure) Also includes + * stuff taken from //depot/google3/javascript/lang.js. + */ + +/* + * ORIGINAL VERSION: http://doctype.googlecode.com/svn/trunk/goog/base.js + * + * LOCAL CHANGES: Tagged "CEEE changes" below. + * + * This file has been changed from its original so that the deps.js file is + * not automatically inserted into the page. In the context of CEEE, there + * is no deps.js file. Furthermore, because this file is being injected into + * the page using the Firefox sandbox mechanism, it does not have the security + * rights to write to the document using the write() method. + * + * The reason for injecting base.js into the file is to use the closure-based + * json.js file. If we no longer need json.js, then it would be possible to + * remove this file too. Firefox does not have native json library that can + * be used by unprivileged js code, but it will in version 3.5. + */ + +/** + * @define {boolean} Overridden to true by the compiler when --closure_pass + * or --mark_as_compiled is specified. + */ +var COMPILED = false; + + +/** + * Base namespace for the Closure library. Checks to see goog is + * already defined in the current scope before assigning to prevent + * clobbering if base.js is loaded more than once. + */ +var goog = goog || {}; // Check to see if already defined in current scope + + +/** + * Reference to the global context. In most cases this will be 'window'. + */ +goog.global = this; + + +/** + * @define {boolean} DEBUG is provided as a convenience so that debugging code + * that should not be included in a production js_binary can be easily stripped + * by specifying --define goog.DEBUG=false to the JSCompiler. For example, most + * toString() methods should be declared inside an "if (goog.DEBUG)" conditional + * because they are generally used for debugging purposes and it is difficult + * for the JSCompiler to statically determine whether they are used. + */ +goog.DEBUG = true; + + +/** + * @define {string} LOCALE defines the locale being used for compilation. It is + * used to select locale specific data to be compiled in js binary. BUILD rule + * can specify this value by "--define goog.LOCALE=<locale_name>" as JSCompiler + * option. + * + * Take into account that the locale code format is important. You should use + * the canonical Unicode format with hyphen as a delimiter. Language must be + * lowercase, Language Script - Capitalized, Region - UPPERCASE. + * There are few examples: pt-BR, en, en-US, sr-Latin-BO, zh-Hans-CN. + * + * See more info about locale codes here: + * http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers + * + * For language codes you should use values defined by ISO 693-1. See it here + * http://www.w3.org/WAI/ER/IG/ert/iso639.htm. There is only one exception from + * this rule: the Hebrew language. For legacy reasons the old code (iw) should + * be used instead of the new code (he), see http://wiki/Main/IIISynonyms. + */ +// CEEE changes: Changed from 'en' to 'en_US' +goog.LOCALE = 'en_US'; // default to en_US + + +/** + * Indicates whether or not we can call 'eval' directly to eval code in the + * global scope. Set to a Boolean by the first call to goog.globalEval (which + * empirically tests whether eval works for globals). @see goog.globalEval + * @type {boolean?} + * @private + */ +goog.evalWorksForGlobals_ = null; + + +/** + * Creates object stubs for a namespace. When present in a file, goog.provide + * also indicates that the file defines the indicated object. Calls to + * goog.provide are resolved by the compiler if --closure_pass is set. + * @param {string} name name of the object that this file defines. + */ +goog.provide = function(name) { + if (!COMPILED) { + // Ensure that the same namespace isn't provided twice. This is intended + // to teach new developers that 'goog.provide' is effectively a variable + // declaration. And when JSCompiler transforms goog.provide into a real + // variable declaration, the compiled JS should work the same as the raw + // JS--even when the raw JS uses goog.provide incorrectly. + if (goog.getObjectByName(name) && !goog.implicitNamespaces_[name]) { + throw Error('Namespace "' + name + '" already declared.'); + } + + var namespace = name; + while ((namespace = namespace.substring(0, namespace.lastIndexOf('.')))) { + goog.implicitNamespaces_[namespace] = true; + } + } + + goog.exportPath_(name); +}; + + +if (!COMPILED) { + /** + * Namespaces implicitly defined by goog.provide. For example, + * goog.provide('goog.events.Event') implicitly declares + * that 'goog' and 'goog.events' must be namespaces. + * + * @type {Object} + * @private + */ + goog.implicitNamespaces_ = {}; +} + + +/** + * Builds an object structure for the provided namespace path, + * ensuring that names that already exist are not overwritten. For + * example: + * "a.b.c" -> a = {};a.b={};a.b.c={}; + * Used by goog.provide and goog.exportSymbol. + * @param {string} name name of the object that this file defines. + * @param {Object} opt_object the object to expose at the end of the path. + * @param {Object} opt_objectToExportTo The object to add the path to; default + * is |goog.global|. + * @private + */ +goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) { + var parts = name.split('.'); + var cur = opt_objectToExportTo || goog.global; + var part; + + // Internet Explorer exhibits strange behavior when throwing errors from + // methods externed in this manner. See the testExportSymbolExceptions in + // base_test.html for an example. + if (!(parts[0] in cur) && cur.execScript) { + cur.execScript('var ' + parts[0]); + } + + // Parentheses added to eliminate strict JS warning in Firefox. + while (parts.length && (part = parts.shift())) { + if (!parts.length && goog.isDef(opt_object)) { + // last part and we have an object; use it + cur[part] = opt_object; + } else if (cur[part]) { + cur = cur[part]; + } else { + cur = cur[part] = {}; + } + } +}; + + +/** + * Returns an object based on its fully qualified external name. If you are + * using a compilation pass that renames property names beware that using this + * function will not find renamed properties. + * + * @param {string} name The fully qualified name. + * @param {Object} opt_obj The object within which to look; default is + * |goog.global|. + * @return {Object?} The object or, if not found, null. + */ +goog.getObjectByName = function(name, opt_obj) { + var parts = name.split('.'); + var cur = opt_obj || goog.global; + for (var part; part = parts.shift(); ) { + if (cur[part]) { + cur = cur[part]; + } else { + return null; + } + } + return cur; +}; + + +/** + * Globalizes a whole namespace, such as goog or goog.lang. + * + * @param {Object} obj The namespace to globalize. + * @param {Object} opt_global The object to add the properties to. + * @deprecated Properties may be explicitly exported to the global scope, but + * this should no longer be done in bulk. + */ +goog.globalize = function(obj, opt_global) { + var global = opt_global || goog.global; + for (var x in obj) { + global[x] = obj[x]; + } +}; + + +/** + * Adds a dependency from a file to the files it requires. + * @param {string} relPath The path to the js file. + * @param {Array} provides An array of strings with the names of the objects + * this file provides. + * @param {Array} requires An array of strings with the names of the objects + * this file requires. + */ +goog.addDependency = function(relPath, provides, requires) { + if (!COMPILED) { + var provide, require; + var path = relPath.replace(/\\/g, '/'); + var deps = goog.dependencies_; + for (var i = 0; provide = provides[i]; i++) { + deps.nameToPath[provide] = path; + if (!(path in deps.pathToNames)) { + deps.pathToNames[path] = {}; + } + deps.pathToNames[path][provide] = true; + } + for (var j = 0; require = requires[j]; j++) { + if (!(path in deps.requires)) { + deps.requires[path] = {}; + } + deps.requires[path][require] = true; + } + } +}; + + +/** + * Implements a system for the dynamic resolution of dependencies + * that works in parallel with the BUILD system. Note that all calls + * to goog.require will be stripped by the JSCompiler when the + * --closure_pass option is used. + * @param {string} rule Rule to include, in the form goog.package.part. + */ +goog.require = function(rule) { + + // if the object already exists we do not need do do anything + if (!COMPILED) { + if (goog.getObjectByName(rule)) { + return; + } + var path = goog.getPathFromDeps_(rule); + if (path) { + goog.included_[path] = true; + goog.writeScripts_(); + } else { + // NOTE(nicksantos): We could always throw an error, but this would break + // legacy users that depended on this failing silently. Instead, the + // compiler should warn us when there are invalid goog.require calls. + // For now, we simply give clients a way to turn strict mode on. + if (goog.useStrictRequires) { + throw new Error('goog.require could not find: ' + rule); + } + } + } +}; + + +/** + * Whether goog.require should throw an exception if it fails. + * @type {boolean} + */ +goog.useStrictRequires = false; + + +/** + * Path for included scripts + * @type {string} + */ +goog.basePath = ''; + + +/** + * Null function used for default values of callbacks, etc. + * @type {!Function} + */ +goog.nullFunction = function() {}; + + +/** + * The identity function. Returns its first argument. + * + * @param {*} var_args The arguments of the function. + * @return {*} The first argument. + * @deprecated Use goog.functions.identity instead. + */ +goog.identityFunction = function(var_args) { + return arguments[0]; +}; + + +/** + * When defining a class Foo with an abstract method bar(), you can do: + * + * Foo.prototype.bar = goog.abstractMethod + * + * Now if a subclass of Foo fails to override bar(), an error + * will be thrown when bar() is invoked. + * + * Note: This does not take the name of the function to override as + * an argument because that would make it more difficult to obfuscate + * our JavaScript code. + * + * @throws {Error} when invoked to indicate the method should be + * overridden. + */ +goog.abstractMethod = function() { + throw Error('unimplemented abstract method'); +}; + + +/** + * Adds a {@code getInstance} static method that always return the same instance + * object. + * @param {!Function} ctor The constructor for the class to add the static + * method to. + */ +goog.addSingletonGetter = function(ctor) { + ctor.getInstance = function() { + return ctor.instance_ || (ctor.instance_ = new ctor()); + }; +}; + + +if (!COMPILED) { + /** + * Object used to keep track of urls that have already been added. This + * record allows the prevention of circular dependencies. + * @type {Object} + * @private + */ + goog.included_ = {}; + + + /** + * This object is used to keep track of dependencies and other data that is + * used for loading scripts + * @private + * @type {Object} + */ + goog.dependencies_ = { + pathToNames: {}, // 1 to many + nameToPath: {}, // 1 to 1 + requires: {}, // 1 to many + visited: {}, // used when resolving dependencies to prevent us from + // visiting the file twice + written: {} // used to keep track of script files we have written + }; + + + /** + * Tries to detect the base path of the base.js script that bootstraps Closure + * @private + */ + goog.findBasePath_ = function() { + var doc = goog.global.document; + if (typeof doc == 'undefined') { + return; + } + if (goog.global.CLOSURE_BASE_PATH) { + goog.basePath = goog.global.CLOSURE_BASE_PATH; + return; + } else { + // HACKHACK to hide compiler warnings :( + goog.global.CLOSURE_BASE_PATH = null; + } + var scripts = doc.getElementsByTagName('script'); + for (var script, i = 0; script = scripts[i]; i++) { + var src = script.src; + var l = src.length; + if (src.substr(l - 7) == 'base.js') { + goog.basePath = src.substr(0, l - 7); + return; + } + } + }; + + + /** + * Writes a script tag if, and only if, that script hasn't already been added + * to the document. (Must be called at execution time) + * @param {string} src Script source. + * @private + */ + goog.writeScriptTag_ = function(src) { + var doc = goog.global.document; + if (typeof doc != 'undefined' && + !goog.dependencies_.written[src]) { + goog.dependencies_.written[src] = true; + doc.write('<script type="text/javascript" src="' + + src + '"></' + 'script>'); + } + }; + + + /** + * Resolves dependencies based on the dependencies added using addDependency + * and calls writeScriptTag_ in the correct order. + * @private + */ + goog.writeScripts_ = function() { + // the scripts we need to write this time + var scripts = []; + var seenScript = {}; + var deps = goog.dependencies_; + + function visitNode(path) { + if (path in deps.written) { + return; + } + + // we have already visited this one. We can get here if we have cyclic + // dependencies + if (path in deps.visited) { + if (!(path in seenScript)) { + seenScript[path] = true; + scripts.push(path); + } + return; + } + + deps.visited[path] = true; + + if (path in deps.requires) { + for (var requireName in deps.requires[path]) { + if (requireName in deps.nameToPath) { + visitNode(deps.nameToPath[requireName]); + } else { + throw Error('Undefined nameToPath for ' + requireName); + } + } + } + + if (!(path in seenScript)) { + seenScript[path] = true; + scripts.push(path); + } + } + + for (var path in goog.included_) { + if (!deps.written[path]) { + visitNode(path); + } + } + + for (var i = 0; i < scripts.length; i++) { + if (scripts[i]) { + goog.writeScriptTag_(goog.basePath + scripts[i]); + } else { + throw Error('Undefined script input'); + } + } + }; + + + /** + * Looks at the dependency rules and tries to determine the script file that + * fulfills a particular rule. + * @param {string} rule In the form goog.namespace.Class or project.script. + * @return {string?} Url corresponding to the rule, or null. + * @private + */ + goog.getPathFromDeps_ = function(rule) { + if (rule in goog.dependencies_.nameToPath) { + return goog.dependencies_.nameToPath[rule]; + } else { + return null; + } + }; + + goog.findBasePath_(); + // start CEEE changes {{ + // This file is injected into the page using the Firefox sandbox mechanism. + // There is no generated deps.js file to inject with it. + //goog.writeScriptTag_(goog.basePath + 'deps.js'); + // }} end CEEE changes +} + + + +//============================================================================== +// Language Enhancements +//============================================================================== + + +/** + * This is a "fixed" version of the typeof operator. It differs from the typeof + * operator in such a way that null returns 'null' and arrays return 'array'. + * @param {*} value The value to get the type of. + * @return {string} The name of the type. + */ +goog.typeOf = function(value) { + var s = typeof value; + if (s == 'object') { + if (value) { + // We cannot use constructor == Array or instanceof Array because + // different frames have different Array objects. In IE6, if the iframe + // where the array was created is destroyed, the array loses its + // prototype. Then dereferencing val.splice here throws an exception, so + // we can't use goog.isFunction. Calling typeof directly returns 'unknown' + // so that will work. In this case, this function will return false and + // most array functions will still work because the array is still + // array-like (supports length and []) even though it has lost its + // prototype. + // Mark Miller noticed that Object.prototype.toString + // allows access to the unforgeable [[Class]] property. + // 15.2.4.2 Object.prototype.toString ( ) + // When the toString method is called, the following steps are taken: + // 1. Get the [[Class]] property of this object. + // 2. Compute a string value by concatenating the three strings + // "[object ", Result(1), and "]". + // 3. Return Result(2). + // and this behavior survives the destruction of the execution context. + if (value instanceof Array || // Works quickly in same execution context. + // If value is from a different execution context then + // !(value instanceof Object), which lets us early out in the common + // case when value is from the same context but not an array. + // The {if (value)} check above means we don't have to worry about + // undefined behavior of Object.prototype.toString on null/undefined. + // + // HACK: In order to use an Object prototype method on the arbitrary + // value, the compiler requires the value be cast to type Object, + // even though the ECMA spec explicitly allows it. + (!(value instanceof Object) && + Object.prototype.toString.call( + /** @type {Object} */(value)) == '[object Array]')) { + return 'array'; + } + // HACK: There is still an array case that fails. + // function ArrayImpostor() {} + // ArrayImpostor.prototype = []; + // var impostor = new ArrayImpostor; + // this can be fixed by getting rid of the fast path + // (value instanceof Array) and solely relying on + // (value && Object.prototype.toString.vall(value) === '[object Array]') + // but that would require many more function calls and is not warranted + // unless closure code is receiving objects from untrusted sources. + + // IE in cross-window calls does not correctly marshal the function type + // (it appears just as an object) so we cannot use just typeof val == + // 'function'. However, if the object has a call property, it is a + // function. + if (typeof value.call != 'undefined') { + return 'function'; + } + } else { + return 'null'; + } + + // In Safari typeof nodeList returns 'function', and on Firefox + // typeof behaves similarly for HTML{Applet,Embed,Object}Elements + // and RegExps. We would like to return object for those and we can + // detect an invalid function by making sure that the function + // object has a call method. + } else if (s == 'function' && typeof value.call == 'undefined') { + return 'object'; + } + return s; +}; + + +/** + * Safe way to test whether a property is enumarable. It allows testing + * for enumerable on objects where 'propertyIsEnumerable' is overridden or + * does not exist (like DOM nodes in IE). Does not use browser native + * Object.propertyIsEnumerable. + * @param {Object} object The object to test if the property is enumerable. + * @param {string} propName The property name to check for. + * @return {boolean} True if the property is enumarable. + * @private + */ +goog.propertyIsEnumerableCustom_ = function(object, propName) { + // KJS in Safari 2 is not ECMAScript compatible and lacks crucial methods + // such as propertyIsEnumerable. We therefore use a workaround. + // Does anyone know a more efficient work around? + if (propName in object) { + for (var key in object) { + if (key == propName && + Object.prototype.hasOwnProperty.call(object, propName)) { + return true; + } + } + } + return false; +}; + + +if (Object.prototype.propertyIsEnumerable) { + /** + * Safe way to test whether a property is enumarable. It allows testing + * for enumerable on objects where 'propertyIsEnumerable' is overridden or + * does not exist (like DOM nodes in IE). + * @param {Object} object The object to test if the property is enumerable. + * @param {string} propName The property name to check for. + * @return {boolean} True if the property is enumarable. + * @private + */ + goog.propertyIsEnumerable_ = function(object, propName) { + // In IE if object is from another window, cannot use propertyIsEnumerable + // from this window's Object. Will raise a 'JScript object expected' error. + if (object instanceof Object) { + return Object.prototype.propertyIsEnumerable.call(object, propName); + } else { + return goog.propertyIsEnumerableCustom_(object, propName); + } + }; +} else { + // CEEE changes: Added the conditional above and this case as a bugfix. + goog.propertyIsEnumerable_ = goog.propertyIsEnumerableCustom_; +} + +/** + * Returns true if the specified value is not |undefined|. + * WARNING: Do not use this to test if an object has a property. Use the in + * operator instead. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is defined. + */ +goog.isDef = function(val) { + return typeof val != 'undefined'; +}; + + +/** + * Returns true if the specified value is |null| + * @param {*} val Variable to test. + * @return {boolean} Whether variable is null. + */ +goog.isNull = function(val) { + return val === null; +}; + + +/** + * Returns true if the specified value is defined and not null + * @param {*} val Variable to test. + * @return {boolean} Whether variable is defined and not null. + */ +goog.isDefAndNotNull = function(val) { + return goog.isDef(val) && !goog.isNull(val); +}; + + +/** + * Returns true if the specified value is an array + * @param {*} val Variable to test. + * @return {boolean} Whether variable is an array. + */ +goog.isArray = function(val) { + return goog.typeOf(val) == 'array'; +}; + + +/** + * Returns true if the object looks like an array. To qualify as array like + * the value needs to be either a NodeList or an object with a Number length + * property. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is an array. + */ +goog.isArrayLike = function(val) { + var type = goog.typeOf(val); + return type == 'array' || type == 'object' && typeof val.length == 'number'; +}; + + +/** + * Returns true if the object looks like a Date. To qualify as Date-like + * the value needs to be an object and have a getFullYear() function. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a like a Date. + */ +goog.isDateLike = function(val) { + return goog.isObject(val) && typeof val.getFullYear == 'function'; +}; + + +/** + * Returns true if the specified value is a string + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a string. + */ +goog.isString = function(val) { + return typeof val == 'string'; +}; + + +/** + * Returns true if the specified value is a boolean + * @param {*} val Variable to test. + * @return {boolean} Whether variable is boolean. + */ +goog.isBoolean = function(val) { + return typeof val == 'boolean'; +}; + + +/** + * Returns true if the specified value is a number + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a number. + */ +goog.isNumber = function(val) { + return typeof val == 'number'; +}; + + +/** + * Returns true if the specified value is a function + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a function. + */ +goog.isFunction = function(val) { + return goog.typeOf(val) == 'function'; +}; + + +/** + * Returns true if the specified value is an object. This includes arrays + * and functions. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is an object. + */ +goog.isObject = function(val) { + var type = goog.typeOf(val); + return type == 'object' || type == 'array' || type == 'function'; +}; + + +/** + * Adds a hash code field to an object. The hash code is unique for the + * given object. + * @param {Object} obj The object to get the hash code for. + * @return {number} The hash code for the object. + */ +goog.getHashCode = function(obj) { + // In IE, DOM nodes do not extend Object so they do not have this method. + // we need to check hasOwnProperty because the proto might have this set. + + if (obj.hasOwnProperty && obj.hasOwnProperty(goog.HASH_CODE_PROPERTY_)) { + var hashCode = obj[goog.HASH_CODE_PROPERTY_]; + // CEEE changes: workaround for Chrome bug 1252508. + if (hashCode) { + return hashCode; + } + } + if (!obj[goog.HASH_CODE_PROPERTY_]) { + obj[goog.HASH_CODE_PROPERTY_] = ++goog.hashCodeCounter_; + } + return obj[goog.HASH_CODE_PROPERTY_]; +}; + + +/** + * Removes the hash code field from an object. + * @param {Object} obj The object to remove the field from. + */ +goog.removeHashCode = function(obj) { + // DOM nodes in IE are not instance of Object and throws exception + // for delete. Instead we try to use removeAttribute + if ('removeAttribute' in obj) { + obj.removeAttribute(goog.HASH_CODE_PROPERTY_); + } + /** @preserveTry */ + try { + delete obj[goog.HASH_CODE_PROPERTY_]; + } catch (ex) { + } +}; + + +/** + * {String} Name for hash code property + * @private + */ +goog.HASH_CODE_PROPERTY_ = 'closure_hashCode_'; + + +/** + * @type {number} Counter for hash codes. + * @private + */ +goog.hashCodeCounter_ = 0; + + +/** + * Clone an object/array (recursively) + * @param {Object} proto Object to clone. + * @return {Object} Clone of x;. + */ +goog.cloneObject = function(proto) { + var type = goog.typeOf(proto); + if (type == 'object' || type == 'array') { + if (proto.clone) { + return proto.clone.call(proto); + } + var clone = type == 'array' ? [] : {}; + for (var key in proto) { + clone[key] = goog.cloneObject(proto[key]); + } + return clone; + } + + return proto; +}; + + +/** + * Forward declaration for the clone method. This is necessary until the + * compiler can better support duck-typing constructs as used in + * goog.cloneObject. + * + * @type {Function} + */ +Object.prototype.clone; + + +/** + * Partially applies this function to a particular 'this object' and zero or + * more arguments. The result is a new function with some arguments of the first + * function pre-filled and the value of |this| 'pre-specified'.<br><br> + * + * Remaining arguments specified at call-time are appended to the pre- + * specified ones.<br><br> + * + * Also see: {@link #partial}.<br><br> + * + * Note that bind and partial are optimized such that repeated calls to it do + * not create more than one function object, so there is no additional cost for + * something like:<br> + * + * <pre>var g = bind(f, obj); + * var h = partial(g, 1, 2, 3); + * var k = partial(h, a, b, c);</pre> + * + * Usage: + * <pre>var barMethBound = bind(myFunction, myObj, 'arg1', 'arg2'); + * barMethBound('arg3', 'arg4');</pre> + * + * @param {Function} fn A function to partially apply. + * @param {Object} selfObj Specifies the object which |this| should point to + * when the function is run. If the value is null or undefined, it will + * default to the global object. + * @param {Object} var_args Additional arguments that are partially + * applied to the function. + * + * @return {!Function} A partially-applied form of the function bind() was + * invoked as a method of. + */ +goog.bind = function(fn, selfObj, var_args) { + var boundArgs = fn.boundArgs_; + + if (arguments.length > 2) { + var args = Array.prototype.slice.call(arguments, 2); + if (boundArgs) { + args.unshift.apply(args, boundArgs); + } + boundArgs = args; + } + + selfObj = fn.boundSelf_ || selfObj; + fn = fn.boundFn_ || fn; + + var newfn; + var context = selfObj || goog.global; + + if (boundArgs) { + newfn = function() { + // Combine the static args and the new args into one big array + var args = Array.prototype.slice.call(arguments); + args.unshift.apply(args, boundArgs); + return fn.apply(context, args); + }; + } else { + newfn = function() { + return fn.apply(context, arguments); + }; + } + + newfn.boundArgs_ = boundArgs; + newfn.boundSelf_ = selfObj; + newfn.boundFn_ = fn; + + return newfn; +}; + + +/** + * Like bind(), except that a 'this object' is not required. Useful when the + * target function is already bound. + * + * Usage: + * var g = partial(f, arg1, arg2); + * g(arg3, arg4); + * + * @param {Function} fn A function to partially apply. + * @param {Object} var_args Additional arguments that are partially + * applied to fn. + * @return {!Function} A partially-applied form of the function bind() was + * invoked as a method of. + */ +goog.partial = function(fn, var_args) { + var args = Array.prototype.slice.call(arguments, 1); + args.unshift(fn, null); + return goog.bind.apply(null, args); +}; + + +/** + * Copies all the members of a source object to a target object. + * @param {Object} target Target. + * @param {Object} source Source. + * @deprecated Use goog.object.extend instead. + */ +goog.mixin = function(target, source) { + for (var x in source) { + target[x] = source[x]; + } + + // For IE the for-in-loop does not contain any properties that are not + // enumerable on the prototype object (for example, isPrototypeOf from + // Object.prototype) but also it will not include 'replace' on objects that + // extend String and change 'replace' (not that it is common for anyone to + // extend anything except Object). +}; + + +/** + * A simple wrapper for new Date().getTime(). + * + * @return {number} An integer value representing the number of milliseconds + * between midnight, January 1, 1970 and the current time. + */ +goog.now = Date.now || (function() { + return new Date().getTime(); +}); + + +/** + * Evals javascript in the global scope. In IE this uses execScript, other + * browsers use goog.global.eval. If goog.global.eval does not evaluate in the + * global scope (for example, in Safari), appends a script tag instead. + * Throws an exception if neither execScript or eval is defined. + * @param {string} script JavaScript string. + */ +goog.globalEval = function(script) { + if (goog.global.execScript) { + goog.global.execScript(script, 'JavaScript'); + } else if (goog.global.eval) { + // Test to see if eval works + if (goog.evalWorksForGlobals_ == null) { + goog.global.eval('var _et_ = 1;'); + if (typeof goog.global['_et_'] != 'undefined') { + delete goog.global['_et_']; + goog.evalWorksForGlobals_ = true; + } else { + goog.evalWorksForGlobals_ = false; + } + } + + if (goog.evalWorksForGlobals_) { + goog.global.eval(script); + } else { + var doc = goog.global.document; + var scriptElt = doc.createElement('script'); + scriptElt.type = 'text/javascript'; + scriptElt.defer = false; + // Note(pupius): can't use .innerHTML since "t('<test>')" will fail and + // .text doesn't work in Safari 2. Therefore we append a text node. + scriptElt.appendChild(doc.createTextNode(script)); + doc.body.appendChild(scriptElt); + doc.body.removeChild(scriptElt); + } + } else { + throw Error('goog.globalEval not available'); + } +}; + + +/** + * Forward declaration of a type name. + * + * A call of the form + * goog.declareType('goog.MyClass'); + * tells JSCompiler "goog.MyClass is not a hard dependency of this file. + * But it may appear in the type annotations here. This is to assure + * you that the class does indeed exist, even if it's not declared in the + * final binary." + * + * In uncompiled code, does nothing. + * @param {string} typeName The name of the type. + */ +goog.declareType = function(typeName) {}; + + +/** + * A macro for defining composite types. + * + * By assigning goog.typedef to a name, this tells JSCompiler that this is not + * the name of a class, but rather it's the name of a composite type. + * + * For example, + * /** @type {Array|NodeList} / goog.ArrayLike = goog.typedef; + * will tell JSCompiler to replace all appearances of goog.ArrayLike in type + * definitions with the union of Array and NodeList. + * + * Does nothing in uncompiled code. + */ +goog.typedef = true; + + +/** + * Handles strings that are intended to be used as CSS class names. + * + * Without JS Compiler the arguments are simple joined with a hyphen and passed + * through unaltered. + * + * With the JS Compiler the arguments are inlined, e.g: + * var x = goog.getCssName('foo'); + * var y = goog.getCssName(this.baseClass, 'active'); + * becomes: + * var x= 'foo'; + * var y = this.baseClass + '-active'; + * + * If a CSS renaming map is passed to the compiler it will replace symbols in + * the classname. If one argument is passed it will be processed, if two are + * passed only the modifier will be processed, as it is assumed the first + * argument was generated as a result of calling goog.getCssName. + * + * Names are split on 'hyphen' and processed in parts such that the following + * are equivalent: + * var base = goog.getCssName('baseclass'); + * goog.getCssName(base, 'modifier'); + * goog.getCSsName('baseclass-modifier'); + * + * If any part does not appear in the renaming map a warning is logged and the + * original, unobfuscated class name is inlined. + * + * @param {string} className The class name. + * @param {string} opt_modifier A modifier to be appended to the class name. + * @return {string} The class name or the concatenation of the class name and + * the modifier. + */ +goog.getCssName = function(className, opt_modifier) { + return className + (opt_modifier ? '-' + opt_modifier : ''); +}; + + +/** + * Abstract implementation of goog.getMsg for use with localized messages. + * @param {string} str Translatable string, places holders in the form {$foo}. + * @param {Object} opt_values Map of place holder name to value. + * @return {string} message with placeholders filled. + */ +goog.getMsg = function(str, opt_values) { + var values = opt_values || {}; + for (var key in values) { + str = str.replace(new RegExp('\\{\\$' + key + '\\}', 'gi'), values[key]); + } + return str; +}; + + +/** + * Exposes an unobfuscated global namespace path for the given object. + * Note that fields of the exported object *will* be obfuscated, + * unless they are exported in turn via this function or + * goog.exportProperty + * + * <p>Also handy for making public items that are defined in anonymous + * closures. + * + * ex. goog.exportSymbol('Foo', Foo); + * + * ex. goog.exportSymbol('public.path.Foo.staticFunction', + * Foo.staticFunction); + * public.path.Foo.staticFunction(); + * + * ex. goog.exportSymbol('public.path.Foo.prototype.myMethod', + * Foo.prototype.myMethod); + * new public.path.Foo().myMethod(); + * + * @param {string} publicPath Unobfuscated name to export. + * @param {Object} object Object the name should point to. + * @param {Object} opt_objectToExportTo The object to add the path to; default + * is |goog.global|. + */ +goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) { + goog.exportPath_(publicPath, object, opt_objectToExportTo); +}; + + +/** + * Exports a property unobfuscated into the object's namespace. + * ex. goog.exportProperty(Foo, 'staticFunction', Foo.staticFunction); + * ex. goog.exportProperty(Foo.prototype, 'myMethod', Foo.prototype.myMethod); + * @param {Object} object Object whose static property is being exported. + * @param {string} publicName Unobfuscated name to export. + * @param {Object} symbol Object the name should point to. + */ +goog.exportProperty = function(object, publicName, symbol) { + object[publicName] = symbol; +}; + + +/** + * Inherit the prototype methods from one constructor into another. + * + * Usage: + * <pre> + * function ParentClass(a, b) { } + * ParentClass.prototype.foo = function(a) { } + * + * function ChildClass(a, b, c) { + * ParentClass.call(this, a, b); + * } + * + * goog.inherits(ChildClass, ParentClass); + * + * var child = new ChildClass('a', 'b', 'see'); + * child.foo(); // works + * </pre> + * + * In addition, a superclass' implementation of a method can be invoked + * as follows: + * + * <pre> + * ChildClass.prototype.foo = function(a) { + * ChildClass.superClass_.foo.call(this, a); + * // other code + * }; + * </pre> + * + * @param {Function} childCtor Child class. + * @param {Function} parentCtor Parent class. + */ +goog.inherits = function(childCtor, parentCtor) { + /** @constructor */ + function tempCtor() {}; + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor(); + childCtor.prototype.constructor = childCtor; +}; + + +//============================================================================== +// Extending Function +//============================================================================== + + +/** + * @define {boolean} Whether to extend Function.prototype. + * Use --define='goog.MODIFY_FUNCTION_PROTOTYPES=false' to change. + */ +goog.MODIFY_FUNCTION_PROTOTYPES = true; + +if (goog.MODIFY_FUNCTION_PROTOTYPES) { + + + /** + * An alias to the {@link goog.bind()} global function. + * + * Usage: + * var g = f.bind(obj, arg1, arg2); + * g(arg3, arg4); + * + * @param {Object} selfObj Specifies the object to which |this| should point + * when the function is run. If the value is null or undefined, it will + * default to the global object. + * @param {Object} var_args Additional arguments that are partially + * applied to fn. + * @return {!Function} A partially-applied form of the Function on which + * bind() was invoked as a method. + * @deprecated Use the static function goog.bind instead. + */ + Function.prototype.bind = function(selfObj, var_args) { + if (arguments.length > 1) { + var args = Array.prototype.slice.call(arguments, 1); + args.unshift(this, selfObj); + return goog.bind.apply(null, args); + } else { + return goog.bind(this, selfObj); + } + }; + + + /** + * An alias to the {@link goog.partial()} static function. + * + * Usage: + * var g = f.partial(arg1, arg2); + * g(arg3, arg4); + * + * @param {Object} var_args Additional arguments that are partially + * applied to fn. + * @return {!Function} A partially-applied form of the function partial() was + * invoked as a method of. + * @deprecated Use the static function goog.partial instead. + */ + Function.prototype.partial = function(var_args) { + var args = Array.prototype.slice.call(arguments); + args.unshift(this, null); + return goog.bind.apply(null, args); + }; + + + /** + * Inherit the prototype methods from one constructor into another. + * @param {Function} parentCtor Parent class. + * @see goog.inherits + * @deprecated Use the static function goog.inherits instead. + */ + Function.prototype.inherits = function(parentCtor) { + goog.inherits(this, parentCtor); + }; + + + /** + * Mixes in an object's properties and methods into the callee's prototype. + * Basically mixin based inheritance, thus providing an alternative method for + * adding properties and methods to a class' prototype. + * + * <pre> + * function X() {} + * X.mixin({ + * one: 1, + * two: 2, + * three: 3, + * doit: function() { return this.one + this.two + this.three; } + * }); + * + * function Y() { } + * Y.mixin(X.prototype); + * Y.prototype.four = 15; + * Y.prototype.doit2 = function() { return this.doit() + this.four; } + * }); + * + * // or + * + * function Y() { } + * Y.inherits(X); + * Y.mixin({ + * one: 10, + * four: 15, + * doit2: function() { return this.doit() + this.four; } + * }); + * </pre> + * + * @param {Object} source from which to copy properties. + * @see goog.mixin + * @deprecated Use the static function goog.object.extend instead. + */ + Function.prototype.mixin = function(source) { + goog.mixin(this.prototype, source); + }; +}
\ No newline at end of file diff --git a/ceee/firefox/content/us/ceee_bootstrap.js b/ceee/firefox/content/us/ceee_bootstrap.js new file mode 100644 index 0000000..24bc65b --- /dev/null +++ b/ceee/firefox/content/us/ceee_bootstrap.js @@ -0,0 +1,254 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file contains a bootstrap for the goog.* and chrome.* + * APIs that are available to user scripts, when run inside of Firefox. This + * should be the first script injected into any page. + * + * @supported Firefox 3.x + */ + +// Define the global namespaces. +var chrome = chrome || {}; +var console = console || {}; +var goog = goog || {}; +var JSON = goog.json; +JSON.stringify = JSON.serialize; +var ceee = ceee || { + /** @const */ API_EVENT_NAME_ : 'ceee-dom-api', + /** @const */ API_ELEMENT_NAME_ : 'ceee-api-element', + /** @const */ DATA_ATTRIBUTE_NAME_ : 'ceee-event-data', + /** @const */ RETURN_ATTRIBUTE_NAME_ : 'ceee-event-return', + + /** + * This object acts as the "chromeHidden" object for the chrome API + * implementation. Access to it is provided by the GetChromeHidden() to + * emulate Chrome's behaviour. + */ + hidden: {} +}; + +/** + * Create and initialize a generic event for sending Google Chrome extension + * API calls from the user script to the CEEE add-on. + * + * @return {!object} The new API event. + * @private + */ +ceee.createApiEvent_ = function() { + var evt = document.createEvent('Events'); + evt.initEvent(this.API_EVENT_NAME_, // name of ceee API event type + true, // event should bubble up through the event chain + false); // event is not cancelable + return evt; +}; + +/** + * Get the DOM element in the document used to hold the arguments and + * return value for Google Chrome user script extension API calls made to the + * ceee add-on. This element is added to the page DOM if not already + * present, and is not visible. + * + * The DATA_ATTRIBUTE_NAME_ attributes of the returned element will be set to + * the empty string upon return of this function. + * + * @param {!string} cmd The API command that will be fired by this event. + * @return {!object} The DOM element. + * @private + */ +ceee.getApiElement_ = function(cmd) { + // Get the element from the DOM used to pass arguments receive results from + // the ceee add-on. If the element does not exist create it now. There + // should not be any threading issues with this object. + var e = document.getElementById(this.API_ELEMENT_NAME_); + if (e == null) { + e = document.createElement(this.API_ELEMENT_NAME_); + document.documentElement.appendChild(e); + } + + // Remove any existing attributes that may be left over from a previous + // API call. + e.setAttribute(this.DATA_ATTRIBUTE_NAME_, ''); + e.setAttribute(this.RETURN_ATTRIBUTE_NAME_, ''); + + // Set the command of the new API call, plus an id so that we can find + // this element again. We don't need to create a bunch of these. + e.setAttribute('id', this.API_ELEMENT_NAME_); + return e; +}; + +/** + * Send a Google Chrome extension API call to the ceee add-on. + * + * @param {!string} cmd The API command that will be fired by this event. + * @param {object} args Arguments of the API call. Note that non-string + * entries in the array will be converted to string via toString(). + * @private + */ +ceee.sendMessage_ = function(cmd, args) { + // Package up the DOM API call into the same format as the UI API. Then + // stuff it into the DOM API element. + var data = {}; + data.name = cmd; + data.args = args; + + var s = goog.json.serialize(data); + var evt = this.createApiEvent_(); + var e = this.getApiElement_(cmd); + + e.setAttribute(this.DATA_ATTRIBUTE_NAME_, s); + e.dispatchEvent(evt); + + // The dispatch call above is synchronous. If there is a return value, it + // has been placed in the DOM API element. Extract it and return. + var r = e.getAttribute(this.RETURN_ATTRIBUTE_NAME_); + if (r && r.length > 0) + return goog.json.parse(r); +}; + +// Add any functions that are missing from console. + +if (!console.info) { + /** + * Write an informational log to the Firefox error console. + * @param {string} message The message to log to the console. + */ + console.info = function(message) { + ceee.sendMessage_('LogToConsole', [message]); + }; +} + +if (!console.log) { + /** + * Write an informational log to the Firefox error console. + * @param {string} message The message to log to the console. + */ + console.log = function(message) { + ceee.sendMessage_('LogToConsole', [message]); + }; +} + +if (!console.warn) { + /** + * Write a warning log to the Firefox error console. + * @param {string} message The message to log to the console. + */ + console.warn = function(message) { + ceee.sendMessage_('LogToConsole', [message]); + }; +} + +if (!console.error) { + /** + * Write an error to the Firefox error console. + * @param {string} message The message to log to the console. + */ + console.error = function(message) { + ceee.sendMessage_('ErrorToConsole', [message]); + }; +} + +if (!console.assert) { + /** + * Conditionally write an error to the Firefox error console. + * @param {string} message The message to log to the console. + */ + console.assert = function(test, message) { + if (!test) { + ceee.sendMessage_('ErrorToConsole', ['Assertion failed: ' + message]); + } + }; +} + +if (!console.count) { + /** + * Send a count message to the Firefox error console. + * @param {string} name The name of the counter. + */ + console.count = function(name) { + var count = console.count.map[name]; + if (count) { + count++; + } else { + count = 1; + } + console.count.map[name] = count; + + ceee.sendMessage_('LogToConsole', [name + ': ' + count]); + }; + + /** + * A map of names to counts. + * @type {Object.<string, number>} + */ + console.count.map = {}; +} + +if (!console.time) { + /** + * Record a time message to be used with console.timeEnd(). + * @param {string} name The name of the timer. + */ + console.time = function(name) { + console.time.map[name] = new Date().getTime(); + }; + + /** + * A map of names to times in ms. + * @type {Object.<string, number>} + */ + console.time.map = {}; +} + +if (!console.timeEnd) { + /** + * Send a time end message to the Firefox error console. + * @param {string} name The name of the timer. + */ + console.timeEnd = function(name) { + if (console.time.map && console.time.map[name]) { + var diff = (new Date().getTime()) - console.time.map[name]; + ceee.sendMessage_('LogToConsole', [name + ': ' + diff + 'ms']); + } + }; +} + +ceee.OpenChannelToExtension = function(sourceId, targetId, name) { + return ceee.sendMessage_('OpenChannelToExtension', [ + sourceId, targetId, name + ]); +}; + +// The following methods are the public bindings required by the chrome +// javascript code implementation. These binds hide the implementation details +// in FF, IE, and Chrome. + +ceee.CloseChannel = function(portId) { + return ceee.sendMessage_('CloseChannel', [portId]); +}; + +ceee.PortAddRef = function(portId) { + return ceee.sendMessage_('PortAddRef', [portId]); +}; + +ceee.PortRelease = function(portId) { + return ceee.sendMessage_('PortRelease', [portId]); +}; + +ceee.PostMessage = function(portId, msg) { + return ceee.sendMessage_('PostMessage', [portId, msg]); +}; + +ceee.GetChromeHidden = function() { + return ceee.hidden; +}; + +ceee.AttachEvent = function(eventName) { + ceee.sendMessage_('AttachEvent', [eventName]); +}; + +ceee.DetachEvent = function(eventName) { + ceee.sendMessage_('DetachEvent', [eventName]); +}; diff --git a/ceee/firefox/content/us/json.js b/ceee/firefox/content/us/json.js new file mode 100644 index 0000000..0ef04e4 --- /dev/null +++ b/ceee/firefox/content/us/json.js @@ -0,0 +1,318 @@ +// Copyright 2006 Google Inc. +// All Rights Reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in +// the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +/** + * @fileoverview JSON utility functions. + */ + +/* + * ORIGINAL VERSION: http://doctype.googlecode.com/svn/trunk/goog/json/json.js + * + * LOCAL CHANGES: None. + */ + +goog.provide('goog.json'); +goog.provide('goog.json.Serializer'); + + + +/** + * Tests if a string is an invalid JSON string. This only ensures that we are + * not using any invalid characters + * @param {string} s The string to test. + * @return {boolean} True if the input is a valid JSON string. + * @private + */ +goog.json.isValid_ = function(s) { + // All empty whitespace is not valid. + if (/^\s*$/.test(s)) { + return false; + } + + // This is taken from http://www.json.org/json2.js which is released to the + // public domain. + // Changes: We dissallow \u2028 Line separator and \u2029 Paragraph separator + // inside strings. We also treat \u2028 and \u2029 as whitespace which they + // are in the RFC but IE and Safari does not match \s to these so we need to + // include them in the reg exps in all places where whitespace is allowed. + // We allowed \x7f inside strings because some tools don't escape it, + // e.g. http://www.json.org/java/org/json/JSONObject.java + + // Parsing happens in three stages. In the first stage, we run the text + // against regular expressions that look for non-JSON patterns. We are + // especially concerned with '()' and 'new' because they can cause invocation, + // and '=' because it can cause mutation. But just to be safe, we want to + // reject all unexpected forms. + + // We split the first stage into 4 regexp operations in order to work around + // crippling inefficiencies in IE's and Safari's regexp engines. First we + // replace all backslash pairs with '@' (a non-JSON character). Second, we + // replace all simple value tokens with ']' characters. Third, we delete all + // open brackets that follow a colon or comma or that begin the text. Finally, + // we look to see that the remaining characters are only whitespace or ']' or + // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + // Don't make these static since they have the global flag. + var backslashesRe = /\\["\\\/bfnrtu]/g; + var simpleValuesRe = + /"[^"\\\n\r\u2028\u2029\x00-\x1f\x80-\x9f]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; + var openBracketsRe = /(?:^|:|,)(?:[\s\u2028\u2029]*\[)+/g; + var remainderRe = /^[\],:{}\s\u2028\u2029]*$/; + + return remainderRe.test(s.replace(backslashesRe, '@'). + replace(simpleValuesRe, ']'). + replace(openBracketsRe, '')); +}; + + +/** + * Parses a JSON string and returns the result. This throws an exception if + * the string is an invalid JSON string. + * + * Note that this is very slow on large strings. If you trust the source of + * the string then you should use unsafeParse instead. + * + * @param {*} s The JSON string to parse. + * @return {Object} The object generated from the JSON string. + */ +goog.json.parse = function(s) { + var o = String(s); + if (goog.json.isValid_(o)) { + /** @preserveTry */ + try { + return eval('(' + o + ')'); + } catch (ex) { + } + } + throw Error('Invalid JSON string: ' + o); +}; + + +/** + * Parses a JSON string and returns the result. This uses eval so it is open + * to security issues and it should only be used if you trust the source. + * + * @param {string} s The JSON string to parse. + * @return {Object} The object generated from the JSON string. + */ +goog.json.unsafeParse = function(s) { + return eval('(' + s + ')'); +}; + +/** + * Serializes an object or a value to a JSON string. + * + * @param {Object} object The object to serialize. + * @throws Error if there are loops in the object graph. + * @return {string} A JSON string representation of the input. + */ +goog.json.serialize = function(object) { + return new goog.json.Serializer().serialize(object); +}; + + + +/** + * Class that is used to serialize JSON objects to a string. + * @constructor + */ +goog.json.Serializer = function() { +}; + + +/** + * Serializes an object or a value to a JSON string. + * + * @param {Object?} object The object to serialize. + * @throws Error if there are loops in the object graph. + * @return {string} A JSON string representation of the input. + */ +goog.json.Serializer.prototype.serialize = function(object) { + var sb = []; + this.serialize_(object, sb); + return sb.join(''); +}; + + +/** + * Serializes a generic value to a JSON string + * @private + * @param {string|number|boolean|undefined|Object|Array} object The object to + * serialize. + * @param {Array} sb Array used as a string builder. + * @throws Error if there are loops in the object graph. + */ +goog.json.Serializer.prototype.serialize_ = function(object, sb) { + switch (typeof object) { + case 'string': + this.serializeString_((/** @type {string} */ object), sb); + break; + case 'number': + this.serializeNumber_((/** @type {number} */ object), sb); + break; + case 'boolean': + sb.push(object); + break; + case 'undefined': + sb.push('null'); + break; + case 'object': + if (object == null) { + sb.push('null'); + break; + } + if (goog.isArray(object)) { + this.serializeArray_(object, sb); + break; + } + // should we allow new String, new Number and new Boolean to be treated + // as string, number and boolean? Most implementations do not and the + // need is not very big + this.serializeObject_(object, sb); + break; + case 'function': + // Skip functions. + break; + default: + throw Error('Unknown type: ' + typeof object); + } +}; + + +/** + * Character mappings used internally for goog.string.quote + * @private + * @type {Object} + */ +goog.json.Serializer.charToJsonCharCache_ = { + '\"': '\\"', + '\\': '\\\\', + '/': '\\/', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + + '\x0B': '\\u000b' // '\v' is not supported in JScript +}; + + +/** + * Regular expression used to match characters that need to be replaced. + * The S60 browser has a bug where unicode characters are not matched by + * regular expressions. The condition below detects such behaviour and + * adjusts the regular expression accordingly. + * @private + * @type {RegExp} + */ +goog.json.Serializer.charsToReplace_ = /\uffff/.test('\uffff') ? + /[\\\"\x00-\x1f\x7f-\uffff]/g : /[\\\"\x00-\x1f\x7f-\xff]/g; + + +/** + * Serializes a string to a JSON string + * @private + * @param {string} s The string to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeString_ = function(s, sb) { + // The official JSON implementation does not work with international + // characters. + sb.push('"', s.replace(goog.json.Serializer.charsToReplace_, function(c) { + // caching the result improves performance by a factor 2-3 + if (c in goog.json.Serializer.charToJsonCharCache_) { + return goog.json.Serializer.charToJsonCharCache_[c]; + } + + var cc = c.charCodeAt(0); + var rv = '\\u'; + if (cc < 16) { + rv += '000'; + } else if (cc < 256) { + rv += '00'; + } else if (cc < 4096) { // \u1000 + rv += '0'; + } + return goog.json.Serializer.charToJsonCharCache_[c] = rv + cc.toString(16); + }), '"'); +}; + + +/** + * Serializes a number to a JSON string + * @private + * @param {number} n The number to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeNumber_ = function(n, sb) { + sb.push(isFinite(n) && !isNaN(n) ? n : 'null'); +}; + + +/** + * Serializes an array to a JSON string + * @private + * @param {Array} arr The array to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeArray_ = function(arr, sb) { + var l = arr.length; + sb.push('['); + var sep = ''; + for (var i = 0; i < l; i++) { + sb.push(sep) + this.serialize_(arr[i], sb); + sep = ','; + } + sb.push(']'); +}; + + +/** + * Serializes an object to a JSON string + * @private + * @param {Object} obj The object to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeObject_ = function(obj, sb) { + sb.push('{'); + var sep = ''; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + var value = obj[key]; + // Skip functions. + if (typeof value != 'function') { + sb.push(sep); + this.serializeString_(key, sb); + sb.push(':'); + this.serialize_(value, sb); + sep = ','; + } + } + } + sb.push('}'); +};
\ No newline at end of file diff --git a/ceee/firefox/content/userscript_api.js b/ceee/firefox/content/userscript_api.js new file mode 100644 index 0000000..aefc66cc --- /dev/null +++ b/ceee/firefox/content/userscript_api.js @@ -0,0 +1,1077 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file contains the implementation of the Chrome extension + * user script APIs for the CEEE Firefox add-on. This file is loaded by the + * overlay.xul file, and requires that overlay.js has already been loaded. + * + * @supported Firefox 3.x + */ + +/** + * Constructor for user script management API object. + * @param {!Object} instance A reference the the main CEEE instance for + * this top-level window. + * @constructor + */ +var CEEE_UserScriptManager; + +(function() { + +/** A reference to the main CEEE instance for this top-level window. */ +var ceee; + +/** + * A map of open ports for this web page for all extensions. The key to the + * map is a portId (number) that is local to this web page. This portId is + * assigned by the call to OpenChannelToExtension. + * + * The port is considered opened once ChromeFrame responds with the "channel + * opened" event. This event will contain another portId assigned on the + * chrome side of the port, referred to here as the remote port Id. Because + * Chrome and ChromeFrame do not know about the portIds in this code, the + * functions below map from portId to remotePortId and back as needed. + */ +var ports = CEEE_globals.ports; + +/** + * An array of "content_scripts", as defined by the Chrome Extension API. + */ +var scripts; + +/** + * Path relative to the add-on root where user-related scripts are found. + * @const + */ +var USER_SCRIPTS_DIR = 'content/us'; + +/** + * Create an nsIURL object from the given string spec. + * + * @param {!string} spec The string that should be converted into an nsURL + * object. + */ +function makeUrl(spec) { + var uri = CEEE_ioService.newURI(spec, null, null); + return uri && uri.QueryInterface(Components.interfaces.nsIURL); +} + +/** + * Create an nsIURL object from the given nsIFile object. + * + * @param {!nsIFile} file The file that should be converted into an nsURL + * object. + */ +function makeFileUrl(file) { + var uri = CEEE_ioService.newFileURI(file); + return uri && uri.QueryInterface(Components.interfaces.nsIURL); +} + +/** + * Is the given pattern a valid host pattern, as defined by the Chrome + * URLPattern class? There are only 3 possible values: + * + * - '*', which matches all hosts + * - '<anychar except '*'>'+, which matches an exact domain + * - '*.<anychar except '*'>'+, which matches a subdomain + * + * Its assumed that the pattern does not contain an slashes (/). + * + * @param {!string} pattern String pattern to test. + * @return true if the pattern is valid. + */ +function isValidHostPattern(pattern) { + return pattern == '*' || pattern.indexOf('*') == -1 || + (pattern.charAt(0) == '*' && pattern.charAt(1) == '.' && + pattern.substr(2).indexOf('*') == -1); +} + +/** + * Does the given URL match one of the URL patterns given? + * + * @param {!Location} url URL string to match. + * @param {Array} patterns Array of patterns to match. + */ +function matchesPattern(url, patterns) { + // Some URLs may not have a host, like about:blank. In this case, we can + // never match. In firefox, this is implemented with the nsSimpleURI class. + // The implementation of this class will always throw an error when accessing + // the host property, so we first check to see if the protocol is not + // "about:" before checking the host. nsSimpleURI is designed specifically + // for about: + var protocol = url.protocol; + if (protocol == 'about:') + return false; + + var host = url.host; + var path = url.pathname + url.search; + + for (var i = 0; i < patterns.length; ++i) { + var pattern = patterns[i]; + + // Make sure protocols match. + if (pattern.protocolPattern != protocol) { + continue; + } + + // Make sure hosts match. + var hostPattern = pattern.hostPattern; + if (hostPattern.length > 0) { + if (hostPattern.charAt(0) != '*') { + if (host != hostPattern) { + continue; + } + } else { + // Make sure the host ends with the pattern (minus initial *). + var sub = hostPattern.substr(1); + if (sub != host.match(sub)) { + continue; + } + } + } + + // Make sure paths match. + var pathPattern = pattern.pathPattern; + if (path && !pathPattern.test(path)) { + continue; + } + + // Found a match! + ceee.logInfo('URL=' + url.toString() + ' matches with ' + + pattern.protocolPattern + ' ' + pattern.hostPattern + ' ' + + pattern.pathPattern); + return true; + } + + return false; +} + +/** + * Checks whether the given window is the top window. + * + * @param {!nsIDOMWindow} w The window to test. + * @return {boolean} true if the given window is the top window. + */ +function isTopWindow(w) { + return w.top == w.self; +} + +/** + * Get a File object for the given user script. Its assumed that the user + * script file name is relative to USER_SCRIPTS_DIR in the add-on. + * + * @param {!string} name Relative file name of the user script. + * @return A File object that represents the user script. + */ +function getScriptFile(name) { + var m = Components.classes['@mozilla.org/extensions/manager;1']. + getService(Components.interfaces.nsIExtensionManager); + var file = m.getInstallLocation(CEEE_globals.ADDON_ID) + .getItemFile(CEEE_globals.ADDON_ID, USER_SCRIPTS_DIR); + + file.append(name); + return file; +} + +/** + * Get a File object for the given user script. Its assumed that the user + * script file name is relative to the Chrome Extension root. + * + * @param {!string} name Relative file name of the user script. + * @return A File object that represents the user script. + */ +function getUserScriptFile(name) { + var file = ceee.getToolstripDir(); + if (file) { + var parts = name.split('/'); + for (var i = 0; i < parts.length; ++i) { + file.append(parts[i]); + } + + if (!file.exists() || !file.isFile()) + file = null; + } + + if (file) + ceee.logInfo('User script: ' + file.path); + + return file; +} + +/** + * Evaluate a user-defined script file in the context of a web page, with + * debugging in mind. Instead of evaluating the script in a sandbox, which + * Firebug cannot see, a <script> element is added to the page. + * + * Note that this depends on the Chrome Extension APIs being avaiable to page + * code, which normally it is not. See prepareSandbox() for how these APIs + * are conditionally injected to be accessed from the page. + * + * For Firefox to allow loading of file URLs into remote pages, the security + * manager needs to be told to allow this. For more details about this see: + * - http://kb.mozillazine.org/Security_Policies + * - http://www.mozilla.org/projects/security/components/ConfigPolicy.html + * + * If this is not setup correctly, Firefox's error console will show a message + * similar to this: + * + * Security Error: Content at <remote-url> may not load or link to <file-url>. + * + * The security manager is tweaked appropriately in the options dialog, see + * the code in options.xul. + * + * @param {nsIDOMWindow} w Content window into which to evaluate the script. + * @param {!nsIFile} file The script to evaluate. + */ +function evalUserScriptFileForDebugging(w, file) { + // Create the script from the given file object. + var d = w.document; + var script = d.createElement('script'); + script.type = 'text/javascript'; + script.src = makeFileUrl(file).spec; + d.body.appendChild(script); + ceee.logInfo('Debugging script: ' + file.path); +} + +/** + * Add the CSS code string to the document of the specified window. + * + * @param {!nsIDOMWindow} w Content window into which to evaluate the script. + * @param {string} code The CSS code string to evaluate. + */ +function addCssCodeToPage(w, code) { + var d = w.document; + var coll = d.getElementsByTagName('head'); + if (!coll || coll.length != 1) { + ceee.logError('Cannot find <head> in doc=' + w.location.toString()); + return; + } + + var head = coll[0]; + var css = d.createElement('style'); + css.type = 'text/css'; + css.appendChild(d.createTextNode(code)); + head.appendChild(css); +} + +/** + * Add the CSS file to the document of the specified window. + * + * @param {!nsIDOMWindow} w Content window into which to evaluate the script. + * @param {nsIFile} file The script to evaluate. + */ +function addCssToPage(w, file) { + // Read the CSS from the file into memory. + var data = {}; + try { + var stream = ceee.openFileStream(file); + stream.readString(-1, data); + addCssCodeToPage(w, data.value); + ceee.logInfo('Loaded CSS file: ' + file.path); + } catch (ex) { + // Note that for now, this will happen regularly if you have more + // than one extension installed and the extension not being + // treated as the "one and only" by CEEE happens to call + // insertCss. + ceee.logError('addCssToPage, exception: ' + ex.message); + } finally { + if (stream) + stream.close(); + } +} + +/** + * Post an open channel message to ChromeFrame for the given port. + * + * @param {number} portId The internal port Id of the port to open. + * @param {string} extId The extension Id associated with this port. + * @param {string} name The name to assign to the port. + * @param {!Object} tabInfo Information about the tab containing the + * content script that is opening the channel. + */ +function postOpenChannel(portId, extId, name, tabInfo) { + var msg = { + rqid: 0, // 0 == open channel + extid: extId, + chname: name, + connid: portId, + tab: tabInfo + }; + + var cfh = ceee.getCfHelper(); + cfh.postMessage(CEEE_json.encode(msg), cfh.TARGET_PORT_REQUEST); +} + +/** + * Post a close channel message to ChromeFrame for the given port. + * + * @param {!Object} info Information about the port. + */ +function postCloseChannel(info) { + // Don't send another close channel message if this port has alredy been + // disconnected. + if (info.disconnected) + return; + + // If the remote port id is not valid, just return. This could happen if + // the port is closed before we get a response from ChromeFrame about the + // port being opened. + var remotePortId = info.remotePortId; + if (!remotePortId || remotePortId == -1) + return; + + var msg = { + rqid: 3, // 3 == close channel + portid: remotePortId + }; + + var cfh = ceee.getCfHelper(); + cfh.postMessage(CEEE_json.encode(msg), cfh.TARGET_PORT_REQUEST); + info.disconnected = true; +} + +/** + * Post a port message to ChromeFrame for the given port. + * + * @param {number} remotePortId The remote port Id of the port. + * @param {string} msg The message to post. + */ +function postPortMessage(remotePortId, msg) { + var msg2 = { + rqid: 2, // 2 == post message + portid: remotePortId, + data: msg + }; + + var cfh = ceee.getCfHelper(); + cfh.postMessage(CEEE_json.encode(msg2), cfh.TARGET_PORT_REQUEST); +} + +/** + * Find a port given its remote port Id. The remote port Id is the Id assigned + * to the port by chrome upon connection, which is different from the port Id + * assigned by the FF CEEE and used as the key in the ports map. + * + * May want to review this code to see if we want to optimize the remote port + * Id lookup. + * + * @param {number} remotePortId An integer value representing the Id. + */ +function findPortByRemoteId(remotePortId) { + for (var portId in ports) { + var info = ports[portId]; + if (remotePortId == info.remotePortId) + return info; + } +} + +/** + * Cleanup all resources used by the port with the given Id. + * + * @param {number} portId An integer value representing the FF CEEE port Id. + */ +function cleanupPort(portId) { + var info = ports[portId]; + if (info) { + postCloseChannel(info); + delete ports[portId]; + ceee.logInfo('cleanupPort: cleaned up portId=' + portId); + } else { + ceee.logInfo('cleanupPort: Invalid portId=' + portId); + } +} + +/** + * Evaluate a user-defined script in the context of a web page. + * + * @param {!Object} w The DOM window in which to run the script. + * @param {string} script A string that represents the javascript code to + * run against a web page. + * @param {string=} opt_filename An optional string that contains the name of + * the script being executed. + * @private + */ +function evalUserScript(w, script, opt_filename) { + evalInSandboxWithCatch(script, prepareSandbox(w), opt_filename); +} + +/** + * Prepare a sandbox for running user scripts. + * + * @param {!Object} w The content window in which the user scripts will run. + * @return A Firefox sandbox properly initialized for running user scripts. + */ +function prepareSandbox(w) { + // If the window already has a FF CEEE sandbox, use it instead of creating + // a new one. + if (w.ceeeSandbox) { + ceee.logInfo('Reusing sandbox for ' + w.location.toString()); + return w.ceeeSandbox; + } + + // We need to be careful to run the user script with limited privilege, + // and in the scope of the given window. We do this by creating a sandbox + // around that window and evaluating the script there. + // + // There are security issues with using the object that is returned from + // evaluating the expression in the sandbox. For now, I ignore the return + // value, see https://developer.mozilla.org/En/Components.utils.evalInSandbox. + // + // We wrap the window given argument because that object comes from the + // unsafe environment of the web page being shown. We want to expose our + // script to a 'safe' environment by default. However, just in case some + // user scripts really want the original window, we'll pass that in too. + // + // To sum up, the code in this file is considered 'privileged' code, and must + // be careful what it executes. There are two sources of unsafe code: the + // user script, which comes from the CEEE add-on, and the document+window + // that come from the window argument. We need to be careful with objects we + // get from both of these places. + var unsafeWindow = w; + var safeWindow = new XPCNativeWrapper(unsafeWindow); + + // Create a sandbox for running the script and initialize the environment. + // TODO(rogerta@chromium.org): for perfomance reasons, we may want + // to cache the user scripts somehow. + var s = Components.utils.Sandbox(safeWindow); + s.window = safeWindow; + s.document = safeWindow.document; + s.__proto__ = s.window; + + // TODO(rogerta@chromium.org): Not sure if I need to set this member. + // s.XPathResult = Components.interfaces.nsIDOMXPathResult; + + // TODO(rogerta@chromium.org): for performance reasons. maybe want + // to concat all these files into one and eval once. Or maybe load + // all files into memory and eval from there instead of from file + // each time. + evalScriptFileInSandbox(getScriptFile('base.js'), s); + evalScriptFileInSandbox(getScriptFile('json.js'), s); + evalScriptFileInSandbox(getScriptFile('ceee_bootstrap.js'), s); + evalScriptFileInSandbox(getScriptFile('event_bindings.js'), s); + evalScriptFileInSandbox(getScriptFile('renderer_extension_bindings.js'), s); + + s.JSON = s.goog && s.goog.json; + + // Firebug may install it own console, so we won't override it here. + s.window.console = s.window.console || s.console; + + // For test purposes, I will add the APIs to the actual page itself, so that + // test code can call it. This is to make the APIs available to javascript + // already in the page, not to the inject scripts, which already have the + // APIs. + if (ceee.contentScriptDebugging) { + w.wrappedJSObject.chrome = w.wrappedJSObject.chrome || s.chrome; + + // When running with Firebug enabled, the window object will already + // have console property. However, Firebug declares console with as getter + // only, so the standard javascript pattern "foo = foo || bar;" causes an + // exception to be thrown. Therefore using an if() check. + if (!w.wrappedJSObject.console) + w.wrappedJSObject.console = s.console; + + w.wrappedJSObject.goog = w.wrappedJSObject.goog || s.goog; + w.wrappedJSObject.JSON = w.wrappedJSObject.JSON || s.JSON; + w.wrappedJSObject.ceee = w.wrappedJSObject.ceee || s.ceee; + } + + // Make sure the chrome extension execution environment is properly setup + // for the user script. + // TODO(rogerta@chromium.org): for now, this assumes only one + // extension at a time. + var createExtensionScript = 'chrome.initExtension("'; + createExtensionScript += ceee.getToolstripExtensionId(); + createExtensionScript += '")'; + evalInSandboxWithCatch(createExtensionScript, s, 'createExtensionScript'); + + w.ceeeSandbox = s; + ceee.logInfo('Created new sandbox for ' + w.location.toString()); + return s; +} + +/** + * Evaluate a script file in the context of a sandbox. + * + * @param {!Object} file A Firefox nsIFile object containing the script. + * @param {!Object} s A Firefox sandbox. + */ +function evalScriptFileInSandbox(file, s) { + if (!file) { + ceee.logError('Trying to eval a null script'); + return; + } + + // Read the script from the file into memory, so that we can execute it. + var data = {}; + try { + var stream = ceee.openFileStream(file); + stream.readString(-1, data); + + // Execute the script. + evalInSandboxWithCatch(data.value, s, file.leafName); + } finally { + if (stream) + stream.close(); + } +} + +/** + * Evaluate a script in the context of a sandbox. + * + * @param {string} script A string that represents the javascript code to + * run against a web page. + * @param {!Object} s A Firefox sandbox. + * @param {string=} opt_filename An optional string that contains the name of + * the script being executed. + * @private + */ +function evalInSandboxWithCatch(script, s, opt_filename) { + try { + // To get around a bug in firefox where it incorrectly determines the line + // number of exceptions thrown from inside a sandbox, create an error + // here to be used later. + var helper = new Error(); + evalInSandboxWrapper(script, s); + } catch (ex) { + var line = ex && ex.lineNumber; + // If the line number from the exception is MAX_INT, then we need to look + // harder for the line number. See if the exception has a location + // property, and if so, if it has a line number. + if (4294967295 == line) { + if (ex.location && ex.location.lineNumber) { + line = ex.location.lineNumber; + } else { + line = 0; + } + } + + // Subtract the line number from the dummy exception created before the + // call into the sandbox. As explained above, this is to get around a + // bug in the way that Firefox reports line numbers in sandboxes. + if (line > helper.lineNumber) { + line -= helper.lineNumber; + } else { + line = 'unknown-line'; + } + + // Build the message to log to the console. If a 'filename' is specified, + // add it to the message. + var message = 'Error in '; + if (opt_filename) + message += opt_filename + ':'; + + ceee.logError('Sandbox: ' + message + line + ':: ' + ex); + } +} + +/** + * Constructor for user script manager object. + * + * @param {!Object} instance A reference the the main CEEE instance for + * this top-level window. + */ +function UserScriptManager(instance) { + // The caller must specify the CEEE instance object. + if (!instance) + throw 'Must specify instance'; + + ceee = instance; + ceee.logInfo('UserScriptManager created'); + + // Register all handlers. + ceee.registerDomHandler('AttachEvent', this, this.AttachEvent); + ceee.registerDomHandler('DetachEvent', this, this.DetachEvent); + ceee.registerDomHandler('OpenChannelToExtension', this, + this.OpenChannelToExtension); + ceee.registerDomHandler('PortAddRef', this, this.PortAddRef); + ceee.registerDomHandler('PortRelease', this, this.PortRelease); + ceee.registerDomHandler('CloseChannel', this, this.CloseChannel); + ceee.registerDomHandler('PostMessage', this, this.PostMessage); + ceee.registerDomHandler('LogToConsole', this, this.LogToConsole); + ceee.registerDomHandler('ErrorToConsole', this, this.ErrorToConsole); +} + +/** + * Find all content scripts parts (javascript code and CSS) in the manifest + * file and load them. + * + * @param {!Object} manifest An object representing a valid Chrome Extension + * manifest. + */ +UserScriptManager.prototype.loadUserScripts = function(manifest) { + // If the user scripts have already been loaded, then nothing to do. This + // happens when the second top-level window is opened, and the extension + // has already been parsed. + if (scripts && scripts.length > 0) + return; + + CEEE_globals.scripts = manifest.content_scripts || []; + scripts = CEEE_globals.scripts; + var count = (scripts && scripts.length) || 0; + + for (var i = 0; i < count; ++i) { + var script = scripts[i]; + var matches = script.matches; + if (!matches) + continue; + + // Convert the URL strings in the matches property to two string: one for + // matching host names, and another for matching paths. These two parts + // are matched independently, as described in the Chrome URLPattern class. + var patterns = []; + for (var j = 0; j < matches.length; ++j) { + var spec = matches[j]; + var url = makeUrl(spec); + + // Make a pattern from the host glob. + var hostPattern = url.host; + if (!isValidHostPattern(hostPattern)) { + ceee.logError('Invalid user script pattern: ' + hostPattern); + continue; + } + + // Make a regex from the path glob pattern. + var pathPattern = url.path; + + // Replace all stars (*) with '.*'. + // Replace all '?' with '\?'. + pathPattern = pathPattern.replace(/\*/g, '.*'); + pathPattern = pathPattern.replace(/\?/g, '\\?'); + + // The nsIURL object's scheme property is the protocol of the URL, but + // without the trailing colon. This will eventually be compared to a + // windows.location object, that has a protocol property *with* the + // trailing the colon. So I am adding the colon here to make things + // easier to compare later. + var pattern = { + protocolPattern: url.scheme + ':', + hostPattern: hostPattern, + pathPattern: new RegExp(pathPattern) + }; + patterns.push(pattern); + + ceee.logInfo('loadUserScripts: url=' + spec + + ' pattern=' + CEEE_json.encode(pattern)); + } + + script.patterns = patterns; + + // Set the default value for all_frames + if (script.all_frames === undefined) { + script.all_frames = false; + } + } + + ceee.logInfo('loadUserScripts: done count=' + count); +}; + +/** + * Run all user scripts installed on the system that match the URL of the + * document attached to the window specified in the w argument. + * + * @param {!Object} w The content window in which to run any matching user + * scripts. + */ +UserScriptManager.prototype.runUserScripts = function(w) { + // Note that we read the userscript files from disk every time that a page + // loads. This offers us the advantage of not having to reload scripts into + // memory when an extension autoupdates in Chrome. If we decide to cache + // these scripts in memory, then we will need a Chrome extension autoupdate + // automation notification. + + ceee.logInfo('runUserScripts: starting'); + var s = prepareSandbox(w); + + // Run each user script whose pattern matches the URL of content window. + for (var i = 0; i < scripts.length; ++i) { + var script = scripts[i]; + if ((script.all_frames || isTopWindow(w)) && + matchesPattern(w.location, script.patterns)) { + // First inject CSS script, in case the javascript code assume the styles + // already exist. + var css = script.css || []; + var css_length = css.length; + for (var j = 0; j < css_length; ++j) { + var f = getUserScriptFile(css[j]); + addCssToPage(w, f); + } + + // Now inject the javascript code. + var js = script.js || []; + var js_length = js.length; + for (var j = 0; j < js_length; ++j) { + var f = getUserScriptFile(js[j]); + if (ceee.contentScriptDebugging) { + evalUserScriptFileForDebugging(w, f); + } else { + evalScriptFileInSandbox(f, s); + } + } + } + } + + ceee.logInfo('runUserScripts: done'); +}; + +/** + * Execute a script file in the context of the given window. + * + * @param {!Object} w Content window context to execute in. + * @param {!Object} scriptDef A script definition object as defined by the + * Chrome Extensions API. + */ +UserScriptManager.prototype.executeScript = function(w, scriptDef) { + if (!ceee.getToolstripDir()) { + var self = this; + ceee.runWhenExtensionsInited(w, 'executeScript', function() { + self.executeScript(w, scriptDef); + }); + ceee.logInfo('Deferred executeScript'); + return; + } + ceee.logInfo('Executing executeScript'); + + var s = prepareSandbox(w); + + if (scriptDef.code && scriptDef.file) + throw new Error('Code and file should not be specified at the same ' + + 'time in the second argument.'); + + if (scriptDef.code) { + evalInSandboxWithCatch(scriptDef.code, s, 'executeScripts.scriptDef.js'); + } else if (scriptDef.file) { + try { + evalScriptFileInSandbox(getUserScriptFile(scriptDef.file), s); + } catch (ex) { + // Note that for now, this will happen regularly if you have more + // than one extension installed and the extension not being + // treated as the "one and only" by CEEE happens to call + // insertCss. + ceee.logError('executeScript, exception: ' + ex.message); + } + } +}; + +/** + * Insert a CSS file or script in the context of the given window. + * + * @param {!Window} w Content window context to execute in. + * @param {!Object} details A script definition object as defined by the + * Chrome Extensions API. + */ +UserScriptManager.prototype.insertCss = function(w, details) { + if (!ceee.getToolstripDir()) { + var self = this; + ceee.runWhenExtensionsInited(w, 'insertCss', function() { + self.insertCss(w, details); + }); + ceee.logInfo('Deferred insertCss'); + return; + } + ceee.logInfo('Executing insertCss'); + + if (details.code && details.file) + throw new Error('Code and file should not be specified at the same ' + + 'time in the second argument.'); + + if (details.code) { + addCssCodeToPage(w, details.code); + } else if (details.file) { + addCssToPage(w, getUserScriptFile(details.file)); + } else { + throw new Error('No source code or file specified.'); + } +}; + +/** + * Perform any cleanup required when a content window is unloaded. + * + * @param {!Object} w Content window being unloaded. + */ +UserScriptManager.prototype.onContentUnloaded = function(w) { + var script = 'ceee.hidden.dispatchOnUnload();'; + evalUserScript(w, script, 'onContentUnloaded_cleanup'); + this.cleanupPortsForWindow(w); + delete w.ceeeSandbox; +}; + +/** + * Cleanup port information for a closing window. + * + * @param {!Object} w Content window being closed. + */ +UserScriptManager.prototype.cleanupPortsForWindow = function(w) { + var toCleanup = []; + + // First look though all the ports, looking for those that need to be cleaned + // up. Remember those for later. + for (portId in ports) { + var info = ports[portId]; + if (w == info.contentWindow) + toCleanup.push(portId); + } + + // Now loop though all ports that need to be cleaned up and do it. + for (var i = 0; i < toCleanup.length; ++i) { + var portId = toCleanup[i]; + cleanupPort(portId); + } +}; + +/** + * Handle the response from a port request. Port requests are created via + * the functions below, like OpenChannelToExtension and PostMessage. + * + * Response of Post Message Request from Userscript Port to Extenstion + * ------------------------------------------------------------------- + * The data property of the evt argument has the form: + * + * { + * 'rqid': 1, // 1 == channel opened + * 'connid': <portId>, // the key to ports map + * 'portid': <remoteId> // the port Id assigned on the extension side + * } + * + * Post Message Request from Extenstion to Userscript Port + * ------------------------------------------------------- + * The data property of the evt argument has the form: + * + * { + * 'rqid': 1, // 2 == post message + * 'portid': <remoteId> // the port Id assigned on the extension side + * 'data': <message> // message being posted. A string + * } + * + * @param {!Object} evt An event object sent from ChromeFrame. + * @private + */ +UserScriptManager.prototype.onPortEvent = function(evt) { + var data = CEEE_json.decode(evt.data); + if (!data) + return; + + if (data.rqid == 1) { // 1 == CHANNEL_OPENED + var info = ports[data.connid]; + if (info) { + info.remotePortId = data.portid; + + if (info.remotePortId != -1) { + ceee.logInfo('onPortEvent: CHANNEL_OPENED portId=' + info.portId + + ' <-> remotePortId=' + info.remotePortId); + + // If this port has any pending messages, post them all now. + var q = info.queue; + if (q) { + ceee.logInfo('onPortEvent: posting deferred messages for portId=' + + info.portId); + while (q.length > 0) { + var msg = q.shift(); + postPortMessage(info.remotePortId, msg); + } + delete info.queue; + } + } else { + ceee.logError('onPortEvent: bad remote port for portId=' + + info.portId); + var q = info.queue; + if (q) { + while (q.length > 0) { + q.shift(); + } + delete info.queue; + } + } + } else { + ceee.logError('onPortEvent: No port object found, rqid=1 connid=' + + data.connid); + } + } else if (data.rqid == 2) { // 2 == POST_MESSAGE + // Fire the given message to the appropriate port in the correct content + // window. + var info = findPortByRemoteId(data.portid); + if (info) { + var msgt = data.data.replace(/\\/gi, '\\\\'); + var msg = msgt.replace(/"/gi, '\\"'); + var script = 'ceee.hidden.Port.dispatchOnMessage("{msg}",{portId});'; + script = script.replace(/{msg}/, msg); + script = script.replace(/{portId}/, info.portId); + + evalUserScript(info.contentWindow, script, 'onPortEvent<post_message>'); + } else { + ceee.logError('Could not find port for remote id=' + data.portid); + } + } else if (data.rqid == 3) { // 3 == CHANNEL_CLOSED + var info = findPortByRemoteId(data.portid); + if (info) { + var script = 'ceee.hidden.Port.dispatchOnDisconnect({portId});'; + script = script.replace(/{portId}/, info.portId); + evalUserScript(info.contentWindow, script, 'onPortEvent<channel_closed>'); + cleanupPort(info.portId); + } + } else { + ceee.logError('onPortEvent: unexptected rqid=' + data.rqid); + } +}; + +UserScriptManager.prototype.AttachEvent = function(w, cmd, data) { + //alert('AttachEvent '+ CEEE_json.encode(data.args)); +}; + +UserScriptManager.prototype.DetachEvent = function(w, cmd, data) { + //alert('DetachEvent '+ CEEE_json.encode(data.args)); +}; + +UserScriptManager.prototype.OpenChannelToExtension = function(w, cmd, data) { + var args = data.args; + if (args.length != 3) { + this.logError_('OpenChannelToExtension: invalid arguments'); + return; + } + + var portId = CEEE_globals.getNextPortId(); + + // Save some information about this port. + var info = {}; + info.refCount = 0; + info.portId = portId; + info.sourceExtId = args[0]; + info.targetExtId = args[1]; + info.name = args[2]; + info.contentWindow = w; + info.disconnected = false; + ports[portId] = info; + var tabInfo = null; + var mainBrowser = document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + var container = mainBrowser && mainBrowser.tabContainer; + if (container) { + // Build a tab info object that corresponds to the sender tab. + // Note that the content script may be running in a child window to the one + // that corresponds to the tab; we use the top-level window here to get the + // correct tab information. + var index = mainBrowser.getBrowserIndexForDocument(w.top.document); + var tab = container.getItemAtIndex(index); + tabInfo = ceee.getTabModule().buildTabValue(mainBrowser, tab); + } + postOpenChannel(portId, info.targetExtId, info.name, tabInfo); + return portId; +}; + +UserScriptManager.prototype.PortAddRef = function(w, cmd, data) { + var args = data.args; + if (args.length != 1) { + this.logError_('PortAddRef: invalid arguments'); + return; + } + + var portId = args[0]; + var info = ports[portId]; + if (info) + info.refCount++; +}; + +UserScriptManager.prototype.PortRelease = function(w, cmd, data) { + var args = data.args; + if (args.length != 1) { + this.logError_('PortRelease: invalid arguments'); + return; + } + + var portId = args[0]; + var info = ports[portId]; + if (info) { + if (--info.refCount == 0) + cleanupPort(portId); + } +}; + +UserScriptManager.prototype.CloseChannel = function(w, cmd, data) { + var args = data.args; + if (args.length != 1) { + this.logError_('CloseChannel: invalid arguments'); + return; + } + + var portId = args[0]; + var info = ports[portId]; + if (info) { + // We tell the other side that the port is closed, but we don't cleanup + // the info object just yet. It will get cleaned up either when the + // reference count goes to zero, or during the cleanup phase of a window + // unload. + postCloseChannel(info); + } +}; + +UserScriptManager.prototype.PostMessage = function(w, cmd, data) { + if (data.args.length != 2) { + this.logError_('PostMessage: invalid arguments'); + return; + } + + var portId = data.args[0]; + var msg = data.args[1]; + var info = ports[portId]; + if (info) { + // Before posting or queuing the message, turn it into a string. + if (typeof msg == 'object') { + if (typeof msg != 'string') + msg = CEEE_json.encode(msg); + } else { + msg = msg.toString(); + } + + // If remotePortId is -1, this means there was a problem opening this port. + // Don't even bother trying to post a message in this case. + if (info.remotePortId != -1) { + if (typeof info.remotePortId != 'undefined') { + postPortMessage(info.remotePortId, msg); + } else { + // Queue up message for port. This message will eventually be sent + // when the port is fully opened. Messages are queued in the order in + // which they are posted. + info.queue = info.queue || []; + info.queue.push(msg); + ceee.logInfo('PostMessage: deferred for portId=' + portId); + } + } + } else { + ceee.logError('PostMessage: No port object found portId=' + portId + + ' doc=' + w.document.location); + } +}; + +UserScriptManager.prototype.LogToConsole = function(w, cmd, data) { + if (data.args.length == 0) { + ceee.logError('LogToConsole: needs one argument'); + return; + } + + ceee.logInfo('Userscript says: ' + data.args[0]); +}; + +UserScriptManager.prototype.ErrorToConsole = function(w, cmd, data) { + if (data.args.length == 0) { + ceee.logError('ErrorToConsole: needs one argument'); + return; + } + + ceee.logError('Userscript says: ' + data.args[0]); +}; + +// Make the constructor visible outside this anonymous block. +CEEE_UserScriptManager = UserScriptManager; + +// This wrapper is purposefully the last function in this file, as far down +// as possible, for code coverage reasons. It seems that code executed by +// evalInSandbox() is counted as code executing in this file, and the line +// number reported to coverage is the line number of the evalInSandbox() call +// plus the line inside script. This messes up all the coverage data for all +// lines in this file after the call to evalInSandbox(). To minimize the +// problem, the call to Components.utils.evalInSandbox() is being placed as far +// to the end of this file as possible. +function evalInSandboxWrapper(script, s) { + Components.utils.evalInSandbox(script, s); +} +})(); diff --git a/ceee/firefox/content/window_api.js b/ceee/firefox/content/window_api.js new file mode 100644 index 0000000..a3909c9 --- /dev/null +++ b/ceee/firefox/content/window_api.js @@ -0,0 +1,355 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file contains the implementation of the Chrome extension + * windows APIs for the CEEE Firefox add-on. This file is loaded by the + * overlay.xul file, and requires that overlay.js has already been loaded. + * + * @supported Firefox 3.x + */ + +var CEEE_window_internal_ = CEEE_window_internal_ || { + /** + * Reference to the instance object for the CEEE. + * @private + */ + ceeeInstance_: null, + + /** + * Log an informational message to the Firefox error console. + * @private + */ + logInfo_: null, + + /** + * Log an error message to the Firefox error console. + * @private + */ + logError_: null, + + /** + * List of additional window sources. For example, infobars API is supposed + * to return window objects which don't correspond to top-level Firefox + * windows. Each element is a function which takes window id and returns a + * window-like object. + * @type Array.<function(number):Object> + */ + fakeWindowLookup_: [] +}; + +/** + * Build an object that represents a "window", as defined by the Google Chrome + * extension API. + * + * @param {!Object} win Firefox chrome window. + * @param {boolean} opt_populate If specified and true, populate the returned + * info object with information about the tabs in this window. + */ +CEEE_window_internal_.buildWindowValue = function(win, opt_populate) { + var winRecent = CEEE_mozilla_windows.service.getMostRecentWindow( + CEEE_mozilla_windows.WINDOW_TYPE); + var info = {}; + info.id = CEEE_mozilla_windows.getWindowId(win); + info.focused = (win == + winRecent.QueryInterface(Components.interfaces.nsIDOMWindow)); + info.top = win.screenY; + info.left = win.screenX; + info.width = win.outerWidth; + info.height = win.outerHeight; + info.incognito = CEEE_globals.privateBrowsingService.isInPrivateBrowsing; + // Type can be any of 'normal', 'popup' or 'app'. Firefox doesn't support app + // windows, and we use chrome to create popups so Firefox doesn't know about + // them (i.e. all windows will be of type 'normal'). + // TODO(mad@chromium.org): detect popups now that we support + // them. Not sure how though... + info.type = 'normal'; + + if (opt_populate) { + info.tabs = []; + var mainBrowser = + win.document.getElementById(CEEE_globals.MAIN_BROWSER_ID); + var container = mainBrowser.tabContainer; + + for (var i = 0; i < container.itemCount; ++i) { + var tab = container.getItemAtIndex(i); + var tabInfo = this.ceeeInstance_.getTabModule().buildTabValue( + mainBrowser, tab); + info.tabs.push(tabInfo); + } + } + + return info; +}; + +/** + * Called when a new top-level window is created. + * + * @param w The top-level window that was created. + * @private + */ +CEEE_window_internal_.onWindowOpened_ = function(w) { + // Send the event notification to ChromeFrame. + var info = [this.buildWindowValue(w)]; + var msg = ['windows.onCreated', CEEE_json.encode(info)]; + this.ceeeInstance_.getCfHelper().postMessage( + CEEE_json.encode(msg), + this.ceeeInstance_.getCfHelper().TARGET_EVENT_REQUEST); +}; + +/** + * Called when a new top-level window is closed. + * + * @param w The top-level window that was closed. + * @private + */ +CEEE_window_internal_.onWindowRemoved_ = function(w) { + // Send the event notification to ChromeFrame. + var info = [CEEE_mozilla_windows.getWindowId(w)]; + var msg = ['windows.onRemoved', CEEE_json.encode(info)]; + this.ceeeInstance_.getCfHelper().postMessage( + CEEE_json.encode(msg), + this.ceeeInstance_.getCfHelper().TARGET_EVENT_REQUEST); +}; + +/** + * Called when a top-level window gets focus. + * + * @param w The top-level window that got focus. + * @private + */ +CEEE_window_internal_.onWindowFocused_ = function(w) { + // Send the event notification to ChromeFrame. + var info = [CEEE_mozilla_windows.getWindowId(w)]; + var msg = ['windows.onFocusChanged', CEEE_json.encode(info)]; + this.ceeeInstance_.getCfHelper().postMessage( + CEEE_json.encode(msg), + this.ceeeInstance_.getCfHelper().TARGET_EVENT_REQUEST); +}; + +CEEE_window_internal_.CMD_GET_WINDOW = 'windows.get'; +CEEE_window_internal_.getWindow_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var id = args[0]; + var win = CEEE_mozilla_windows.findWindow(id); + if (!win) { + throw(new Error(CEEE_window_internal_.CMD_GET_WINDOW + + ': invalid window id=' + id)); + } + + return this.buildWindowValue(win); +}; + +CEEE_window_internal_.CMD_GET_CURRENT_WINDOW = 'windows.getCurrent'; +CEEE_window_internal_.getCurrentWindow_ = function(cmd, data) { + return this.buildWindowValue(window); +}; + +CEEE_window_internal_.CMD_GET_LAST_FOCUSED_WINDOW = + 'windows.getLastFocused'; +CEEE_window_internal_.getLastFocusedWindow_ = function(cmd, data) { + var win = CEEE_mozilla_windows.service.getMostRecentWindow( + CEEE_mozilla_windows.WINDOW_TYPE); + return this.buildWindowValue(win); +}; + +CEEE_window_internal_.CMD_GET_ALL_WINDOWS = 'windows.getAll'; +CEEE_window_internal_.getAllWindows_ = function(cmd, data) { + var args = data.args && CEEE_json.decode(data.args); + var populate = args && args[0] && args[0].populate; + var e = CEEE_mozilla_windows.service.getEnumerator( + CEEE_mozilla_windows.WINDOW_TYPE); + var ret = []; + while (e.hasMoreElements()) { + var win = e.getNext(); + var info = this.buildWindowValue(win, populate); + ret.push(info); + } + + return ret; +}; + +CEEE_window_internal_.CMD_CREATE_WINDOW = 'windows.create'; +CEEE_window_internal_.createWindow_ = function(cmd, data) { + var args_list = data.args && CEEE_json.decode(data.args); + var args = args_list && args_list[0]; + var url = ''; + if (args && args.url) + url = args.url; + + // TODO(rogerta@chromium.org): need to properly validate URL string. + // I noticed that if its a stripped down string, like 'google.ca', + // it tries to open a file from the local hard disk like + // jar:file:///C:/Program%20Files%20(x86)/Mozilla%20Firefox/chrome/ + // browser.jar!/content/browser/google.ca + + // To avoid flickering, we set the position, size and popup attributes + // as part of the features string of the open function. + + // Unfortunately, the behavior is a little weird here. + // FF documentation excerpt: + // https://developer.mozilla.org/En/DOM/Window.open + // "If you define the strWindowFeatures parameter, then the features + // that are not listed, requested in the string will be disabled or + // removed (except titlebar and close which are by default yes)." + // So we must specify the default values ourselves based on current + // window state. + + // We tried to find a way to dynamically enumerate the window + // properties that have a visible sub-property but failed. This would + // have allowed us to support new visibility features as they get added. + // One of the problems with this approach is that the feature name is not + // always the same as the window property name, like location[bar] or + // status[bar]. Ho Well... + + // Build the feature object with some default values. + var features = { + toolbar: window.toolbar.visible ? 'yes' : 'no', + location: window.locationbar.visible ? 'yes' : 'no', + menubar: window.menubar.visible ? 'yes' : 'no', + statusbar: window.menubar.visible ? 'yes' : 'no', + personalbar: window.personalbar.visible ? 'yes' : 'no', + scrollbars: window.scrollbars.visible ? 'yes' : 'no', + resizable: 'yes' // We can't get the current state of the window. + }; + + if (args) { + if (args.top) + features.top = args.top; + if (args.left) + features.left = args.left; + if (args.width) + features.outerWidth = args.width; + if (args.height) + features.outerHeight = args.height; + if (args.type == 'popup') { + features.toolbar = 'no'; + features.location = 'no'; + features.menubar = 'no'; + features.status = 'no'; + } + } + + var options = []; + for (var i in features) { + options.push(i + '=' + features[i]); + } + options = options.join(','); + + // Window name of '_blank' means 'always open a new window'. Note that the + // return value of open() is the content window used to display the specified + // URL, and not the top level navigator window we actually want here. Hence + // we need to map from the former to the latter. I have verified that this + // always opens a new top level window, even if the user has specified the + // option "New pages should be opened in a new tab". + var win = open(url, '_blank', options); + var winTopLevel = + CEEE_mozilla_windows.findWindowFromContentWindow(win).window; + if (winTopLevel) { + return this.buildWindowValue(winTopLevel); + } + + throw(new Error(CEEE_window_internal_.CMD_CREATE_WINDOW + + ': error url=' + url)); +}; + +CEEE_window_internal_.CMD_UPDATE_WINDOW = 'windows.update'; +CEEE_window_internal_.updateWindow_ = function(cmd, data) { + var args = data.args && CEEE_json.decode(data.args); + if (!args || args.length < 2 || !args[1]) { + throw(new Error(CEEE_window_internal_.CMD_UPDATE_WINDOW + + ': invalid arguments')); + } + + var id = args[0]; + var win = CEEE_mozilla_windows.findWindow(id); + if (!win) { + throw(new Error(CEEE_window_internal_.CMD_UPDATE_WINDOW + + ': invalid window id=' + id)); + } + + var info = args[1]; + + if (info.top) + win.screenY = info.top; + + if (info.left) + win.screenX = info.left; + + if (info.width) + win.outerWidth = info.width; + + if (info.height) + win.outerHeight = info.height; + + return this.buildWindowValue(win); +}; + +CEEE_window_internal_.CMD_REMOVE_WINDOW = 'windows.remove'; +CEEE_window_internal_.removeWindow_ = function(cmd, data) { + var args = CEEE_json.decode(data.args); + var id = args[0]; + var win = CEEE_mozilla_windows.findWindow(id); + var lookupList = CEEE_window_internal_.fakeWindowLookup_; + for (var i = 0; !win && i < lookupList.length; ++i) { + win = lookupList[i](id); + } + if (!win) { + throw(new Error(CEEE_window_internal_.CMD_REMOVE_WINDOW + + ': invalid window id=' + id)); + } + + win.close(); +}; + +/** + * Add lookup function for fake windows (e.g. infobar). Lookup function is + * passed window id and should return window-like object if there is a + * correspondence. + * @param {function(number):Object} lookup + */ +CEEE_window_internal_.registerLookup = function(lookup) { + CEEE_window_internal_.fakeWindowLookup_.push(lookup); +}; + +/** + * Initialization routine for the CEEE Windows API module. + * @param {!Object} ceeeInstance Reference to the global ceee instance. + * @return {Object} Reference to the window module. + * @public + */ +function CEEE_initialize_windows(ceeeInstance) { + CEEE_window_internal_.ceeeInstance_ = ceeeInstance; + var windows = CEEE_window_internal_; + + // Register the extension handling functions with the CEEE instance. + ceeeInstance.registerExtensionHandler(windows.CMD_GET_WINDOW, + windows, + windows.getWindow_); + ceeeInstance.registerExtensionHandler(windows.CMD_GET_CURRENT_WINDOW, + windows, + windows.getCurrentWindow_); + ceeeInstance.registerExtensionHandler(windows.CMD_GET_LAST_FOCUSED_WINDOW, + windows, + windows.getLastFocusedWindow_); + ceeeInstance.registerExtensionHandler(windows.CMD_GET_ALL_WINDOWS, + windows, + windows.getAllWindows_); + ceeeInstance.registerExtensionHandler(windows.CMD_CREATE_WINDOW, + windows, + windows.createWindow_); + ceeeInstance.registerExtensionHandler(windows.CMD_UPDATE_WINDOW, + windows, + windows.updateWindow_); + ceeeInstance.registerExtensionHandler(windows.CMD_REMOVE_WINDOW, + windows, + windows.removeWindow_); + + // Install the error/status reporting methods for this module from the + // owning CEEE instance. + windows.logInfo_ = ceeeInstance.logInfo; + windows.logError_ = ceeeInstance.logError; + + return windows; +} |