diff options
Diffstat (limited to 'comm/suite/browser')
93 files changed, 22951 insertions, 0 deletions
diff --git a/comm/suite/browser/SuiteBrowser.manifest b/comm/suite/browser/SuiteBrowser.manifest new file mode 100644 index 0000000000..773ea9573c --- /dev/null +++ b/comm/suite/browser/SuiteBrowser.manifest @@ -0,0 +1,23 @@ +component {c2343730-dc2c-11d3-98b3-001083010e9b} nsBrowserContentHandler.js +contract @mozilla.org/uriloader/content-handler;1?type=text/html {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=application/vnd.mozilla.xul+xml {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=image/svg+xml {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=text/rdf {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=text/xml {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=application/xhtml+xml {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=text/css {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=text/plain {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=image/gif {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=image/jpeg {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=image/jpg {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=image/png {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=image/bmp {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=image/x-icon {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=image/vnd.microsoft.icon {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/uriloader/content-handler;1?type=application/http-index-format {c2343730-dc2c-11d3-98b3-001083010e9b} +contract @mozilla.org/commandlinehandler/general-startup;1?type=browser {c2343730-dc2c-11d3-98b3-001083010e9b} +category command-line-handler x-default @mozilla.org/commandlinehandler/general-startup;1?type=browser +category command-line-validator b-default @mozilla.org/commandlinehandler/general-startup;1?type=browser +component {45c8f75b-a299-4178-a461-f63690389055} nsTypeAheadFind.js +contract @mozilla.org/suite/typeaheadfind;1 {45c8f75b-a299-4178-a461-f63690389055} +category app-startup SuiteTypeAheadFind service,@mozilla.org/suite/typeaheadfind;1 diff --git a/comm/suite/browser/browser-places.js b/comm/suite/browser/browser-places.js new file mode 100644 index 0000000000..83584456e0 --- /dev/null +++ b/comm/suite/browser/browser-places.js @@ -0,0 +1,983 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyScriptGetter(this, ["PlacesToolbar", "PlacesMenu", + "PlacesPanelview", "PlacesPanelMenuView"], + "chrome://communicator/content/places/browserPlacesViews.js"); + +var StarUI = { + _itemGuids: null, + uri: null, + _batching: false, + + _element: function(aID) { + return document.getElementById(aID); + }, + + // Edit-bookmark panel + get panel() { + delete this.panel; + var element = this._element("editBookmarkPanel"); + // initially the panel is hidden + // to avoid impacting startup / new window performance + element.hidden = false; + element.addEventListener("popuphidden", this); + element.addEventListener("keypress", this); + element.addEventListener("keypress", this); + element.addEventListener("mousedown", this); + element.addEventListener("mouseout", this); + element.addEventListener("mousemove", this); + element.addEventListener("compositionstart", this); + element.addEventListener("compositionend", this); + element.addEventListener("input", this); + element.addEventListener("popuphidden", this); + element.addEventListener("popupshown", this); + return this.panel = element; + }, + + // Array of command elements to disable when the panel is opened. + get _blockedCommands() { + delete this._blockedCommands; + return this._blockedCommands = + ["cmd_close", "cmd_closeWindow"].map(id => this._element(id)); + }, + + _blockCommands: function SU__blockCommands() { + this._blockedCommands.forEach(function (elt) { + // make sure not to permanently disable this item (see bug 409155) + if (elt.hasAttribute("wasDisabled")) + return; + if (elt.getAttribute("disabled") == "true") { + elt.setAttribute("wasDisabled", "true"); + } else { + elt.setAttribute("wasDisabled", "false"); + elt.setAttribute("disabled", "true"); + } + }); + }, + + _restoreCommandsState: function SU__restoreCommandsState() { + this._blockedCommands.forEach(function (elt) { + if (elt.getAttribute("wasDisabled") != "true") + elt.removeAttribute("disabled"); + elt.removeAttribute("wasDisabled"); + }); + }, + + // nsIDOMEventListener + handleEvent: function SU_handleEvent(aEvent) { + switch (aEvent.type) { + case "popuphidden": + if (aEvent.originalTarget == this.panel) { + if (!this._element("editBookmarkPanelContent").hidden) + this.quitEditMode(); + + this._restoreCommandsState(); + let guidsForRemoval = this._itemGuids; + this._itemGuids = null; + + if (this._batching) { + this.endBatch(); + } + + switch (this._actionOnHide) { + case "cancel": { + PlacesTransactions.undo().catch(Cu.reportError); + break; + } + case "remove": { + PlacesTransactions.Remove(guidsForRemoval) + .transact().catch(Cu.reportError); + break; + } + } + this._actionOnHide = ""; + } + break; + case "keypress": + if (aEvent.defaultPrevented) { + // The event has already been consumed inside of the panel. + break; + } + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_ESCAPE: + if (!this._element("editBookmarkPanelContent").hidden) + this.cancelButtonOnCommand(); + break; + case KeyEvent.DOM_VK_RETURN: + if (aEvent.target.className == "expander-up" || + aEvent.target.className == "expander-down" || + aEvent.target.id == "editBMPanel_newFolderButton") { + //XXX Why is this necessary? The defaultPrevented check should + // be enough. + break; + } + this.panel.hidePopup(true); + break; + } + break; + } + }, + + _overlayLoaded: false, + _overlayLoading: false, + async showEditBookmarkPopup(aNode, aAnchorElement, aPosition, aIsNewBookmark, aUrl) { + // Slow double-clicks (not true double-clicks) shouldn't + // cause the panel to flicker. + if (this.panel.state == "showing" || + this.panel.state == "open") { + return; + } + + this._isNewBookmark = aIsNewBookmark; + this._uriForRemoval = ""; + this._itemGuids = null; + + // Performance: load the overlay the first time the panel is opened + // (see bug 392443). + if (this._overlayLoading) + return; + + if (this._overlayLoaded) { + await this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition, aUrl); + return; + } + + this._overlayLoading = true; + document.loadOverlay( + "chrome://communicator/content/places/editBookmarkOverlay.xul", + (aSubject, aTopic, aData) => { + // Move the header (star, title, button) into the grid, + // so that it aligns nicely with the other items (bug 484022). + let header = this._element("editBookmarkPanelHeader"); + let rows = this._element("editBookmarkPanelGrid").lastChild; + rows.insertBefore(header, rows.firstChild); + header.hidden = false; + + this._overlayLoading = false; + this._overlayLoaded = true; + this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition, aUrl); + } + ); + }, + + async _doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition, aUrl) { + if (this.panel.state != "closed") + return; + + this._blockCommands(); // un-done in the popuphiding handler + + // Set panel title: + // if we are batching, i.e. the bookmark has been added now, + // then show Page Bookmarked, else if the bookmark did already exist, + // we are about editing it, then use Edit This Bookmark. + this._element("editBookmarkPanelTitle").value = + this._isNewBookmark ? + gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") : + gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle"); + + this._element("editBookmarkPanelBottomButtons").hidden = false; + this._element("editBookmarkPanelContent").hidden = false; + + // The label of the remove button differs if the URI is bookmarked + // multiple times. + this._itemGuids = []; + + await PlacesUtils.bookmarks.fetch({url: aUrl}, + bookmark => this._itemGuids.push(bookmark.guid)); + + let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label"); + let bookmarksCount = this._itemGuids.length; + let label = PluralForm.get(bookmarksCount, forms) + .replace("#1", bookmarksCount); + this._element("editBookmarkPanelRemoveButton").label = label; + + this.beginBatch(); + + let onPanelReady = fn => { + let target = this.panel; + if (target.parentNode) { + // By targeting the panel's parent and using a capturing listener, we + // can have our listener called before others waiting for the panel to + // be shown (which probably expect the panel to be fully initialized) + target = target.parentNode; + } + target.addEventListener("popupshown", function(event) { + fn(); + }, {"capture": true, "once": true}); + }; + gEditItemOverlay.initPanel({ node: aNode, + onPanelReady, + hiddenRows: ["description", "location", + "loadInSidebar", "keyword"], + focusedElement: "preferred"}); + this.panel.openPopup(aAnchorElement, aPosition); + }, + + panelShown: + function SU_panelShown(aEvent) { + if (aEvent.target == this.panel) { + if (!this._element("editBookmarkPanelContent").hidden) { + let fieldToFocus = "editBMPanel_" + + Services.prefs.getCharPref("browser.bookmarks.editDialog.firstEditField"); + var elt = this._element(fieldToFocus); + elt.focus(); + elt.select(); + } + else { + // Note this isn't actually used anymore, we should remove this + // once we decide not to bring back the page bookmarked notification + this.panel.focus(); + } + } + }, + + quitEditMode: function SU_quitEditMode() { + this._element("editBookmarkPanelContent").hidden = true; + this._element("editBookmarkPanelBottomButtons").hidden = true; + gEditItemOverlay.uninitPanel(true); + }, + + editButtonCommand: function SU_editButtonCommand() { + this.showEditBookmarkPopup(); + }, + + cancelButtonOnCommand: function SU_cancelButtonOnCommand() { + this._actionOnHide = "cancel"; + this.panel.hidePopup(); + }, + + removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() { + this._removeBookmarksOnPopupHidden = true; + this._actionOnHide = "remove"; + this.panel.hidePopup(); + }, + + _batchBlockingDeferred: null, + beginBatch() { + if (this._batching) + return; + this._batchBlockingDeferred = PromiseUtils.defer(); + PlacesTransactions.batch(async () => { + await this._batchBlockingDeferred.promise; + }); + this._batching = true; + }, + + endBatch() { + if (!this._batching) + return; + + this._batchBlockingDeferred.resolve(); + this._batchBlockingDeferred = null; + this._batching = false; + }, +}; + +var PlacesCommandHook = { + + /** + * Adds a bookmark to the page loaded in the given browser using the + * properties dialog. + * + * @param aBrowser + * a <browser> element. + * @param [optional] aShowEditUI + * whether or not to show the edit-bookmark UI for the bookmark item + * @param [optional] aUrl + * Option to provide a URL to bookmark rather than the current page + * @param [optional] aTitle + * Option to provide a title for a bookmark to use rather than the + * getting the current page's title + */ + async bookmarkPage(aBrowser, aShowEditUI, aUrl = null, aTitle = null) { + // If aUrl is provided, we want to bookmark that url rather than the + // the current page + let url = aUrl ? new URL(aUrl) : new URL(aBrowser.currentURI.spec); + let info = await PlacesUtils.bookmarks.fetch({ url }); + let isNewBookmark = !info; + if (!info) { + let parentGuid = PlacesUtils.bookmarks.unfiledGuid; + info = { url, parentGuid }; + let description = null; + let charset = null; + + let docInfo = aUrl ? {} : await this._getPageDetails(aBrowser); + + try { + if (docInfo.isErrorPage) { + let entry = await PlacesUtils.history.fetch(aBrowser.currentURI); + if (entry) { + info.title = entry.title; + } + } else { + info.title = aTitle || aBrowser.contentTitle; + } + info.title = info.title || url.href; + description = docInfo.description; + charset = aUrl ? null : aBrowser.characterSet; + } catch (e) { + Cu.reportError(e); + } + + if (aShowEditUI && isNewBookmark) { + // If we bookmark the page here but open right into a cancelable + // state (i.e. new bookmark in Library), start batching here so + // all of the actions can be undone in a single undo step. + StarUI.beginBatch(); + } + + if (description) { + info.annotations = [{ name: PlacesUIUtils.DESCRIPTION_ANNO, + value: description }]; + } + + info.guid = await PlacesTransactions.NewBookmark(info).transact(); + + // Set the character-set + if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) + PlacesUtils.setCharsetForURI(makeURI(url.href), charset); + } + + // If it was not requested to open directly in "edit" mode, we are done. + if (!aShowEditUI) + return; + + let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(info); + + // Dock the panel to the star icon when possible, otherwise dock + // it to the content area. + if (aBrowser.contentWindow == window.content) { + let ubIcons = aBrowser.ownerDocument.getElementById("urlbar-icons"); + if (ubIcons) { + await StarUI.showEditBookmarkPopup(node, ubIcons, + "bottomcenter topright", + isNewBookmark, url); + return; + } + } + + await StarUI.showEditBookmarkPopup(node, aBrowser, "overlap", + isNewBookmark, url); + }, + + _getPageDetails(browser) { + return new Promise(resolve => { + let mm = browser.messageManager; + mm.addMessageListener("Bookmarks:GetPageDetails:Result", function listener(msg) { + mm.removeMessageListener("Bookmarks:GetPageDetails:Result", listener); + resolve(msg.data); + }); + + mm.sendAsyncMessage("Bookmarks:GetPageDetails", { }); + }); + }, + + /** + * Adds a bookmark to the page targeted by a link. + * @param parentId + * The folder in which to create a new bookmark if aURL isn't + * bookmarked. + * @param url (string) + * the address of the link target + * @param title + * The link text + * @param [optional] description + * The linked page description, if available + */ + async bookmarkLink(parentId, url, title, description = "") { + let bm = await PlacesUtils.bookmarks.fetch({url}); + if (bm) { + let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm); + PlacesUIUtils.showBookmarkDialog({ action: "edit", node }, + window.top); + return; + } + + let parentGuid = parentId == PlacesUtils.bookmarksMenuFolderId ? + PlacesUtils.bookmarks.menuGuid : + await PlacesUtils.promiseItemGuid(parentId); + let defaultInsertionPoint = new PlacesInsertionPoint({ parentId, parentGuid }); + + PlacesUIUtils.showBookmarkDialog({ action: "add", + type: "bookmark", + uri: makeURI(url), + title, + description, + defaultInsertionPoint, + hiddenRows: [ "description", + "location", + "loadInSidebar", + "keyword" ] + }, window.top); + }, + + /** + * List of nsIURI objects characterizing the tabs currently open in the + * browser. The URIs will be in the order in which their + * corresponding tabs appeared and duplicates are discarded. + */ + get uniqueCurrentPages() { + let seenURIs = {}; + let URIs = []; + + gBrowser.tabs.forEach(tab => { + let browser = tab.linkedBrowser; + let uri = browser.currentURI; + // contentTitle is usually empty. + let title = browser.contentTitle || tab.label; + let spec = uri.spec; + if (!(spec in seenURIs)) { + // add to the set of seen URIs + seenURIs[uri.spec] = null; + URIs.push({ uri, title }); + } + }); + + return URIs; + }, + + /** + * Adds a folder with bookmarks to all of the currently open tabs in this + * window. + */ + bookmarkCurrentPages: function PCH_bookmarkCurrentPages() { + let pages = this.uniqueCurrentPages; + if (pages.length > 1) { + PlacesUIUtils.showBookmarkDialog({ action: "add", + type: "folder", + URIList: pages, + hiddenRows: [ "description" ] + }, window); + } + }, + + /** + * Updates disabled state for the "Bookmark All Tabs" command. + */ + updateBookmarkAllTabsCommand: + function PCH_updateBookmarkAllTabsCommand() { + // There's nothing to do in non-browser windows. + if (window.location.href != getBrowserURL()) + return; + + // Disable "Bookmark All Tabs" if there are less than two + // "unique current pages". + goSetCommandEnabled("Browser:BookmarkAllTabs", + this.uniqueCurrentPages.length >= 2); + }, + + /** + * Adds a Live Bookmark to a feed associated with the current page. + * @param url + * The nsIURI of the page the feed was attached to + * @title title + * The title of the feed. Optional. + * @subtitle subtitle + * A short description of the feed. Optional. + */ + async addLiveBookmark(url, feedTitle, feedSubtitle) { + let toolbarIP = new PlacesInsertionPoint({ + parentId: PlacesUtils.toolbarFolderId, + parentGuid: PlacesUtils.bookmarks.toolbarGuid + }); + + let feedURI = makeURI(url); + let title = feedTitle || gBrowser.contentTitle; + let description = feedSubtitle; + if (!description) { + description = (await this._getPageDetails(gBrowser.selectedBrowser)).description; + } + + PlacesUIUtils.showBookmarkDialog({ action: "add", + type: "livemark", + feedURI, + siteURI: gBrowser.currentURI, + title, + description, + defaultInsertionPoint: toolbarIP, + hiddenRows: [ "feedLocation", + "siteLocation", + "description" ] + }, window); + }, + + /** + * Opens the Places Organizer. + * @param {String} item The item to select in the organizer window, + * options are (case sensitive): + * BookmarksMenu, BookmarksToolbar, UnfiledBookmarks, + * AllBookmarks, History. + */ + showPlacesOrganizer(item) { + var organizer = Services.wm.getMostRecentWindow("Places:Organizer"); + // Due to bug 528706, getMostRecentWindow can return closed windows. + if (!organizer || organizer.closed) { + // No currently open places window, so open one with the specified mode. + openDialog("chrome://communicator/content/places/places.xul", + "", "chrome,toolbar=yes,dialog=no,resizable", item); + } else { + organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(item); + organizer.focus(); + } + }, +}; + +/** + * Functions for handling events in the Bookmarks Toolbar and menu. + */ +var BookmarksEventHandler = { + + onMouseUp(aEvent) { + // Handles left-click with modifier if not browser.bookmarks.openInTabClosesMenu. + if (aEvent.button != 0 || PlacesUIUtils.openInTabClosesMenu) + return; + let target = aEvent.originalTarget; + if (target.tagName != "menuitem") + return; + let modifKey = AppConstants.platform === "macosx" ? aEvent.metaKey + : aEvent.ctrlKey; + // Don't keep menu open for 'Open all in Tabs'. + if (modifKey && !target.classList.contains("openintabs-menuitem")) { + target.setAttribute("closemenu", "none"); + } + }, + + /** + * Handler for click event for an item in the bookmarks toolbar or menu. + * Menus and submenus from the folder buttons bubble up to this handler. + * Left-click is handled in the onCommand function. + * When items are middle-clicked (or clicked with modifier), open in tabs. + * If the click came through a menu, close the menu. + * @param aEvent + * DOMEvent for the click + * @param aView + * The places view which aEvent should be associated with. + */ + onClick: function BEH_onClick(aEvent, aView) { + // Only handle middle-click or left-click with modifiers. + if (aEvent.button == 2 || (aEvent.button == 0 && !aEvent.shiftKey && + !aEvent.ctrlKey && !aEvent.metaKey)) + return; + + var target = aEvent.originalTarget; + // If this event bubbled up from a menu or menuitem, + // close the menus if browser.bookmarks.openInTabClosesMenu. + if ((PlacesUIUtils.openInTabClosesMenu && target.tagName == "menuitem") || + target.tagName == "menu" || + target.classList.contains("openintabs-menuitem")) { + closeMenus(aEvent.target); + } + // Command already precesssed so remove any closemenu attr set in onMouseUp. + if (aEvent.button == 0 && + target.tagName == "menuitem" && + target.getAttribute("closemenu") == "none") { + // On Mac we need to extend when we remove the flag, to avoid any pre-close + // animations. + setTimeout(() => { + target.removeAttribute("closemenu"); + }, 500); + } + + if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) { + // Don't open the root folder in tabs when the empty area on the toolbar + // is middle-clicked or when a non-bookmark item except for Open in Tabs) + // in a bookmarks menupopup is middle-clicked. + if (target.localName == "menu" || target.localName == "toolbarbutton") + PlacesUIUtils.openContainerNodeInTabs(target._placesNode, aEvent, aView); + } + else if (aEvent.button == 1) { + // left-clicks with modifier are already served by onCommand + this.onCommand(aEvent); + } + }, + + /** + * Handler for command event for an item in the bookmarks toolbar. + * Menus and submenus from the folder buttons bubble up to this handler. + * Opens the item. + * @param aEvent + * DOMEvent for the command + */ + onCommand: function BEH_onCommand(aEvent) { + var target = aEvent.originalTarget; + if (target._placesNode) + PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent); + }, + + onPopupShowing: function BEH_onPopupShowing(aEvent) { + var browser = getBrowser(); + if (!aEvent.currentTarget.parentNode._placesView) + new PlacesMenu(aEvent, 'place:folder=BOOKMARKS_MENU'); + + document.getElementById("Browser:BookmarkAllTabs") + .setAttribute("disabled", !browser || browser.tabs.length == 1); + }, + + fillInBHTooltip: function BEH_fillInBHTooltip(aDocument, aEvent) { + var node; + var cropped = false; + var targetURI; + + if (aDocument.tooltipNode.localName == "treechildren") { + var tree = aDocument.tooltipNode.parentNode; + var tbo = tree.treeBoxObject; + var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY); + if (cell.row == -1) + return false; + node = tree.view.nodeForTreeIndex(cell.row); + cropped = tbo.isCellCropped(cell.row, cell.col); + } + else { + // Check whether the tooltipNode is a Places node. + // In such a case use it, otherwise check for targetURI attribute. + var tooltipNode = aDocument.tooltipNode; + if (tooltipNode._placesNode) + node = tooltipNode._placesNode; + else { + // This is a static non-Places node. + targetURI = tooltipNode.getAttribute("targetURI"); + } + } + + if (!node && !targetURI) + return false; + + // Show node.label as tooltip's title for non-Places nodes. + var title = node ? node.title : tooltipNode.label; + + // Show URL only for Places URI-nodes or nodes with a targetURI attribute. + var url; + if (targetURI || PlacesUtils.nodeIsURI(node)) + url = targetURI || node.uri; + + // Show tooltip for containers only if their title is cropped. + if (!cropped && !url) + return false; + + var tooltipTitle = aDocument.getElementById("bhtTitleText"); + tooltipTitle.hidden = (!title || (title == url)); + if (!tooltipTitle.hidden) + tooltipTitle.textContent = title; + + var tooltipUrl = aDocument.getElementById("bhtUrlText"); + tooltipUrl.hidden = !url; + if (!tooltipUrl.hidden) + tooltipUrl.value = url; + + // Show tooltip. + return true; + } +}; + + +// Handles special drag and drop functionality for Places menus that are not +// part of a Places view (e.g. the bookmarks menu in the menubar). +var PlacesMenuDNDHandler = { + _springLoadDelay: 350, // milliseconds + _loadTimer: null, + + /** + * Called when the user enters the <menu> element during a drag. + * @param event + * The DragEnter event that spawned the opening. + */ + onDragEnter: function PMDH_onDragEnter(event) { + // Opening menus in a Places popup is handled by the view itself. + if (!this._isStaticContainer(event.target)) + return; + + this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._loadTimer.initWithCallback(function() { + PlacesMenuDNDHandler._loadTimer = null; + event.target.lastChild.setAttribute("autoopened", "true"); + event.target.lastChild.showPopup(event.target.lastChild); + }, this._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Handles dragexit on the <menu> element. + * @returns true if the element is a container element (menu or + * menu-toolbarbutton), false otherwise. + */ + onDragExit: function PMDH_onDragExit(event) { + // Closing menus in a Places popup is handled by the view itself. + if (!this._isStaticContainer(event.target)) + return; + + if (this._loadTimer) { + this._loadTimer.cancel(); + this._loadTimer = null; + } + let closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + closeTimer.initWithCallback(function() { + let node = PlacesControllerDragHelper.currentDropTarget; + let inHierarchy = false; + while (node && !inHierarchy) { + inHierarchy = node == event.target; + node = node.parentNode; + } + if (!inHierarchy && event.target.lastChild && + event.target.lastChild.hasAttribute("autoopened")) { + event.target.lastChild.removeAttribute("autoopened"); + event.target.lastChild.hidePopup(); + } + }, this._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT); + }, + + /** + * Determines if a XUL element represents a static container. + * @returns true if the element is a container element (menu or + * menu-toolbarbutton), false otherwise. + */ + _isStaticContainer: function PMDH__isContainer(node) { + let isMenu = node.localName == "menu" || + (node.localName == "toolbarbutton" && + node.getAttribute("type") == "menu"); + let isStatic = !("_placesNode" in node) && node.lastChild && + node.lastChild.hasAttribute("placespopup") && + !node.parentNode.hasAttribute("placespopup"); + return isMenu && isStatic; + }, + + /** + * Called when the user drags over the <menu> element. + * @param event + * The DragOver event. + */ + onDragOver: function PMDH_onDragOver(event) { + let ip = new PlacesInsertionPoint({ + parentId: PlacesUtils.bookmarksMenuFolderId, + parentGuid: PlacesUtils.bookmarks.menuGuid + }); + if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer)) + event.preventDefault(); + + event.stopPropagation(); + }, + + /** + * Called when the user drops on the <menu> element. + * @param event + * The Drop event. + */ + onDrop: function PMDH_onDrop(event) { + // Put the item at the end of bookmark menu. + let ip = new PlacesInsertionPoint({ + parentId: PlacesUtils.bookmarksMenuFolderId, + parentGuid: PlacesUtils.bookmarks.menuGuid + }); + PlacesControllerDragHelper.onDrop(ip, event.dataTransfer); + event.stopPropagation(); + } +}; + + +var BookmarkingUI = { + _hasBookmarksObserver: false, + _itemGuids: new Set(), + + uninit: function BUI_uninit() + { + if (this._hasBookmarksObserver) { + PlacesUtils.bookmarks.removeObserver(this); + } + + if (this._pendingUpdate) { + delete this._pendingUpdate; + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver + ]), + + get _starredTooltip() + { + delete this._starredTooltip; + return this._starredTooltip = + gNavigatorBundle.getString("starButtonOn.tooltip"); + }, + + get _unstarredTooltip() + { + delete this._unstarredTooltip; + return this._unstarredTooltip = + gNavigatorBundle.getString("starButtonOff.tooltip"); + }, + + updateStarState: function BUI_updateStarState() { + this._uri = gBrowser.currentURI; + this._itemGuids = []; + let aItemGuids = []; + + // those objects are use to check if we are in the current iteration before + // returning any result. + let pendingUpdate = this._pendingUpdate = {}; + + PlacesUtils.bookmarks.fetch({url: this._uri}, b => aItemGuids.push(b.guid)) + .catch(Cu.reportError) + .then(() => { + if (pendingUpdate != this._pendingUpdate) { + return; + } + + // It's possible that onItemAdded gets called before the async statement + // calls back. For such an edge case, retain all unique entries from the + // array. + this._itemGuids = this._itemGuids.filter( + guid => !aItemGuids.includes(guid) + ).concat(aItemGuids); + + this._updateStar(); + + // Start observing bookmarks if needed. + if (!this._hasBookmarksObserver) { + try { + PlacesUtils.bookmarks.addObserver(this); + this._hasBookmarksObserver = true; + } catch (ex) { + Cu.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex); + } + } + + delete this._pendingUpdate; + }); + }, + + _updateStar: function BUI__updateStar() + { + let starIcon = document.getElementById("star-button"); + if (this._itemGuids.length > 0) { + starIcon.setAttribute("starred", "true"); + starIcon.setAttribute("tooltiptext", this._starredTooltip); + } + else { + starIcon.removeAttribute("starred"); + starIcon.setAttribute("tooltiptext", this._unstarredTooltip); + } + }, + + onClick: function BUI_onClick(aEvent) + { + // Ignore clicks on the star while we update its state. + if (aEvent.button == 0 && !this._pendingUpdate) + PlacesCommandHook.bookmarkPage(gBrowser.selectedBrowser, + this._itemGuids.length > 0); + + }, + + // nsINavBookmarkObserver + onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGuid) { + if (aURI && aURI.equals(this._uri)) { + // If a new bookmark has been added to the tracked uri, register it. + if (!this._itemGuids.includes(aGuid)) { + this._itemGuids.push(aGuid); + // Only need to update the UI if it wasn't marked as starred before: + if (this._itemGuids.length == 1) { + this._updateStar(); + } + } + } + }, + + onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGuid) { + let index = this._itemGuids.indexOf(aGuid); + // If one of the tracked bookmarks has been removed, unregister it. + if (index != -1) { + this._itemGuids.splice(index, 1); + // Only need to update the UI if the page is no longer starred + if (this._itemGuids.length == 0) { + this._updateStar(); + } + } + }, + + onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, + aItemType, aParentId, aGuid) { + if (aProperty == "uri") { + let index = this._itemGuids.indexOf(aGuid); + // If the changed bookmark was tracked, check if it is now pointing to + // a different uri and unregister it. + if (index != -1 && aNewValue != this._uri.spec) { + this._itemGuids.splice(index, 1); + // Only need to update the UI if the page is no longer starred + if (this._itemGuids.length == 0) { + this._updateStar(); + } + } else if (index == -1 && aNewValue == this._uri.spec) { + // If another bookmark is now pointing to the tracked uri, register it. + this._itemGuids.push(aGuid); + // Only need to update the UI if it wasn't marked as starred before: + if (this._itemGuids.length == 1) { + this._updateStar(); + } + } + } + }, + + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onItemVisited: function () {}, + onItemMoved: function () {} +}; + + +// This object handles the initialization and uninitialization of the bookmarks +// toolbar. updateStarState is called when the browser window is opened and +// after closing the toolbar customization dialog. +var PlacesToolbarHelper = { + _place: "place:folder=TOOLBAR", + get _viewElt() { + return document.getElementById("PlacesToolbar"); + }, + + init: function PTH_init() { + let viewElt = this._viewElt; + if (!viewElt || viewElt._placesView) + return; + + // There is no need to initialize the toolbar if customizing because + // init() will be called when the customization is done. + if (this._isCustomizing) + return; + + new PlacesToolbar(this._place); + }, + + customizeStart: function PTH_customizeStart() { + let viewElt = this._viewElt; + if (viewElt && viewElt._placesView) + viewElt._placesView.uninit(); + + this._isCustomizing = true; + }, + + customizeDone: function PTH_customizeDone() { + this._isCustomizing = false; + this.init(); + } +}; + + +// Handles the bookmarks menu popup +var BookmarksMenu = { + _popupInitialized: {}, + onPopupShowing: function BM_onPopupShowing(aEvent, aPrefix) { + if (!(aPrefix in this._popupInitialized)) { + // First popupshowing event, initialize immutable attributes. + this._popupInitialized[aPrefix] = true; + + // Need to set the label on Unsorted Bookmarks menu. + let unsortedBookmarksElt = + document.getElementById(aPrefix + "unsortedBookmarksFolderMenu"); + unsortedBookmarksElt.label = + PlacesUtils.getString("OtherBookmarksFolderTitle"); + } + }, +}; diff --git a/comm/suite/browser/content.js b/comm/suite/browser/content.js new file mode 100644 index 0000000000..40dd01d7c2 --- /dev/null +++ b/comm/suite/browser/content.js @@ -0,0 +1,901 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This content script should work in any browser or iframe and should not + * depend on the frame being contained in tabbrowser. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter(this, "LoginManagerContent", + "resource://gre/modules/LoginManagerContent.jsm"); +ChromeUtils.defineModuleGetter(this, "InsecurePasswordUtils", + "resource://gre/modules/InsecurePasswordUtils.jsm"); +ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +ChromeUtils.defineModuleGetter(this, "LoginFormFactory", + "resource://gre/modules/LoginManagerContent.jsm"); +ChromeUtils.defineModuleGetter(this, "PlacesUIUtils", + "resource:///modules/PlacesUIUtils.jsm"); +ChromeUtils.defineModuleGetter(this, "setTimeout", + "resource://gre/modules/Timer.jsm"); +ChromeUtils.defineModuleGetter(this, "Feeds", + "resource:///modules/Feeds.jsm"); +ChromeUtils.defineModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gPipNSSBundle", function() { + return Services.strings.createBundle("chrome://pipnss/locale/pipnss.properties"); +}); +XPCOMUtils.defineLazyGetter(this, "gNSSErrorsBundle", function() { + return Services.strings.createBundle("chrome://pipnss/locale/nsserrors.properties"); +}); + +addMessageListener("RemoteLogins:fillForm", message => { + LoginManagerContent.receiveMessage(message, content); +}); + +addEventListener("DOMFormHasPassword", event => { + LoginManagerContent.onDOMFormHasPassword(event, content); + let formLike = LoginFormFactory.createFromForm(event.target); + InsecurePasswordUtils.reportInsecurePasswords(formLike); +}); + +addEventListener("DOMInputPasswordAdded", event => { + LoginManagerContent.onDOMInputPasswordAdded(event, content); + let formLike = LoginFormFactory.createFromField(event.target); + InsecurePasswordUtils.reportInsecurePasswords(formLike); +}); + +addEventListener("pageshow", event => { + LoginManagerContent.onPageShow(event, content); +}, true); + +addEventListener("DOMAutoComplete", event => { + LoginManagerContent.onUsernameInput(event); +}); + +addEventListener("blur", event => { + LoginManagerContent.onUsernameInput(event); +}); + +addMessageListener("Bookmarks:GetPageDetails", (message) => { + let doc = content.document; + let isErrorPage = /^about:(neterror|certerror|blocked)/.test(doc.documentURI); + sendAsyncMessage("Bookmarks:GetPageDetails:Result", + { isErrorPage, + description: PlacesUIUtils.getDescriptionFromDocument(doc) }); +}); + +/* The following code, in particular AboutCertErrorListener and + * AboutNetErrorListener, is mostly copied from content browser.js and content.js. + * Certificate error handling should be unified to remove this duplicated code. + */ + +const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE; +const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE; + +const SEC_ERROR_EXPIRED_CERTIFICATE = SEC_ERROR_BASE + 11; +const SEC_ERROR_UNKNOWN_ISSUER = SEC_ERROR_BASE + 13; +const SEC_ERROR_UNTRUSTED_ISSUER = SEC_ERROR_BASE + 20; +const SEC_ERROR_UNTRUSTED_CERT = SEC_ERROR_BASE + 21; +const SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE = SEC_ERROR_BASE + 30; +const SEC_ERROR_CA_CERT_INVALID = SEC_ERROR_BASE + 36; +const SEC_ERROR_OCSP_FUTURE_RESPONSE = SEC_ERROR_BASE + 131; +const SEC_ERROR_OCSP_OLD_RESPONSE = SEC_ERROR_BASE + 132; +const SEC_ERROR_REUSED_ISSUER_AND_SERIAL = SEC_ERROR_BASE + 138; +const SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED = SEC_ERROR_BASE + 176; +const MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE = MOZILLA_PKIX_ERROR_BASE + 5; +const MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE = MOZILLA_PKIX_ERROR_BASE + 6; +const MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT = MOZILLA_PKIX_ERROR_BASE + 14; +const MOZILLA_PKIX_ERROR_MITM_DETECTED = MOZILLA_PKIX_ERROR_BASE + 15; + + +const SSL_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE; +const SSL_ERROR_SSL_DISABLED = SSL_ERROR_BASE + 20; +const SSL_ERROR_SSL2_DISABLED = SSL_ERROR_BASE + 14; + +var AboutNetAndCertErrorListener = { + init(chromeGlobal) { + addEventListener("AboutNetAndCertErrorLoad", this, false, true); + }, + + get isNetErrorSite() { + return content.document.documentURI.startsWith("about:neterror"); + }, + + get isCertErrorSite() { + return content.document.documentURI.startsWith("about:certerror"); + }, + + _getErrorMessageFromCode(securityInfo, doc) { + let uri = Services.io.newURI(doc.location); + let hostString = uri.host; + if (uri.port != 443 && uri.port != -1) { + hostString += ":" + uri.port; + } + + let id_str = ""; + switch (securityInfo.errorCode) { + case SSL_ERROR_SSL_DISABLED: + id_str = "PSMERR_SSL_Disabled"; + break; + case SSL_ERROR_SSL2_DISABLED: + id_str = "PSMERR_SSL2_Disabled"; + break; + case SEC_ERROR_REUSED_ISSUER_AND_SERIAL: + id_str = "PSMERR_HostReusedIssuerSerial"; + break; + } + let nss_error_id_str = securityInfo.errorCodeString; + let msg2 = ""; + if (id_str) { + msg2 = gPipNSSBundle.GetStringFromName(id_str) + "\n"; + } else if (nss_error_id_str) { + msg2 = gNSSErrorsBundle.GetStringFromName(nss_error_id_str) + "\n"; + } + + if (!msg2) { + // We couldn't get an error message. Use the error string. + // Note that this is different from before where we used PR_ErrorToString. + msg2 = nss_error_id_str; + } + let msg = gPipNSSBundle.formatStringFromName("SSLConnectionErrorPrefix2", + [hostString, msg2], 2); + + if (nss_error_id_str) { + msg += gPipNSSBundle.formatStringFromName("certErrorCodePrefix3", + [nss_error_id_str], 1); + } + let id = content.document.getElementById("errorShortDescText"); + id.textContent = msg; + id.className = "wrap"; + }, + + _setTechDetails(sslStatus, securityInfo, location) { + if (!securityInfo || !sslStatus || !location) { + return; + } + let validity = sslStatus.serverCert.validity; + + let doc = content.document; + // CSS class and error code are set from nsDocShell. + let searchParams = new URLSearchParams(doc.documentURI.split("?")[1]); + let cssClass = searchParams.get("s"); + let error = searchParams.get("e"); + let technicalInfo = doc.getElementById("technicalContentText"); + + let uri = Services.io.newURI(location); + let hostString = uri.host; + if (uri.port != 443 && uri.port != -1) { + hostString += ":" + uri.port; + } + + let msg = gPipNSSBundle.formatStringFromName("certErrorIntro", + [hostString], 1); + msg += "\n\n"; + + if (sslStatus.isUntrusted) { + switch (securityInfo.errorCode) { + case MOZILLA_PKIX_ERROR_MITM_DETECTED: + msg += gPipNSSBundle.GetStringFromName("certErrorTrust_MitM") + "\n"; + break; + case SEC_ERROR_UNKNOWN_ISSUER: + msg += gPipNSSBundle.GetStringFromName("certErrorTrust_UnknownIssuer") + "\n"; + msg += gPipNSSBundle.GetStringFromName("certErrorTrust_UnknownIssuer2") + "\n"; + msg += gPipNSSBundle.GetStringFromName("certErrorTrust_UnknownIssuer3") + "\n"; + break; + case SEC_ERROR_CA_CERT_INVALID: + msg += gPipNSSBundle.GetStringFromName("certErrorTrust_CaInvalid") + "\n"; + break; + case SEC_ERROR_UNTRUSTED_ISSUER: + msg += gPipNSSBundle.GetStringFromName("certErrorTrust_Issuer") + "\n"; + break; + case SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED: + msg += gPipNSSBundle.GetStringFromName("certErrorTrust_SignatureAlgorithmDisabled") + "\n"; + break; + case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE: + msg += gPipNSSBundle.GetStringFromName("certErrorTrust_ExpiredIssuer") + "\n"; + break; + case MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT: + msg += gPipNSSBundle.GetStringFromName("certErrorTrust_SelfSigned") + "\n"; + break; + case SEC_ERROR_UNTRUSTED_CERT: + default: + msg += gPipNSSBundle.GetStringFromName("certErrorTrust_Untrusted") + "\n"; + } + } + + technicalInfo.appendChild(doc.createTextNode(msg)); + + if (sslStatus.isDomainMismatch) { + let subjectAltNamesList = sslStatus.serverCert.subjectAltNames; + let subjectAltNames = subjectAltNamesList.split(","); + let numSubjectAltNames = subjectAltNames.length; + if (numSubjectAltNames != 0) { + if (numSubjectAltNames == 1) { + // Let's check if we want to make this a link. + let okHost = subjectAltNamesList; + let href = ""; + let thisHost = doc.location.hostname; + let proto = doc.location.protocol + "//"; + // If okHost is a wildcard domain ("*.example.com") let's + // use "www" instead. "*.example.com" isn't going to + // get anyone anywhere useful. bug 432491 + okHost = okHost.replace(/^\*\./, "www."); + /* case #1: + * example.com uses an invalid security certificate. + * + * The certificate is only valid for www.example.com + * + * Make sure to include the "." ahead of thisHost so that + * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com" + * + * We'd normally just use a RegExp here except that we lack a + * library function to escape them properly (bug 248062), and + * domain names are famous for having '.' characters in them, + * which would allow spurious and possibly hostile matches. + */ + if (okHost.endsWith("." + thisHost)) { + href = proto + okHost; + } + /* case #2: + * browser.garage.maemo.org uses an invalid security certificate. + * + * The certificate is only valid for garage.maemo.org + */ + if (thisHost.endsWith("." + okHost)) { + href = proto + okHost; + } + + // If we set a link, meaning there's something helpful for + // the user here, expand the section by default + if (href && cssClass != "expertBadCert") { + doc.getElementById("technicalContentText").style.display = "block"; + } + + let msgPrefix = + gPipNSSBundle.GetStringFromName("certErrorMismatchSinglePrefix"); + + // Set the link if we want it. + if (href) { + let referrerlink = doc.createElement("a"); + referrerlink.append(subjectAltNamesList + "\n"); + referrerlink.title = subjectAltNamesList; + referrerlink.id = "cert_domain_link"; + referrerlink.href = href; + msg = BrowserUtils.getLocalizedFragment(doc, msgPrefix, + referrerlink); + } else { + msg = BrowserUtils.getLocalizedFragment(doc, msgPrefix, + subjectAltNamesList); + } + } else { + msg = gPipNSSBundle.GetStringFromName("certErrorMismatchMultiple") + "\n"; + for (let i = 0; i < numSubjectAltNames; i++) { + msg += subjectAltNames[i]; + if (i != (numSubjectAltNames - 1)) { + msg += ", "; + } + } + } + } else { + msg = gPipNSSBundle.formatStringFromName("certErrorMismatch", + [hostString], 1); + } + technicalInfo.append(msg + "\n"); + } + + if (sslStatus.isNotValidAtThisTime) { + let nowTime = new Date().getTime() * 1000; + let dateOptions = { year: "numeric", month: "long", day: "numeric", + hour: "numeric", minute: "numeric" }; + let now = new Services.intl.DateTimeFormat(undefined, dateOptions).format(new Date()); + if (validity.notBefore) { + if (nowTime > validity.notAfter) { + msg = gPipNSSBundle.formatStringFromName("certErrorExpiredNow", + [validity.notAfterLocalTime, now], 2) + "\n"; + } else { + msg = gPipNSSBundle.formatStringFromName("certErrorNotYetValidNow", + [validity.notBeforeLocalTime, now], 2) + "\n"; + } + } else { + // If something goes wrong, we assume the cert expired. + msg = gPipNSSBundle.formatStringFromName("certErrorExpiredNow", + ["", now], 2) + "\n"; + } + technicalInfo.append(msg); + } + technicalInfo.append("\n"); + + // Add link to certificate and error message. + msg = gPipNSSBundle.formatStringFromName("certErrorCodePrefix3", + [securityInfo.errorCodeString], 1); + technicalInfo.append(msg); + }, + + handleEvent(aEvent) { + if (!this.isNetErrorSite && !this.isCertErrorSite) { + return; + } + + if (aEvent.type != "AboutNetAndCertErrorLoad") { + return; + } + + if (this.isNetErrorSite) { + let {securityInfo} = docShell.failedChannel; + // We don't have a securityInfo when this is for example a DNS error. + if (securityInfo) { + securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); + this._getErrorMessageFromCode(securityInfo, + aEvent.originalTarget.ownerGlobal); + } + return; + } + + let ownerDoc = aEvent.originalTarget.ownerGlobal; + let securityInfo = docShell.failedChannel && docShell.failedChannel.securityInfo; + securityInfo.QueryInterface(Ci.nsITransportSecurityInfo) + .QueryInterface(Ci.nsISerializable); + let sslStatus = securityInfo.QueryInterface(Ci.nsISSLStatusProvider) + .SSLStatus; + this._setTechDetails(sslStatus, securityInfo, ownerDoc.location.href); + }, +}; +AboutNetAndCertErrorListener.init(); + +const MathMLNS = "http://www.w3.org/1998/Math/MathML"; +const XLinkNS = "http://www.w3.org/1999/xlink"; + +let PageInfoListener = { + + init: function() { + addMessageListener("PageInfo:getData", this); + }, + + receiveMessage: function(message) { + let strings = message.data.strings; + let window; + let document; + + let frameOuterWindowID = message.data.frameOuterWindowID; + + // If inside frame then get the frame's window and document. + if (frameOuterWindowID) { + window = Services.wm.getOuterWindowWithId(frameOuterWindowID); + document = window.document; + } + else { + document = content.document; + window = content.window; + } + + let imageElement = message.objects.imageElement; + + let pageInfoData = {metaViewRows: this.getMetaInfo(document), + docInfo: this.getDocumentInfo(document), + feeds: this.getFeedsInfo(document, strings), + windowInfo: this.getWindowInfo(window), + imageInfo: this.getImageInfo(imageElement)}; + + sendAsyncMessage("PageInfo:data", pageInfoData); + + // Separate step so page info dialog isn't blank while waiting for this + // to finish. + this.getMediaInfo(document, window, strings); + }, + + getImageInfo: function(imageElement) { + let imageInfo = null; + if (imageElement) { + imageInfo = { + currentSrc: imageElement.currentSrc, + width: imageElement.width, + height: imageElement.height, + imageText: imageElement.title || imageElement.alt + }; + } + return imageInfo; + }, + + getMetaInfo: function(document) { + let metaViewRows = []; + + // Get the meta tags from the page. + let metaNodes = document.getElementsByTagName("meta"); + + for (let metaNode of metaNodes) { + metaViewRows.push([metaNode.name || metaNode.httpEquiv || + metaNode.getAttribute("property"), + metaNode.content]); + } + + return metaViewRows; + }, + + getWindowInfo: function(window) { + let windowInfo = {}; + windowInfo.isTopWindow = window == window.top; + + let hostName = null; + try { + hostName = window.location.host; + } + catch (exception) { } + + windowInfo.hostName = hostName; + return windowInfo; + }, + + getDocumentInfo: function(document) { + let docInfo = {}; + docInfo.title = document.title; + docInfo.location = document.location.toString(); + docInfo.referrer = document.referrer; + docInfo.compatMode = document.compatMode; + docInfo.contentType = document.contentType; + docInfo.characterSet = document.characterSet; + docInfo.lastModified = document.lastModified; + docInfo.principal = document.nodePrincipal; + + let documentURIObject = {}; + documentURIObject.spec = document.documentURIObject.spec; + docInfo.documentURIObject = documentURIObject; + + docInfo.isContentWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(content); + + return docInfo; + }, + + getFeedsInfo: function(document, strings) { + let feeds = []; + // Get the feeds from the page. + let linkNodes = document.getElementsByTagName("link"); + let length = linkNodes.length; + for (let i = 0; i < length; i++) { + let link = linkNodes[i]; + if (!link.href) { + continue; + } + let rel = link.rel && link.rel.toLowerCase(); + let rels = {}; + + if (rel) { + for (let relVal of rel.split(/\s+/)) { + rels[relVal] = true; + } + } + + if (rels.feed || (link.type && rels.alternate && !rels.stylesheet)) { + let type = Feeds.isValidFeed(link, document.nodePrincipal, "feed" in rels); + if (type) { + type = strings[type] || strings["application/rss+xml"]; + feeds.push([link.title, type, link.href]); + } + } + } + return feeds; + }, + + // Only called once to get the media tab's media elements from the content + // page. + getMediaInfo: function(document, window, strings) + { + let frameList = this.goThroughFrames(document, window); + this.processFrames(document, frameList, strings); + }, + + goThroughFrames: function(document, window) + { + let frameList = [document]; + if (window && window.frames.length > 0) { + let num = window.frames.length; + for (let i = 0; i < num; i++) { + // Recurse through the frames. + frameList = + frameList.concat(this.goThroughFrames(window.frames[i].document, + window.frames[i])); + } + } + return frameList; + }, + + async processFrames(document, frameList, strings) + { + let nodeCount = 0; + for (let doc of frameList) { + let iterator = doc.createTreeWalker(doc, content.NodeFilter.SHOW_ELEMENT); + + // Goes through all the elements on the doc. + while (iterator.nextNode()) { + this.getMediaItems(document, strings, iterator.currentNode); + + if (++nodeCount % 500 == 0) { + // setTimeout every 500 elements so we don't keep blocking the + // content process. + await new Promise(resolve => setTimeout(resolve, 10)); + } + } + } + // Send that page info media fetching has finished. + sendAsyncMessage("PageInfo:mediaData", {isComplete: true}); + }, + + getMediaItems: function(document, strings, elem) + { + // Check for images defined in CSS (e.g. background, borders). + let computedStyle = elem.ownerDocument.defaultView.getComputedStyle(elem, ""); + // A node can have multiple media items associated with it - for example, + // multiple background images. + let imageItems = []; + let formItems = []; + let linkItems = []; + + let addImage = (url, type, alt, elem, isBg) => { + let element = this.serializeElementInfo(document, url, type, alt, elem, isBg); + imageItems.push([url, type, alt, element, isBg]); + }; + + if (computedStyle) { + let addImgFunc = (label, val) => { + if (val.primitiveType == content.CSSPrimitiveValue.CSS_URI) { + addImage(val.getStringValue(), label, strings.notSet, elem, true); + } + else if (val.primitiveType == content.CSSPrimitiveValue.CSS_STRING) { + // This is for -moz-image-rect. + // TODO: Reimplement once bug 714757 is fixed. + let strVal = val.getStringValue(); + if (strVal.search(/^.*url\(\"?/) > -1) { + let url = strVal.replace(/^.*url\(\"?/,"").replace(/\"?\).*$/,""); + addImage(url, label, strings.notSet, elem, true); + } + } + else if (val.cssValueType == content.CSSValue.CSS_VALUE_LIST) { + // Recursively resolve multiple nested CSS value lists. + for (let i = 0; i < val.length; i++) { + addImgFunc(label, val.item(i)); + } + } + }; + + addImgFunc(strings.mediaBGImg, computedStyle.getPropertyCSSValue("background-image")); + addImgFunc(strings.mediaBorderImg, computedStyle.getPropertyCSSValue("border-image-source")); + addImgFunc(strings.mediaListImg, computedStyle.getPropertyCSSValue("list-style-image")); + addImgFunc(strings.mediaCursor, computedStyle.getPropertyCSSValue("cursor")); + } + + let addForm = (elem) => { + let element = this.serializeFormInfo(document, elem, strings); + formItems.push([elem.name, elem.method, elem.action, element]); + }; + + // One swi^H^H^Hif-else to rule them all. + if (elem instanceof content.HTMLAnchorElement) { + linkItems.push([this.getValueText(elem), elem.href, strings.linkAnchor, + elem.target, elem.accessKey]); + } + else if (elem instanceof content.HTMLImageElement) { + addImage(elem.src, strings.mediaImg, + (elem.hasAttribute("alt")) ? elem.alt : strings.notSet, + elem, false); + } + else if (elem instanceof content.HTMLAreaElement) { + linkItems.push([elem.alt, elem.href, strings.linkArea, elem.target, ""]); + } + else if (elem instanceof content.HTMLVideoElement) { + addImage(elem.currentSrc, strings.mediaVideo, "", elem, false); + } + else if (elem instanceof content.HTMLAudioElement) { + addImage(elem.currentSrc, strings.mediaAudio, "", elem, false); + } + else if (elem instanceof content.HTMLLinkElement) { + if (elem.rel) { + let rel = elem.rel; + if (/\bicon\b/i.test(rel)) { + addImage(elem.href, strings.mediaLink, "", elem, false); + } + else if (/(?:^|\s)stylesheet(?:\s|$)/i.test(rel)) { + linkItems.push([elem.rel, elem.href, strings.linkStylesheet, + elem.target, ""]); + } + else { + linkItems.push([elem.rel, elem.href, strings.linkRel, + elem.target, ""]); + } + } + else { + linkItems.push([elem.rev, elem.href, strings.linkRev, elem.target, ""]); + } + } + else if (elem instanceof content.HTMLInputElement || + elem instanceof content.HTMLButtonElement) { + switch (elem.type.toLowerCase()) { + case "image": + addImage(elem.src, strings.mediaInput, + (elem.hasAttribute("alt")) ? elem.alt : strings.notSet, + elem, false); + // Fall through, <input type="image"> submits, too + case "submit": + if ("form" in elem && elem.form) { + linkItems.push([elem.value || this.getValueText(elem) || + strings.linkSubmit, elem.form.action, + strings.linkSubmission, elem.form.target, ""]); + } + else { + linkItems.push([elem.value || this.getValueText(elem) || + strings.linkSubmit, "", + strings.linkSubmission, "", ""]); + } + } + } + else if (elem instanceof content.HTMLFormElement) { + addForm(elem); + } + else if (elem instanceof content.HTMLObjectElement) { + addImage(elem.data, strings.mediaObject, this.getValueText(elem), + elem, false); + } + else if (elem instanceof content.HTMLEmbedElement) { + addImage(elem.src, strings.mediaEmbed, "", elem, false); + } + else if (elem.namespaceURI == MathMLNS && elem.hasAttribute("href")) { + let href = elem.getAttribute("href"); + try { + href = makeURLAbsolute(elem.baseURI, href, + elem.ownerDocument.characterSet); + } catch (e) {} + linkItems.push([this.getValueText(elem), href, strings.linkX, "", ""]); + } + else if (elem.hasAttributeNS(XLinkNS, "href")) { + let href = elem.getAttributeNS(XLinkNS, "href"); + try { + href = makeURLAbsolute(elem.baseURI, href, + elem.ownerDocument.characterSet); + } catch (e) {} + // SVG images without an xlink:href attribute are ignored + if (elem instanceof content.SVGImageElement) { + addImage(href, strings.mediaImg, "", elem, false); + } + else { + linkItems.push([this.getValueText(elem), href, strings.linkX, "", ""]); + } + } + else if (elem instanceof content.HTMLScriptElement) { + linkItems.push([elem.type || elem.getAttribute("language") || + strings.notSet, elem.src || strings.linkScriptInline, + strings.linkScript, "", "", ""]); + } + if (imageItems.length || formItems.length || linkItems.length) { + sendAsyncMessage("PageInfo:mediaData", + {imageItems, formItems, linkItems, isComplete: false}); + } + }, + + /** + * Set up a JSON element object with all the instanceOf and other infomation + * that makePreview in pageInfo.js uses to figure out how to display the + * preview. + */ + + serializeElementInfo: function(document, url, type, alt, item, isBG) + { + let result = {}; + + let imageText; + if (!isBG && + !(item instanceof content.SVGImageElement) && + !(document instanceof content.ImageDocument)) { + imageText = item.title || item.alt; + + if (!imageText && !(item instanceof content.HTMLImageElement)) { + imageText = this.getValueText(item); + } + } + + result.imageText = imageText; + result.longDesc = item.longDesc; + result.numFrames = 1; + + if (item instanceof content.HTMLObjectElement || + item instanceof content.HTMLEmbedElement || + item instanceof content.HTMLLinkElement) { + result.mimeType = item.type; + } + + if (!result.mimeType && !isBG && + item instanceof Ci.nsIImageLoadingContent) { + let imageRequest = + item.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); + if (imageRequest) { + result.mimeType = imageRequest.mimeType; + let image = !(imageRequest.imageStatus & imageRequest.STATUS_ERROR) && + imageRequest.image; + if (image) { + result.numFrames = image.numFrames; + } + } + } + + // If we have a data url, get the MIME type from the url. + if (!result.mimeType && url.startsWith("data:")) { + let dataMimeType = /^data:(image\/[^;,]+)/i.exec(url); + if (dataMimeType) + result.mimeType = dataMimeType[1].toLowerCase(); + } + + result.HTMLLinkElement = item instanceof content.HTMLLinkElement; + result.HTMLInputElement = item instanceof content.HTMLInputElement; + result.HTMLImageElement = item instanceof content.HTMLImageElement; + result.HTMLObjectElement = item instanceof content.HTMLObjectElement; + result.HTMLEmbedElement = item instanceof content.HTMLEmbedElement; + result.SVGImageElement = item instanceof content.SVGImageElement; + result.HTMLVideoElement = item instanceof content.HTMLVideoElement; + result.HTMLAudioElement = item instanceof content.HTMLAudioElement; + + if (isBG) { + // Items that are showing this image as a background + // image might not necessarily have a width or height, + // so we'll dynamically generate an image and send up the + // natural dimensions. + let img = content.document.createElement("img"); + img.src = url; + result.naturalWidth = img.naturalWidth; + result.naturalHeight = img.naturalHeight; + } else { + // Otherwise, we can use the current width and height + // of the image. + result.width = item.width; + result.height = item.height; + } + + if (item instanceof content.SVGImageElement) { + result.SVGImageElementWidth = item.width.baseVal.value; + result.SVGImageElementHeight = item.height.baseVal.value; + } + + result.baseURI = item.baseURI; + + return result; + }, + + serializeFormInfo: function(document, form, strings) + { + let result = {}; + + if (form.name) + result.name = form.name; + + result.encoding = form.encoding; + result.target = form.target; + result.formfields = []; + + function findFirstControl(node, document) { + function FormControlFilter(node) { + if (node instanceof content.HTMLInputElement || + node instanceof content.HTMLSelectElement || + node instanceof content.HTMLButtonElement || + node instanceof content.HTMLTextAreaElement || + node instanceof content.HTMLObjectElement) + return content.NodeFilter.FILTER_ACCEPT; + return content.NodeFilter.FILTER_SKIP; + } + + if (node.hasAttribute("for")) { + return document.getElementById(node.getAttribute("for")); + } + + var iterator = document.createTreeWalker(node, content.NodeFilter.SHOW_ELEMENT, FormControlFilter, true); + + return iterator.nextNode(); + } + + var whatfor; + var labels = []; + for (let label of form.getElementsByTagName("label")) { + var whatfor = findFirstControl(label, document); + + if (whatfor && (whatfor.form == form)) { + labels.push({label: whatfor, labeltext: this.getValueText(label)}); + } + } + + result.formfields = []; + + var val; + for (let formfield of form.elements) { + if (formfield instanceof content.HTMLButtonElement) + val = this.getValueText(formfield); + else + val = (/^password$/i.test(formfield.type)) ? strings.formPassword : formfield.value; + + var fieldlabel = ""; + for (let labelfor of labels) { + if (formfield == labelfor.label) { + fieldlabel = labelfor.labeltext; + } + } + result.formfields.push([fieldlabel, formfield.name, formfield.type, val]); + } + + return result; + }, + + //******** Other Misc Stuff + // Modified from the Links Panel v2.3, + // http://segment7.net/mozilla/links/links.html + // parse a node to extract the contents of the node + getValueText: function(node) + { + + let valueText = ""; + + // Form input elements don't generally contain information that is useful + // to our callers, so return nothing. + if (node instanceof content.HTMLInputElement || + node instanceof content.HTMLSelectElement || + node instanceof content.HTMLTextAreaElement) { + return valueText; + } + + // Otherwise recurse for each child. + let length = node.childNodes.length; + + for (let i = 0; i < length; i++) { + let childNode = node.childNodes[i]; + let nodeType = childNode.nodeType; + + // Text nodes are where the goods are. + if (nodeType == content.Node.TEXT_NODE) { + valueText += " " + childNode.nodeValue; + } + // And elements can have more text inside them. + else if (nodeType == content.Node.ELEMENT_NODE) { + // Images are special, we want to capture the alt text as if the image + // weren't there. + if (childNode instanceof content.HTMLImageElement) { + valueText += " " + this.getAltText(childNode); + } + else { + valueText += " " + this.getValueText(childNode); + } + } + } + + return this.stripWS(valueText); + }, + + // Copied from the Links Panel v2.3, + // http://segment7.net/mozilla/links/links.html. + // Traverse the tree in search of an img or area element and grab its alt tag. + getAltText: function(node) + { + let altText = ""; + + if (node.alt) { + return node.alt; + } + let length = node.childNodes.length; + for (let i = 0; i < length; i++) { + if ((altText = this.getAltText(node.childNodes[i]) != undefined)) { // stupid js warning... + return altText; + } + } + return ""; + }, + + // Copied from the Links Panel v2.3, + // http://segment7.net/mozilla/links/links.html. + // Strip leading and trailing whitespace, and replace multiple consecutive + // whitespace characters with a single space. + stripWS: function(text) + { + let middleRE = /\s+/g; + let endRE = /(^\s+)|(\s+$)/g; + + text = text.replace(middleRE, " "); + return text.replace(endRE, ""); + } +}; +PageInfoListener.init(); diff --git a/comm/suite/browser/fullScreen.js b/comm/suite/browser/fullScreen.js new file mode 100644 index 0000000000..30a666cf57 --- /dev/null +++ b/comm/suite/browser/fullScreen.js @@ -0,0 +1,107 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var FullScreen = +{ + toggle: function() + { + var show = !window.fullScreen; + // show/hide all menubars, toolbars, and statusbars (except the full screen toolbar) + this.showXULChrome("toolbar", show); + this.showXULChrome("statusbar", show); + + var toolbox = getNavToolbox(); + if (show) + toolbox.removeAttribute("inFullscreen"); + else + toolbox.setAttribute("inFullscreen", true); + + var controls = document.getElementsByAttribute("fullscreencontrol", "true"); + for (let i = 0; i < controls.length; ++i) + controls[i].hidden = show; + + controls = document.getElementsByAttribute("domfullscreenhidden", "true"); + if (document.mozFullScreen) { + for (let i = 0; i < controls.length; ++i) + controls[i].setAttribute("moz-collapsed", "true"); + getBrowser().mStrip.setAttribute("moz-collapsed", "true"); + } else { + for (let i = 0; i < controls.length; ++i) + controls[i].removeAttribute("moz-collapsed"); + getBrowser().mStrip.removeAttribute("moz-collapsed"); + } + getBrowser().getNotificationBox().notificationsHidden = document.mozFullScreen; + }, + + showXULChrome: function(aTag, aShow) + { + var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var els = document.getElementsByTagNameNS(XULNS, aTag); + + var i; + for (i = 0; i < els.length; ++i) { + // XXX don't interfere with previously collapsed toolbars + if (els[i].getAttribute("fullscreentoolbar") == "true" && + !document.mozFullScreen) { + if (!aShow) { + var toolbarMode = els[i].getAttribute("mode"); + if (toolbarMode != "text") { + els[i].setAttribute("saved-mode", toolbarMode); + els[i].setAttribute("saved-iconsize", + els[i].getAttribute("iconsize")); + els[i].setAttribute("mode", "icons"); + els[i].setAttribute("iconsize", "small"); + } + + // XXX See bug 202978: we disable the context menu + // to prevent customization while in fullscreen, which + // causes menu breakage. + els[i].setAttribute("saved-context", + els[i].getAttribute("context")); + els[i].removeAttribute("context"); + + // Set the inFullscreen attribute to allow specific styling + // in fullscreen mode + els[i].setAttribute("inFullscreen", true); + } + else { + this.restoreAttribute(els[i], "mode"); + this.restoreAttribute(els[i], "iconsize"); + this.restoreAttribute(els[i], "context"); // XXX see above + + els[i].removeAttribute("inFullscreen"); + els[i].removeAttribute("moz-collapsed"); + } + } else if (els[i].getAttribute("type") == "menubar") { + if (aShow) { + this.restoreAttribute(els[i], "autohide"); + } + else { + els[i].setAttribute("saved-autohide", + els[i].getAttribute("autohide")); + els[i].setAttribute("autohide", "true"); + } + } else { + // use moz-collapsed so it doesn't persist hidden/collapsed, + // so that new windows don't have missing toolbars + if (aShow) + els[i].removeAttribute("moz-collapsed"); + else + els[i].setAttribute("moz-collapsed", "true"); + } + } + }, + + restoreAttribute: function(element, attributeName) + { + var savedAttribute = "saved-" + attributeName; + if (element.hasAttribute(savedAttribute)) { + var savedValue = element.getAttribute(savedAttribute); + element.setAttribute(attributeName, savedValue); + element.removeAttribute(savedAttribute); + } + } + +}; diff --git a/comm/suite/browser/hiddenWindow.xul b/comm/suite/browser/hiddenWindow.xul new file mode 100644 index 0000000000..ac769bf12c --- /dev/null +++ b/comm/suite/browser/hiddenWindow.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xul-overlay href="chrome://navigator/content/navigatorOverlay.xul"?> + +<!-- hiddenwindow is a "minimal" XUL window intended for creating the, + er, hidden window. This window is never shown, but on platforms + which leave the app running after the last (visible) window is shut + down, this window does hold a browser menubar. + Though this window looks a lot like navigator.xul, that xul + is unsuitable because it's subject to the whims of its associated + appcore, which among other things causes it to load content documents + undesirable for this window. + Arguably a simpler menu structure could be substituted, but + the full one was included for now in anticipation of the whole thing + becoming an included file someday. --> + +<!-- Localizable string definitions from navigator.xul. --> +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % navigatorDTD SYSTEM "chrome://navigator/locale/navigator.dtd" > +%navigatorDTD; +]> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + title="hidden" + titlemodifier="&mainWindow.titlemodifier;" + titlemenuseparator="&mainWindow.titlemodifiermenuseparator;" + onload="hiddenWindowStartup();" + onunload="Shutdown();"> + + <stringbundleset id="stringbundleset"/> + + <!-- keys are appended from the overlay --> + <keyset id="navKeys"/> + + <!-- commands are appended from the overlay --> + <commandset id="commands"/> + + <broadcasterset id="navBroadcasters"/> + + <!-- it's the whole navigator.xul menubar! hidden windows need to + have a menubar for situations where they're the only window remaining + on a platform that wants to leave the app running, like the Mac. + --> + <toolbox id="toolbox"> + <menubar id="main-menubar" position="1"/> + </toolbox> + +</window> diff --git a/comm/suite/browser/jar.mn b/comm/suite/browser/jar.mn new file mode 100644 index 0000000000..8034462f4c --- /dev/null +++ b/comm/suite/browser/jar.mn @@ -0,0 +1,41 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +comm.jar: +% content navigator %content/navigator/ +% content navigator-region %content/navigator-region/ + content/navigator/browser-places.js + content/navigator/content.js + content/navigator/fullScreen.js + content/navigator/hiddenWindow.xul + content/navigator/linkToolbarHandler.js + content/navigator/linkToolbarItem.js + content/navigator/linkToolbarOverlay.js + content/navigator/linkToolbarOverlay.xul + content/navigator/mailNavigatorOverlay.js +* content/navigator/mailNavigatorOverlay.xul + content/navigator/metadata.js + content/navigator/metadata.xul + content/navigator/navigator.css + content/navigator/navigator.js + content/navigator/navigator.xul + content/navigator/navigatorDD.js +* content/navigator/navigatorOverlay.xul + content/navigator/nsBrowserContentListener.js + content/navigator/nsBrowserStatusHandler.js + content/navigator/sessionHistoryUI.js + content/navigator/safeBrowsingOverlay.js + content/navigator/safeBrowsingOverlay.xul + content/navigator/tabbrowser.xml + content/navigator/urlbarBindings.xml + content/navigator/webDeveloperOverlay.js + content/navigator/webDeveloperOverlay.xul + + content/navigator/pageinfo/feeds.js (pageinfo/feeds.js) + content/navigator/pageinfo/feeds.xml (pageinfo/feeds.xml) + content/navigator/pageinfo/pageInfo.css (pageinfo/pageInfo.css) + content/navigator/pageinfo/pageInfo.js (pageinfo/pageInfo.js) + content/navigator/pageinfo/pageInfo.xul (pageinfo/pageInfo.xul) + content/navigator/pageinfo/permissions.js (pageinfo/permissions.js) + content/navigator/pageinfo/security.js (pageinfo/security.js) diff --git a/comm/suite/browser/linkToolbarHandler.js b/comm/suite/browser/linkToolbarHandler.js new file mode 100644 index 0000000000..eb3e0cfb38 --- /dev/null +++ b/comm/suite/browser/linkToolbarHandler.js @@ -0,0 +1,295 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +ChromeUtils.defineModuleGetter(this, "Feeds", + "resource:///modules/Feeds.jsm"); + +/** + * LinkToolbarHandler is a Singleton that displays LINK elements + * and nodeLists of LINK elements in the Link Toolbar. It + * associates the LINK with a corresponding LinkToolbarItem based + * on it's REL attribute and the toolbar item's ID attribute. + * LinkToolbarHandler is also a Factory and will create + * LinkToolbarItems as necessary. + */ +function LinkToolbarHandler() +{ + this.items = new Array(); + this.hasItems = false; +} + +LinkToolbarHandler.prototype.handle = +function(element) +{ + // XXX: if you're going to re-enable handling of anchor elements, + // you'll want to change this to AnchorElementDecorator + var linkElement = new LinkElementDecorator(element); + + if (linkElement.isIgnored()) return; + + for (var i = 0; i < linkElement.relValues.length; i++) { + // Skip "alternate" when we have e.g. "alternate XXX". + if (linkElement.relValues.length > 1 && + linkElement.relValues[i] == "alternate") + continue; + + var linkType = LinkToolbarHandler.getLinkType(linkElement.relValues[i], element); + if (linkType) { + if (!this.hasItems) { + this.hasItems = true; + linkToolbarUI.activate(); + } + this.getItemForLinkType(linkType).displayLink(linkElement); + } + } +} + +LinkToolbarHandler.getLinkType = +function(relAttribute, element) +{ + var isFeed = false; + switch (relAttribute.toLowerCase()) { + case "top": + case "origin": + return "top"; + + case "up": + case "parent": + return "up"; + + case "start": + case "begin": + case "first": + return "first"; + + case "next": + case "child": + return "next"; + + case "prev": + case "previous": + return "prev"; + + case "end": + case "last": + return "last"; + + case "author": + case "made": + return "author"; + + case "contents": + case "toc": + return "toc"; + + case "feed": + isFeed = true; + // fall through + case "alternate": + if (Feeds.isValidFeed(element, element.nodePrincipal, isFeed)) { + return "feed"; + } + + if (!isFeed) { + return "alternate"; + } + // fall through + case "prefetch": + return null; + + default: + return relAttribute.toLowerCase(); + } +} + +LinkToolbarHandler.prototype.getItemForLinkType = +function(linkType) { + if (!(linkType in this.items && this.items[linkType])) + this.items[linkType] = LinkToolbarHandler.createItemForLinkType(linkType); + + return this.items[linkType]; +} + +LinkToolbarHandler.createItemForLinkType = +function(linkType) +{ + if (!document.getElementById("link-" + linkType)) + return new LinkToolbarTransientMenu(linkType); + + // XXX: replace switch with polymorphism + var element = document.getElementById("link-" + linkType); + switch (element.getAttribute("type") || element.localName) { + case "toolbarbutton": + return new LinkToolbarButton(linkType); + + case "menuitem": + return new LinkToolbarItem(linkType); + + case "menu": + return new LinkToolbarMenu(linkType); + + default: + return new LinkToolbarTransientMenu(linkType); + } +} + +LinkToolbarHandler.prototype.clearAllItems = +function() +{ + // Hide the 'miscellaneous' separator + document.getElementById("misc-separator").setAttribute("collapsed", "true"); + + // Disable the individual items + for (var linkType in this.items) + this.items[linkType].clear(); + + // Store the fact that the toolbar is empty + this.hasItems = false; +} + +var linkToolbarHandler = new LinkToolbarHandler(); + +function LinkElementDecorator(element) { + /* + * XXX: this is an incomplete decorator, because it doesn't implement + * the full Element interface. If you need to use a method + * or member in the Element interface, just add it here and + * have it delegate to this.element + * + * XXX: would rather add some methods to Element.prototype instead of + * using a decorator, but Element.prototype is no longer exposed + * since the XPCDOM landing, see bug 83433 + */ + + if (!element) return; // skip the rest on foo.prototype = new ThisClass calls + + this.element = element; + + this.rel = LinkElementDecorator.convertRevMade(element.rel, element.rev); + if (this.rel) + this.relValues = this.rel.split(" "); + this.rev = element.rev; + this.title = element.title; + this.href = element.href; + this.hreflang = element.hreflang; + this.media = element.media; + this.longTitle = null; +} + +LinkElementDecorator.prototype.isIgnored = +function() +{ + if (!this.rel) return true; + for (var i = 0; i < this.relValues.length; i++) + if (/^stylesheet$|^icon$|^fontdef$|^p3pv|^schema./i.test(this.relValues[i])) + return true; + return false; +} + +LinkElementDecorator.convertRevMade = +function(rel, rev) +{ + if (!rel && rev && /\bmade\b/i.test(rev)) + return rev; + else + return rel; +} + +LinkElementDecorator.prototype.getTooltip = +function() +{ + return this.getLongTitle() || this.href; +} + +LinkElementDecorator.prototype.getLabel = +function() +{ + return this.getLongTitle() || this.rel; +} + +LinkElementDecorator.prototype.getLongTitle = +function() +{ + if (this.longTitle == null) + this.longTitle = this.makeLongTitle(); + + return this.longTitle; +} + +LinkElementDecorator.prototype.makeLongTitle = +function() +{ + let prefix = ""; + + // XXX: lookup more meaningful and localized version of media, + // i.e. media="print" becomes "Printable" or some such + // XXX: use localized version of ":" separator + if (this.media && !/\ball\b|\bscreen\b/i.test(this.media)) + prefix += this.media + ": "; + if (this.hreflang) { + try { + let languageBundle = document.getElementById("languageBundle"); + let regionBundle = document.getElementById("regionBundle"); + + // In case hreflang contains region code. + let ISOcode = this.hreflang.split("-"); + + prefix += languageBundle.getString(ISOcode[0].toLowerCase()); + + // Test if region code exists. + if (ISOcode[1]) + prefix += " (" + regionBundle.getString(ISOcode[1].toLowerCase()) + ")"; + } + catch (e) { + // Return if language or region is not recognized. + prefix += gNavigatorBundle.getFormattedString("unknownLanguage", + [this.hreflang]); + } + + prefix += ": "; + } + + return this.title ? prefix + this.title : prefix; +} + +function AnchorElementDecorator(element) { + this.constructor(element); +} +AnchorElementDecorator.prototype = new LinkElementDecorator; + +AnchorElementDecorator.prototype.getLongTitle = +function() +{ + return this.title ? this.__proto__.getLongTitle.apply(this) + : getText(this.element); +} + +AnchorElementDecorator.prototype.getText = +function(element) +{ + return condenseWhitespace(getTextRecursive(element)); +} + +AnchorElementDecorator.prototype.getTextRecursive = +function(node) +{ + var text = ""; + node.normalize(); + if (node.hasChildNodes()) { + for (var i = 0; i < node.childNodes.length; i++) { + if (node.childNodes.item(i).nodeType == Node.TEXT_NODE) + text += node.childNodes.item(i).nodeValue; + else if (node.childNodes.item(i).nodeType == Node.ELEMENT_NODE) + text += getTextRecursive(node.childNodes.item(i)); + } + } + + return text; +} + +AnchorElementDecorator.prototype.condenseWhitespace = +function(text) +{ + return text.replace(/\W*$/, "").replace(/^\W*/, "").replace(/\W+/g, " "); +} diff --git a/comm/suite/browser/linkToolbarItem.js b/comm/suite/browser/linkToolbarItem.js new file mode 100644 index 0000000000..1ae956a64d --- /dev/null +++ b/comm/suite/browser/linkToolbarItem.js @@ -0,0 +1,215 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * LinkToolbarItem and its subclasses represent the buttons, menuitems, + * and menus that handle the various link types. + */ +function LinkToolbarItem (linkType) { + this.linkType = linkType; + this.xulElementId = "link-" + linkType; + this.xulPopupId = this.xulElementId + "-popup"; + this.parentMenuButton = null; + + this.getXULElement = function() { + return document.getElementById(this.xulElementId); + } + + this.clear = function() { + this.disableParentMenuButton(); + this.getXULElement().setAttribute("disabled", "true"); + this.getXULElement().removeAttribute("href"); + } + + this.displayLink = function(linkElement) { + if (this.getXULElement().hasAttribute("href")) return false; + + this.setItem(linkElement); + this.enableParentMenuButton(); + return true; + } + + this.setItem = function(linkElement) { + this.getXULElement().setAttribute("href", linkElement.href); + this.getXULElement().removeAttribute("disabled"); + } + + this.enableParentMenuButton = function() { + if(this.getParentMenuButton()) + this.getParentMenuButton().removeAttribute("disabled"); + } + + this.disableParentMenuButton = function() { + if (!this.parentMenuButton) return; + + this.parentMenuButton.setAttribute("disabled", "true"); + this.parentMenuButton = null; + } + + this.getParentMenuButton = function() { + if (!this.parentMenuButton) + this.parentMenuButton = getParentMenuButtonRecursive( + this.getXULElement()); + + return this.parentMenuButton; + } + + function getParentMenuButtonRecursive(xulElement) { + if (!xulElement) return null; + + if (xulElement.tagName == "toolbarbutton") + return xulElement; + + return getParentMenuButtonRecursive(xulElement.parentNode) + } +} + + +function LinkToolbarButton (linkType) { + this.constructor(linkType); + + this.clear = function() { + this.__proto__.clear.apply(this); + + this.getXULElement().removeAttribute("tooltiptext"); + } + + this.setItem = function(linkElement) { + this.__proto__.setItem.apply(this, [linkElement]); + + this.getXULElement().setAttribute("tooltiptext", linkElement.getTooltip()); + } + + this.enableParentMenuButton = function() { /* do nothing */ } + this.disableParentMenuButton = function() { /* do nothing */ } +} +LinkToolbarButton.prototype = new LinkToolbarItem; + + +function LinkToolbarMenu (linkType) { + this.constructor(linkType); + + this.clear = function() { + this.disableParentMenuButton(); + this.getXULElement().setAttribute("disabled", "true"); + clearPopup(this.getPopup()); + } + + function clearPopup(popup) { + while (popup.hasChildNodes()) + popup.lastChild.remove(); + } + + this.getPopup = function() { + return document.getElementById(this.xulPopupId); + } + + this.displayLink = function(linkElement) { + this.addMenuItem(linkElement); + this.getXULElement().removeAttribute("disabled"); + this.enableParentMenuButton(); + return true; + } + + function match(first, second) { + if (!first && !second) return true; + if (!first || !second) return false; + + return first == second; + } + + this.addMenuItem = function(linkElement) { + this.getPopup().appendChild(this.createMenuItem(linkElement)); + } + + this.createMenuItem = function(linkElement) { + // XXX: clone a prototypical XUL element instead of hardcoding these + // attributes + var menuitem = document.createElement("menuitem"); + menuitem.setAttribute("label", linkElement.getLabel()); + menuitem.setAttribute("href", linkElement.href); + menuitem.setAttribute("class", "menuitem-iconic bookmark-item"); + + return menuitem; + } +} +LinkToolbarMenu.prototype = new LinkToolbarItem; + + +function LinkToolbarTransientMenu (linkType) { + this.constructor(linkType); + + this.getXULElement = function() { + if (this.__proto__.getXULElement.apply(this)) + return this.__proto__.getXULElement.apply(this); + else + return this.createXULElement(); + } + + this.createXULElement = function() { + // XXX: clone a prototypical XUL element instead of hardcoding these + // attributes + var menu = document.createElement("menu"); + menu.setAttribute("id", this.xulElementId); + menu.setAttribute("label", this.linkType); + menu.setAttribute("disabled", "true"); + menu.setAttribute("class", "menu-iconic bookmark-item"); + menu.setAttribute("container", "true"); + + document.getElementById("more-menu-popup").appendChild(menu); + + return menu; + } + + this.getPopup = function() { + if (!this.__proto__.getPopup.apply(this)) + this.getXULElement().appendChild(this.createPopup()); + + return this.__proto__.getPopup.apply(this) + } + + this.createPopup = function() { + var popup = document.createElement("menupopup"); + popup.setAttribute("id", this.xulPopupId); + + return popup; + } + + this.clear = function() { + this.__proto__.clear.apply(this); + + // XXX: we really want to use this instead of removeXULElement + //this.hideXULElement(); + this.removeXULElement(); + } + + this.hideXULElement = function() { + /* + * XXX: using "hidden" or "collapsed" leads to a crash when you + * open the More menu under certain circumstances. Maybe + * related to bug 83906. As of 0.9.2 I it doesn't seem + * to crash anymore. + */ + this.getXULElement().setAttribute("collapsed", "true"); + } + + this.removeXULElement = function() { + // XXX: stop using this method once it's safe to use hideXULElement + if (this.__proto__.getXULElement.apply(this)) + this.__proto__.getXULElement.apply(this).remove(); + } + + this.displayLink = function(linkElement) { + if(!this.__proto__.displayLink.apply(this, [linkElement])) return false; + + this.getXULElement().removeAttribute("collapsed"); + + // Show the 'miscellaneous' separator + document.getElementById("misc-separator").removeAttribute("collapsed"); + return true; + } +} + +LinkToolbarTransientMenu.prototype = new LinkToolbarMenu; + diff --git a/comm/suite/browser/linkToolbarOverlay.js b/comm/suite/browser/linkToolbarOverlay.js new file mode 100644 index 0000000000..94927ad310 --- /dev/null +++ b/comm/suite/browser/linkToolbarOverlay.js @@ -0,0 +1,213 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var LinkToolbarUI = function() +{ +} + +LinkToolbarUI.prototype.linkAdded = +function(event) +{ + var element = event.originalTarget; + + if (element.ownerDocument != getBrowser().contentDocument || + !linkToolbarUI.isLinkToolbarEnabled() || + !(ChromeUtils.getClassName(element) === "HTMLLinkElement") || + !element.href || (!element.rel && !element.rev)) + return; + + linkToolbarHandler.handle(element); +} + +LinkToolbarUI.prototype.isLinkToolbarEnabled = +function() +{ + if (document.getElementById("linktoolbar").getAttribute("hidden") == "true") + return false; + else + return true; +} + +LinkToolbarUI.prototype.clear = +function(event) +{ + if (event.originalTarget != getBrowser().contentDocument || + !linkToolbarUI.isLinkToolbarEnabled() || + !linkToolbarHandler.hasItems) + return; + + linkToolbarHandler.clearAllItems(); +} + +LinkToolbarUI.prototype.tabSelected = +function(event) +{ + if (event.originalTarget.localName != "tabs" || + !linkToolbarUI.isLinkToolbarEnabled()) + return; + + linkToolbarHandler.clearAllItems(); + linkToolbarUI.deactivate(); + linkToolbarUI.fullSlowRefresh(); +} + +LinkToolbarUI.prototype.fullSlowRefresh = +function() +{ + var currentNode = getBrowser().contentDocument.documentElement; + if (!ChromeUtils.getClassName(currentNode) === "HTMLHtmlElement") + return; + currentNode = currentNode.firstChild; + + while(currentNode) + { + if (ChromeUtils.getClassName(currentNode) === "HTMLHeadElement") { + currentNode = currentNode.firstChild; + + while(currentNode) + { + if (ChromeUtils.getClassName(currentNode) === "HTMLLinkElement") + linkToolbarUI.linkAdded({originalTarget: currentNode}); + currentNode = currentNode.nextSibling; + } + } + else if (currentNode.nodeType == currentNode.ELEMENT_NODE) { + // head is supposed to be the first element inside html. + // Got something else instead. returning + return; + } + else + { + // Got a comment node or something like that. Moving on. + currentNode = currentNode.nextSibling; + } + } +} + +LinkToolbarUI.prototype.toolbarActive = false; + +LinkToolbarUI.prototype.activate = +function() +{ + if (!linkToolbarUI.toolbarActive) { + linkToolbarUI.toolbarActive = true; + document.getElementById("linktoolbar").setAttribute("hasitems", "true"); + var contentArea = document.getElementById("appcontent"); + contentArea.addEventListener("pagehide", linkToolbarUI.clear, true); + contentArea.addEventListener("pageshow", linkToolbarUI.deactivate, true); + contentArea.addEventListener("DOMHeadLoaded", linkToolbarUI.deactivate, + true); + } +} + +LinkToolbarUI.prototype.deactivate = +function() +{ + // This function can never be called unless the toolbar is active, because + // it's a handler that's only activated in that situation, so there's no need + // to check toolbarActive. On the other hand, by the time this method is + // called the toolbar might have been populated again already, in which case + // we don't want to deactivate. + if (!linkToolbarHandler.hasItems) { + linkToolbarUI.toolbarActive = false; + document.getElementById("linktoolbar").setAttribute("hasitems", "false"); + var contentArea = document.getElementById("appcontent"); + contentArea.removeEventListener("pagehide", linkToolbarUI.clear, true); + contentArea.removeEventListener("pageshow", linkToolbarUI.deactivate, true); + contentArea.removeEventListener("DOMHeadLoaded", linkToolbarUI.deactivate, + true); + } +} + +/* called whenever something on the toolbar gets an oncommand event */ +LinkToolbarUI.prototype.commanded = +function(event) +{ + // Return if this is one of the menubuttons. + if (event.target.getAttribute("type") == "menu") return; + + if (!event.target.getAttribute("href")) return; + + var destURL = event.target.getAttribute("href"); + + // We have to do a security check here, because we are loading URIs given + // to us by a web page from chrome, which is privileged. + try { + urlSecurityCheck(destURL, content.document.nodePrincipal, + Ci.nsIScriptSecurityManager.STANDARD); + loadURI(destURL, content.document.documentURIObject); + } catch (e) { + dump("Error: it is not permitted to load this URI from a <link> element: " + e); + } +} + +// functions for twiddling XUL elements in the toolbar + +LinkToolbarUI.prototype.toggleLinkToolbar = +function(checkedItem) +{ + this.goToggleTristateToolbar("linktoolbar", checkedItem); + this.initHandlers(); + if (this.isLinkToolbarEnabled()) + this.fullSlowRefresh(); + else + linkToolbarHandler.clearAllItems(); +} + +LinkToolbarUI.prototype.initLinkbarVisibilityMenu = +function() +{ + var state = document.getElementById("linktoolbar").getAttribute("hidden"); + if (!state) + state = "maybe"; + var checkedItem = document.getElementById("cmd_viewlinktoolbar_" + state); + checkedItem.setAttribute("checked", true); + checkedItem.checked = true; +} + +LinkToolbarUI.prototype.goToggleTristateToolbar = +function(id, checkedItem) +{ + var toolbar = document.getElementById(id); + if (toolbar) + { + toolbar.setAttribute("hidden", checkedItem.value); + document.persist(id, "hidden"); + } +} + +LinkToolbarUI.prototype.addHandlerActive = false; + +LinkToolbarUI.prototype.initialized = false; + +LinkToolbarUI.prototype.initHandlers = +function() +{ + var contentArea = document.getElementById("appcontent"); + if (linkToolbarUI.isLinkToolbarEnabled()) + { + if (!linkToolbarUI.addHandlerActive) { + contentArea.addEventListener("select", linkToolbarUI.tabSelected); + contentArea.addEventListener("DOMLinkAdded", linkToolbarUI.linkAdded, + true); + linkToolbarUI.addHandlerActive = true; + } + } else + { + if (linkToolbarUI.addHandlerActive) { + contentArea.removeEventListener("select", linkToolbarUI.tabSelected); + contentArea.removeEventListener("DOMLinkAdded", linkToolbarUI.linkAdded, + true); + linkToolbarUI.addHandlerActive = false; + } + } + if (!linkToolbarUI.initialized) + { + linkToolbarUI.initialized = true; + document.removeEventListener("pageshow", linkToolbarUI.initHandlers, true); + } +} + +var linkToolbarUI = new LinkToolbarUI; + diff --git a/comm/suite/browser/linkToolbarOverlay.xul b/comm/suite/browser/linkToolbarOverlay.xul new file mode 100644 index 0000000000..8d9b4c1503 --- /dev/null +++ b/comm/suite/browser/linkToolbarOverlay.xul @@ -0,0 +1,165 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://navigator/skin/linkToolbar.css" type="text/css"?> +<!DOCTYPE overlay SYSTEM "chrome://navigator/locale/linkToolbar.dtd"> + +<overlay id="linkToolbarOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:rdf="http://www.mozilla.org/rdf"> + + <!-- classes --> + <script src="chrome://global/content/globalOverlay.js"/> + <script src="chrome://navigator/content/linkToolbarHandler.js" /> + <script src="chrome://navigator/content/linkToolbarItem.js" /> + + <!-- functions --> + <script src="chrome://navigator/content/linkToolbarOverlay.js" /> + + <script> + <![CDATA[ + document.addEventListener("pageshow", linkToolbarUI.initHandlers, true); + ]]> + </script> + + <stringbundleset> + <stringbundle id="languageBundle" + src="chrome://global/locale/languageNames.properties"/> + <stringbundle id="regionBundle" + src="chrome://global/locale/regionNames.properties"/> + </stringbundleset> + + <menupopup id="view_toolbars_popup"> + <menu label="&linkToolbar.label;" + accesskey="&linkToolbar.accesskey;" + insertbefore="menuitem_showhide_tabbar" + onpopupshowing="linkToolbarUI.initLinkbarVisibilityMenu();" + oncommand="linkToolbarUI.toggleLinkToolbar(event.target);"> + <menupopup> + <menuitem label="&linkToolbarAlways.label;" + accesskey="&linkToolbarAlways.accesskey;" + class="menuitem-iconic" type="radio" value="false" + name="cmd_viewlinktoolbar" id="cmd_viewlinktoolbar_false" /> + <menuitem label="&linkToolbarAsNeeded.label;" + accesskey="&linkToolbarAsNeeded.accesskey;" + class="menuitem-iconic" type="radio" value="maybe" + name="cmd_viewlinktoolbar" id="cmd_viewlinktoolbar_maybe" /> + <menuitem label="&linkToolbarNever.label;" + accesskey="&linkToolbarNever.accesskey;" + class="menuitem-iconic" type="radio" value="true" + name="cmd_viewlinktoolbar" id="cmd_viewlinktoolbar_true" /> + </menupopup> + </menu> + </menupopup> + + <toolbox id="navigator-toolbox"> + <toolbar id="linktoolbar" + grippytooltiptext="&linkToolbar.tooltip;" + class="chromeclass-directories" + hidden="true" + hasitems="false" + nowindowdrag="true" + oncommand="event.stopPropagation(); return linkToolbarUI.commanded(event);"> + + <toolbarbutton id="link-top" class="bookmark-item" + label="&topButton.label;" disabled="true"/> + + <toolbarbutton id="link-up" class="bookmark-item" + label="&upButton.label;" disabled="true"/> + + <toolbarseparator /> + + <toolbarbutton id="link-first" class="bookmark-item" + label="&firstButton.label;" disabled="true"/> + + <toolbarbutton id="link-prev" class="bookmark-item" + label="&prevButton.label;" disabled="true"/> + + <toolbarbutton id="link-next" class="bookmark-item" + label="&nextButton.label;" disabled="true"/> + + <toolbarbutton id="link-last" class="bookmark-item" + label="&lastButton.label;" disabled="true"/> + + <toolbarseparator /> + + <toolbarbutton id="document-menu" class="bookmark-item" + type="menu" + container="true" + label="&documentButton.label;" disabled="true"> + <menupopup id="document-menu-popup"> + + <menuitem id="link-toc" label="&tocButton.label;" disabled="true" + class="menuitem-iconic bookmark-item"/> + <menu id="link-chapter" label="&chapterButton.label;" disabled="true" + class="menu-iconic bookmark-item" container="true"> + <menupopup id="link-chapter-popup" /> + </menu> + <menu id="link-section" label="§ionButton.label;" disabled="true" + class="menu-iconic bookmark-item" container="true"> + <menupopup id="link-section-popup" /> + </menu> + <menu id="link-subsection" label="&subSectionButton.label;" disabled="true" + class="menu-iconic bookmark-item" container="true"> + <menupopup id="link-subsection-popup" /> + </menu> + <menu id="link-appendix" label="&appendixButton.label;" disabled="true" + class="menu-iconic bookmark-item" container="true"> + <menupopup id="link-appendix-popup" /> + </menu> + <menuitem id="link-glossary" label="&glossaryButton.label;" disabled="true" + class="menuitem-iconic bookmark-item"/> + <menuitem id="link-index" label="&indexButton.label;" disabled="true" + class="menuitem-iconic bookmark-item"/> + + </menupopup> + </toolbarbutton> + + <toolbarbutton id="more-menu" class="bookmark-item" + type="menu" + container="true" + label="&moreButton.label;" disabled="true"> + <menupopup id="more-menu-popup"> + + <menuitem id="link-help" label="&helpButton.label;" disabled="true" + class="menuitem-iconic bookmark-item"/> + <menuitem id="link-search" label="&searchButton.label;" disabled="true" + class="menuitem-iconic bookmark-item"/> + + <menuseparator /> + + <menuitem id="link-author" label="&authorButton.label;" disabled="true" + class="menuitem-iconic bookmark-item"/> + <menuitem id="link-copyright" label="©rightButton.label;" disabled="true" + class="menuitem-iconic bookmark-item"/> + + <menuseparator /> + + <menu id="link-bookmark" label="&bookmarkButton.label;" disabled="true" + class="menu-iconic bookmark-item" container="true"> + <menupopup id="link-bookmark-popup" /> + </menu> + + <menuseparator /> + + <menu id="link-alternate" label="&alternateButton.label;" disabled="true" + class="menu-iconic bookmark-item" container="true"> + <menupopup id="link-alternate-popup" /> + </menu> + + <menuseparator collapsed="true" id="misc-separator" /> + + </menupopup> + </toolbarbutton> + + <toolbarbutton id="link-feed" class="bookmark-item" + type="menu" + container="true" + label="&feedButton.label;" disabled="true"> + <menupopup id="link-feed-popup"/> + </toolbarbutton> + </toolbar> + </toolbox> +</overlay> diff --git a/comm/suite/browser/mailNavigatorOverlay.js b/comm/suite/browser/mailNavigatorOverlay.js new file mode 100644 index 0000000000..495d124eba --- /dev/null +++ b/comm/suite/browser/mailNavigatorOverlay.js @@ -0,0 +1,176 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gUseExternalMailto; + +// attachment: 0 - link +// 1 - page +// 2 - image +function openComposeWindow(url, title, attachment, charset) +{ + if (gUseExternalMailto) + { + openExternalMailer(url, title); + } + else + { + var params = Cc["@mozilla.org/messengercompose/composeparams;1"] + .createInstance(Ci.nsIMsgComposeParams); + + params.composeFields = Cc["@mozilla.org/messengercompose/composefields;1"] + .createInstance(Ci.nsIMsgCompFields); + if (attachment == 0 || attachment == 1) + { + params.composeFields.body = url; + params.composeFields.subject = title; + params.bodyIsLink = true; + } + + if (attachment == 1 || attachment == 2) + { + var attachmentData = Cc["@mozilla.org/messengercompose/attachment;1"] + .createInstance(Ci.nsIMsgAttachment); + attachmentData.url = url; + attachmentData.urlCharset = charset; + params.composeFields.addAttachment(attachmentData); + } + + var composeService = Cc["@mozilla.org/messengercompose;1"] + .getService(Ci.nsIMsgComposeService); + + // it is possible you won't have a default identity + // like if you've never launched mail before on a new profile. + // see bug #196073 + try + { + params.identity = composeService.defaultIdentity; + } + catch (ex) + { + params.identity = null; + } + + composeService.OpenComposeWindowWithParams(null, params); + } +} + +function openExternalMailer(url, title) { + var extProtocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + var mailto = url ? "mailto:?body=" + encodeURIComponent(url) + + "&subject=" + + encodeURIComponent(title) : "mailto:"; + var uri = Services.io.newURI(mailto); + + extProtocolSvc.loadURI(uri); +} + +function openNewCardDialog() +{ + window.openDialog("chrome://messenger/content/addressbook/abNewCardDialog.xul", + "", "chrome,modal,resizable=no,centerscreen"); +} + +function goOpenNewMessage() +{ + if (gUseExternalMailto) + { + openExternalMailer(); + } + else if ("MsgNewMessage" in window) + { + MsgNewMessage(null); + } + else + { + var msgComposeService = Cc["@mozilla.org/messengercompose;1"] + .getService(Ci.nsIMsgComposeService); + msgComposeService.OpenComposeWindow(null, null, null, + Ci.nsIMsgCompType.New, + Ci.nsIMsgCompFormat.Default, + null, null, null); + } +} + +function sendLink(aURL) +{ + var title = ""; + if (!aURL) + { + aURL = window.content.document.URL; + title = window.content.document.title; + } + try + { + openComposeWindow(aURL, title, 0, null); + } + catch(ex) + { + dump("Cannot Send Link: " + ex + "\n"); + } +} + +function sendMedia(mediaURL) +{ + try + { + var charset = getCharsetforSave(null); + openComposeWindow(mediaURL, null, 2, charset); + } + catch(ex) + { + dump("Cannot Send Media: " + ex + "\n"); + } +} + +function sendPage(aDocument) +{ + if (!aDocument) + aDocument = window.content.document; + + try + { + var charset = getCharsetforSave(aDocument); + openComposeWindow(aDocument.URL, aDocument.title, 1, charset); + } + catch(ex) + { + dump("Cannot Send Page: " + ex + "\n"); + } +} + +function initMailContextMenuItems(aEvent) +{ + var shouldShowSendPage = !(gContextMenu.onTextInput || gContextMenu.isContentSelected || + gContextMenu.onVideo || gContextMenu.onAudio) && + !gContextMenu.onLink && + !gUseExternalMailto; + gContextMenu.showItem("context-sendpage", shouldShowSendPage); + + gContextMenu.showItem("context-sep-apps", gContextMenu.shouldShowSeparator("context-sep-apps")); +} + +function initMailContextMenuPopupListener(aEvent) +{ + var popup = document.getElementById("contentAreaContextMenu"); + if (popup) + popup.addEventListener("popupshowing", initMailContextMenuItems); +} + +function hideMenuitems() { + document.getElementById("menu_newCard").hidden = gUseExternalMailto; + var menu_sendPage = document.getElementById("menu_sendPage"); + if (menu_sendPage) + menu_sendPage.hidden = gUseExternalMailto; +} + +function initOverlay(aEvent) { + gUseExternalMailto = Services.io.getProtocolHandler("mailto") instanceof + Ci.nsIExternalProtocolHandler; + initMailContextMenuPopupListener(aEvent); + hideMenuitems(); +} + +addEventListener("load", initOverlay, false); diff --git a/comm/suite/browser/mailNavigatorOverlay.xul b/comm/suite/browser/mailNavigatorOverlay.xul new file mode 100644 index 0000000000..5221ce68f8 --- /dev/null +++ b/comm/suite/browser/mailNavigatorOverlay.xul @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay SYSTEM "chrome://navigator/locale/mailNavigatorOverlay.dtd" > + +<overlay id="mailNavigatorOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://navigator/content/mailNavigatorOverlay.js"/> + + <!-- navigator specific commands --> + <commandset id="tasksCommands"> + <command id="cmd_newMessage" oncommand="goOpenNewMessage();"/> + <command id="cmd_newCard" oncommand="openNewCardDialog()"/> + <command id="cmd_sendPage" oncommand="sendPage();"/> + <command id="Browser:SendLink" oncommand="sendLink();"/> + </commandset> + <keyset id="tasksKeys"> +#ifdef XP_MACOSX + <key id="key_newMessage" key="&newMessageCmd.key;" + modifiers="accel,shift" command="cmd_newMessage"/> +#else + <key id="key_newMessage" key="&newMessageCmd.key;" + modifiers="accel" command="cmd_newMessage"/> +#endif + </keyset> + + <!-- navigator specific UI items --> + <menupopup id="menu_NewPopup"> + <menuitem id="menu_newCard" + label="&newContactCmd.label;" + accesskey="&newContactCmd.accesskey;" + command="cmd_newCard" + insertafter="navBeginGlobalNewItems"/> + <menuitem id="menu_newMessage" + label="&newMessageCmd.label;" + accesskey="&newMessageCmd.accesskey;" + command="cmd_newMessage" + key="key_newMessage" + insertafter="navBeginGlobalNewItems"/> + </menupopup> + + <menupopup id="menu_FilePopup"> + <menuitem id="menu_sendPage" + label="&sendPage.label;" + accesskey="&sendPage.accesskey;" + command="cmd_sendPage" + position="9"/> + <menuitem id="menu_sendLink" + label="&sendLinkCmd.label;" + accesskey="&sendLinkCmd.accesskey;" + command="Browser:SendLink" + position="10"/> + </menupopup> + + <menupopup id="contentAreaContextMenu"> + <menuitem id="context-sendpage" + label="&contextSendThisPage.label;" + accesskey="&contextSendThisPage.accesskey;" + oncommand="sendPage();" + insertafter="context-savepage"/> + <menuitem id="context-sendimage" + label="&contextSendImage.label;" + accesskey="&contextSendImage.accesskey;" + oncommand="sendMedia(gContextMenu.mediaURL);" + insertafter="context-saveimage"/> + <menuitem id="context-sendvideo" + label="&contextSendVideo.label;" + accesskey="&contextSendVideo.accesskey;" + oncommand="sendMedia(gContextMenu.mediaURL);" + insertafter="context-savevideo"/> + <menuitem id="context-sendaudio" + label="&contextSendAudio.label;" + accesskey="&contextSendAudio.accesskey;" + oncommand="sendMedia(gContextMenu.mediaURL);" + insertafter="context-saveaudio"/> + <menuitem id="context-sendlink" + label="&contextSendThisLink.label;" + accesskey="&contextSendThisLink.accesskey;" + oncommand="sendLink(gContextMenu.linkURL);" + insertafter="context-savelink"/> + <menu id="frame"> + <menupopup id="frame_popup"> + <menuitem id="context-sendframe" + insertafter="context-saveframe" + label="&contextSendFrame.label;" + accesskey="&contextSendFrame.accesskey;" + oncommand="sendPage(gContextMenu.target.ownerDocument);"/> + </menupopup> + </menu> + </menupopup> + +</overlay> + diff --git a/comm/suite/browser/metadata.js b/comm/suite/browser/metadata.js new file mode 100644 index 0000000000..604ef54bb5 --- /dev/null +++ b/comm/suite/browser/metadata.js @@ -0,0 +1,526 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const MathMLNS = "http://www.w3.org/1998/Math/MathML"; +const XLinkNS = "http://www.w3.org/1999/xlink"; +const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const XMLNS = "http://www.w3.org/XML/1998/namespace"; +const XHTMLNS = "http://www.w3.org/1999/xhtml"; +var gMetadataBundle; +var gLangBundle; +var gRegionBundle; +var nodeView; +var htmlMode = false; + +var onLink = false; +var onImage = false; +var onInsDel = false; +var onQuote = false; +var onMisc = false; +var onTable = false; +var onTitle = false; +var onLang = false; + +function onLoad() +{ + gMetadataBundle = document.getElementById("bundle_metadata"); + gLangBundle = document.getElementById("bundle_languages"); + gRegionBundle = document.getElementById("bundle_regions"); + + showMetadataFor(window.arguments[0]); + + nodeView = window.arguments[0].ownerDocument.defaultView; +} + +function showMetadataFor(elem) +{ + // skip past non-element nodes + while (elem && elem.nodeType != Node.ELEMENT_NODE) + elem = elem.parentNode; + + if (!elem) { + alert(gMetadataBundle.getString("unableToShowProps")); + window.close(); + } + + if (elem.ownerDocument.getElementsByName && !elem.ownerDocument.namespaceURI) + htmlMode = true; + + // htmllocalname is "" if it's not an html tag, or the name of the tag if it is. + var htmllocalname = ""; + if (isHTMLElement(elem,"")) { + htmllocalname = elem.localName.toLowerCase(); + } + + // We only look for images once + checkForImage(elem, htmllocalname); + + // Walk up the tree, looking for elements of interest. + // Each of them could be at a different level in the tree, so they each + // need their own boolean to tell us to stop looking. + while (elem && elem.nodeType == Node.ELEMENT_NODE) { + htmllocalname = ""; + if (isHTMLElement(elem,"")) { + htmllocalname = elem.localName.toLowerCase(); + } + + if (!onLink) checkForLink(elem, htmllocalname); + if (!onInsDel) checkForInsDel(elem, htmllocalname); + if (!onQuote) checkForQuote(elem, htmllocalname); + if (!onTable) checkForTable(elem, htmllocalname); + if (!onTitle) checkForTitle(elem, htmllocalname); + if (!onLang) checkForLang(elem, htmllocalname); + + elem = elem.parentNode; + } + + // Decide which sections to show + var onMisc = onTable || onTitle || onLang; + if (!onMisc) hideNode("misc-sec"); + if (!onLink) hideNode("link-sec"); + if (!onImage) hideNode("image-sec"); + if (!onInsDel) hideNode("insdel-sec"); + if (!onQuote) hideNode("quote-sec"); + + // Fix the Misc section visibilities + if (onMisc) { + if (!onTable) hideNode("misc-tblsummary"); + if (!onLang) hideNode("misc-lang"); + if (!onTitle) hideNode("misc-title"); + } + + // Get rid of the "No properties" message. This is a backstop - + // it should really never show, as long as nsContextMenu.js's + // checking doesn't get broken. + if (onLink || onImage || onInsDel || onQuote || onMisc) + hideNode("no-properties") +} + +var cacheListener = { + onCacheEntryAvailable: function onCacheEntryAvailable(descriptor) { + if (descriptor) { + var kbSize = descriptor.dataSize / 1024; + kbSize = Math.round(kbSize * 100) / 100; + setInfo("image-filesize", gMetadataBundle.getFormattedString("imageSize", + [formatNumber(kbSize), + formatNumber(descriptor.dataSize)])); + } else { + setInfo("image-filesize", gMetadataBundle.getString("imageSizeUnknown")); + } + }, + onCacheEntryCheck: function onCacheEntryCheck() { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + } +}; + +function checkForImage(elem, htmllocalname) +{ + var img; + var imgType; // "img" = <img> + // "object" = <object> + // "input" = <input type=image> + // "background" = css background (to be added later) + var ismap = false; + + if (htmllocalname === "img") { + img = elem; + imgType = "img"; + + } else if (htmllocalname === "object" && + elem.type.substring(0,6) == "image/" && + elem.data) { + img = elem; + imgType = "object"; + + } else if (htmllocalname === "input" && + elem.type.toUpperCase() == "IMAGE") { + img = elem; + imgType = "input"; + + } else if (htmllocalname === "area" || htmllocalname === "a") { + + // Clicked in image map? + var map = elem; + ismap = true; + setAlt(map); + + while (map && map.nodeType == Node.ELEMENT_NODE && !isHTMLElement(map,"map") ) + map = map.parentNode; + + if (map && map.nodeType == Node.ELEMENT_NODE) { + img = getImageForMap(map); + var imgLocalName = img && img.localName.toLowerCase(); + if (imgLocalName == "img" || imgLocalName == "object") { + imgType = imgLocalName; + } + } + + } + + if (img) { + + var imgURL = imgType == "object" ? img.data : img.src; + setInfo("image-url", imgURL); + + const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + const LoadContextInfo = Services.loadContextInfo; + var loadContextInfo = opener.gPrivate ? LoadContextInfo.private : + LoadContextInfo.default; + const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + Services.cache2 + .getService(Ci.nsICacheStorageService) + .diskCacheStorage(loadContextInfo, false) + .asyncOpenURI(NetUtil.newURI(imgURL), null, + Ci.nsICacheStorage.OPEN_READONLY, cacheListener); + + if ("width" in img && img.width != "") { + setInfo("image-width", gMetadataBundle.getFormattedString("imageWidth", [formatNumber(img.width)])); + setInfo("image-height", gMetadataBundle.getFormattedString("imageHeight", [formatNumber(img.height)])); + } + else { + setInfo("image-width", ""); + setInfo("image-height", ""); + } + + if (imgType == "img") { + setInfo("image-desc", img.longDesc); + } else { + setInfo("image-desc", ""); + } + + onImage = true; + } + + if (!ismap) { + if (imgType == "img" || imgType == "input") { + setAlt(img); + } else { + hideNode("image-alt"); + } + } +} + +function checkForLink(elem, htmllocalname) +{ + if ((htmllocalname === "a" && elem.href != "") || + htmllocalname === "area") { + + setLink(elem.href, elem.getAttribute("type"), + convertLanguageCode(elem.getAttribute("hreflang")), + elem.getAttribute("rel"), elem.getAttribute("rev")); + + var target = elem.target; + + switch (target) { + case "_top": + setInfo("link-target", gMetadataBundle.getString("sameWindowText")); + break; + case "_parent": + setInfo("link-target", gMetadataBundle.getString("parentFrameText")); + break; + case "_blank": + setInfo("link-target", gMetadataBundle.getString("newWindowText")); + break; + case "": + case "_self": + if (elem.ownerDocument.defaultView) { + if (elem.ownerDocument != elem.ownerDocument.defaultView.content.document) + setInfo("link-target", gMetadataBundle.getString("sameFrameText")); + else + setInfo("link-target", gMetadataBundle.getString("sameWindowText")); + } else { + hideNode("link-target"); + } + break; + default: + setInfo("link-target", "\"" + target + "\""); + } + + onLink = true; + } + else if (elem.namespaceURI == MathMLNS && elem.hasAttribute("href")) { + setLink(makeHrefAbsolute(elem.getAttribute("href"), elem)); + + setInfo("link-target", ""); + + onLink = true; + } + else if (elem.getAttributeNS(XLinkNS, "href")) { + setLink(makeHrefAbsolute(elem.getAttributeNS(XLinkNS, "href"), elem)); + + switch (elem.getAttributeNS(XLinkNS,"show")) { + case "embed": + setInfo("link-target", gMetadataBundle.getString("embeddedText")); + break; + case "new": + setInfo("link-target", gMetadataBundle.getString("newWindowText")); + break; + case null: + case "": + case "replace": + if (elem.ownerDocument != elem.ownerDocument.defaultView.content.document) + setInfo("link-target", gMetadataBundle.getString("sameFrameText")); + else + setInfo("link-target", gMetadataBundle.getString("sameWindowText")); + break; + default: + setInfo("link-target", ""); + break; + } + + onLink = true; + } +} + +function checkForInsDel(elem, htmllocalname) +{ + if ((htmllocalname === "ins" || htmllocalname === "del") && + (elem.cite || elem.dateTime)) { + setInfo("insdel-cite", elem.cite); + setInfo("insdel-date", elem.dateTime); + onInsDel = true; + } +} + + +function checkForQuote(elem, htmllocalname) +{ + if ((htmllocalname === "q" || htmllocalname === "blockquote") && elem.cite) { + setInfo("quote-cite", elem.cite); + onQuote = true; + } +} + +function checkForTable(elem, htmllocalname) +{ + if (htmllocalname === "table" && elem.summary) { + setInfo("misc-tblsummary", elem.summary); + onTable = true; + } +} + +function checkForLang(elem, htmllocalname) +{ + if ((htmllocalname && elem.lang) || elem.getAttributeNS(XMLNS, "lang")) { + var abbr; + if (htmllocalname && elem.lang) + abbr = elem.lang; + else + abbr = elem.getAttributeNS(XMLNS, "lang"); + + setInfo("misc-lang", convertLanguageCode(abbr)); + onLang = true; + } +} + +function checkForTitle(elem, htmllocalname) +{ + if (htmllocalname && elem.title) { + setInfo("misc-title", elem.title); + onTitle = true; + } +} + +/* + * Set five link properties at once. + * All parameters are optional. + */ +function setLink(url, lang, type, rel, rev) +{ + setInfo("link-url", url); + setInfo("link-type", type); + setInfo("link-lang", lang); + setInfo("link-rel", rel); + setInfo("link-rev", rev); +} + +/* + * Set text of node id to value + * if value="" the node with specified id is hidden. + * Node should be have one of these forms + * <xul:label id="id-text" value=""/> + * <xul:description id="id-text"/> + */ +function setInfo(id, value) +{ + if (!value) { + hideNode(id); + return; + } + + var node = document.getElementById(id+"-text"); + + if (node.namespaceURI == XULNS && node.localName == "label" || + (node.namespaceURI == XULNS && node.localName == "textbox")) { + node.setAttribute("value",value); + + } else if (node.namespaceURI == XULNS && node.localName == "description") { + node.textContent = value; + } +} + +// Hide node with specified id +function hideNode(id) +{ + var style = document.getElementById(id).getAttribute("style"); + document.getElementById(id).setAttribute("style", "display:none;" + style); +} + +/* + * Find <img> or <object> which uses an imagemap. + * If more then one object is found we can't determine which one + * was clicked. + * + * This code has to be changed once bug 1882 is fixed. + * Once bug 72527 is fixed this code should use the .images collection. + */ +function getImageForMap(map) +{ + var mapuri = "#" + map.getAttribute("name"); + var multipleFound = false; + var img; + + var list = getHTMLElements(map.ownerDocument, "img"); + for (var i=0; i < list.length; i++) { + if (list.item(i).getAttribute("usemap") == mapuri) { + if (img) { + multipleFound = true; + break; + } else { + img = list.item(i); + imgType = "img"; + } + } + } + + list = getHTMLElements(map.ownerDocument, "object"); + for (i = 0; i < list.length; i++) { + if (list.item(i).getAttribute("usemap") == mapuri) { + if (img) { + multipleFound = true; + break; + } else { + img = list.item(i); + imgType = "object"; + } + } + } + + if (multipleFound) + img = null; + + return img; +} + +function getHTMLElements(node, name) +{ + if (htmlMode) + return node.getElementsByTagName(name); + return node.getElementsByTagNameNS(XHTMLNS, name); +} + +// name should be in lower case +function isHTMLElement(node, name) +{ + if (node.nodeType != Node.ELEMENT_NODE) + return false; + + if (htmlMode) + return !name || node.localName.toLowerCase() == name; + + return (!name || node.localName == name) && node.namespaceURI == XHTMLNS; +} + +// This function coded according to the spec at: +// http://www.bath.ac.uk/~py8ieh/internet/discussion/metadata.txt +function convertLanguageCode(abbr) +{ + if (!abbr) return ""; + var result; + var region = ""; + var tokens = abbr.split("-"); + var language = tokens.shift(); + + if (language == "x" || language == "i") + { + // x and i prefixes mean unofficial ones. So we proper-case the next + // word and leave the rest. + if (tokens.length > 0) + { + // Upper-case first letter + language = tokens[0].substr(0, 1).toUpperCase() + tokens[0].substr(1); + tokens.shift(); + + // Add on the rest as space-separated strings inside the brackets + region = tokens.join(" "); + } + } + else + { + // Otherwise we treat the first as a lang, the second as a region + // and the rest as strings. + try + { + language = gLangBundle.getString(language.toLowerCase()); + } + catch (e) + { + } + + if (tokens.length > 0) + { + try + { + tokens[0] = gRegionBundle.getString(tokens[0].toLowerCase()); + } + catch (e) + { + } + region = tokens.join(" "); + } + } + + if (region) { + result = gMetadataBundle.getFormattedString("languageRegionFormat", + [language, region]); + } else { + result = language; + } + return result; +} + +function setAlt(elem) { + var altText = document.getElementById("image-alt-text"); + if (elem.hasAttribute("alt")) { + if (elem.alt != "") { + altText.value = elem.alt; + altText.setAttribute("style","font-style:inherit"); + } else { + altText.value = gMetadataBundle.getString("altTextBlank"); + altText.setAttribute("style","font-style:italic"); + } + } else { + altText.value = gMetadataBundle.getString("altTextMissing"); + altText.setAttribute("style","font-style:italic"); + } + +} + +function formatNumber(number) +{ + return (+number).toLocaleString(); // coerce number to a numeric value before calling toLocaleString() +} + +function makeHrefAbsolute(href, elem) +{ + const {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + try { + var baseURI = NetUtil.newURI(elem.baseURI, elem.ownerDocument.characterSet); + href = NetUtil.newURI(href, elem.ownerDocument.characterSet, baseURI).spec; + } catch (e) { + } + return href; +} diff --git a/comm/suite/browser/metadata.xul b/comm/suite/browser/metadata.xul new file mode 100644 index 0000000000..43d0f7b00b --- /dev/null +++ b/comm/suite/browser/metadata.xul @@ -0,0 +1,192 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://navigator/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://navigator/skin/pageInfo.css" type="text/css"?> + +<!DOCTYPE dialog [ + <!ENTITY % metadataDTD SYSTEM "chrome://navigator/locale/metadata.dtd" > + %metadataDTD; +]> + +<dialog id="metadata" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&caption.label;" + onload="onLoad()" + minwidth="350" + buttons="none" + persist="screenX screenY" + screenX="24" screenY="24"> + + <script src="chrome://navigator/content/metadata.js"/> + + <stringbundle src="chrome://navigator/locale/metadata.properties" id="bundle_metadata"/> + <stringbundle src="chrome://global/locale/languageNames.properties" id="bundle_languages"/> + <stringbundle src="chrome://global/locale/regionNames.properties" id="bundle_regions"/> + + <label id="no-properties" value="&no-properties.label;"/> + + <vbox id="link-sec"> + <label value="&link-sec.label;"/> + <separator class="groove"/> + <grid> + <columns> + <column/> + <column/> + <column flex="1"/> + </columns> + <rows> + <row id="link-url"> + <separator orient="vertical"/> + <label value="&link-url.label; " control="link-url-text"/> + <textbox readonly="true" id="link-url-text" class="meta-properties"/> + </row> + <row id="link-target"> + <separator orient="vertical"/> + <label value="&link-target.label; " control="link-target-text"/> + <textbox readonly="true" id="link-target-text" class="meta-properties"/> + </row> + <row id="link-type"> + <separator orient="vertical"/> + <label value="&link-type.label; " control="link-type-text"/> + <textbox readonly="true" id="link-type-text" class="meta-properties"/> + </row> + <row id="link-lang"> + <separator orient="vertical"/> + <label value="&link-lang.label; " control="link-lang-text"/> + <textbox readonly="true" id="link-lang-text" class="meta-properties"/> + </row> + <row id="link-rel"> + <separator orient="vertical"/> + <label value="&link-rel.label; " control="link-rel-text"/> + <textbox readonly="true" id="link-rel-text" class="meta-properties"/> + </row> + <row id="link-rev"> + <separator orient="vertical"/> + <label value="&link-rev.label; " control="link-rev-text"/> + <textbox readonly="true" id="link-rev-text" class="meta-properties"/> + </row> + </rows> + </grid> + <separator/> + </vbox> + <vbox id="image-sec"> + <label value="&image-sec.label;"/> + <separator class="groove"/> + <grid> + <columns> + <column/> + <column/> + <column flex="1"/> + </columns> + <rows> + <row id="image-url"> + <separator orient="vertical"/> + <label value="&image-url.label; " control="image-url-text"/> + <textbox readonly="true" id="image-url-text" class="meta-properties"/> + </row> + <row id="image-width"> + <separator orient="vertical"/> + <label value="&image-width.label; " control="image-width-text"/> + <textbox readonly="true" id="image-width-text" class="meta-properties"/> + </row> + <row id="image-height"> + <separator orient="vertical"/> + <label value="&image-height.label; " control="image-height-text"/> + <textbox readonly="true" id="image-height-text" class="meta-properties"/> + </row> + <row id="image-filesize"> + <separator orient="vertical"/> + <label value="&image-filesize.label; " control="image-filesize-text"/> + <textbox readonly="true" id="image-filesize-text" value="&image-filesize.value;" class="meta-properties"/> + </row> + <row id="image-alt"> + <separator orient="vertical"/> + <label value="&image-alt.label; " control="image-alt-text"/> + <textbox readonly="true" id="image-alt-text" class="meta-properties"/> + </row> + <row id="image-desc"> + <separator orient="vertical"/> + <label value="&image-desc.label; " control="image-desc-text"/> + <textbox readonly="true" id="image-desc-text" class="meta-properties"/> + </row> + </rows> + </grid> + <separator/> + </vbox> + <vbox id="insdel-sec"> + <label value="&insdel-sec.label;"/> + <separator class="groove"/> + <grid> + <columns> + <column/> + <column/> + <column flex="1"/> + </columns> + <rows> + <row id="insdel-cite"> + <separator orient="vertical"/> + <label value="&insdel-cite.label; " control="insdel-cite-text"/> + <textbox readonly="true" id="insdel-cite-text" class="meta-properties"/> + </row> + <row id="insdel-date"> + <separator orient="vertical"/> + <label value="&insdel-date.label; " control="insdel-date-text"/> + <textbox readonly="true" id="insdel-date-text" class="meta-properties"/> + </row> + </rows> + </grid> + <separator/> + </vbox> + <vbox id="quote-sec"> + <label value=""e-sec.label;"/> + <separator class="groove"/> + <grid> + <columns> + <column/> + <column/> + <column flex="1"/> + </columns> + <rows> + <row id="quote-cite"> + <separator orient="vertical"/> + <label value=""e-cite.label; " control="quote-cite-text"/> + <textbox readonly="true" id="quote-cite-text" class="meta-properties"/> + </row> + </rows> + </grid> + <separator/> + </vbox> + <vbox id="misc-sec"> + <label value="&misc-sec.label;"/> + <separator class="groove"/> + <grid> + <columns> + <column/> + <column/> + <column flex="1"/> + </columns> + <rows> + <row id="misc-lang"> + <separator orient="vertical"/> + <label value="&misc-lang.label; " control="misc-lang-text"/> + <textbox readonly="true" id="misc-lang-text" class="meta-properties"/> + </row> + <row id="misc-title"> + <separator orient="vertical"/> + <label value="&misc-title.label; " control="misc-title-text"/> + <textbox readonly="true" id="misc-title-text" class="meta-properties"/> + </row> + <row id="misc-tblsummary"> + <separator orient="vertical"/> + <label value="&misc-tblsummary.label; " control="misc-tblsummary-text"/> + <textbox readonly="true" id="misc-tblsummary-text" class="meta-properties"/> + </row> + </rows> + </grid> + <separator/> + </vbox> + </dialog> diff --git a/comm/suite/browser/moz.build b/comm/suite/browser/moz.build new file mode 100644 index 0000000000..6740f038ae --- /dev/null +++ b/comm/suite/browser/moz.build @@ -0,0 +1,22 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"] +MOCHITEST_CHROME_MANIFESTS += ["test/chrome/chrome.ini"] +MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"] + +EXTRA_COMPONENTS += [ + "nsBrowserContentHandler.js", + "nsTypeAheadFind.js", + "SuiteBrowser.manifest", +] + +JAR_MANIFESTS += ["jar.mn"] + +for var in ("MOZ_APP_NAME", "MOZ_APP_DISPLAYNAME", "MOZ_APP_VERSION"): + DEFINES[var] = '"%s"' % CONFIG[var] + +if CONFIG["MOZILLA_OFFICIAL"]: + DEFINES["OFFICIAL_BUILD"] = 1 diff --git a/comm/suite/browser/navigator.css b/comm/suite/browser/navigator.css new file mode 100644 index 0000000000..8b9c11ada3 --- /dev/null +++ b/comm/suite/browser/navigator.css @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +@namespace html url("http://www.w3.org/1999/xhtml"); + +/* ::::: Hide the link toolbar if it is set to autohide and has no items. ::::: */ + +#linktoolbar[hidden="maybe"][hasitems="false"] { + display: none; +} + +/* ::::: tabbed browser ::::: */ + +tabbrowser { + -moz-binding: url("chrome://navigator/content/tabbrowser.xml#tabbrowser"); +} + +.tabbrowser-tabs { + -moz-binding: url("chrome://navigator/content/tabbrowser.xml#tabbrowser-tabs"); +} + +.tabbrowser-tab:-moz-lwtheme:not([customization-lwtheme]) { + color: var(--lwt-text-color); + text-shadow: var(--lwt-accent-color); +} + +.tabbrowser-arrowscrollbox { + -moz-binding: url("chrome://navigator/content/tabbrowser.xml#tabbrowser-arrowscrollbox"); +} + +.tabs-alltabs-popup { + -moz-binding: url("chrome://navigator/content/tabbrowser.xml#tabbrowser-alltabs-popup"); +} + +.tabs-closebutton-box > .tabs-closebutton { + -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton"); +} + +/* ::::: search suggestions autocomplete ::::: */ +#PopupAutoComplete > richlistbox > richlistitem > .ac-type-icon, +#PopupAutoComplete > richlistbox > richlistitem > .ac-site-icon, +#PopupAutoComplete > richlistbox > richlistitem > .ac-tags, +#PopupAutoComplete > richlistbox > richlistitem > .ac-separator, +#PopupAutoComplete > richlistbox > richlistitem > .ac-url { + display: none; +} + +/* ::::: urlbar autocomplete ::::: */ + +#urlbar { + -moz-binding: url("chrome://navigator/content/urlbarBindings.xml#urlbar"); +} + +.paste-and-go { + -moz-binding: url("chrome://navigator/content/urlbarBindings.xml#input-box-paste"); +} + +panel[for="urlbar"] { + -moz-binding: url("chrome://navigator/content/urlbarBindings.xml#autocomplete-result-popup") !important; +} + +.autocomplete-search-box { + -moz-binding: url("chrome://navigator/content/urlbarBindings.xml#autocomplete-search-box"); +} + +.autocomplete-search-engine { + -moz-binding: url("chrome://navigator/content/urlbarBindings.xml#autocomplete-search-engine"); + -moz-box-align: center; +} + +#DateTimePickerPanel[active="true"] { + -moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup"); +} + +.popup-anchor { + /* should occupy space but not be visible */ + opacity: 0; + pointer-events: none; + -moz-stack-sizing: ignore; +} + +/* ::::: search bar ::::: */ +searchbar { + -moz-binding: url("chrome://communicator/content/search/search.xml#searchbar"); +} + +#wrapper-search-container > #search-container > #searchbar > .searchbar-textbox > .autocomplete-textbox-container > .textbox-input-box > html|*.textbox-input { + visibility: hidden; +} + +/* ::::: bookmarks menu ::::: */ + +.isempty:not(:last-child) { + display: none; +} + +/* ::::: bookmarks toolbar ::::: */ + +#wrapper-personal-bookmarks[place="palette"] > toolbaritem > #PlacesToolbar { + display: none; +} + +/* notification anchors should only be visible when their associated + notifications are */ +.notification-anchor-icon { + -moz-user-focus: normal; +} + +.notification-anchor-icon:not([showing]) { + display: none; +} + +popupnotification-centeritem { + -moz-binding: url("chrome://communicator/content/bindings/notification.xml#center-item"); +} + +#addon-install-started-notification { + -moz-binding: url("chrome://communicator/content/bindings/notification.xml#addon-progress-popup-notification"); +} + +/* ::::: Wallpaper fix for Bug 435652. Remove when Bug 204743 is fixed ::::: */ + +.textbox-input-box { + overflow-x: hidden; +} + +/* Developer Tools: Error counter */ + +#developer-toolbar-toolbox-button[error-count]:before { + content: attr(error-count); + display: -moz-box; + -moz-box-pack: center; +} diff --git a/comm/suite/browser/navigator.js b/comm/suite/browser/navigator.js new file mode 100644 index 0000000000..2321906fb9 --- /dev/null +++ b/comm/suite/browser/navigator.js @@ -0,0 +1,3311 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +const {AeroPeek} = ChromeUtils.import("resource:///modules/WindowsPreviewPerTab.jsm"); +var {AppConstants} = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + NetUtil: "resource://gre/modules/NetUtil.jsm", + PluralForm: "resource://gre/modules/PluralForm.jsm", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", + PromiseUtils: "resource://gre/modules/PromiseUtils.jsm", + SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm", + SitePermissions: "resource:///modules/SitePermissions.jsm", +}); + +XPCOMUtils.defineLazyScriptGetter(this, "gEditItemOverlay", + "chrome://communicator/content/places/editBookmarkOverlay.js"); + +const REMOTESERVICE_CONTRACTID = "@mozilla.org/toolkit/remote-service;1"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const nsIWebNavigation = Ci.nsIWebNavigation; + +var gPrintSettingsAreGlobal = true; +var gSavePrintSettings = true; +var gChromeState = null; // chrome state before we went into print preview +var gInPrintPreviewMode = false; +var gNavToolbox = null; +var gFindInstData; +var gURLBar = null; +var gProxyButton = null; +var gProxyFavIcon = null; +var gProxyDeck = null; +var gNavigatorBundle; +var gBrandBundle; +var gNavigatorRegionBundle; +var gLastValidURLStr = ""; +var gLastValidURL = null; +var gClickSelectsAll = false; +var gClickAtEndSelects = false; +var gIgnoreFocus = false; +var gIgnoreClick = false; + +// Listeners for updating zoom value in status bar +var ZoomListeners = +{ + + // Identifies the setting in the content prefs database. + name: "browser.content.full-zoom", + + QueryInterface: + XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsIContentPrefObserver, + Ci.nsIContentPrefCallback2, + Ci.nsISupportsWeakReference, + Ci.nsISupports]), + + init: function () + { + Cc["@mozilla.org/content-pref/service;1"] + .getService(Ci.nsIContentPrefService2) + .addObserverForName(this.name, this); + + Services.prefs.addObserver("browser.zoom.", this, true); + this.updateVisibility(); + }, + + destroy: function () + { + Cc["@mozilla.org/content-pref/service;1"] + .getService(Ci.nsIContentPrefService2) + .removeObserverForName(this.name, this); + + Services.prefs.removeObserver("browser.zoom.", this); + }, + + observe: function (aSubject, aTopic, aData) + { + if (aTopic == "nsPref:changed"){ + switch (aData) { + case "browser.zoom.siteSpecific": + updateZoomStatus(); + break; + case "browser.zoom.showZoomStatusPanel": + this.updateVisibility(); + break; + } + } + }, + + updateVisibility: function() + { + let hidden = !Services.prefs.getBoolPref("browser.zoom.showZoomStatusPanel"); + document.getElementById("zoomOut-button").setAttribute('hidden', hidden); + document.getElementById("zoomIn-button").setAttribute('hidden', hidden); + document.getElementById("zoomLevel-display").setAttribute('hidden', hidden); + }, + + onContentPrefSet: function (aGroup, aName, aValue) + { + // Make sure zoom values loaded before updating + window.setTimeout(updateZoomStatus, 0); + }, + + onContentPrefRemoved: function (aGroup, aName) + { + // Make sure zoom values loaded before updating + window.setTimeout(updateZoomStatus, 0); + }, + + handleResult: function(aPref) + { + updateZoomStatus(); + }, + + onLocationChange: function(aURI) + { + updateZoomStatus(); + } +}; + +var gInitialPages = new Set([ + "about:blank", + "about:privatebrowsing", + "about:sessionrestore" +]); + +//cached elements +var gBrowser = null; + +var gTabStripPrefListener = +{ + domain: "browser.tabs.autoHide", + observe: function(subject, topic, prefName) + { + // verify that we're changing the tab browser strip auto hide pref + if (topic != "nsPref:changed") + return; + + if (gBrowser.tabContainer.childNodes.length == 1 && window.toolbar.visible) { + var stripVisibility = !Services.prefs.getBoolPref(prefName); + gBrowser.setStripVisibilityTo(stripVisibility); + Services.prefs.setBoolPref("browser.tabs.forceHide", false); + } + } +}; + +var gHomepagePrefListener = +{ + domain: "browser.startup.homepage", + observe: function(subject, topic, prefName) + { + // verify that we're changing the home page pref + if (topic != "nsPref:changed") + return; + + updateHomeButtonTooltip(); + } +}; + +var gStatusBarPopupIconPrefListener = +{ + domain: "privacy.popups.statusbar_icon_enabled", + observe: function(subject, topic, prefName) + { + if (topic != "nsPref:changed" || prefName != this.domain) + return; + + var popupIcon = document.getElementById("popupIcon"); + if (!Services.prefs.getBoolPref(prefName)) + popupIcon.hidden = true; + + else if (gBrowser.getNotificationBox().popupCount) + popupIcon.hidden = false; + } +}; + +var gFormSubmitObserver = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver, + Ci.nsIObserver]), + + panel: null, + + init: function() + { + this.panel = document.getElementById("invalid-form-popup"); + }, + + notifyInvalidSubmit: function (aFormElement, aInvalidElements) + { + // We are going to handle invalid form submission attempt by focusing the + // first invalid element and show the corresponding validation message in a + // panel attached to the element. + if (!aInvalidElements.length) { + return; + } + + // Don't show the popup if the current tab doesn't contain the invalid form. + if (aFormElement.ownerDocument.defaultView.top != content) { + return; + } + + let element = aInvalidElements.queryElementAt(0, Ci.nsISupports); + + if (!(element instanceof HTMLInputElement || + element instanceof HTMLTextAreaElement || + element instanceof HTMLSelectElement || + element instanceof HTMLButtonElement)) { + return; + } + + this.panel.firstChild.textContent = element.validationMessage; + + element.focus(); + + // If the user interacts with the element and makes it valid or leaves it, + // we want to remove the popup. + // We could check for clicks but a click already removes the popup. + function blurHandler() { + gFormSubmitObserver.panel.hidePopup(); + } + function inputHandler(e) { + if (e.originalTarget.validity.valid) { + gFormSubmitObserver.panel.hidePopup(); + } else { + // If the element is now invalid for a new reason, we should update the + // error message. + if (gFormSubmitObserver.panel.firstChild.textContent != + e.originalTarget.validationMessage) { + gFormSubmitObserver.panel.firstChild.textContent = + e.originalTarget.validationMessage; + } + } + } + element.addEventListener("input", inputHandler); + element.addEventListener("blur", blurHandler); + + // One event to bring them all and in the darkness bind them. + this.panel.addEventListener("popuphiding", function popupHidingHandler(aEvent) { + aEvent.target.removeEventListener("popuphiding", popupHidingHandler); + element.removeEventListener("input", inputHandler); + element.removeEventListener("blur", blurHandler); + }); + + this.panel.hidden = false; + + var win = element.ownerDocument.defaultView; + var style = win.getComputedStyle(element, null); + var scale = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .fullZoom; + + var offset = style.direction == 'rtl' ? parseInt(style.paddingRight) + + parseInt(style.borderRightWidth) : + parseInt(style.paddingLeft) + + parseInt(style.borderLeftWidth); + + offset = Math.round(offset * scale); + this.panel.openPopup(element, "after_start", offset, 0); + } +}; + +/** +* Pref listener handler functions. +* Both functions assume that observer.domain is set to +* the pref domain we want to start/stop listening to. +*/ +function addPrefListener(observer) +{ + try { + Services.prefs.addObserver(observer.domain, observer); + } catch(ex) { + dump("Failed to observe prefs: " + ex + "\n"); + } +} + +function removePrefListener(observer) +{ + try { + Services.prefs.removeObserver(observer.domain, observer); + } catch(ex) { + dump("Failed to remove pref observer: " + ex + "\n"); + } +} + +function addPopupPermListener(observer) +{ + Services.obs.addObserver(observer, "popup-perm-close"); +} + +function removePopupPermListener(observer) +{ + Services.obs.removeObserver(observer, "popup-perm-close"); +} + +function addFormSubmitObserver(observer) +{ + observer.init(); + Services.obs.addObserver(observer, "invalidformsubmit"); +} + +function removeFormSubmitObserver(observer) +{ + Services.obs.removeObserver(observer, "invalidformsubmit"); +} + +/** +* We can avoid adding multiple load event listeners and save some time by adding +* one listener that calls all real handlers. +*/ + +function pageShowEventHandlers(event) +{ + checkForDirectoryListing(); +} + +/** + * Determine whether or not the content area is displaying a page with frames, + * and if so, toggle the display of the 'save frame as' menu item. + **/ +function getContentAreaFrameCount() +{ + var saveFrameItem = document.getElementById("saveframe"); + if (!content || !content.frames.length || !isContentFrame(document.commandDispatcher.focusedWindow)) + saveFrameItem.setAttribute("hidden", "true"); + else { + var autoDownload = Services.prefs.getBoolPref("browser.download.useDownloadDir"); + goSetMenuValue("saveframe", autoDownload ? "valueSave" : "valueSaveAs"); + saveFrameItem.removeAttribute("hidden"); + } +} + +function saveFrameDocument() +{ + var focusedWindow = document.commandDispatcher.focusedWindow; + if (isContentFrame(focusedWindow)) + saveDocument(focusedWindow.document, true); +} + +function updateHomeButtonTooltip() +{ + var homePage = getHomePage(); + var tooltip = document.getElementById("home-button-tooltip-inner"); + + while (tooltip.hasChildNodes()) + tooltip.lastChild.remove(); + + for (var i in homePage) { + var label = document.createElementNS(XUL_NS, "label"); + label.setAttribute("value", homePage[i]); + tooltip.appendChild(label); + } +} +function getWebNavigation() +{ + try { + return getBrowser().webNavigation; + } catch (e) { + return null; + } +} + +function BrowserReloadWithFlags(reloadFlags) +{ + // Reset temporary permissions on the current tab. This is done here + // because we only want to reset permissions on user reload. + SitePermissions.clearTemporaryPermissions(gBrowser.selectedBrowser); + + // First, we'll try to use the session history object to reload so + // that framesets are handled properly. If we're in a special + // window (such as view-source) that has no session history, fall + // back on using the web navigation's reload method. + let webNav = getWebNavigation(); + try { + let sh = webNav.sessionHistory; + if (sh) + webNav = sh.QueryInterface(Components.interfaces.nsIWebNavigation); + } catch (e) { + } + + try { + webNav.reload(reloadFlags); + } catch (e) { + } +} + +function toggleAffectedChrome(aHide) +{ + // chrome to toggle includes: + // (*) menubar + // (*) navigation bar + // (*) personal toolbar + // (*) tab browser ``strip'' + // (*) sidebar + // (*) statusbar + // (*) findbar + + if (!gChromeState) + gChromeState = new Object; + + var statusbar = document.getElementById("status-bar"); + getNavToolbox().hidden = aHide; + var notificationBox = gBrowser.getNotificationBox(); + var findbar = document.getElementById("FindToolbar") + + // sidebar states map as follows: + // hidden => hide/show nothing + // collapsed => hide/show only the splitter + // shown => hide/show the splitter and the box + if (aHide) + { + // going into print preview mode + gChromeState.sidebar = SidebarGetState(); + SidebarSetState("hidden"); + + // deal with tab browser + gBrowser.mStrip.setAttribute("moz-collapsed", "true"); + + // deal with the Status Bar + gChromeState.statusbarWasHidden = statusbar.hidden; + statusbar.hidden = true; + + // deal with the notification box + gChromeState.notificationsWereHidden = notificationBox.notificationsHidden; + notificationBox.notificationsHidden = true; + + if (findbar) + { + gChromeState.findbarWasHidden = findbar.hidden; + findbar.close(); + } + else + { + gChromeState.findbarWasHidden = true; + } + + gChromeState.syncNotificationsOpen = false; + var syncNotifications = document.getElementById("sync-notifications"); + if (syncNotifications) + { + gChromeState.syncNotificationsOpen = !syncNotifications.notificationsHidden; + syncNotifications.notificationsHidden = true; + } + } + else + { + // restoring normal mode (i.e., leaving print preview mode) + SidebarSetState(gChromeState.sidebar); + + // restore tab browser + gBrowser.mStrip.removeAttribute("moz-collapsed"); + + // restore the Status Bar + statusbar.hidden = gChromeState.statusbarWasHidden; + + // restore the notification box + notificationBox.notificationsHidden = gChromeState.notificationsWereHidden; + + if (!gChromeState.findbarWasHidden) + findbar.open(); + + if (gChromeState.syncNotificationsOpen) + document.getElementById("sync-notifications").notificationsHidden = false; + } + + // if we are unhiding and sidebar used to be there rebuild it + if (!aHide && gChromeState.sidebar == "visible") + SidebarRebuild(); +} + +var PrintPreviewListener = { + _printPreviewTab: null, + _tabBeforePrintPreview: null, + + getPrintPreviewBrowser: function () { + if (!this._printPreviewTab) { + this._tabBeforePrintPreview = getBrowser().selectedTab; + this._printPreviewTab = getBrowser().addTab("about:blank"); + getBrowser().selectedTab = this._printPreviewTab; + } + return getBrowser().getBrowserForTab(this._printPreviewTab); + }, + getSourceBrowser: function () { + return this._tabBeforePrintPreview ? + getBrowser().getBrowserForTab(this._tabBeforePrintPreview) : + getBrowser().selectedBrowser; + }, + getNavToolbox: function () { + return window.getNavToolbox(); + }, + onEnter: function () { + gInPrintPreviewMode = true; + toggleAffectedChrome(true); + }, + onExit: function () { + getBrowser().selectedTab = this._tabBeforePrintPreview; + this._tabBeforePrintPreview = null; + gInPrintPreviewMode = false; + toggleAffectedChrome(false); + getBrowser().removeTab(this._printPreviewTab, { disableUndo: true }); + this._printPreviewTab = null; + } +}; + +function getNavToolbox() +{ + if (!gNavToolbox) + gNavToolbox = document.getElementById("navigator-toolbox"); + return gNavToolbox; +} + +function BrowserPrintPreview() +{ + PrintUtils.printPreview(PrintPreviewListener); +} + +function BrowserSetCharacterSet(aEvent) +{ + if (aEvent.target.hasAttribute("charset")) + getBrowser().docShell.charset = aEvent.target.getAttribute("charset"); + BrowserCharsetReload(); +} + +function BrowserCharsetReload() +{ + BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); +} + +function BrowserUpdateCharsetMenu(aNode) +{ + var wnd = document.commandDispatcher.focusedWindow; + if (wnd.top != content) + wnd = content; + UpdateCharsetMenu(wnd.document.characterSet, aNode); +} + +function EnableCharsetMenu() +{ + var menuitem = document.getElementById("charsetMenu"); + if (getBrowser() && getBrowser().docShell && + getBrowser().docShell.mayEnableCharacterEncodingMenu) + menuitem.removeAttribute("disabled"); + else + menuitem.setAttribute("disabled", "true"); +} + +function getFindInstData() +{ + if (!gFindInstData) { + gFindInstData = new nsFindInstData(); + gFindInstData.browser = getBrowser(); + // defaults for rootSearchWindow and currentSearchWindow are fine here + } + return gFindInstData; +} + +function BrowserFind() +{ + findInPage(getFindInstData()); +} + +function BrowserFindAgain(reverse) +{ + findAgainInPage(getFindInstData(), reverse); +} + +function BrowserCanFindAgain() +{ + return canFindAgainInPage(); +} + +function getMarkupDocumentViewer() +{ + return getBrowser().markupDocumentViewer; +} + +function getBrowser() +{ + if (!gBrowser) + gBrowser = document.getElementById("content"); + return gBrowser; +} + +function getHomePage() +{ + var URIs = []; + URIs[0] = GetLocalizedStringPref("browser.startup.homepage"); + var count = Services.prefs.getIntPref("browser.startup.homepage.count"); + for (var i = 1; i < count; ++i) + URIs[i] = GetLocalizedStringPref("browser.startup.homepage." + i); + + return URIs; +} + +function UpdateBackForwardButtons() +{ + var backBroadcaster = document.getElementById("canGoBack"); + var forwardBroadcaster = document.getElementById("canGoForward"); + var upBroadcaster = document.getElementById("canGoUp"); + var browser = getBrowser(); + + // Avoid setting attributes on broadcasters if the value hasn't changed! + // Remember, guys, setting attributes on elements is expensive! They + // get inherited into anonymous content, broadcast to other widgets, etc.! + // Don't do it if the value hasn't changed! - dwh + + var backDisabled = backBroadcaster.hasAttribute("disabled"); + var forwardDisabled = forwardBroadcaster.hasAttribute("disabled"); + var upDisabled = upBroadcaster.hasAttribute("disabled"); + if (backDisabled == browser.canGoBack) { + if (backDisabled) + backBroadcaster.removeAttribute("disabled"); + else + backBroadcaster.setAttribute("disabled", true); + } + if (forwardDisabled == browser.canGoForward) { + if (forwardDisabled) + forwardBroadcaster.removeAttribute("disabled"); + else + forwardBroadcaster.setAttribute("disabled", true); + } + if (upDisabled != !browser.currentURI.spec.replace(/[#?].*$/, "").match(/\/[^\/]+\/./)) { + if (upDisabled) + upBroadcaster.removeAttribute("disabled"); + else + upBroadcaster.setAttribute("disabled", true); + } +} + +const nsIBrowserDOMWindow = Ci.nsIBrowserDOMWindow; +const nsIInterfaceRequestor = Ci.nsIInterfaceRequestor; + +function nsBrowserAccess() { +} + +nsBrowserAccess.prototype = { + createContentWindow(aURI, aOpener, aWhere, aFlags, aTriggeringPrincipal = null) { + return this.getContentWindowOrOpenURI(null, aOpener, aWhere, aFlags, + aTriggeringPrincipal); + }, + + openURI: function (aURI, aOpener, aWhere, aFlags, aTriggeringPrincipal = null) { + if (!aURI) { + Cu.reportError("openURI should only be called with a valid URI"); + throw Cr.NS_ERROR_FAILURE; + } + return this.getContentWindowOrOpenURI(aURI, aOpener, aWhere, aFlags, + aTriggeringPrincipal); + }, + + getContentWindowOrOpenURI(aURI, aOpener, aWhere, aFlags, aTriggeringPrincipal) { + var isExternal = !!(aFlags & nsIBrowserDOMWindow.OPEN_EXTERNAL); + + if (aOpener && isExternal) { + Cu.reportError("nsBrowserAccess.openURI did not expect an opener to be " + + "passed if the context is OPEN_EXTERNAL."); + throw Cr.NS_ERROR_FAILURE; + } + + if (aWhere == nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) { + if (isExternal) + aWhere = Services.prefs.getIntPref("browser.link.open_external"); + else + aWhere = Services.prefs.getIntPref("browser.link.open_newwindow"); + } + + let referrer = aOpener ? aOpener.QueryInterface(nsIInterfaceRequestor) + .getInterface(nsIWebNavigation) + .currentURI : null; + let referrerPolicy = Ci.nsIHttpChannel.REFERRER_POLICY_UNSET; + if (aOpener && aOpener.document) { + referrerPolicy = aOpener.document.referrerPolicy; + } + var uri = aURI ? aURI.spec : "about:blank"; + + switch (aWhere) { + case nsIBrowserDOMWindow.OPEN_NEWWINDOW: + return window.openDialog(getBrowserURL(), "_blank", "all,dialog=no", + uri, null, null); + case nsIBrowserDOMWindow.OPEN_NEWTAB: + var bgLoad = Services.prefs.getBoolPref("browser.tabs.loadDivertedInBackground"); + var isRelated = referrer ? true : false; + // If we have an opener, that means that the caller is expecting access + // to the nsIDOMWindow of the opened tab right away. + let userContextId = aOpener && aOpener.document + ? aOpener.document.nodePrincipal.originAttributes.userContextId + : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID; + let openerWindow = (aFlags & nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener; + + var newTab = gBrowser.loadOneTab(uri, {triggeringPrincipal: aTriggeringPrincipal, + referrerURI: referrer, + referrerPolicy, + inBackground: bgLoad, + fromExternal: isExternal, + relatedToCurrent: isRelated, + userContextId: userContextId, + opener: openerWindow, + }); + var contentWin = gBrowser.getBrowserForTab(newTab).contentWindow; + if (!bgLoad) + contentWin.focus(); + return contentWin; + default: + var loadflags = isExternal ? + nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL : + nsIWebNavigation.LOAD_FLAGS_NONE; + + if (!aOpener) { + if (aURI) { + gBrowser.loadURIWithFlags(aURI.spec, { + flags: loadflags, + referrerURI: referrer, + referrerPolicy, + triggeringPrincipal: aTriggeringPrincipal, + }); + } + return content; + } + aOpener = aOpener.top; + if (aURI) { + try { + aOpener.QueryInterface(nsIInterfaceRequestor) + .getInterface(nsIWebNavigation) + .loadURI(uri, loadflags, referrer, null, null, + aTriggeringPrincipal); + } catch (e) {} + } + return aOpener; + } + }, + isTabContentWindow: function isTabContentWindow(aWindow) { + return gBrowser.browsers.some(browser => browser.contentWindow == aWindow); + } +} + +function HandleAppCommandEvent(aEvent) +{ + aEvent.stopPropagation(); + switch (aEvent.command) { + case "Back": + BrowserBack(); + break; + case "Forward": + BrowserForward(); + break; + case "Reload": + BrowserReloadSkipCache(); + break; + case "Stop": + BrowserStop(); + break; + case "Search": + BrowserSearch.webSearch(); + break; + case "Bookmarks": + toBookmarksManager(); + break; + case "Home": + BrowserHome(null); + break; + default: + break; + } +} + +/* window.arguments[0]: URL(s) to load + (string, with one or more URLs separated by \n) + * [1]: character set (string) + * [2]: referrer (nsIURI) + * [3]: postData (nsIInputStream) + * [4]: allowThirdPartyFixup (bool) + */ +function Startup() +{ + // init globals + gNavigatorBundle = document.getElementById("bundle_navigator"); + gBrandBundle = document.getElementById("bundle_brand"); + gNavigatorRegionBundle = document.getElementById("bundle_navigator_region"); + + gBrowser = document.getElementById("content"); + gURLBar = document.getElementById("urlbar"); + + SetPageProxyState("invalid", null); + + var webNavigation; + try { + webNavigation = getWebNavigation(); + if (!webNavigation) + throw "no XBL binding for browser"; + } catch (e) { + alert("Error launching browser window:" + e); + window.close(); // Give up. + return; + } + + // Do all UI building here: + UpdateNavBar(); + updateWindowState(); + + // set home button tooltip text + updateHomeButtonTooltip(); + + var lc = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); + if (lc.usePrivateBrowsing) { + gPrivate = window; + let docElement = document.documentElement; + var titlemodifier = docElement.getAttribute("titlemodifier"); + if (titlemodifier) + titlemodifier += " "; + titlemodifier += docElement.getAttribute("titleprivate"); + docElement.setAttribute("titlemodifier", titlemodifier); + document.title = titlemodifier; + docElement.setAttribute("privatebrowsingmode", + PrivateBrowsingUtils.permanentPrivateBrowsing ? "permanent" : "temporary"); + } + + // initialize observers and listeners + var xw = lc.QueryInterface(Ci.nsIDocShellTreeItem) + .treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow); + xw.XULBrowserWindow = window.XULBrowserWindow = new nsBrowserStatusHandler(); + + if (!window.content.opener && + Services.prefs.getBoolPref("browser.doorhanger.enabled")) { + var tmp = {}; + ChromeUtils.import("resource://gre/modules/PopupNotifications.jsm", tmp); + window.PopupNotifications = new tmp.PopupNotifications( + getBrowser(), + document.getElementById("notification-popup"), + document.getElementById("notification-popup-box")); + // Setting the popup notification attribute causes the XBL to bind + // and call the constructor again, so we have to destroy it first. + gBrowser.getNotificationBox().destroy(); + gBrowser.setAttribute("popupnotification", "true"); + // The rebind also resets popup window scrollbar visibility, so override it. + if (!(xw.chromeFlags & Ci.nsIWebBrowserChrome.CHROME_SCROLLBARS)) + gBrowser.selectedBrowser.style.overflow = "hidden"; + } + + addPrefListener(gTabStripPrefListener); + addPrefListener(gHomepagePrefListener); + addPrefListener(gStatusBarPopupIconPrefListener); + addFormSubmitObserver(gFormSubmitObserver); + + window.browserContentListener = + new nsBrowserContentListener(window, getBrowser()); + + // Add a capturing event listener to the content area + // (rjc note: not the entire window, otherwise we'll get sidebar pane loads too!) + // so we'll be notified when onloads complete. + var contentArea = document.getElementById("appcontent"); + contentArea.addEventListener("pageshow", function callPageShowHandlers(aEvent) { + // Filter out events that are not about the document load we are interested in. + if (aEvent.originalTarget == content.document) + setTimeout(pageShowEventHandlers, 0, aEvent); + }, true); + + // Set a sane starting width/height for all resolutions on new profiles. + if (!document.documentElement.hasAttribute("width")) { + var defaultHeight = screen.availHeight; + var defaultWidth= screen.availWidth; + + // Create a narrower window for large or wide-aspect displays, to suggest + // side-by-side page view. + if (screen.availWidth >= 1440) + defaultWidth /= 2; + + // Tweak sizes to be sure we don't grow outside the screen + defaultWidth = defaultWidth - 20; + defaultHeight = defaultHeight - 10; + + // On X, we're not currently able to account for the size of the window + // border. Use 28px as a guess (titlebar + bottom window border) + if (navigator.appVersion.includes("X11")) + defaultHeight -= 28; + + // On small screens, default to maximized state + if (defaultHeight <= 600) + document.documentElement.setAttribute("sizemode", "maximized"); + + document.documentElement.setAttribute("width", defaultWidth); + document.documentElement.setAttribute("height", defaultHeight); + // Make sure we're safe at the left/top edge of screen + document.documentElement.setAttribute("screenX", screen.availLeft); + document.documentElement.setAttribute("screenY", screen.availTop); + } + + // hook up UI through progress listener + getBrowser().addProgressListener(window.XULBrowserWindow); + // setup the search service DOMLinkAdded listener + getBrowser().addEventListener("DOMLinkAdded", BrowserSearch); + // hook up drag'n'drop + getBrowser().droppedLinkHandler = handleDroppedLink; + + var uriToLoad = ""; + + // Check window.arguments[0]. If not null then use it for uriArray + // otherwise the new window is being called when another browser + // window already exists so use the New Window pref for uriArray + if ("arguments" in window && window.arguments.length >= 1) { + var uriArray; + if (window.arguments[0]) { + uriArray = window.arguments[0].toString().split('\n'); // stringify and split + } else { + switch (Services.prefs.getIntPref("browser.windows.loadOnNewWindow", 0)) + { + default: + uriArray = ["about:blank"]; + break; + case 1: + uriArray = getHomePage(); + break; + case 2: + uriArray = [Services.prefs.getStringPref("browser.history.last_page_visited", "")]; + break; + } + } + uriToLoad = uriArray.splice(0, 1)[0]; + + if (uriArray.length > 0) + window.setTimeout(function(arg) { for (var i in arg) gBrowser.addTab(arg[i]); }, 0, uriArray); + } + + if (/^\s*$/.test(uriToLoad)) + uriToLoad = "about:blank"; + + var browser = getBrowser(); + + if (uriToLoad != "about:blank") { + if (!gInitialPages.has(uriToLoad)) { + gURLBar.value = uriToLoad; + browser.userTypedValue = uriToLoad; + } + + if ("arguments" in window && window.arguments.length >= 3) { + // window.arguments[2]: referrer (nsIURI | string) + // [3]: postData (nsIInputStream) + // [4]: allowThirdPartyFixup (bool) + // [5]: referrerPolicy (int) + // [6]: userContextId (int) + // [7]: originPrincipal (nsIPrincipal) + // [8]: triggeringPrincipal (nsIPrincipal) + let referrerURI = window.arguments[2]; + if (typeof(referrerURI) == "string") { + try { + referrerURI = makeURI(referrerURI); + } catch (e) { + referrerURI = null; + } + } + let referrerPolicy = (window.arguments[5] != undefined ? + window.arguments[5] : Ci.nsIHttpChannel.REFERRER_POLICY_UNSET); + + let userContextId = (window.arguments[6] != undefined ? + window.arguments[6] : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID); + loadURI(uriToLoad, referrerURI, window.arguments[3] || null, + window.arguments[4] || false, referrerPolicy, userContextId, + // pass the origin principal (if any) and force its use to create + // an initial about:blank viewer if present: + window.arguments[7], !!window.arguments[7], window.arguments[8]); + } else { + // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3. + // Such callers expect that window.arguments[0] is handled as a single URI. + loadOneOrMoreURIs(uriToLoad, Services.scriptSecurityManager.getSystemPrincipal()); + } + } + + // Focus content area unless we're loading a blank or other initial + // page, or if we weren't passed any arguments. This "breaks" the + // javascript:window.open(); case where we don't get any arguments + // either, but we're loading about:blank, but focusing the content + // area is arguably correct in that case as well since the opener + // is very likely to put some content in the new window, and then + // the focus should be in the content area. + var navBar = document.getElementById("nav-bar"); + if ("arguments" in window && gInitialPages.has(uriToLoad) && + isElementVisible(gURLBar)) + setTimeout(WindowFocusTimerCallback, 0, gURLBar); + else + setTimeout(WindowFocusTimerCallback, 0, content); + + // hook up browser access support + window.browserDOMWindow = new nsBrowserAccess(); + + // hook up remote support + if (!gPrivate && REMOTESERVICE_CONTRACTID in Cc) { + var remoteService = + Cc[REMOTESERVICE_CONTRACTID] + .getService(Ci.nsIRemoteService); + remoteService.registerWindow(window); + } + + // ensure login manager is loaded + Cc["@mozilla.org/login-manager;1"].getService(); + + // called when we go into full screen, even if it is + // initiated by a web page script + addEventListener("fullscreen", onFullScreen, true); + + addEventListener("PopupCountChanged", UpdateStatusBarPopupIcon, true); + + addEventListener("AppCommand", HandleAppCommandEvent, true); + + addEventListener("sizemodechange", updateWindowState, false); + + // does clicking on the urlbar select its contents? + gClickSelectsAll = Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"); + gClickAtEndSelects = Services.prefs.getBoolPref("browser.urlbar.clickAtEndSelects"); + + // BiDi UI + gShowBiDi = isBidiEnabled(); + if (gShowBiDi) { + document.getElementById("documentDirection-swap").hidden = false; + document.getElementById("textfieldDirection-separator").hidden = false; + document.getElementById("textfieldDirection-swap").hidden = false; + } + + // Before and after callbacks for the customizeToolbar code + getNavToolbox().customizeInit = BrowserToolboxCustomizeInit; + getNavToolbox().customizeDone = BrowserToolboxCustomizeDone; + getNavToolbox().customizeChange = BrowserToolboxCustomizeChange; + + PlacesToolbarHelper.init(); + + gBrowser.mPanelContainer.addEventListener("InstallBrowserTheme", LightWeightThemeWebInstaller, false, true); + gBrowser.mPanelContainer.addEventListener("PreviewBrowserTheme", LightWeightThemeWebInstaller, false, true); + gBrowser.mPanelContainer.addEventListener("ResetBrowserThemePreview", LightWeightThemeWebInstaller, false, true); + + AeroPeek.onOpenWindow(window); + + if (!gPrivate) { + // initialize the sync UI + // gSyncUI.init(); + + // initialize the session-restore service + setTimeout(InitSessionStoreCallback, 0); + } + + ZoomListeners.init(); + gBrowser.addTabsProgressListener(ZoomListeners); + + window.addEventListener("MozAfterPaint", DelayedStartup); +} + +// Minimal gBrowserInit shim to keep the Addon-SDK happy. +var gBrowserInit = { + delayedStartupFinished: false, +} + +function DelayedStartup() { + window.removeEventListener("MozAfterPaint", DelayedStartup); + + // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008. + setTimeout(function() { SafeBrowsing.init(); }, 2000); + + gBrowserInit.delayedStartupFinished = true; + Services.obs.notifyObservers(window, "browser-delayed-startup-finished"); +} + +function UpdateNavBar() +{ + var elements = getNavToolbox().getElementsByClassName("nav-bar-class"); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.classList.remove("nav-bar-last"); + element.classList.remove("nav-bar-first"); + var next = element.nextSibling; + if (!next || !next.classList.contains("nav-bar-class")) + element.classList.add("nav-bar-last"); + var previous = element.previousSibling; + if (!previous || !previous.classList.contains("nav-bar-class")) + element.classList.add("nav-bar-first"); + } + UpdateUrlbarSearchSplitterState(); +} + +function UpdateUrlbarSearchSplitterState() +{ + var splitter = document.getElementById("urlbar-search-splitter"); + var urlbar = document.getElementById("nav-bar-inner"); + var searchbar = document.getElementById("search-container"); + + var ibefore = null; + if (isElementVisible(urlbar) && isElementVisible(searchbar)) { + if (searchbar.matches("#nav-bar-inner ~ #search-container")) + ibefore = searchbar; + else if (urlbar.matches("#search-container ~ #nav-bar-inner")) + ibefore = searchbar.nextSibling; + } + + if (ibefore) { + splitter = document.createElement("splitter"); + splitter.id = "urlbar-search-splitter"; + splitter.setAttribute("resizebefore", "flex"); + splitter.setAttribute("resizeafter", "flex"); + splitter.setAttribute("skipintoolbarset", "true"); + splitter.setAttribute("overflows", "false"); + splitter.classList.add("chromeclass-toolbar-additional", + "nav-bar-class"); + ibefore.parentNode.insertBefore(splitter, ibefore); + } +} + +function updateWindowState() +{ + getBrowser().showWindowResizer = + window.windowState == window.STATE_NORMAL && + !isElementVisible(document.getElementById("status-bar")); + getBrowser().docShellIsActive = + window.windowState != window.STATE_MINIMIZED; +} + +function InitSessionStoreCallback() +{ + try { + var ss = Cc["@mozilla.org/suite/sessionstore;1"] + .getService(Ci.nsISessionStore); + ss.init(window); + + //Check if we have "Deferred Session Restore" + let restoreItem = document.getElementById("historyRestoreLastSession"); + + if (ss.canRestoreLastSession) + restoreItem.removeAttribute("disabled"); + } catch(ex) { + dump("nsSessionStore could not be initialized: " + ex + "\n"); + } +} + +function WindowFocusTimerCallback(element) +{ + // This function is a redo of the fix for jag bug 91884. + // See Bug 97067 and Bug 89214 for details. + if (window == Services.ww.activeWindow) { + element.focus(); + } else { + // set the element in command dispatcher so focus will restore properly + // when the window does become active + + if (element instanceof Ci.nsIDOMWindow) { + document.commandDispatcher.focusedWindow = element; + document.commandDispatcher.focusedElement = null; + } else if (Element.isInstance(element)) { + document.commandDispatcher.focusedWindow = element.ownerDocument.defaultView; + document.commandDispatcher.focusedElement = element; + } + } +} + +function Shutdown() +{ + AeroPeek.onCloseWindow(window); + + BookmarkingUI.uninit(); + + // shut down browser access support + window.browserDOMWindow = null; + + getBrowser().removeEventListener("DOMLinkAdded", BrowserSearch); + + try { + getBrowser().removeProgressListener(window.XULBrowserWindow); + } catch (ex) { + // Perhaps we didn't get around to adding the progress listener + } + + window.XULBrowserWindow.destroy(); + window.XULBrowserWindow = null; + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow) + .XULBrowserWindow = null; + + // unregister us as a pref listener + removePrefListener(gTabStripPrefListener); + removePrefListener(gHomepagePrefListener); + removePrefListener(gStatusBarPopupIconPrefListener); + removeFormSubmitObserver(gFormSubmitObserver); + + window.browserContentListener.close(); +} + +function Translate() +{ + var service = GetLocalizedStringPref("browser.translation.service"); + var serviceDomain = GetLocalizedStringPref("browser.translation.serviceDomain"); + var targetURI = getWebNavigation().currentURI.spec; + + // if we're already viewing a translated page, then just reload + if (targetURI.includes(serviceDomain)) + BrowserReload(); + else { + loadURI(encodeURI(service) + encodeURIComponent(targetURI)); + } +} + +function GetTypePermFromId(aId) +{ + // Get type and action from splitting id, first is type, second is action. + var [type, action] = aId.split("_"); + var perm = "ACCESS_" + action.toUpperCase(); + return [type, Ci.nsICookiePermission[perm]]; +} + +// Determine current state and check/uncheck the appropriate menu items. +function CheckPermissionsMenu(aType, aNode) +{ + var currentPerm = Services.perms.testPermission(getBrowser().currentURI, aType); + var items = aNode.getElementsByAttribute("name", aType); + for (let item of items) { + // Get type and perm from id. + var [type, perm] = GetTypePermFromId(item.id); + item.setAttribute("checked", perm == currentPerm); + } +} + +// Perform a Cookie, Image or Popup action. +function CookieImagePopupAction(aElement) +{ + var uri = getBrowser().currentURI; + // Get type and perm from id. + var [type, perm] = GetTypePermFromId(aElement.id); + if (Services.perms.testPermission(uri, type) == perm) + return; + + Services.perms.add(uri, type, perm); + + Services.prompt.alert(window, aElement.getAttribute("title"), + aElement.getAttribute("msg")); +} +function OpenSessionHistoryIn(aWhere, aDelta, aTab) +{ + var win = aWhere == "window" ? null : window; + aTab = aTab || getBrowser().selectedTab; + var tab = Cc["@mozilla.org/suite/sessionstore;1"] + .getService(Ci.nsISessionStore) + .duplicateTab(win, aTab, aDelta, true); + + var loadInBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + + switch (aWhere) { + case "tabfocused": + // forces tab to be focused + loadInBackground = true; + // fall through + case "tabshifted": + loadInBackground = !loadInBackground; + // fall through + case "tab": + if (!loadInBackground) { + getBrowser().selectedTab = tab; + window.content.focus(); + } + } +} + +/* Firefox compatibility shim * + * duplicateTabIn duplicates tab in a place specified by the parameter |where|. + * + * |where| can be: + * "tab" new tab + * "tabshifted" same as "tab" but in background if default is to select new + * tabs, and vice versa + * "tabfocused" same as "tab" but override any background preferences and + * focus the new tab + * "window" new window + * + * delta is the offset to the history entry that you want to load. + */ +function duplicateTabIn(aTab, aWhere, aDelta) +{ + OpenSessionHistoryIn(aWhere, aDelta, aTab); +} + +function gotoHistoryIndex(aEvent) +{ + var index = aEvent.target.getAttribute("index"); + if (!index) + return false; + + var where = whereToOpenLink(aEvent); + if (where == "current") { + // Normal click. Go there in the current tab and update session history. + try { + getWebNavigation().gotoIndex(index); + } + catch(ex) { + return false; + } + } + else { + // Modified click. Go there in a new tab/window. Include session history. + var delta = index - getWebNavigation().sessionHistory.index; + OpenSessionHistoryIn(where, delta); + } + return true; +} + +function BrowserBack(aEvent) +{ + var where = whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + getBrowser().goBack(); + } + catch(ex) {} + } + else { + OpenSessionHistoryIn(where, -1); + } +} + +function BrowserHandleBackspace() +{ + switch (Services.prefs.getIntPref("browser.backspace_action")) { + case 0: + BrowserBack(); + break; + case 1: + goDoCommand("cmd_scrollPageUp"); + break; + } +} + +function BrowserForward(aEvent) +{ + var where = whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + getBrowser().goForward(); + } + catch(ex) { + } + } + else { + OpenSessionHistoryIn(where, 1); + } +} + +function BrowserUp() +{ + loadURI(getBrowser().currentURI.spec.replace(/[#?].*$/, "").replace(/\/[^\/]*.$/, "/")); +} + +function BrowserHandleShiftBackspace() +{ + switch (Services.prefs.getIntPref("browser.backspace_action")) { + case 0: + BrowserForward(); + break; + case 1: + goDoCommand("cmd_scrollPageDown"); + break; + } +} + +function BrowserBackMenu(event) +{ + return FillHistoryMenu(event.target, "back"); +} + +function BrowserForwardMenu(event) +{ + return FillHistoryMenu(event.target, "forward"); +} + +function BrowserStop() +{ + try { + const stopFlags = nsIWebNavigation.STOP_ALL; + getWebNavigation().stop(stopFlags); + } + catch(ex) { + } +} + +function BrowserReload(aEvent) +{ + var where = whereToOpenLink(aEvent, false, true); + if (where == "current") + BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_NONE); + else if (where == null && aEvent.shiftKey) + BrowserReloadSkipCache(); + else + OpenSessionHistoryIn(where, 0); +} + +function BrowserReloadSkipCache() +{ + // Bypass proxy and cache. + const reloadFlags = nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + BrowserReloadWithFlags(reloadFlags); +} + +function BrowserHome(aEvent) +{ + var homePage = getHomePage(); + var where = whereToOpenLink(aEvent, false, true); + openUILinkArrayIn(homePage, where); +} + +var BrowserSearch = { + handleEvent: function (event) { // "DOMLinkAdded" event + var link = event.originalTarget; + + var isSearch = /(?:^|\s)search(?:\s|$)/i.test(link.rel) && link.title && + /^(https?|ftp):/i.test(link.href) && + /(?:^|\s)application\/opensearchdescription\+xml(?:;?.*)$/i.test(link.type); + + if (isSearch) { + this.addEngine(link, link.ownerDocument); + } + }, + + addEngine: function(engine, targetDoc) { + if (!this.searchBar) + return; + + var browser = getBrowser().getBrowserForDocument(targetDoc); + // ignore search engines from subframes (see bug 479408) + if (!browser) + return; + + // Check to see whether we've already added an engine with this title + if (browser.engines) { + if (browser.engines.some(e => e.title == engine.title)) + return; + } + + // Append the URI and an appropriate title to the browser data. + // Use documentURIObject in the check so that we do the right + // thing with about:-style error pages. Bug 453442 + var iconURL = null; + var aURI = targetDoc.documentURIObject; + try { + aURI = Services.uriFixup.createExposableURI(aURI); + } catch (e) { + } + + if (aURI && ("schemeIs" in aURI) && + (aURI.schemeIs("http") || aURI.schemeIs("https"))) + iconURL = getBrowser().buildFavIconString(aURI); + + var hidden = false; + // If this engine (identified by title) is already in the list, add it + // to the list of hidden engines rather than to the main list. + // XXX This will need to be changed when engines are identified by URL; + // see bug 335102. + if (Services.search.getEngineByName(engine.title)) + hidden = true; + + var engines = (hidden ? browser.hiddenEngines : browser.engines) || []; + + engines.push({ uri: engine.href, + title: engine.title, + icon: iconURL }); + + if (hidden) + browser.hiddenEngines = engines; + else { + browser.engines = engines; + if (browser == getBrowser().selectedBrowser) + this.updateSearchButton(); + } + }, + + /** + * Update the browser UI to show whether or not additional engines are + * available when a page is loaded or the user switches tabs to a page that + * has search engines. + */ + updateSearchButton: function() { + var searchBar = this.searchBar; + + // The search bar binding might not be applied even though the element is + // in the document (e.g. when the navigation toolbar is hidden), so check + // for .searchButton specifically. + if (!searchBar || !searchBar.searchButton) + return; + + searchBar.updateSearchButton(); + }, + + /** + * Gives focus to the search bar, if it is present on the toolbar, or to the + * search sidebar, if it is open, or loads the default engine's search form + * otherwise. For Mac, opens a new window or focuses an existing window, if + * necessary. + */ + webSearch: function BrowserSearch_webSearch() { + if (!gBrowser) { + var win = getTopWin(); + if (win) { + // If there's an open browser window, it should handle this command + win.focus(); + win.BrowserSearch.webSearch(); + return; + } + + // If there are no open browser windows, open a new one + function webSearchCallback() { + // This needs to be in a timeout so that we don't end up refocused + // in the url bar + setTimeout(BrowserSearch.webSearch, 0); + } + + win = window.openDialog(getBrowserURL(), "_blank", + "chrome,all,dialog=no", "about:blank"); + win.addEventListener("load", webSearchCallback); + return; + } + + if (isElementVisible(this.searchBar)) { + this.searchBar.select(); + this.searchBar.focus(); + } else if (this.searchSidebar) { + this.searchSidebar.focus(); + } else { + loadURI(Services.search.defaultEngine.searchForm); + window.content.focus(); + } + }, + + /** + * Loads a search results page, given a set of search terms. Uses the current + * engine if the search bar is visible, or the default engine otherwise. + * + * @param aSearchText + * The search terms to use for the search. + * + * @param [optional] aNewWindowOrTab + * A boolean if set causes the search to load in a new window or tab + * (depending on "browser.search.openintab"). Otherwise the search + * loads in the current tab. + * + * @param [optional] aEvent + * The event object passed from the caller. + */ + loadSearch: function BrowserSearch_search(aSearchText, aNewWindowOrTab, aEvent) { + var engine; + + // If the search bar is visible, use the current engine, otherwise, fall + // back to the default engine. + if (isElementVisible(this.searchBar) || + this.searchSidebar) + engine = Services.search.currentEngine; + else + engine = Services.search.defaultEngine; + + var submission = engine.getSubmission(aSearchText); // HTML response + + // getSubmission can return null if the engine doesn't have a URL + // with a text/html response type. This is unlikely (since + // SearchService._addEngineToStore() should fail for such an engine), + // but let's be on the safe side. + // If you change the code here, remember to make the corresponding + // changes in suite/mailnews/mailWindowOverlay.js->MsgOpenSearch + if (!submission) + return; + + if (aNewWindowOrTab) { + let newTabPref = Services.prefs.getBoolPref("browser.search.opentabforcontextsearch"); + let where = newTabPref ? aEvent && aEvent.shiftKey ? "tabshifted" : "tab" : "window"; + openUILinkIn(submission.uri.spec, where, null, submission.postData); + if (where == "window") + return; + } else { + loadURI(submission.uri.spec, null, submission.postData, false); + window.content.focus(); + } + }, + + /** + * Returns the search bar element if it is present in the toolbar, null otherwise. + */ + get searchBar() { + return document.getElementById("searchbar"); + }, + + /** + * Returns the search sidebar textbox if the search sidebar is present in + * the sidebar and selected, null otherwise. + */ + get searchSidebar() { + if (sidebarObj.never_built) + return null; + var panel = sidebarObj.panels.get_panel_from_id("urn:sidebar:panel:search"); + return panel && isElementVisible(panel.get_iframe()) && + panel.get_iframe() + .contentDocument.getElementById("sidebar-search-text"); + }, + + loadAddEngines: function BrowserSearch_loadAddEngines() { + loadAddSearchEngines(); // for compatibility + }, + + /** + * Reveal the search sidebar panel. + */ + revealSidebar: function BrowserSearch_revealSidebar() { + // first lets check if the search panel will be shown at all + // by checking the sidebar datasource to see if there is an entry + // for the search panel, and if it is excluded for navigator or not + + var searchPanelExists = false; + + var myPanel = document.getElementById("urn:sidebar:panel:search"); + if (myPanel) { + var panel = sidebarObj.panels.get_panel_from_header_node(myPanel); + searchPanelExists = !panel.is_excluded(); + + } else if (sidebarObj.never_built) { + // XXXsearch: in theory, this should work when the sidebar isn't loaded, + // in practice, it fails as sidebarObj.datasource_uri isn't defined + try { + var datasource = RDF.GetDataSourceBlocking(sidebarObj.datasource_uri); + var aboutValue = RDF.GetResource("urn:sidebar:panel:search"); + + // check if the panel is even in the list by checking for its content + var contentProp = RDF.GetResource("http://home.netscape.com/NC-rdf#content"); + var content = datasource.GetTarget(aboutValue, contentProp, true); + + if (content instanceof Ci.nsIRDFLiteral) { + // the search panel entry exists, now check if it is excluded + // for navigator + var excludeProp = RDF.GetResource("http://home.netscape.com/NC-rdf#exclude"); + var exclude = datasource.GetTarget(aboutValue, excludeProp, true); + + if (exclude instanceof Ci.nsIRDFLiteral) { + searchPanelExists = !exclude.Value.includes("navigator:browser"); + } else { + // panel exists and no exclude set + searchPanelExists = true; + } + } + } catch (e) { + searchPanelExists = false; + } + } + + if (searchPanelExists) { + // make sure the sidebar is open, else SidebarSelectPanel() will fail + if (sidebar_is_hidden()) + SidebarShowHide(); + + if (sidebar_is_collapsed()) + SidebarExpandCollapse(); + + var searchPanel = document.getElementById("urn:sidebar:panel:search"); + if (searchPanel) + SidebarSelectPanel(searchPanel, true, true); // lives in sidebarOverlay.js + } + } +} + +function QualifySearchTerm() +{ + // If the text in the URL bar is the same as the currently loaded + // page's URL then treat this as an empty search term. This way + // the user is taken to the search page where s/he can enter a term. + if (gBrowser.userTypedValue !== null) + return gURLBar.value; + return ""; +} + +function BrowserOpenWindow() +{ + //opens a window where users can select a web location to open + var params = { action: gPrivate ? "4" : "0", url: "" }; + openDialog("chrome://communicator/content/openLocation.xul", "_blank", + "chrome,modal,titlebar", params); + + getShortcutOrURIAndPostData(params.url).then(data => { + switch (params.action) { + case "0": // current window + loadURI(data.url, null, data.postData, true); + break; + case "1": // new window + openDialog(getBrowserURL(), "_blank", "all,dialog=no", data.url, null, null, + data.postData, true); + break; + case "2": // edit + editPage(data.url); + break; + case "3": // new tab + gBrowser.selectedTab = gBrowser.addTab(data.url, + {allowThirdPartyFixup: true, + postData: data.postData}); + break; + case "4": // private + openNewPrivateWith(params.url); + break; + } + }); +} + +function BrowserOpenTab() +{ + if (!gInPrintPreviewMode) { + var uriToLoad; + var tabPref = Services.prefs.getIntPref("browser.tabs.loadOnNewTab", 0); + switch (tabPref) + { + default: + uriToLoad = "about:blank"; + break; + case 1: + uriToLoad = GetLocalizedStringPref("browser.startup.homepage"); + break; + case 2: + uriToLoad = Services.prefs.getStringPref("browser.history.last_page_visited", ""); + break; + } + + if (!gBrowser) { + var win = getTopWin(); + if (win) { + // If there's an open browser window, it should handle this command + win.focus(); + win.BrowserOpenTab(); + return; + } + + // If there are no open browser windows, open a new one + openDialog(getBrowserURL(), "_blank", "chrome,all,dialog=no", uriToLoad); + return; + } + + if (tabPref == 2) + OpenSessionHistoryIn("tabfocused", 0); + else + gBrowser.selectedTab = gBrowser.addTab(uriToLoad); + + if (uriToLoad == "about:blank" && isElementVisible(gURLBar)) + setTimeout(WindowFocusTimerCallback, 0, gURLBar); + else + setTimeout(WindowFocusTimerCallback, 0, content); + } +} + +function BrowserOpenSyncTabs() +{ + switchToTabHavingURI("about:sync-tabs", true); +} +// Class for saving the last directory and filter Index in the prefs. +// Used for open file and upload file. +class RememberLastDir { + + // The pref names are constructed from the prefix parameter in the constructor. + // The pref names should not be changed later. + constructor(prefPrefix) { + this._prefLastDir = prefPrefix + ".lastDir"; + this._prefFilterIndex = prefPrefix + ".filterIndex"; + this._lastDir = null; + this._lastFilterIndex = null; + } + + get path() { + if (!this._lastDir || !this._lastDir.exists()) { + try { + this._lastDir = Services.prefs.getComplexValue(this._prefLastDir, + Ci.nsIFile); + if (!this._lastDir.exists()) { + this._lastDir = null; + } + } catch (e) {} + } + return this._lastDir; + } + + set path(val) { + try { + if (!val || !val.isDirectory()) { + return; + } + } catch (e) { + return; + } + this._lastDir = val.clone(); + + // Don't save the last open directory pref inside the Private Browsing mode + if (!gPrivate) { + Services.prefs.setComplexValue(this._prefLastDir, + Ci.nsIFile, + this._lastDir); + } + } + + get filterIndex() { + if (!this._lastFilterIndex) { + // use a pref to remember the filterIndex selected by the user. + this._lastFilterIndex = + Services.prefs.getIntPref(this._prefFilterIndex, 0); + } + return this._lastFilterIndex; + } + + set filterIndex(val) { + // If the default is picked the filter is null. + this._lastFilterIndex = val ? val : 0; + + // Don't save the last filter index inside the Private Browsing mode + if (!gPrivate) { + Services.prefs.setIntPref(this._prefFilterIndex, + this._lastFilterIndex); + } + } + + // This is currently not used. + reset() { + this._lastDir = null; + this._lastFilterIndex = null; + } +} + +var gLastOpenDirectory; + +function BrowserOpenFileWindow() { + + if (!gLastOpenDirectory) { + gLastOpenDirectory = new RememberLastDir("browser.open"); + }; + + // Get filepicker component. + try { + const nsIFilePicker = Ci.nsIFilePicker; + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK) { + try { + // Set last path and file index only if file is ok. + if (fp.file) { + gLastOpenDirectory.filterIndex = fp.filterIndex; + gLastOpenDirectory.path = + fp.file.parent.QueryInterface(Ci.nsIFile); + } + } catch (ex) { + } + openUILinkIn(fp.fileURL.spec, "current"); + } + }; + + fp.init(window, gNavigatorBundle.getString("openFile"), + nsIFilePicker.modeOpen); + fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText | + nsIFilePicker.filterImages | nsIFilePicker.filterXML | + nsIFilePicker.filterHTML); + fp.filterIndex = gLastOpenDirectory.filterIndex; + fp.displayDirectory = gLastOpenDirectory.path; + fp.open(fpCallback); + } catch (ex) { + } +} + +function updateCloseItems() +{ + var browser = getBrowser(); + + var hideCloseWindow = Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab") && + (!browser || browser.tabContainer.childNodes.length <= 1); + document.getElementById("menu_closeWindow").hidden = hideCloseWindow; + var closeItem = document.getElementById("menu_close"); + if (hideCloseWindow) { + closeItem.setAttribute("label", gNavigatorBundle.getString("tabs.close.label")); + closeItem.setAttribute("accesskey", gNavigatorBundle.getString("tabs.close.accesskey")); + } else { + closeItem.setAttribute("label", gNavigatorBundle.getString("tabs.closeTab.label")); + closeItem.setAttribute("accesskey", gNavigatorBundle.getString("tabs.closeTab.accesskey")); + } + + var hideClose = !browser || !browser.getStripVisibility(); + document.getElementById("menu_closeOtherTabs").hidden = hideClose; + if (!hideClose) + document.getElementById("cmd_closeOtherTabs").disabled = hideCloseWindow; + + hideClose = !browser || + (browser.getTabsToTheEndFrom(browser.mCurrentTab).length == 0); + document.getElementById("menu_closeTabsToTheEnd").hidden = hideClose; + if (!hideClose) + document.getElementById("cmd_closeTabsToTheEnd").disabled = hideClose; +} + +function updateRecentMenuItems() +{ + var browser = getBrowser(); + var ss = Cc["@mozilla.org/suite/sessionstore;1"] + .getService(Ci.nsISessionStore); + + var recentTabsItem = document.getElementById("menu_recentTabs"); + recentTabsItem.setAttribute("disabled", !browser || browser.getUndoList().length == 0); + var recentWindowsItem = document.getElementById("menu_recentWindows"); + recentWindowsItem.setAttribute("disabled", ss.getClosedWindowCount() == 0); +} + +function updateRecentTabs(menupopup) +{ + var browser = getBrowser(); + + while (menupopup.hasChildNodes()) + menupopup.lastChild.remove(); + + var list = browser.getUndoList(); + for (var i = 0; i < list.length; i++) { + var menuitem = document.createElement("menuitem"); + var label = list[i]; + if (i < 9) { + label = gNavigatorBundle.getFormattedString("tabs.recentlyClosed.format", [i + 1, label]); + menuitem.setAttribute("accesskey", i + 1); + } + + if (i == 0) + menuitem.setAttribute("key", "key_restoreTab"); + + menuitem.setAttribute("label", label); + menuitem.setAttribute("value", i); + menupopup.appendChild(menuitem); + } +} + +function updateRecentWindows(menupopup) +{ + var ss = Cc["@mozilla.org/suite/sessionstore;1"] + .getService(Ci.nsISessionStore); + + while (menupopup.hasChildNodes()) + menupopup.lastChild.remove(); + + var undoItems = JSON.parse(ss.getClosedWindowData()); + for (var i = 0; i < undoItems.length; i++) { + var menuitem = document.createElement("menuitem"); + var label = undoItems[i].title; + if (i < 9) { + label = gNavigatorBundle.getFormattedString("windows.recentlyClosed.format", [i + 1, label]); + menuitem.setAttribute("accesskey", i + 1); + } + + if (i == 0) + menuitem.setAttribute("key", "key_restoreWindow"); + + menuitem.setAttribute("label", label); + menuitem.setAttribute("value", i); + menupopup.appendChild(menuitem); + } +} + +function undoCloseWindow(aIndex) +{ + var ss = Cc["@mozilla.org/suite/sessionstore;1"] + .getService(Ci.nsISessionStore); + + return ss.undoCloseWindow(aIndex); +} + +function restoreLastSession() { + let ss = Cc["@mozilla.org/suite/sessionstore;1"] + .getService(Ci.nsISessionStore); + ss.restoreLastSession(); +} + +/* + * Determines if a tab is "empty" using isBrowserEmpty from utilityOverlay.js + */ +function isTabEmpty(aTab) +{ + return isBrowserEmpty(aTab.linkedBrowser); +} + +function BrowserCloseOtherTabs() +{ + var browser = getBrowser(); + browser.removeAllTabsBut(browser.mCurrentTab); +} + +function BrowserCloseTabsToTheEnd() +{ + var browser = getBrowser(); + browser.removeTabsToTheEndFrom(browser.mCurrentTab); +} + +function BrowserCloseTabOrWindow() +{ + var browser = getBrowser(); + if (browser.tabContainer.childNodes.length > 1 || + !Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab")) { + // Just close up a tab. + browser.removeCurrentTab(); + return; + } + + BrowserCloseWindow(); +} + +function BrowserTryToCloseWindow() +{ + if (WindowIsClosing()) + BrowserCloseWindow(); +} + +function BrowserCloseWindow() +{ + // This code replicates stuff in Shutdown(). It is here because + // window.screenX and window.screenY have real values. We need + // to fix this eventually but by replicating the code here, we + // provide a means of saving position (it just requires that the + // user close the window via File->Close (vs. close box). + + // Get the current window position/size. + var x = window.screenX; + var y = window.screenY; + var h = window.outerHeight; + var w = window.outerWidth; + + // Store these into the window attributes (for persistence). + var win = document.getElementById( "main-window" ); + win.setAttribute( "x", x ); + win.setAttribute( "y", y ); + win.setAttribute( "height", h ); + win.setAttribute( "width", w ); + + window.close(); +} + +function loadURI(uri, referrer, postData, allowThirdPartyFixup, referrerPolicy, + userContextId, originPrincipal, + forceAboutBlankViewerInCurrent, triggeringPrincipal) { + try { + openLinkIn(uri, "current", + { referrerURI: referrer, + referrerPolicy: referrerPolicy, + postData: postData, + allowThirdPartyFixup: allowThirdPartyFixup, + userContextId: userContextId, + originPrincipal, + triggeringPrincipal, + forceAboutBlankViewerInCurrent, }); + } catch (e) {} +} + +function loadOneOrMoreURIs(aURIString, aTriggeringPrincipal) { + // we're not a browser window, pass the URI string to a new browser window + if (window.location.href != getBrowserURL()) { + window.openDialog(getBrowserURL(), "_blank", "all,dialog=no", aURIString); + return; + } + + // This function throws for certain malformed URIs, so use exception handling + // so that we don't disrupt startup + try { + gBrowser.loadTabs(aURIString.split("|"), { + inBackground: false, + replace: true, + triggeringPrincipal: aTriggeringPrincipal, + }); + } catch (e) { + } +} + +function handleURLBarCommand(aUserAction, aTriggeringEvent) +{ + // Remove leading and trailing spaces first + var url = gURLBar.value.trim(); + try { + addToUrlbarHistory(url); + } catch (ex) { + // Things may go wrong when adding url to the location bar history, + // but don't let that interfere with the loading of the url. + } + + if (url.match(/^view-source:/)) { + gViewSourceUtils.viewSource(url.replace(/^view-source:/, ""), null, null); + return; + } + + getShortcutOrURIAndPostData(url).then(data => { + // Check the pressed modifiers: (also see bug 97123) + // Modifier Mac | Modifier PC | Action + // -------------+-------------+----------- + // Command | Control | New Window/Tab + // Shift+Cmd | Shift+Ctrl | New Window/Tab behind current one + // Option | Shift | Save URL (show Filepicker) + + // If false, the save modifier is Alt, which is Option on Mac. + var modifierIsShift = Services.prefs.getBoolPref("ui.key.saveLink.shift", true); + + var shiftPressed = false; + var saveModifier = false; // if the save modifier was pressed + if (aTriggeringEvent && 'shiftKey' in aTriggeringEvent && + 'altKey' in aTriggeringEvent) { + saveModifier = modifierIsShift ? aTriggeringEvent.shiftKey + : aTriggeringEvent.altKey; + shiftPressed = aTriggeringEvent.shiftKey; + } + + var browser = getBrowser(); + // Accept both Control and Meta (=Command) as New-Window-Modifiers + if (aTriggeringEvent && + (('ctrlKey' in aTriggeringEvent && aTriggeringEvent.ctrlKey) || + ('metaKey' in aTriggeringEvent && aTriggeringEvent.metaKey) || + ('button' in aTriggeringEvent && aTriggeringEvent.button == 1))) { + // Check if user requests Tabs instead of windows + if (Services.prefs.getBoolPref("browser.tabs.opentabfor.urlbar", false)) { + // Reset url in the urlbar + URLBarSetURI(); + // Open link in new tab + var t = browser.addTab(data.url, { + postData: data.postData, + allowThirdPartyFixup: true, + }); + + // Focus new tab unless shift is pressed + if (!shiftPressed) + browser.selectedTab = t; + } else { + // Open a new window with the URL + var newWin = openDialog(getBrowserURL(), "_blank", "all,dialog=no", data.url, + null, null, data.postData, true); + // Reset url in the urlbar + URLBarSetURI(); + + // Focus old window if shift was pressed, as there's no + // way to open a new window in the background + // XXX this doesn't seem to work + if (shiftPressed) { + //newWin.blur(); + content.focus(); + } + } + } else if (saveModifier) { + try { + // Firstly, fixup the url so that (e.g.) "www.foo.com" works + url = Services.uriFixup.createFixupURI(data.url, Ci.nsIURIFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI).spec; + // Open filepicker to save the url + saveURL(url, null, null, false, true, null, document); + } + catch(ex) { + // XXX Do nothing for now. + // Do we want to put up an alert in the future? Mmm, l10n... + } + } else { + // No modifier was pressed, load the URL normally and + // focus the content area + loadURI(data.url, null, data.postData, true); + content.focus(); + } + }); +} + +/** + * Given a string, will generate a more appropriate urlbar value if a Places + * keyword or a search alias is found at the beginning of it. + * + * @param url + * A string that may begin with a keyword or an alias. + * + * @return {Promise} + * @resolves { url, postData, mayInheritPrincipal }. If it's not possible + * to discern a keyword or an alias, url will be the input string. + */ +function getShortcutOrURIAndPostData(url) { + return (async function() { + let mayInheritPrincipal = false; + let postData = null; + // Split on the first whitespace. + let [keyword, param = ""] = url.trim().split(/\s(.+)/, 2); + + if (!keyword) { + return { url, postData, mayInheritPrincipal }; + } + + let engine = Services.search.getEngineByAlias(keyword); + if (engine) { + let submission = engine.getSubmission(param, null, "keyword"); + return { url: submission.uri.spec, + postData: submission.postData, + mayInheritPrincipal }; + } + + // A corrupt Places database could make this throw, breaking navigation + // from the location bar. + let entry = null; + try { + entry = await PlacesUtils.keywords.fetch(keyword); + } catch (ex) { + Cu.reportError(`Unable to fetch Places keyword "${keyword}": ${ex}`); + } + if (!entry || !entry.url) { + // This is not a Places keyword. + return { url, postData, mayInheritPrincipal }; + } + + try { + [url, postData] = + await BrowserUtils.parseUrlAndPostData(entry.url.href, + entry.postData, + param); + if (postData) { + postData = getPostDataStream(postData); + } + + // Since this URL came from a bookmark, it's safe to let it inherit the + // current document's principal. + mayInheritPrincipal = true; + } catch (ex) { + // It was not possible to bind the param, just use the original url value. + } + + return { url, postData, mayInheritPrincipal }; + })().then(data => { + return data; + }); +} + +function getPostDataStream(aStringData, aKeyword, aEncKeyword, aType) +{ + var dataStream = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + aStringData = aStringData.replace(/%s/g, aEncKeyword).replace(/%S/g, aKeyword); + dataStream.data = aStringData; + + var mimeStream = Cc["@mozilla.org/network/mime-input-stream;1"] + .createInstance(Ci.nsIMIMEInputStream); + mimeStream.addHeader("Content-Type", aType); + mimeStream.setData(dataStream); + return mimeStream.QueryInterface(Ci.nsIInputStream); +} + +// handleDroppedLink has the following 2 overloads: +// handleDroppedLink(event, url, name, triggeringPrincipal) +// handleDroppedLink(event, links, triggeringPrincipal) +function handleDroppedLink(event, urlOrLinks, nameOrTriggeringPrincipal, triggeringPrincipal) +{ + let links; + if (Array.isArray(urlOrLinks)) { + links = urlOrLinks; + triggeringPrincipal = nameOrTriggeringPrincipal; + } else { + links = [{ url: urlOrLinks, nameOrTriggeringPrincipal, type: "" }]; + } + + let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange; + + // Usually blank for SeaMonkey. + let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid"); + + // event is null if links are dropped in content process. + // inBackground should be false, as it's loading into current browser. + let inBackground = false; + if (event) { + inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + if (event.shiftKey) + inBackground = !inBackground; + } + + (async function() { + let urls = []; + let postDatas = []; + for (let link of links) { + let data = await getShortcutOrURIAndPostData(link.url); + urls.push(data.url); + postDatas.push(data.postData); + } + if (lastLocationChange == gBrowser.selectedBrowser.lastLocationChange) { + gBrowser.loadTabs(urls, { + inBackground, + replace: true, + allowThirdPartyFixup: false, + postDatas, + userContextId, + triggeringPrincipal, + }); + } + })(); + + // If links are dropped in content process, event.preventDefault() should be + // called in content process. + if (event) { + // Keep the event from being handled by the dragDrop listeners + // built-in to gecko if they happen to be above us. + event.preventDefault(); + } +} + +function readFromClipboard() +{ + var url; + + try { + // Get the clipboard. + const clipboard = Services.clipboard; + + // Create a transferable that will transfer the text. + var trans = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + + trans.init(null); + trans.addDataFlavor("text/unicode"); + // If available, use the selection clipboard, otherwise use the global one. + if (clipboard.supportsSelectionClipboard()) + clipboard.getData(trans, clipboard.kSelectionClipboard); + else + clipboard.getData(trans, clipboard.kGlobalClipboard); + + var data = {}; + trans.getTransferData("text/unicode", data, {}); + + if (data.value) { + data = data.value.QueryInterface(Ci.nsISupportsString); + url = data.data; + } + } catch (ex) { + } + + return url; +} + +/** + * Open the View Source dialog. + * + * @param aArgsOrDocument + * Either an object or a Document. Passing a Document is deprecated, + * and is not supported with e10s. This function will throw if + * aArgsOrDocument is a CPOW. + * + * If aArgsOrDocument is an object, that object can take the + * following properties: + * + * URL (required): + * A string URL for the page we'd like to view the source of. + * browser (optional): + * The browser containing the document that we would like to view the + * source of. This is required if outerWindowID is passed. + * outerWindowID (optional): + * The outerWindowID of the content window containing the document that + * we want to view the source of. You only need to provide this if you + * want to attempt to retrieve the document source from the network + * cache. + * lineNumber (optional): + * The line number to focus on once the source is loaded. + */ +function BrowserViewSourceOfDocument(aArgsOrDocument) { + if (aArgsOrDocument instanceof Document) { + // Deprecated API - callers should pass args object instead. + if (Cu.isCrossProcessWrapper(aArgsOrDocument)) { + throw new Error("BrowserViewSourceOfDocument cannot accept a CPOW " + + "as a document."); + } + + let requestor = aArgsOrDocument.defaultView + .QueryInterface(Ci.nsIInterfaceRequestor); + let browser = requestor.getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + let outerWindowID = requestor.getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + let URL = browser.currentURI.spec; + aArgsOrDocument = { browser, outerWindowID, URL }; + } + + gViewSourceUtils.viewSource(aArgsOrDocument); +} + +/** + * Opens the View Source dialog for the source loaded in the root + * top-level document of the browser. + * + * @param aBrowser + * The browser that we want to load the source of. + */ +function BrowserViewSource(aBrowser) { + gViewSourceUtils.viewSource({ + browser: aBrowser, + outerWindowID: aBrowser.outerWindowID, + URL: aBrowser.currentURI.spec, + }); +} + +// documentURL - URL of the document to view, or null for this window's document +// initialTab - id of the initial tab to display, or null for the first tab +// imageElement - image to load in the Media Tab of the Page Info window; +// can be null/omitted +// frameOuterWindowID - the id of the frame that the context menu opened in; +// can be null/omitted +// browser - the browser containing the document we're interested in inspecting; +// can be null/omitted +function BrowserPageInfo(documentURL, initialTab, imageElement, + frameOuterWindowID, browser) { + if (documentURL instanceof HTMLDocument) { + Deprecated.warning("Please pass the location URL instead of the document " + + "to BrowserPageInfo() as the first argument.", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1575830"); + documentURL = documentURL.location; + } + + let args = { initialTab, imageElement, frameOuterWindowID, browser }; + var windows = Services.wm.getEnumerator("Browser:page-info"); + + documentURL = documentURL || window.gBrowser.selectedBrowser.currentURI.spec; + + // Check for windows matching the url. + while (windows.hasMoreElements()) { + let win = windows.getNext(); + if (win.closed) { + continue; + } + if (win.document.documentElement + .getAttribute("relatedUrl") == documentURL) { + win.focus(); + win.resetPageInfo(args); + return win; + } + } + // We didn't find a matching window, so open a new one. + return window.openDialog("chrome://navigator/content/pageinfo/pageInfo.xul", + "_blank", + "chrome,dialog=no,resizable", + args); +} + +function hiddenWindowStartup() +{ + // focus the hidden window + window.focus(); + + // Disable menus which are not appropriate + var disabledItems = ['cmd_close', 'cmd_sendPage', 'Browser:SendLink', + 'Browser:EditPage', 'Browser:SavePage', 'cmd_printSetup', + 'cmd_print', 'canGoBack', 'canGoForward', + 'Browser:AddBookmark', 'Browser:AddBookmarkAs', + 'cmd_undo', 'cmd_redo', 'cmd_cut', 'cmd_copy', + 'cmd_paste', 'cmd_delete', 'cmd_selectAll', + 'cmd_findTypeText', 'cmd_findTypeLinks', 'cmd_find', + 'cmd_findNext', 'cmd_findPrev', 'menu_Toolbars', + 'menuitem_reload', 'menu_UseStyleSheet', 'charsetMenu', + 'View:PageSource', 'View:PageInfo', 'menu_translate', + 'cookie_deny', 'cookie_default', 'View:FullScreen', + 'cookie_session', 'cookie_allow', 'image_deny', + 'image_default', 'image_allow', 'popup_deny', + 'popup_default','popup_allow', 'menu_zoom', + 'cmd_minimizeWindow', 'cmd_zoomWindow']; + var broadcaster; + + for (var id in disabledItems) { + broadcaster = document.getElementById(disabledItems[id]); + if (broadcaster) + broadcaster.setAttribute("disabled", "true"); + } + + // also hide the window list separator + var separator = document.getElementById("sep-window-list"); + if (separator) + separator.setAttribute("hidden", "true"); + + // init string bundles + gNavigatorBundle = document.getElementById("bundle_navigator"); + gNavigatorRegionBundle = document.getElementById("bundle_navigator_region"); + gBrandBundle = document.getElementById("bundle_brand"); +} + +function checkForDirectoryListing() +{ + if ( "HTTPIndex" in content && + content.HTTPIndex instanceof Ci.nsIHTTPIndex ) { + var forced = getBrowser().docShell.forcedCharset; + if (forced) { + content.defaultCharacterset = forced; + } + } +} + +function URLBarSetURI(aURI, aValid) { + var uri = aURI || getWebNavigation().currentURI; + var value; + + // If the url has "wyciwyg://" as the protocol, strip it off. + // Nobody wants to see it on the urlbar for dynamically generated pages. + try { + uri = Services.uriFixup.createExposableURI(uri); + } catch (ex) {} + + // Replace "about:blank" and other initial pages with an empty string + // only if there's no opener (bug 370555). + if (gInitialPages.has(uri.spec)) + value = (content.opener || getWebNavigation().canGoBack) ? uri.spec : ""; + else + value = losslessDecodeURI(uri); + + gURLBar.value = value; + // In some cases, setting the urlBar value causes userTypedValue to + // become set because of oninput, so reset it to null. + getBrowser().userTypedValue = null; + + SetPageProxyState((value && (!aURI || aValid)) ? "valid" : "invalid", uri); +} + +function losslessDecodeURI(aURI) { + var value = aURI.spec; + var scheme = aURI.scheme; + + var decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme); + // Try to decode as UTF-8 if there's no encoding sequence that we would break. + if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) { + if (decodeASCIIOnly) { + // This only decodes ascii characters (hex) 20-7e, except 25 (%). + // This avoids both cases stipulated below (%-related issues, and \r, \n + // and \t, which would be %0d, %0a and %09, respectively) as well as any + // non-US-ascii characters. + value = value.replace(/%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, decodeURI); + } else { + try { + value = decodeURI(value) + // 1. decodeURI decodes %25 to %, which creates unintended + // encoding sequences. Re-encode it, unless it's part of + // a sequence that survived decodeURI, i.e. one for: + // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#' + // (RFC 3987 section 3.2) + // 2. Ee-encode all adjacent whitespace, to prevent spoofing + // attempts where invisible characters would push part of + // the URL to overflow the location bar (bug 1395508). + .replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)|\s(?=\s)|\s$/ig, + encodeURIComponent); + } catch (e) {} + } + } + + // Encode invisible characters (soft hyphen, zero-width space, BOM, + // line and paragraph separator, word joiner, invisible times, + // invisible separator, object replacement character, + // C0/C1 controls). (bug 452979, bug 909264) + // Encode bidirectional formatting characters. + // (RFC 3987 sections 3.2 and 4.1 paragraph 6) + // Re-encode whitespace so that it doesn't get eaten away + // by the location bar (bug 410726). + return value.replace(/[\u0000-\u001f\u007f-\u00a0\u00ad\u034f\u061c\u115f\u1160\u17b4\u17b5\u180b-\u180d\u200b\u200e\u200f\u2028-\u202e\u2060-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufff8\ufffc]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]/g, encodeURIComponent); +} + +/** + * Use Stylesheet functions. + * Written by Tim Hill (bug 6782) + * Frameset handling by Neil Rashbrook <neil@parkwaycc.co.uk> + **/ +/** + * Adds this frame's stylesheet sets to the View > Use Style submenu + * + * If the frame has no preferred stylesheet set then the "Default style" + * menuitem should be shown. Note that it defaults to checked, hidden. + * + * If this frame has a selected stylesheet set then its menuitem should + * be checked (unless the "None" style is currently selected), and the + * "Default style" menuitem should to be unchecked. + * + * The stylesheet sets may match those of other frames. In that case, the + * checkmark should be removed from sets that are not selected in this frame. + * + * @param menuPopup The submenu's popup child + * @param frame The frame whose sets are to be added + * @param styleDisabled True if the "None" style is currently selected + * @param itemPersistentOnly The "Default style" menuitem element + */ +function stylesheetFillFrame(menuPopup, frame, styleDisabled, itemPersistentOnly) +{ + if (!frame.document.preferredStyleSheetSet) + itemPersistentOnly.hidden = false; + + var title = frame.document.selectedStyleSheetSet; + if (title) + itemPersistentOnly.removeAttribute("checked"); + + var styleSheetSets = frame.document.styleSheetSets; + for (var i = 0; i < styleSheetSets.length; i++) { + var styleSheetSet = styleSheetSets[i]; + var menuitem = menuPopup.getElementsByAttribute("data", styleSheetSet).item(0); + if (menuitem) { + if (styleSheetSet != title) + menuitem.removeAttribute("checked"); + } else { + var menuItem = document.createElement("menuitem"); + menuItem.setAttribute("type", "radio"); + menuItem.setAttribute("label", styleSheetSet); + menuItem.setAttribute("data", styleSheetSet); + menuItem.setAttribute("checked", styleSheetSet == title && !styleDisabled); + menuPopup.appendChild(menuItem); + } + } +} +/** + * Adds all available stylesheet sets to the View > Use Style submenu + * + * If all frames have preferred stylesheet sets then the "Default style" + * menuitem should remain hidden, otherwise it should be shown, and + * if some frames have a selected stylesheet then the "Default style" + * menuitem should be unchecked, otherwise it should remain checked. + * + * A stylesheet set's menuitem should not be checked if the "None" style + * is currently selected. Otherwise a stylesheet set may be available in + * more than one frame. In such a case the menuitem should only be checked + * if it is selected in all frames in which it is available. + * + * @param menuPopup The submenu's popup child + * @param frameset The frameset whose sets are to be added + * @param styleDisabled True if the "None" style is currently selected + * @param itemPersistentOnly The "Default style" menuitem element + */ +function stylesheetFillAll(menuPopup, frameset, styleDisabled, itemPersistentOnly) +{ + stylesheetFillFrame(menuPopup, frameset, styleDisabled, itemPersistentOnly); + for (var i = 0; i < frameset.frames.length; i++) { + stylesheetFillAll(menuPopup, frameset.frames[i], styleDisabled, itemPersistentOnly); + } +} +/** + * Populates the View > Use Style submenu with all available stylesheet sets + * @param menuPopup The submenu's popup child + */ +function stylesheetFillPopup(menuPopup) +{ + /* Clear menu */ + var itemPersistentOnly = menuPopup.firstChild.nextSibling; + while (itemPersistentOnly.nextSibling) + itemPersistentOnly.nextSibling.remove(); + + /* Reset permanent items */ + var styleDisabled = getMarkupDocumentViewer().authorStyleDisabled; + menuPopup.firstChild.setAttribute("checked", styleDisabled); + itemPersistentOnly.setAttribute("checked", !styleDisabled); + itemPersistentOnly.hidden = true; + + stylesheetFillAll(menuPopup, window.content, styleDisabled, itemPersistentOnly); +} +/** + * Switches all frames in a frameset to the same stylesheet set + * + * Only frames that support the given title will be switched + * + * @param frameset The frameset whose frames are to be switched + * @param title The name of the stylesheet set to switch to + */ +function stylesheetSwitchAll(frameset, title) { + if (!title || frameset.document.styleSheetSets.contains(title)) { + frameset.document.selectedStyleSheetSet = title; + } + for (var i = 0; i < frameset.frames.length; i++) { + stylesheetSwitchAll(frameset.frames[i], title); + } +} + +function setStyleDisabled(disabled) { + getMarkupDocumentViewer().authorStyleDisabled = disabled; +} + +function URLBarFocusHandler(aEvent) +{ + if (gIgnoreFocus) + gIgnoreFocus = false; + else if (gClickSelectsAll) + gURLBar.select(); +} + +function URLBarMouseDownHandler(aEvent) +{ + if (gURLBar.hasAttribute("focused")) { + gIgnoreClick = true; + } else { + gIgnoreFocus = true; + gIgnoreClick = false; + gURLBar.setSelectionRange(0, 0); + } +} + +function URLBarClickHandler(aEvent) +{ + if (!gIgnoreClick && gClickSelectsAll && gURLBar.selectionStart == gURLBar.selectionEnd) + if (gClickAtEndSelects || gURLBar.selectionStart < gURLBar.value.length) + gURLBar.select(); +} + +function ShowAndSelectContentsOfURLBar() +{ + if (!isElementVisible(gURLBar)) { + BrowserOpenWindow(); + return; + } + + if (gURLBar.value) + gURLBar.select(); + else + gURLBar.focus(); +} + +// If "ESC" is pressed in the url bar, we replace the urlbar's value with the url of the page +// and highlight it, unless it is about:blank, where we reset it to "". +function handleURLBarRevert() +{ + var url = getWebNavigation().currentURI.spec; + var throbberElement = document.getElementById("navigator-throbber"); + + var isScrolling = gURLBar.userAction == "scrolling"; + + // don't revert to last valid url unless page is NOT loading + // and user is NOT key-scrolling through autocomplete list + if (!throbberElement.hasAttribute("busy") && !isScrolling) { + URLBarSetURI(); + + // If the value isn't empty, select it. + if (gURLBar.value) + gURLBar.select(); + } + + // tell widget to revert to last typed text only if the user + // was scrolling when they hit escape + return isScrolling; +} + +function UpdatePageProxyState() +{ + if (gURLBar.value != gLastValidURLStr) + SetPageProxyState("invalid", null); +} + +function SetPageProxyState(aState, aURI) +{ + if (!gProxyButton) + gProxyButton = document.getElementById("page-proxy-button"); + if (!gProxyFavIcon) + gProxyFavIcon = document.getElementById("page-proxy-favicon"); + if (!gProxyDeck) + gProxyDeck = document.getElementById("page-proxy-deck"); + + gProxyButton.setAttribute("pageproxystate", aState); + + if (aState == "valid") { + gLastValidURLStr = gURLBar.value; + gURLBar.addEventListener("input", UpdatePageProxyState); + if (gBrowser.shouldLoadFavIcon(aURI)) { + var favStr = gBrowser.buildFavIconString(aURI); + if (favStr != gProxyFavIcon.src) { + gBrowser.loadFavIcon(aURI, "src", gProxyFavIcon); + gProxyDeck.selectedIndex = 0; + } + else gProxyDeck.selectedIndex = 1; + } + else { + gProxyDeck.selectedIndex = 0; + gProxyFavIcon.removeAttribute("src"); + } + } else if (aState == "invalid") { + gURLBar.removeEventListener("input", UpdatePageProxyState); + gProxyDeck.selectedIndex = 0; + gProxyFavIcon.removeAttribute("src"); + } +} + +function handlePageProxyClick(aEvent) +{ + switch (aEvent.button) { + case 0: + // bug 52784 - select location field contents + gURLBar.select(); + break; + case 1: + // bug 111337 - load url/keyword from clipboard + middleMousePaste(aEvent); + break; + } +} + +function updateComponentBarBroadcaster() +{ + var compBarBroadcaster = document.getElementById('cmd_viewcomponentbar'); + var taskBarBroadcaster = document.getElementById('cmd_viewtaskbar'); + var compBar = document.getElementById('component-bar'); + if (taskBarBroadcaster.getAttribute('checked') == 'true') { + compBarBroadcaster.removeAttribute('disabled'); + if (compBar.getAttribute('hidden') != 'true') + compBarBroadcaster.setAttribute('checked', 'true'); + } + else { + compBarBroadcaster.setAttribute('disabled', 'true'); + compBarBroadcaster.removeAttribute('checked'); + } +} + +function updateToolbarStates(aEvent) +{ + onViewToolbarsPopupShowing(aEvent); + updateComponentBarBroadcaster(); + + const tabbarMenuItem = document.getElementById("menuitem_showhide_tabbar"); + // Make show/hide menu item reflect current state + const visibility = gBrowser.getStripVisibility(); + tabbarMenuItem.setAttribute("checked", visibility); + + // Don't allow the tab bar to be shown/hidden when more than one tab is open + // or when we have 1 tab and the autoHide pref is set + const disabled = gBrowser.browsers.length > 1 || + Services.prefs.getBoolPref("browser.tabs.autoHide"); + tabbarMenuItem.setAttribute("disabled", disabled); +} + +function showHideTabbar() +{ + const visibility = gBrowser.getStripVisibility(); + Services.prefs.setBoolPref("browser.tabs.forceHide", visibility); + gBrowser.setStripVisibilityTo(!visibility); +} + +function BrowserFullScreen() +{ + window.fullScreen = !window.fullScreen; +} + +function onFullScreen() +{ + FullScreen.toggle(); +} + +function UpdateStatusBarPopupIcon(aEvent) +{ + if (aEvent && aEvent.originalTarget != gBrowser.getNotificationBox()) + return; + + var showIcon = Services.prefs.getBoolPref("privacy.popups.statusbar_icon_enabled"); + if (showIcon) { + var popupIcon = document.getElementById("popupIcon"); + popupIcon.hidden = !gBrowser.getNotificationBox().popupCount; + } +} +function StatusbarViewPopupManager() +{ + // Open Data Manager permissions pane site and type prefilled to add. + toDataManager(hostUrl() + "|permissions|add|popup"); +} + +function popupBlockerMenuShowing(event) +{ + var separator = document.getElementById("popupMenuSeparator"); + + if (separator) + separator.hidden = !createShowPopupsMenu(event.target, gBrowser.selectedBrowser); +} + +function WindowIsClosing() +{ + var browser = getBrowser(); + var cn = browser.tabContainer.childNodes; + var numtabs = cn.length; + + if (!gPrivate && AppConstants.platform != "macosx" && isClosingLastBrowser()) { + let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(closingCanceled, "browser-lastwindow-close-requested"); + if (closingCanceled.data) + return false; + + Services.obs.notifyObservers(null, "browser-lastwindow-close-granted"); + + return true; + } + + var reallyClose = gPrivate || + browser.warnAboutClosingTabs(browser.closingTabsEnum.ALL); + + for (var i = 0; reallyClose && i < numtabs; ++i) { + var ds = browser.getBrowserForTab(cn[i]).docShell; + + if (ds.contentViewer && !ds.contentViewer.permitUnload()) + reallyClose = false; + } + + return reallyClose; +} + +/** + * Checks whether this is the last full *browser* window around. + * @returns true if closing last browser window, false if not. + */ +function isClosingLastBrowser() { + // Popups aren't considered full browser windows. + if (!toolbar.visible) + return false; + + // Figure out if there's at least one other browser window around. + var e = Services.wm.getEnumerator("navigator:browser"); + while (e.hasMoreElements()) { + let win = e.getNext(); + if (!win.closed && win != window && win.toolbar.visible) + return false; + } + + return true; +} + +/** + * file upload support + */ + +/* This function returns the URI of the currently focused content frame + * or frameset. + */ +function getCurrentURI() +{ + var focusedWindow = document.commandDispatcher.focusedWindow; + var contentFrame = isContentFrame(focusedWindow) ? focusedWindow : window.content; + + var nav = contentFrame.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation); + return nav.currentURI; +} + +var gLastOpenUploadDirectory; + +function BrowserUploadFile() +{ + if (!gLastOpenUploadDirectory) { + gLastOpenUploadDirectory = new RememberLastDir("browser.upload"); + }; + + const nsIFilePicker = Ci.nsIFilePicker; + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + fp.init(window, gNavigatorBundle.getString("uploadFile"), nsIFilePicker.modeOpen); + fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText | nsIFilePicker.filterImages | + nsIFilePicker.filterXML | nsIFilePicker.filterHTML); + + // use a pref to remember the filterIndex selected by the user. + fp.filterIndex = gLastOpenUploadDirectory.filterIndex; + + // Use a pref to remember the displayDirectory selected by the user. + fp.displayDirectory = gLastOpenUploadDirectory.path; + + fp.open(rv => { + if (rv != nsIFilePicker.returnOK || !fp.fileURL) { + return; + } + gLastOpenUploadDirectory.filterIndex = fp.filterIndex; + gLastOpenUploadDirectory.path = fp.file.parent.QueryInterface(Ci.nsIFile); + + try { + var targetBaseURI = getCurrentURI(); + // Generate the target URI. We use fileURL.file.leafName to get the + // unicode value of the target filename w/o any URI-escaped chars. + // this gives the protocol handler the best chance of generating a + // properly formatted URI spec. we pass null for the origin charset + // parameter since we want the URI to inherit the origin charset + // property from targetBaseURI. + var leafName = fp.fileURL.QueryInterface(Ci.nsIFileURL).file.leafName; + var targetURI = Services.io.newURI(leafName, null, targetBaseURI); + + // ok, start uploading... + openDialog("chrome://communicator/content/downloads/uploadProgress.xul", "", + "titlebar,centerscreen,minimizable,dialog=no", fp.fileURL, targetURI); + } catch (e) {} + }); +} + +/* This function is called whenever the file menu is about to be displayed. + * Enable the upload menu item if appropriate. */ +function updateFileUploadItem() +{ + var canUpload = false; + try { + canUpload = getCurrentURI().schemeIs('ftp'); + } catch (e) {} + + var item = document.getElementById('Browser:UploadFile'); + if (canUpload) + item.removeAttribute('disabled'); + else + item.setAttribute('disabled', 'true'); +} + +function isBidiEnabled() +{ + // first check the pref. + if (Services.prefs.getBoolPref("bidi.browser.ui", false)) { + return true; + } + + // now see if the app locale is an RTL one. + const isRTL = Services.locale.isAppLocaleRTL; + + if (isRTL) { + Services.prefs.setBoolPref("bidi.browser.ui", true); + } + return isRTL; +} + +function SwitchDocumentDirection(aWindow) +{ + aWindow.document.dir = (aWindow.document.dir == "ltr" ? "rtl" : "ltr"); + + for (var run = 0; run < aWindow.frames.length; run++) + SwitchDocumentDirection(aWindow.frames[run]); +} + +function updateSavePageItems() +{ + var autoDownload = Services.prefs + .getBoolPref("browser.download.useDownloadDir"); + goSetMenuValue("savepage", autoDownload ? "valueSave" : "valueSaveAs"); +} + +function convertFromUnicode(charset, str) +{ + try { + var unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + unicodeConverter.charset = charset; + str = unicodeConverter.ConvertFromUnicode(str); + return str + unicodeConverter.Finish(); + } catch(ex) { + return null; + } +} + +function getNotificationBox(aWindow) +{ + return aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.parentNode.wrappedJSObject; +} + +function BrowserToolboxCustomizeInit() +{ + SetPageProxyState("invalid", null); + toolboxCustomizeInit("main-menubar"); + PlacesToolbarHelper.customizeStart(); + + var splitter = document.getElementById("urlbar-search-splitter"); + if (splitter) + splitter.remove(); +} + +function BrowserToolboxCustomizeDone(aToolboxChanged) +{ + toolboxCustomizeDone("main-menubar", getNavToolbox(), aToolboxChanged); + + UpdateNavBar(); + + // Update the urlbar + var value = gBrowser.userTypedValue; + if (value == null) + URLBarSetURI(); + else + gURLBar.value = value; + + PlacesToolbarHelper.customizeDone(); +} + +function BrowserToolboxCustomizeChange(event) +{ + toolboxCustomizeChange(getNavToolbox(), event); +} + +var LightWeightThemeWebInstaller = { + handleEvent: function (event) { + switch (event.type) { + case "InstallBrowserTheme": + case "PreviewBrowserTheme": + case "ResetBrowserThemePreview": + // ignore requests from background tabs + if (event.target.ownerDocument.defaultView.top != content) + return; + } + switch (event.type) { + case "InstallBrowserTheme": + this._installRequest(event); + break; + case "PreviewBrowserTheme": + this._preview(event); + break; + case "ResetBrowserThemePreview": + this._resetPreview(event); + break; + case "pagehide": + case "TabSelect": + this._resetPreview(); + break; + } + }, + + get _manager () { + delete this._manager; + return this._manager = LightweightThemeManager; + }, + + _installRequest: function (event) { + var node = event.target; + var data = this._getThemeFromNode(node); + if (!data) + return; + + if (this._isAllowed(node)) { + this._install(data); + return; + } + + this._removePreviousNotifications(); + getBrowser().getNotificationBox().lwthemeInstallRequest( + node.ownerDocument.location.host, + this._install.bind(this, data)); + }, + + _install: function (newTheme) { + this._removePreviousNotifications(); + + var previousTheme = this._manager.currentTheme; + this._manager.currentTheme = newTheme; + if (this._manager.currentTheme && + this._manager.currentTheme.id == newTheme.id) + getBrowser().getNotificationBox().lwthemeInstallNotification(function() { + LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id); + LightWeightThemeWebInstaller._manager.currentTheme = previousTheme; + }); + else + getBrowser().getNotificationBox().lwthemeNeedsRestart(newTheme.name); + + // We've already destroyed the permission notification, + // so tell the former that it's closed already. + return true; + }, + + _removePreviousNotifications: function () { + getBrowser().getNotificationBox().removeNotifications( + ["lwtheme-install-request", "lwtheme-install-notification"]); + }, + + _previewWindow: null, + _preview: function (event) { + if (!this._isAllowed(event.target)) + return; + + var data = this._getThemeFromNode(event.target); + if (!data) + return; + + this._resetPreview(); + + this._previewWindow = event.target.ownerDocument.defaultView; + this._previewWindow.addEventListener("pagehide", this, true); + gBrowser.tabContainer.addEventListener("TabSelect", this); + + this._manager.previewTheme(data); + }, + + _resetPreview: function (event) { + if (!this._previewWindow || + event && !this._isAllowed(event.target)) + return; + + this._previewWindow.removeEventListener("pagehide", this, true); + this._previewWindow = null; + gBrowser.tabContainer.removeEventListener("TabSelect", this); + + this._manager.resetPreview(); + }, + + _isAllowed: function (node) { + var uri = node.ownerDocument.documentURIObject; + return Services.perms.testPermission(uri, "install") == Services.perms.ALLOW_ACTION; + }, + + _getThemeFromNode: function (node) { + return this._manager.parseTheme(node.getAttribute("data-browsertheme"), + node.baseURI); + } +} + +function AddKeywordForSearchField() { + var node = document.popupNode; + var doc = node.ownerDocument; + var charset = doc.characterSet; + var title = gNavigatorBundle.getFormattedString("addKeywordTitleAutoFill", + [doc.title]); + var description = PlacesUIUtils.getDescriptionFromDocument(doc); + var postData = null; + var form = node.form; + var spec = form.action || doc.documentURI; + + function encodeNameValuePair(aName, aValue) { + return encodeURIComponent(aName) + "=" + encodeURIComponent(aValue); + } + + let el = null; + let type = null; + let formData = []; + for (var i = 0; i < form.elements.length; i++) { + el = form.elements[i]; + + if (!el.type) // happens with fieldsets + continue; + + if (el == node) { + formData.push(encodeNameValuePair(el.name, "") + "%s"); + continue; + } + + type = el.type; + + if (((el instanceof HTMLInputElement && el.mozIsTextField(true)) || + type == "hidden" || type == "textarea") || + ((type == "checkbox" || type == "radio") && el.checked)) { + formData.push(encodeNameValuePair(el.name, el.value)); + } else if (el instanceof HTMLSelectElement && el.selectedIndex >= 0) { + for (var j = 0; j < el.options.length; j++) { + if (el.options[j].selected) + formData.push(encodeNameValuePair(el.name, el.options[j].value)); + } + } + } + + if (form.method == "post" && + form.enctype == "application/x-www-form-urlencoded") { + postData = formData.join("&"); + } else { // get + spec += spec.includes("?") ? "&" : "?"; + spec += formData.join("&"); + } + + PlacesUIUtils.showMinimalAddBookmarkUI(makeURI(spec), title, description, null, + null, null, "", postData, charset); +} + +function getCert() +{ + var sslStatus = getBrowser().securityUI + .QueryInterface(Ci.nsISSLStatusProvider) + .SSLStatus; + + return sslStatus && sslStatus.serverCert; +} + +function viewCertificate() +{ + var cert = getCert(); + + if (cert) + { + Cc["@mozilla.org/nsCertificateDialogs;1"] + .getService(Ci.nsICertificateDialogs) + .viewCert(window, cert); + } +} + +function openCertManager() +{ + toOpenWindowByType("mozilla:certmanager", "chrome://pippki/content/certManager.xul", + "resizable,dialog=no,centerscreen"); +} + +function onViewSecurityContextMenu() +{ + document.getElementById("viewCertificate").disabled = !getCert(); +} + +function updateZoomStatus() { + let newLabel = Math.round(ZoomManager.zoom * 100) + " %"; + let zoomStatusElement = document.getElementById("zoomLevel-display"); + if (zoomStatusElement.getAttribute('label') != newLabel){ + zoomStatusElement.setAttribute('label', newLabel); + } +} + +function zoomIn() { + FullZoom.enlarge(); + updateZoomStatus(); +} + +function zoomOut() { + FullZoom.reduce(); + updateZoomStatus(); +} + +/** + * Determine whether or not a given focused DOMWindow is in the content area. + **/ +function isContentFrame(aFocusedWindow) { + if (!aFocusedWindow) + return false; + + return (aFocusedWindow.top == window.content); +} + +var browserDragAndDrop = { + canDropLink: aEvent => Services.droppedLinkHandler.canDropLink(aEvent, true), + + dragOver(aEvent) { + if (this.canDropLink(aEvent)) { + aEvent.preventDefault(); + } + }, + + getTriggeringPrincipal(aEvent) { + return Services.droppedLinkHandler.getTriggeringPrincipal(aEvent); + }, + + dropLinks(aEvent, aDisallowInherit) { + return Services.droppedLinkHandler.dropLinks(aEvent, aDisallowInherit); + } +}; diff --git a/comm/suite/browser/navigator.xul b/comm/suite/browser/navigator.xul new file mode 100644 index 0000000000..89a0dae076 --- /dev/null +++ b/comm/suite/browser/navigator.xul @@ -0,0 +1,571 @@ +<?xml version="1.0"?> <!-- -*- Mode: HTML; indent-tabs-mode: nil -*- --> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xml-stylesheet href="chrome://navigator/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://communicator/content/places/places.css" type="text/css"?> + +<?xul-overlay href="chrome://navigator/content/navigatorOverlay.xul"?> +<?xul-overlay href="chrome://navigator/content/linkToolbarOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/contentAreaContextOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/sidebar/sidebarOverlay.xul"?> +<?xul-overlay href="chrome://navigator/content/safeBrowsingOverlay.xul"?> +<?xul-overlay href="chrome://navigator/content/webDeveloperOverlay.xul"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % navigatorDTD SYSTEM "chrome://navigator/locale/navigator.dtd" > +%navigatorDTD; +]> + +<window id="main-window" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="Startup()" onunload="Shutdown()" + onclose="return WindowIsClosing();" + contenttitlesetting="true" + title="&mainWindow.title;" + titlemodifier="&mainWindow.titlemodifier;" + titlemenuseparator="&mainWindow.titlemodifiermenuseparator;" + titleprivate="&mainWindow.titleprivate;" + toggletoolbar="true" + lightweightthemes="true" + lightweightthemesfooter="status-bar" + windowtype="navigator:browser" + macanimationtype="document" + retargetdocumentfocus="urlbar" + drawtitle="true" + persist="screenX screenY width height sizemode"> + + <!-- Generic Utility --> + <script src="chrome://global/content/viewSourceUtils.js"/> + + <!-- Content Area --> + <script src="chrome://navigator/content/nsBrowserStatusHandler.js"/> + <script src="chrome://navigator/content/nsBrowserContentListener.js"/> + <script src="chrome://communicator/content/contentAreaClick.js"/> + <script src="chrome://communicator/content/findUtils.js"/> + <script src="chrome://global/content/printUtils.js"/> + + <!-- Navigator --> + <script src="chrome://navigator/content/fullScreen.js"/> + <script src="chrome://navigator/content/navigatorDD.js"/> + <script src="chrome://navigator/content/sessionHistoryUI.js"/> + + <!-- Places Bookmarks Utilities --> + <script src="chrome://navigator/content/browser-places.js"/> + + <!-- hook for stringbundle overlays --> + <stringbundleset id="stringbundleset"> + <stringbundle id="findBundle" src="chrome://global/locale/finddialog.properties"/> + </stringbundleset> + + <commandset id="commands"> + <commandset id="findTypeMenuItems"/> + <command id="toggleSidebar"/> + </commandset> + + <commandset id="mainCommandSet"/> <!-- Firefox extension compatibility --> + + <!-- broadcasters are appended from the overlay --> + <broadcasterset id="navBroadcasters"/> + <broadcasterset id="mainBroadcasterSet"/> <!-- Firefox extension compatibility --> + + <!-- keys are appended from the overlay --> + <keyset id="navKeys"> + <key id="showHideSidebar"/> + </keyset> + <keyset id="mainKeyset"/> <!-- Firefox extension compatibility --> + + <popupset id="mainPopupSet"> + <menupopup id="backMenu" + position="after_start" + onpopupshowing="return BrowserBackMenu(event);" + oncommand="gotoHistoryIndex(event);" + onclick="checkForMiddleClick(this, event);"/> + <menupopup id="forwardMenu" + position="after_start" + onpopupshowing="return BrowserForwardMenu(event);" + oncommand="gotoHistoryIndex(event);" + onclick="checkForMiddleClick(this, event);"/> + <tooltip id="aHTMLTooltip" + onpopupshowing="return FillInHTMLTooltip(document.tooltipNode);"/> + <menupopup id="sidebarPopup"/> + + <tooltip id="home-button-tooltip" noautohide="true"> + <vbox id="home-button-tooltip-inner" flex="1"/> + </tooltip> + + <menupopup id="toolbar-context-menu"/> + + <menupopup id="feedsPopup" popupanchor="bottomright" popupalign="topright" + onpopupshowing="window.XULBrowserWindow.populateFeeds(this);" + oncommand="subscribeToFeed(event.target.statusText, event);" + onclick="checkForMiddleClick(this, event);"/> + + <!-- for content formfill and password manager --> + <panel id="PopupAutoComplete" + type="autocomplete-richlistbox" + noautofocus="true" + hidden="true"/> + + <!-- for date/time picker. consumeoutsideclicks is set to never, so that + clicks on the anchored input box are never consumed. --> + <panel id="DateTimePickerPanel" + type="arrow" + hidden="true" + orient="vertical" + noautofocus="true" + norolluponanchor="true" + consumeoutsideclicks="never" + level="parent"> + </panel> + + <!-- for invalid form error message --> + <panel id="invalid-form-popup" noautofocus="true" hidden="true" level="parent"> + <description/> + </panel> + + <panel id="editBookmarkPanel" + type="arrow" + animate="false" + orient="vertical" + ignorekeys="true" + hidden="true" + onpopupshown="StarUI.panelShown(event);" + aria-labelledby="editBookmarkPanelTitle"> + <row id="editBookmarkPanelHeader" align="center" hidden="true"> + <vbox align="center"> + <image id="editBookmarkPanelStarIcon"/> + </vbox> + <label id="editBookmarkPanelTitle"/> + </row> + <vbox id="editBookmarkPanelContent" hidden="true"/> + <hbox id="editBookmarkPanelBottomButtons"> + <button id="editBookmarkPanelRemoveButton" + class="editBookmarkPanelHeaderButton" + oncommand="StarUI.removeBookmarkButtonCommand();" + accesskey="&editBookmark.removeBookmark.accessKey;"/> + <spacer flex="1"/> + <button id="editBookmarkPanelDeleteButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.cancel.label;" + oncommand="StarUI.cancelButtonOnCommand();"/> + <button id="editBookmarkPanelDoneButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.done.label;" + default="true" + oncommand="StarUI.panel.hidePopup();"/> + </hbox> + </panel> + + <menupopup id="placesContext"/> + + <!-- Bookmarks and history tooltip --> + <tooltip id="bhTooltip"/> + + <panel id="notification-popup" + type="arrow" + animate="false" + position="after_start" + hidden="true" + role="alert"/> + + <menupopup id="popupBlockerMenu" + oncommand="popupBlockerMenuCommand(event.target);" + onpopupshowing="return popupBlockerMenuShowing(event);" + onpopuphiding="RemovePopupsItems(this);"/> + <!-- Items are generated, see popupBlockerMenuShowing() --> + <menupopup id="popupNotificationMenu"/> + <menupopup id="networkProperties"/> + + <menupopup id="security-context-menu" + onpopupshowing="onViewSecurityContextMenu();"> + <menuitem id="viewSecurityInfo" + default="true" + label="&viewSecurityInfo.label;" + accesskey="&viewSecurityInfo.accesskey;" + oncommand="BrowserPageInfo(null, 'securityTab');"/> + <menuitem id="viewCertificate" + label="&viewCertificate.label;" + accesskey="&viewCertificate.accesskey;" + oncommand="viewCertificate();"/> + <menuseparator/> + <menuitem id="viewCertManager" + label="&viewCertManager.label;" + accesskey="&viewCertManager.accesskey;" + oncommand="openCertManager();"/> + </menupopup> + + <popupnotification id="password-notification" hidden="true"> + <popupnotificationcontent orient="vertical"> + <textbox id="password-notification-username"/> + <textbox id="password-notification-password" type="password" show-content=""/> + <checkbox id="password-notification-visibilityToggle" hidden="true"/> + </popupnotificationcontent> + </popupnotification> + + </popupset> + + <!-- context menu --> + <popupset id="contentAreaContextSet"/> + + <vbox id="titlebar"/> + + <toolbox id="navigator-toolbox" class="toolbox-top" deferattached="true" + mode="full" defaultmode="full"> + <!-- Menu --> + <toolbar type="menubar" id="toolbar-menubar" class="chromeclass-menubar" + persist="collapsed" grippytooltiptext="&menuBar.tooltip;" customizable="true" + defaultset="menubar-items" + mode="icons" iconsize="small" + defaultmode="icons" defaulticonsize="small" + context="toolbar-context-menu"> + <toolbaritem id="menubar-items" class="menubar-items" align="center"> + <menubar id="main-menubar"/> + </toolbaritem> + </toolbar> + + <toolbar class="toolbar-primary chromeclass-toolbar" id="nav-bar" persist="collapsed" + grippytooltiptext="&navigationToolbar.tooltip;" + fullscreentoolbar="true" customizable="true" + toolbarname="&navbarCmd.label;" accesskey="&navbarCmd.accesskey;" + defaultset="back-button,forward-button,reload-button,stop-button,nav-bar-inner,search-button-container,print-button,throbber-box,window-controls" + context="toolbar-context-menu"> + + <hbox id="window-controls" hidden="true" fullscreencontrol="true"> + <toolbarbutton id="minimize-button" + tooltiptext="&minimizeButton.tooltip;" + oncommand="window.minimize();"/> + + <toolbarbutton id="restore-button" + tooltiptext="&restoreButton.tooltip;" + oncommand="BrowserFullScreen();"/> + + <toolbarbutton id="close-button" + tooltiptext="&closeWindow.label;" + oncommand="BrowserTryToCloseWindow();"/> + </hbox> + </toolbar> + + <toolbarset id="customToolbars" context="toolbar-context-menu"/> + + <toolbar id="PersonalToolbar" + accesskey="&bookmarksToolbarCmd.accesskey;" + class="chromeclass-directories" + persist="collapsed" + grippytooltiptext="&bookmarksToolbar.tooltip;" + toolbarname="&bookmarksToolbarCmd.label;" + nowindowdrag="true" + customizable="true" + defaultset="home-button,separator,bookmarks-button,personal-bookmarks" + mode="full" + iconsize="small" + labelalign="end" + defaultmode="full" + defaulticonsize="small" + defaultlabelalign="end" + context="toolbar-context-menu"> + </toolbar> + + <toolbarpalette id="BrowserToolbarPalette"> + + <!-- Nav bar buttons --> + <toolbarbutton id="back-button" type="menu-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&backButton.label;" + oncommand="if (event.target==this) BrowserBack(event); else gotoHistoryIndex(event);" + onclick="checkForMiddleClick(this, event);" + context="backMenu" + tooltiptext="&backButton.tooltip;"> + <observes element="canGoBack" attribute="disabled"/> + <menupopup context="" onpopupshowing="BrowserBackMenu(event);"/> + </toolbarbutton> + + <toolbarbutton id="forward-button" type="menu-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&forwardButton.label;" + oncommand="if (event.target==this) BrowserForward(event); else gotoHistoryIndex(event);" + onclick="checkForMiddleClick(this, event);" + context="forwardMenu" + tooltiptext="&forwardButton.tooltip;"> + <observes element="canGoForward" attribute="disabled"/> + <menupopup context="" onpopupshowing="BrowserForwardMenu(event);"/> + </toolbarbutton> + + <toolbarbutton id="reload-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&reloadButton.label;" + oncommand="BrowserReload(event);" + onclick="checkForMiddleClick(this, event);" + tooltiptext="&reloadButton.tooltip;"/> + + <toolbarbutton id="stop-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&stopButton.label;" + oncommand="BrowserStop();" observes="canStop" + disabled="true" + tooltiptext="&stopButton.tooltip;"> + </toolbarbutton> + + <!-- XXXRatty ? class="toolbarbutton-1 chromeclass-toolbar-additional" ? --> + <toolbarbutton id="home-button" + class="toolbarbutton-1" + label="&homeButton.label;" + oncommand="BrowserHome(event);" + onclick="if (event.button == 1) BrowserHome(event);" + tooltip="home-button-tooltip" + ondragstart="homeButtonObserver.onDragStart(event);" + ondrop="homeButtonObserver.onDrop(event);" + ondragenter="event.stopPropagation();" + ondragexit="homeButtonObserver.onDragExit(event);" + ondragover="homeButtonObserver.onDragOver(event);"/> + + <toolbaritem id="nav-bar-inner" + flex="4" + persist="width" + class="chromeclass-location nav-bar-class" + title="&locationBar.title;"> + <textbox id="urlbar" class="chromeclass-location uri-element" flex="1" + type="autocomplete" autocompletesearch="history file" + timeout="50" maxrows="6" + enablehistory="true" accesskey="&locationBar.accesskey;" + defaultSearchEngine="true" tabscrolling="true" + showcommentcolumn="true" + placeholder="&locationBar.tooltip;" + newlines="stripsurroundingwhitespace" + aria-label="&locationBar.title;" + oninput="gBrowser.userTypedValue = this.value;" + ontextentered="return handleURLBarCommand(eventParam, domEvent);" + ontextreverted="return handleURLBarRevert();" + onfocus="URLBarFocusHandler(event);" + onmousedown="URLBarMouseDownHandler(event);" + onclick="URLBarClickHandler(event);"> + <box id="notification-popup-box" hidden="true" align="center" + onmousedown="event.stopPropagation();" + onclick="event.stopPropagation();"> + <image id="default-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="geo-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="addons-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="indexedDB-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="password-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="plugins-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="web-notifications-notification-icon" class="notification-anchor-icon" role="button"/> + </box> + <deck id="page-proxy-deck" + class="urlbar-icons" + ondragstart="proxyIconDNDObserver.onDragStart(event);" + onclick="handlePageProxyClick(event);"> + <image id="page-proxy-button" + tooltiptext="&proxyIcon.tooltip;"/> + <image id="page-proxy-favicon" validate="never" + onload="this.parentNode.selectedIndex = 1; + event.stopPropagation();" + onerror="gBrowser.addToMissedIconCache(this.src);" + tooltiptext="&proxyIcon.tooltip;"/> + </deck> + <hbox id="urlbar-icons" class="urlbar-icons" + onmousedown="event.stopPropagation();" + onclick="event.stopPropagation();"> + <image id="feedsButton" hidden="true" popup="feedsPopup"/> + <image id="ev-button" hidden="true" + onclick="if (event.button == 0) BrowserPageInfo(null, 'securityTab');"/> + <image id="star-button" + onclick="BookmarkingUI.onClick(event);"/> + </hbox> + <menupopup id="ubhist-popup" class="autocomplete-history-popup" + popupalign="topleft" popupanchor="bottomleft" + onpopupshowing="createUBHistoryMenu(event.target);" + oncommand="executeUrlBarHistoryCommand(event.target);"/> + </textbox> + </toolbaritem> + + <toolbaritem id="go-button-container" + class="nav-bar-class" + title="&goButton.label;"> + <button id="go-button" + class="button-toolbar chromeclass-location" + label="&goButton.label;" + tooltiptext="&goButton.tooltip;" + default="true" + oncommand="handleURLBarCommand('none', event);" + onclick="checkForMiddleClick(this, event);" + ondragover="goButtonObserver.onDragOver(event);" + ondrop="goButtonObserver.onDrop(event);"/> + </toolbaritem> + + <toolbaritem id="search-button-container" + class="nav-bar-class" + title="&searchButton.label;"> + <button id="search-button" + class="button-toolbar chromeclass-location" + label="&searchButton.label;" + tooltiptext="&searchButton.tooltip;" + oncommand="BrowserSearch.loadSearch(QualifySearchTerm());" + ondragover="searchButtonObserver.onDragOver(event);" + ondrop="searchButtonObserver.onDrop(event);"/> + </toolbaritem> + + <toolbaritem id="search-container" title="&searchItem.title;" + align="center" class="chromeclass-toolbar-additional nav-bar-class" + flex="1" persist="width" removable="true"> + <searchbar id="searchbar" flex="1"/> + </toolbaritem> + + <toolbarbutton id="print-button" + label="&printButton.label;" + tooltiptext="&printButton.tooltip;"/> + + <toolbaritem id="throbber-box"/> + + <!-- "Bookmarks" button on Bookmarks Toolbar --> + <toolbarbutton type="menu" id="bookmarks-button" + class="bookmark-item" container="true" + label="&bookmarksButton.label;" + ondragenter="PlacesMenuDNDHandler.onDragEnter(event);" + ondragover="PlacesMenuDNDHandler.onDragOver(event);" + ondragexit="PlacesMenuDNDHandler.onDragExit(event);" + ondrop="PlacesMenuDNDHandler.onDrop(event);"> + <menupopup id="BMB_bookmarksPopup" + placespopup="true" + context="placesContext" + openInTabs="children" + oncommand="BookmarksEventHandler.onCommand(event);" + onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);" + onpopupshowing="BookmarksMenu.onPopupShowing(event, 'BMB_'); + BookmarksEventHandler.onPopupShowing(event);" + tooltip="bhTooltip" popupsinherittooltip="true"> + <menuitem command="Browser:AddBookmark"/> + <menuitem command="Browser:AddBookmarkAs"/> + <menuitem command="Browser:BookmarkAllTabs"/> + <menuitem command="Browser:ManageBookmark"/> + <menuseparator/> + <menu id="BMB_feedsMenu" class="menu-iconic feedsMenu" command="feedsMenu" + label="&feedsMenu.label;" accesskey="&feedsMenu.accesskey;"> + <menupopup onpopupshowing="window.XULBrowserWindow.populateFeeds(this);" + oncommand="subscribeToFeed(event.target.statusText, event);" + onclick="checkForMiddleClick(this, event);"/> + </menu> + <menuseparator/> + <menu id="BMB_bookmarksToolbarFolderMenu" + class="menu-iconic bookmark-item" + label="&bookmarksToolbarCmd.label;" + container="true"> + <menupopup id="BMB_bookmarksToolbarFolderPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=TOOLBAR');"/> + </menu> + <menu id="BMB_unsortedBookmarksFolderMenu" + class="menu-iconic bookmark-item" + container="true"> + <menupopup id="BMB_unsortedBookmarksFolderPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=UNFILED_BOOKMARKS');"/> + </menu> + <menuseparator/> + </menupopup> + </toolbarbutton> + + <toolbaritem flex="1" id="personal-bookmarks" title="&bookmarksToolbarItem.label;" + removable="true"> + <hbox flex="1" + id="PlacesToolbar" + context="placesContext" + onclick="BookmarksEventHandler.onClick(event, this._placesView);" + oncommand="BookmarksEventHandler.onCommand(event);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <toolbarbutton class="bookmark-item bookmarks-toolbar-customize" + mousethrough="never" + label="&bookmarksToolbarItem.label;"/> + <hbox flex="1"> + <hbox align="center"> + <image id="PlacesToolbarDropIndicator" + mousethrough="always" + collapsed="true"/> + </hbox> + <scrollbox orient="horizontal" + id="PlacesToolbarItems" + flex="1"/> + <toolbarbutton type="menu" + id="PlacesChevron" + class="chevron" + mousethrough="never" + collapsed="true" + tooltiptext="&bookmarksToolbarChevron.tooltip;" + onpopupshowing="this.parentNode.parentNode + ._placesView._onChevronPopupShowing(event);"> + <menupopup id="PlacesChevronPopup" + placespopup="true" + tooltip="bhTooltip" popupsinherittooltip="true" + context="placesContext"/> + </toolbarbutton> + </hbox> + </hbox> + </toolbaritem> + + <!-- see utilityOverlay.xul + <toolbarbutton id="sync-button"/> --> + </toolbarpalette> + </toolbox> + + <hbox flex="1"> + <vbox id="sidebar-box" class="chromeclass-extrachrome" domfullscreenhidden="true"/> + <splitter id="sidebar-splitter" class="chromeclass-extrachrome" domfullscreenhidden="true"/> + + <vbox id="appcontent" flex="1"> + <findbar id="FindToolbar" browserid="content" domfullscreenhidden="true"/> + + <!-- this box is temporary, pending XBLified <browser> --> + <hbox id="browser" flex="1"> + <tabbrowser id="content" + flex="1" contenttooltip="aHTMLTooltip" + contentcontextmenu="contentAreaContextMenu" + onnewtab="BrowserOpenTab();" + onnewtabclick="checkForMiddleClick(this, event);" + autocompletepopup="PopupAutoComplete" + datetimepicker="DateTimePickerPanel" + onbookmarkgroup="PlacesCommandHook.bookmarkCurrentPages();" + oncontentclick="return contentAreaClick(event);" + oncommand="BrowserOnCommand(event);"/> + <!-- The oncommand listener above lets us fix bugs like 401575 which + require error page UI to do privileged things, without letting + error pages have any privilege themselves. --> + </hbox> + </vbox> + </hbox> + + <panel id="customizeToolbarSheetPopup"/> + + <statusbar id="status-bar" class="chromeclass-status"> + <statusbarpanel id="component-bar"/> + <statusbarpanel id="statusbar-display" label="&statusText.label;" flex="1"/> + <statusbarpanel class="statusbarpanel-progress" id="statusbar-progresspanel" collapsed="true"> + <progressmeter class="progressmeter-statusbar" id="statusbar-icon" mode="normal" value="0"/> + </statusbarpanel> + <statusbarpanel class="statusbarpanel-iconic-text zoom-button-align" id="zoomOut-button" label="[-]" + oncommand="zoomOut();" + tooltiptext="&zoomOut.tooltiptext;"/> + <statusbarpanel class="statusbarpanel-iconic-text" id="zoomLevel-display" label="" oncommand="zoomReset();"/> + <statusbarpanel class="statusbarpanel-iconic-text zoom-button-align" id="zoomIn-button" label="[+]" + oncommand="zoomIn();" + tooltiptext="&zoomIn.tooltiptext;"/> + <statusbarpanel id="popupIcon" class="statusbarpanel-iconic" hidden="true" + oncommand="StatusbarViewPopupManager()" + tooltiptext="&popupIcon.tooltiptext;" + context="popupBlockerMenu"/> + <statusbarpanel class="statusbarpanel-iconic" id="offline-status"/> + <statusbarpanel class="statusbarpanel-backgroundbox" + id="security-button" dir="reverse" + context="security-context-menu" + oncommand="BrowserPageInfo(null, 'securityTab')"/> + </statusbar> +</window> diff --git a/comm/suite/browser/navigatorDD.js b/comm/suite/browser/navigatorDD.js new file mode 100644 index 0000000000..19c4b8f228 --- /dev/null +++ b/comm/suite/browser/navigatorDD.js @@ -0,0 +1,121 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function htmlEscape(aString) +{ + return aString.replace(/&/g, "&") + .replace(/>/g, ">") + .replace(/</g, "<") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function BeginDragLink(aEvent, aHref, aTitle) +{ + var dt = aEvent.dataTransfer; + dt.setData("text/x-moz-url", aHref + "\n" + aTitle); + dt.setData("text/uri-list", aHref); + dt.setData("text/html", "<a href=\"" + htmlEscape(aHref) + + "\">" + htmlEscape(aTitle) + "</a>"); + dt.setData("text/plain", aHref); +} + +function DragLinkOver(aEvent) +{ + if (Services.droppedLinkHandler.canDropLink(aEvent, true)) + aEvent.preventDefault(); +} + +var proxyIconDNDObserver = { + onDragStart: function (aEvent) + { + if (gProxyButton.getAttribute("pageproxystate") != "valid") + return; + + BeginDragLink(aEvent, window.content.location.href, + window.content.document.title); + } +}; + +var homeButtonObserver = { + onDragStart: function (aEvent) + { + var homepage = GetLocalizedStringPref("browser.startup.homepage", + "about:blank"); + + if (homepage) + { + // XXX find a readable title string for homepage, + // perhaps do a history lookup. + BeginDragLink(aEvent, homepage, homepage); + } + }, + + onDrop: function (aEvent) + { + aEvent.stopPropagation(); + // disallow setting home pages that inherit the principal + var url = Services.droppedLinkHandler.dropLink(aEvent, {}, true); + setTimeout(openHomeDialog, 0, url); + }, + + onDragOver: function (aEvent) + { + if (aEvent.target == aEvent.dataTransfer.mozSourceNode) + return; + + DragLinkOver(aEvent); + aEvent.dropEffect = "link"; + var statusTextFld = document.getElementById("statusbar-display"); + statusTextFld.label = gNavigatorBundle.getString("droponhomebutton"); + }, + + onDragExit: function (aEvent) + { + aEvent.stopPropagation(); + document.getElementById("statusbar-display").label = ""; + } +}; + +function openHomeDialog(aURL) +{ + var promptTitle = gNavigatorBundle.getString("droponhometitle"); + var promptMsg = gNavigatorBundle.getString("droponhomemsg"); + var okButton = gNavigatorBundle.getString("droponhomeokbutton"); + if (Services.prompt.confirmEx(window, promptTitle, promptMsg, + (Services.prompt.BUTTON_TITLE_IS_STRING * + Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_CANCEL * + Services.prompt.BUTTON_POS_1), + okButton, null, null, null, + {value: false}) == 0) + SetStringPref("browser.startup.homepage", aURL); +} + +var goButtonObserver = { + onDragOver: DragLinkOver, + + onDrop: function (aEvent) + { + var url = Services.droppedLinkHandler.dropLink(aEvent, {}); + + getShortcutOrURIAndPostData(url).then(data => { + if (data.url) + loadURI(data.url, null, data.postData, false); + }); + } +}; + +var searchButtonObserver = { + onDragOver: DragLinkOver, + + onDrop: function (aEvent) + { + var name = {}; + var url = Services.droppedLinkHandler.dropLink(aEvent, name); + if (url) + BrowserSearch.loadSearch(name.value || url); + } +}; diff --git a/comm/suite/browser/navigatorOverlay.xul b/comm/suite/browser/navigatorOverlay.xul new file mode 100644 index 0000000000..45e256b123 --- /dev/null +++ b/comm/suite/browser/navigatorOverlay.xul @@ -0,0 +1,716 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/viewZoomOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/viewApplyThemeOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/places/placesOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/charsetOverlay.xul"?> +<?xul-overlay href="chrome://navigator/content/mailNavigatorOverlay.xul"?> + +<!DOCTYPE overlay [ +<!ENTITY % navigatorDTD SYSTEM "chrome://navigator/locale/navigator.dtd" > +%navigatorDTD; +<!ENTITY % navigatorOverlayDTD SYSTEM "chrome://navigator/locale/navigatorOverlay.dtd"> +%navigatorOverlayDTD; +<!ENTITY % contentAreaCommandsDTD SYSTEM "chrome://communicator/locale/contentAreaCommands.dtd" > +%contentAreaCommandsDTD; +]> + +<overlay id="navigatorOverlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <!-- Navigator --> + <script src="chrome://navigator/content/navigator.js"/> + + <!-- Places Bookmarks Utilities --> + <script src="chrome://navigator/content/browser-places.js"/> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_navigator" + src="chrome://navigator/locale/navigator.properties"/> + <stringbundle id="bundle_brand" + src="chrome://branding/locale/brand.properties"/> + <stringbundle id="bundle_navigator_region" + src="chrome://navigator-region/locale/region.properties"/> + <stringbundle id="bundle_viewZoom"/> + <stringbundle id="bundle_viewApplyTheme"/> + </stringbundleset> + + <!-- Keysets --> + <keyset id="navKeys"> + <!-- File Menu --> + <key id="key_newNavigatorTab" key="&tabCmd.commandkey;" modifiers="accel" command="cmd_newNavigatorTab"/> + <key id="key_newNavigator"/> + <key id="key_newPrivateWindow"/> + <key id="key_restoreTab" key="&recentTabs.commandkey;" modifiers="accel,shift" oncommand="gBrowser.undoCloseTab(0);"/> + <key id="key_restoreWindow" key="&recentWindows.commandkey;" modifiers="accel,shift" oncommand="undoCloseWindow();"/> + <key id="key_newBlankPage"/> + <key id="focusURLBar" key="&openCmd.commandkey;" oncommand="ShowAndSelectContentsOfURLBar();" + modifiers="accel"/> + <key id="openLocationKb" key="&openCmd.commandkey;" command="Browser:Open" modifiers="accel,shift"/> + <key id="openFileKb" key="&openFileCmd.commandkey;" command="Browser:OpenFile" modifiers="accel"/> + <key id="key_savePage" key="&savePageCmd.commandkey;" command="Browser:SavePage" modifiers="accel"/> + <key id="key_editPage" key="&editPageCmd.commandkey;" command="Browser:EditPage" modifiers="accel"/> + <key id="key_print"/> + <key id="key_close"/> + <key id="key_closeWindow"/> + + <!-- Edit Menu --> + <key id="key_undo"/> + <key id="key_redo"/> + <key id="key_cut"/> + <key id="key_copy"/> + <key id="key_paste"/> + <key id="key_delete"/> + <key id="key_delete2"/> + <key id="key_selectAll"/> + <key id="key_switchTextDirection"/> + + <!-- View Menu --> + <key id="key_reload" key="&reloadCmd.commandkey;" oncommand="BrowserReload();" modifiers="accel"/> + <key key="&reloadCmd.commandkey;" oncommand="BrowserReloadSkipCache();" modifiers="accel,shift"/> + <key id="key_viewSource" key="&pageSourceCmd.commandkey;" command="View:PageSource" modifiers="accel"/> + <key id="key_viewInfo" key="&pageInfoCmd.commandkey;" command="View:PageInfo" modifiers="accel"/> + <key id="key_viewNextSidebarPanel" keycode="VK_PAGE_DOWN" oncommand="SidebarGetRelativePanel(1);" modifiers="alt" /> + <key id="key_viewPrevSidebarPanel" keycode="VK_PAGE_UP" oncommand="SidebarGetRelativePanel(-1);" modifiers="alt" /> + + <!-- Search Menu --> + <keyset id="findKeys"/> + + <!-- Go Menu --> + <key keycode="VK_BACK" command="cmd_handleBackspace"/> + <key keycode="VK_BACK" command="cmd_handleShiftBackspace" modifiers="shift"/> + + <!-- Bookmarks Menu --> + <key id="addBookmarkKb" key="&addCurPageAsCmd.commandkey;" command="Browser:AddBookmark" modifiers="accel,shift"/> + <key id="addBookmarkAsKb" key="&addCurPageAsCmd.commandkey;" command="Browser:AddBookmarkAs" modifiers="accel"/> + <key id="manBookmarkKb" key="&manBookmarksCmd.commandkey;" command="Browser:ManageBookmark" modifiers="accel"/> + + <!-- Tools Menu --> + <key id="searchInternetKb" key="&searchInternet.commandKey;" modifiers="accel,shift" command="Browser:SearchInternet"/> + + <!-- Misc --> + <!-- the amazing fishcam, suppress warning by ',' at the beginning of modifiers, see bug 496322 --> + <key key="f" modifiers=",control,alt" oncommand="loadURI('http://www.fishcam.com/');"/> + <key id="goUpKb" keycode="VK_UP" command="Browser:Up" modifiers="alt"/> + <key id="key_gotoHistory" +#ifndef XP_MACOSX + key="&history.commandKey;" + modifiers="accel" +#else + key="&historyCmd.key;" + modifiers="accel,shift" +#endif + oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/> + <keyset id="viewZoomKeys"/> + <keyset id="navigationKeys"> +#ifndef XP_MACOSX + <key id="goBackKb" keycode="VK_LEFT" + command="Browser:Back" modifiers="alt"/> + <key id="goForwardKb" keycode="VK_RIGHT" + command="Browser:Forward" modifiers="alt"/> +#else + <key id="goBackKb" keycode="VK_LEFT" + command="Browser:Back" modifiers="accel"/> + <key id="goForwardKb" keycode="VK_RIGHT" + command="Browser:Forward" modifiers="accel"/> +#endif +#ifndef XP_WIN + <key id="goBackKb2" key="&goBackCmd.commandKey;" + command="Browser:Back" modifiers="accel"/> + <key id="goForwardKb2" key="&goForwardCmd.commandKey;" + command="Browser:Forward" modifiers="accel"/> +#endif + <key id="key_stop" keycode="VK_ESCAPE" oncommand="BrowserStop();"/> +#ifdef XP_MACOSX + <key id="key_stop_mac" key="&stopCmd.macCommandKey;" + oncommand="BrowserStop();" modifiers="accel"/> +#endif + +#ifndef XP_MACOSX + <key keycode="VK_F5" oncommand="BrowserReload();"/> + <key keycode="VK_F5" + oncommand="BrowserReloadSkipCache();" modifiers="control"/> + <key id="goHome" keycode="VK_HOME" + oncommand="BrowserHome();" modifiers="alt"/> +#else + <key id="goHome" keycode="VK_HOME" + oncommand="BrowserHome();" modifiers="meta"/> +#endif + +#ifdef XP_MACOSX + <key keycode="VK_F11" command="View:FullScreen"/> + <key id="key_fullScreen" key="&fullScreenCmd.commandKey;" + command="View:FullScreen" modifiers="accel,shift"/> +#else + <key id="key_fullScreen" keycode="VK_F11" command="View:FullScreen"/> + <key id="key_newTabWithTargetBg" keycode="VK_INSERT" + command="cmd_newTabWithTarget"/> +#endif + <key id="key_newTabWithTargetFg" keycode="VK_INSERT" + modifiers="alt" command="cmd_newTabWithTarget"/> + </keyset> + <keyset id="tasksKeys"/> + <key id="key_sanitize" keycode="VK_DELETE" + command="Tools:Sanitize" modifiers="accel,shift"/> +#ifdef XP_MACOSX + <key id="key_sanitize_mac" keycode="VK_BACK" + command="Tools:Sanitize" modifiers="accel,shift"/> +#endif + </keyset> + + <commandset id="commands"> + <command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab();"/> + <command id="cmd_newNavigator"/> + <command id="cmd_newPrivateWindow"/> + <command id="cmd_newTabWithTarget" oncommand="contentAreaClick(event);"/> + <command id="cmd_handleBackspace" oncommand="BrowserHandleBackspace();" /> + <command id="cmd_handleShiftBackspace" oncommand="BrowserHandleShiftBackspace();" /> + + <command id="cmd_newEditor"/> + <!-- NOT IMPLEMENTED + <command id="cmd_newEditorTemplate"/> + <command id="cmd_newEditorDraft"/> --> + <command id="Browser:OpenFile" oncommand="BrowserOpenFileWindow();"/> + <command id="Browser:SavePage" oncommand="saveDocument(window.content.document, true);"/> + <command id="Browser:EditPage" oncommand="editPageOrFrame();" observes="isImage"/> + <command id="Browser:UploadFile" oncommand="BrowserUploadFile();"/> + <command id="Browser:Open" oncommand="BrowserOpenWindow();"/> + <command id="cmd_printSetup" oncommand="PrintUtils.showPageSetup();"/> + <command id="cmd_print" oncommand="PrintUtils.printWindow(window.gBrowser.selectedBrowser.outerWindowID, window.gBrowser.selectedBrowser);"/> + <command id="cmd_printpreview" oncommand="BrowserPrintPreview();"/> + <command id="cmd_close" oncommand="BrowserCloseTabOrWindow()"/> + <command id="cmd_closeOtherTabs" oncommand="BrowserCloseOtherTabs()"/> + <command id="cmd_closeTabsToTheEnd" + oncommand="BrowserCloseTabsToTheEnd();"/> + <command id="cmd_closeWindow" oncommand="BrowserTryToCloseWindow()"/> + + <!-- Edit Menu --> + <command id="cmd_undo"/> + <command id="cmd_redo"/> + <command id="cmd_cut"/> + <command id="cmd_copy"/> + <command id="cmd_paste"/> + <command id="cmd_delete"/> + <command id="cmd_selectAll" observes="isImage"/> + <command id="cmd_switchTextDirection"/> + <commandset id="globalEditMenuItems"/> + <commandset id="selectEditMenuItems"/> + <commandset id="undoEditMenuItems"/> + <commandset id="clipboardEditMenuItems"/> + + <!-- Content area context menu --> + <command id="cmd_copyLink"/> + <command id="cmd_copyImage"/> + + <!-- View Menu --> + <command id="View:PageSource" oncommand="BrowserViewSource(gBrowser.selectedBrowser);" observes="isImage"/> + <command id="View:PageInfo" oncommand="BrowserPageInfo();"/> + <command id="View:FullScreen" oncommand="BrowserFullScreen();"/> + <command id="cmd_SwitchDocumentDirection" oncommand="SwitchDocumentDirection(window.content);" /> + + <!-- Search Menu --> + <command id="cmd_find" + oncommand="BrowserFind();" + observes="isImage"/> + <command id="cmd_findNext" + oncommand="BrowserFindAgain(false);" + observes="isImage"/> + <command id="cmd_findPrev" + oncommand="BrowserFindAgain(true);" + observes="isImage"/> + <command id="cmd_findTypeText" observes="isImage"/> + <command id="cmd_findTypeLinks" observes="isImage"/> + + <!-- Bookmarks Menu --> + <command id="Browser:AddBookmark" + label="&addCurPageCmd.label;" accesskey="&addCurPageCmd.accesskey;" + oncommand="PlacesCommandHook.bookmarkPage(gBrowser.selectedBrowser, + false);"/> + <command id="Browser:AddBookmarkAs" + label="&addCurPageAsCmd.label;" accesskey="&addCurPageAsCmd.accesskey;" + oncommand="PlacesCommandHook.bookmarkPage(gBrowser.selectedBrowser, + true);"/> + <!-- The command is disabled for the hidden window. Otherwise its enabled + state is handled by BookmarksEventHandler.onPopupShowing. --> + <command id="Browser:BookmarkAllTabs" + label="&addCurTabsAsCmd.label;" accesskey="&addCurTabsAsCmd.accesskey;" + oncommand="PlacesCommandHook.bookmarkCurrentPages();" + disabled="true"/> + <command id="Browser:ManageBookmark" + label="&manBookmarksCmd.label;" accesskey="&manBookmarksCmd.accesskey;" + oncommand="PlacesCommandHook.showPlacesOrganizer('AllBookmarks');"/> + <command id="feedsMenu" disabled="true"/> + <commandset id="placesCommands"/> + + <!-- Go Menu --> + <command id="Browser:Home" oncommand="BrowserHome(event);"/> + <command id="Browser:Back" oncommand="BrowserBack();" observes="canGoBack"/> + <command id="Browser:Forward" oncommand="BrowserForward();" observes="canGoForward"/> + <command id="Browser:Up" oncommand="BrowserUp();" observes="canGoUp"/> + <commandset id="viewZoomCommands"/> + <commandset id="tasksCommands"/> + + <!-- Tools Menu --> + <command id="Browser:SearchInternet" oncommand="BrowserSearch.webSearch();"/> + <command id="Tools:Sanitize" + oncommand="Cc['@mozilla.org/suite/suiteglue;1'].getService(Ci.nsISuiteGlue).sanitize(window);"/> + + </commandset> + + <broadcasterset id="navBroadcasters"> + <broadcaster id="canGoBack" disabled="true"/> + <broadcaster id="canGoForward" disabled="true"/> + <broadcaster id="canGoUp" disabled="true"/> + <broadcaster id="Communicator:WorkMode"/> + <broadcaster id="cmd_viewtaskbar" + checked="true" + oncommand="goToggleToolbar('status-bar', 'cmd_viewtaskbar'); + updateWindowState();"/> + <broadcaster id="cmd_viewcomponentbar" oncommand="goToggleToolbar('component-bar', 'cmd_viewcomponentbar');" checked="true"/> + <broadcaster id="isImage"/> + </broadcasterset> + + <!-- Menu --> + <menubar id="main-menubar" class="chromeclass-menubar"> + <menu id="menu_File"> + <menupopup id="menu_FilePopup" onpopupshowing="updateCloseItems();getContentAreaFrameCount();updateSavePageItems();updateFileUploadItem();"> + <menu id="menu_New"> + <menupopup id="menu_NewPopup"> + <!-- From utilityOverlay.xul --> + <menuitem id="menu_newNavigatorTab" command="cmd_newNavigatorTab" key="key_newNavigatorTab" + label="&tabCmd.label;" accesskey="&tabCmd.accesskey;"/> + <menuitem id="menu_newNavigator"/> + <menuitem id="menu_newPrivateWindow"/> + <menuseparator id="navBeginGlobalNewItems"/> + <menuitem id="menu_newEditor" command="cmd_newEditor"/> + </menupopup> + </menu> + <menuitem id="menu_openLocation" + label="&openCmd.label;" + accesskey="&openCmd.accesskey;" + key="openLocationKb" + command="Browser:Open"/> + <menuitem id="menu_openFile" + label="&openFileCmd.label;" + accesskey="&openFileCmd.accesskey;" + key="openFileKb" + command="Browser:OpenFile"/> + <menuitem id="menu_close"/> + <menuitem id="menu_closeOtherTabs" command="cmd_closeOtherTabs" label="&closeOtherTabs.label;" accesskey="&closeOtherTabs.accesskey;"/> + <menuitem id="menu_closeTabsToTheEnd" + label="&closeTabsToTheEnd.label;" + accesskey="&closeTabsToTheEnd.accesskey;" + command="cmd_closeTabsToTheEnd"/> + <menuitem id="menu_closeWindow" hidden="true" command="cmd_closeWindow" key="key_closeWindow" label="&closeWindow.label;" accesskey="&closeWindow.accesskey;"/> + <menuseparator/> + <menuitem id="savepage" valueSaveAs="&savePageAsCmd.label;" valueSave="&savePageCmd.label;" + accesskey="&savePageCmd.accesskey;" key="key_savePage" command="Browser:SavePage"/> + <menuitem id="saveframe" valueSaveAs="&saveFrameAsCmd.label;" valueSave="&saveFrameCmd.label;" + accesskey="&saveFrameCmd.accesskey;" oncommand="saveFrameDocument();" hidden="true"/> + <menuseparator id="saveMenuBlockEnd"/> + <menuitem label="&editPageCmd.label;" accesskey="&editPageCmd.accesskey;" key="key_editPage" command="Browser:EditPage" /> + <menuseparator/> + <menuitem command="Browser:UploadFile" label="&uploadFile.label;" accesskey="&uploadFile.accesskey;"/> + <menuseparator/> + <menuitem id="menu_printSetup"/> + <menuitem id="menu_printPreview"/> + <menuitem id="menu_print"/> + <menuseparator/> + <menuitem id="offlineGoOfflineCmd"/> + </menupopup> + </menu> + + <menu id="menu_Edit"> + <menupopup id="menu_EditPopup"> + <menuitem id="menu_undo"/> + <menuitem id="menu_redo"/> + <menuseparator/> + <menuitem id="menu_cut"/> + <menuitem id="menu_copy"/> + <menuitem id="menu_paste"/> + <menuitem id="menu_delete"/> + <menuseparator/> + <menuitem id="menu_selectAll"/> + <menuseparator/> + <menuitem id="menu_find" label="&findOnCmd.label;"/> + <menuitem id="menu_findNext"/> + <menuitem id="menu_findPrev"/> + <menuseparator/> + <menuitem id="menu_findTypeLinks"/> + <menuitem id="menu_findTypeText"/> + + <menuseparator id="textfieldDirection-separator" hidden="true"/> + <menuitem id="textfieldDirection-swap" hidden="true"/> + + <menuseparator id="menu_PrefsSeparator"/> + <menuitem id="menu_preferences" oncommand="goPreferences('navigator_pane')"/> + </menupopup> + </menu> + + <menu id="menu_View"> + <menupopup id="menu_View_Popup" onpopupshowing="EnableCharsetMenu();"> + <menu label="&toolbarsCmd.label;" accesskey="&toolbarsCmd.accesskey;" id="menu_Toolbars"> + <menupopup id="view_toolbars_popup" + onpopupshowing="updateToolbarStates(event);" + oncommand="onViewToolbarCommand(event);"> + <menuitem id="menuitem_showhide_tabbar" + label="&tabbarCmd.label;" + accesskey="&tabbarCmd.accesskey;" + class="menuitem-iconic" + type="checkbox" + oncommand="showHideTabbar();" + checked="true"/> + <menuitem id="menuitem_taskbarCmd" + label="&taskbarCmd.label;" + accesskey="&taskbarCmd.accesskey;" + class="menuitem-iconic" + type="checkbox" + observes="cmd_viewtaskbar"/> + <menuitem id="menuitem_componentbarCmd" + label="&componentbarCmd.label;" + accesskey="&componentbarCmd.accesskey;" + class="menuitem-iconic" + type="checkbox" + observes="cmd_viewcomponentbar"/> + </menupopup> + </menu> + <menuitem id="menuitem_fullScreen" + label="&fullScreenCmd.label;" + accesskey="&fullScreenCmd.accesskey;" + key="key_fullScreen" + command="View:FullScreen"/> + <menuseparator /> + <menuitem id="menuitem-stop" + label="&stopCmd.label;" + accesskey="&stopCmd.accesskey;" + disabled="true" + oncommand="BrowserStop();" + key="key_stop"/> + <menuitem id="menuitem_reload" + label="&reloadCmd.label;" + accesskey="&reloadCmd.accesskey;" + key="key_reload" + oncommand="BrowserReload(event);" + onclick="checkForMiddleClick(this, event);"/> + <menuseparator /> + + <!-- overlayed from viewZoomOverlay.xul --> + <menu id="menu_zoom"/> + + <menu id="menu_UseStyleSheet" + label="&useStyleSheetMenu.label;" + accesskey="&useStyleSheetMenu.accesskey;" + disabled="false" + observes="isImage"> + <menupopup id="menupopup_stylesheetFill" onpopupshowing="stylesheetFillPopup(this);" + oncommand="stylesheetSwitchAll(window.content, event.target.getAttribute('data')); setStyleDisabled(false);" type="radio"> + <menuitem id="menu_pageStyleNoStyle" + label="&useStyleSheetNone.label;" + accesskey="&useStyleSheetNone.accesskey;" + oncommand="setStyleDisabled(true); event.stopPropagation();" + type="radio"/> + <menuitem id="menu_pageStylePersistentOnly" + label="&useStyleSheetPersistentOnly.label;" + accesskey="&useStyleSheetPersistentOnly.accesskey;" + type="radio"/> + </menupopup> + </menu> + <menu id="charsetMenu" + onpopupshowing="BrowserUpdateCharsetMenu(this);" + oncommand="BrowserSetCharacterSet(event);"/> + <menuitem hidden="true" id="documentDirection-swap" + label="&bidiSwitchPageDirectionItem.label;" + accesskey="&bidiSwitchPageDirectionItem.accesskey;" + command="cmd_SwitchDocumentDirection"/> + <menuseparator /> + <menuitem id="menuitem_pageSourceCmd" + label="&pageSourceCmd.label;" + accesskey="&pageSourceCmd.accesskey;" + key="key_viewSource" + command="View:PageSource"/> + <menuitem id="menuitem_pageInfoCmd" + label="&pageInfoCmd.label;" + accesskey="&pageInfoCmd.accesskey;" + key="key_viewInfo" + command="View:PageInfo"/> + <menuseparator /> + <!-- overlayed from viewApplyThemeOverlay.xul --> + <menu id="menu_viewApplyTheme"/> + </menupopup> + </menu> + + <menu id="history-menu" + label="&goMenu.label;" + accesskey="&goMenu.accesskey;" + oncommand="gotoHistoryIndex(event);" + onclick="checkForMiddleClick(this, event);"> + <menupopup id="goPopup" + onpopupshowing="updateGoMenu(event);"> + <menuitem id="historyMenuBack" + label="&goBackCmd.label;" + accesskey="&goBackCmd.accesskey;" + key="goBackKb" + oncommand="BrowserBack(event);" + onclick="checkForMiddleClick(this, event);" + observes="canGoBack"/> + <menuitem id="historyMenuForward" + label="&goForwardCmd.label;" + accesskey="&goForwardCmd.accesskey;" + key="goForwardKb" + oncommand="BrowserForward(event);" + onclick="checkForMiddleClick(this, event);" + observes="canGoForward"/> + <menuitem id="historyMenuUp" + label="&goUpCmd.label;" + accesskey="&goUpCmd.accesskey;" + key="goUpKb" + command="Browser:Up"/> + <menuitem id="historyMenuHome" + label="&goHomeCmd.label;" + accesskey="&goHomeCmd.accesskey;" + command="Browser:Home" + onclick="checkForMiddleClick(this, event);" + key="goHome"/> + <menuseparator/> + <menu id="menu_recentTabs" + label="&recentTabs.label;" + accesskey="&recentTabs.accesskey;"> + <menupopup id="menu_recentTabsPopup" + onpopupshowing="event.stopPropagation(); updateRecentTabs(this);" + oncommand="gBrowser.undoCloseTab(event.target.value);"/> + </menu> + <menu id="menu_recentWindows" + label="&recentWindows.label;" + accesskey="&recentWindows.accesskey;"> + <menupopup id="menu_recentWindowsPopup" + onpopupshowing="event.stopPropagation(); updateRecentWindows(this);" + oncommand="undoCloseWindow(event.target.value);"/> + </menu> + <menuitem id="historyRestoreLastSession" + label="&historyRestoreLastSession.label;" + accesskey="&historyRestoreLastSession.accesskey;" + oncommand="restoreLastSession();" + disabled="true"/> + <menuseparator/> + <menuitem id="menu_showAllHistory" + label="&historyCmd.label;" + accesskey="&historyCmd.accesskey;" + oncommand="PlacesCommandHook.showPlacesOrganizer('History');" + key="key_gotoHistory"/> + <menuseparator id="startHistorySeparator" hidden="true"/> + <menuseparator id="endHistorySeparator" hidden="true"/> + <!-- Dead for now. + <menuitem id="sync-tabs-menuitem" + label="&syncTabsMenu.label;" + oncommand="BrowserOpenSyncTabs();" + disabled="true"/> --> + </menupopup> + </menu> + + <menu id="bookmarksMenu" + label="&bookmarksMenu.label;" + accesskey="&bookmarksMenu.accesskey;" + ondragenter="PlacesMenuDNDHandler.onDragEnter(event);" + ondragover="PlacesMenuDNDHandler.onDragOver(event);" + ondrop="PlacesMenuDNDHandler.onDrop(event);"> + <menupopup id="bookmarksMenuPopup" + placespopup="true" + context="placesContext" + openInTabs="children" + oncommand="BookmarksEventHandler.onCommand(event);" + onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);" + onpopupshowing="BookmarksMenu.onPopupShowing(event, ''); + BookmarksEventHandler.onPopupShowing(event);" + tooltip="bhTooltip" popupsinherittooltip="true"> + <menuitem id="menu_bookmarkThisPage" + command="Browser:AddBookmark" + key="addBookmarkKb"/> + <menuitem id="menu_bookmarkThisPageAs" + command="Browser:AddBookmarkAs" + key="addBookmarkAsKb"/> + <menuitem id="menu_bookmarkAllTabs" + command="Browser:BookmarkAllTabs"/> + <menuitem id="menu_bookmarkManager" + command="Browser:ManageBookmark" + key="manBookmarkKb"/> + <menuseparator id="organizeBookmarksSeparator"/> + <menu id="menu_iconic_feedsMenu" + label="&feedsMenu.label;" + accesskey="&feedsMenu.accesskey;" + class="menu-iconic feedsMenu" + command="feedsMenu"> + <menupopup onpopupshowing="window.XULBrowserWindow.populateFeeds(this);" + oncommand="subscribeToFeed(event.target.statusText, event);" + onclick="checkForMiddleClick(this, event);"/> + </menu> + <menuseparator/> + <menu id="bookmarksToolbarFolderMenu" + class="menu-iconic bookmark-item" + label="&bookmarksToolbarCmd.label;" + container="true"> + <menupopup id="bookmarksToolbarFolderPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=TOOLBAR');"/> + </menu> + <menu id="unsortedBookmarksFolderMenu" + class="menu-iconic bookmark-item" + container="true"> + <menupopup id="unsortedBookmarksFolderPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=UNFILED_BOOKMARKS');"/> + </menu> + <menuseparator/> + </menupopup> + </menu> + + <menu id="tasksMenu"> + <menupopup id="taskPopup"> + <menuitem id="menu_searchWeb" + label="&searchInternetCmd.label;" + accesskey="&searchInternetCmd.accesskey;" + key="searchInternetKb" + command="Browser:SearchInternet"/> + <menuitem id="menu_translate" + label="&translateMenu.label;" + accesskey="&translateMenu.accesskey;" + oncommand="Translate();"/> + <menu id="menu_cookieManager" + label="&cookieCookieManager.label;" + accesskey="&cookieCookieManager.accesskey;" + oncommand="if (event.target.id.startsWith('cookie_')) + CookieImagePopupAction(event.target);"> + <menupopup id="menupopup_checkPermissions_cookie" + onpopupshowing="CheckPermissionsMenu('cookie', this);"> + <menuitem id="cookie_deny" + label="&cookieBlockCookiesCmd.label;" + accesskey="&cookieBlockCookiesCmd.accesskey;" + title="&cookieMessageTitle.label;" + msg="&cookieBlockCookiesMsg.label;" + type="radio" + name="cookie"/> + <menuitem id="cookie_default" + label="&cookieCookiesDefaultCmd.label;" + accesskey="&cookieCookiesDefaultCmd.accesskey;" + title="&cookieMessageTitle.label;" + msg="&cookieCookiesDefaultMsg.label;" + type="radio" + name="cookie"/> + <menuitem id="cookie_session" + label="&cookieAllowSessionCookiesCmd.label;" + accesskey="&cookieAllowSessionCookiesCmd.accesskey;" + title="&cookieMessageTitle.label;" + msg="&cookieAllowSessionCookiesMsg.label;" + type="radio" + name="cookie"/> + <menuitem id="cookie_allow" + label="&cookieAllowCookiesCmd.label;" + accesskey="&cookieAllowCookiesCmd.accesskey;" + title="&cookieMessageTitle.label;" + msg="&cookieAllowCookiesMsg.label;" + type="radio" + name="cookie"/> + <menuseparator/> + <menuitem id="menuitem_cookieDisplay" + label="&cookieDisplayCookiesCmd.label;" + accesskey="&cookieDisplayCookiesCmd.accesskey;" + oncommand="toDataManager(hostUrl() + '|cookies'); + event.stopPropagation();"/> + </menupopup> + </menu> + <menu id="menu_imageManager" + label="&cookieImageManager.label;" + accesskey="&cookieImageManager.accesskey;" + oncommand="if (event.target.id.startsWith('image_')) + CookieImagePopupAction(event.target);"> + <menupopup id="menupopup_checkPermissions_image" + onpopupshowing="CheckPermissionsMenu('image', this);"> + <menuitem id="image_deny" + label="&cookieBlockImagesCmd.label;" + accesskey="&cookieBlockImagesCmd.accesskey;" + title="&cookieImageMessageTitle.label;" + msg="&cookieBlockImagesMsg.label;" + type="radio" + name="image"/> + <menuitem id="image_default" + label="&cookieImagesDefaultCmd.label;" + accesskey="&cookieImagesDefaultCmd.accesskey;" + title="&cookieImageMessageTitle.label;" + msg="&cookieImagesDefaultMsg.label;" + type="radio" + name="image"/> + <menuitem id="image_allow" + label="&cookieAllowImagesCmd.label;" + accesskey="&cookieAllowImagesCmd.accesskey;" + title="&cookieImageMessageTitle.label;" + msg="&cookieAllowImagesMsg.label;" + type="radio" + name="image"/> + <menuseparator/> + <menuitem id="menuitem_imageDisplay" + label="&cookieDisplayImagesCmd.label;" + accesskey="&cookieDisplayImagesCmd.accesskey;" + oncommand="toDataManager(hostUrl() + '|permissions|add|image'); + event.stopPropagation();"/> + </menupopup> + </menu> + <menu id="menu_popupManager" + label="&popupsManager.label;" + accesskey="&popupsManager.accesskey;" + oncommand="if (event.target.id.startsWith('popup_')) + CookieImagePopupAction(event.target);"> + <menupopup id="menupopup_checkForVisibility" + onpopupshowing="CheckPermissionsMenu('popup', this);" + onpopuphiding="RemovePopupsItems(this);"> + <menuitem id="popup_deny" + label="&popupBlockCmd.label;" + accesskey="&popupBlockCmd.accesskey;" + title="&popupsMessageChangeTitle.label;" + msg="&popupBlockMsg.label;" + type="radio" + name="popup"/> + <menuitem id="popup_default" + label="&popupDefaultCmd.label;" + accesskey="&popupDefaultCmd.accesskey;" + title="&popupsMessageChangeTitle.label;" + msg="&popupDefaultMsg.label;" + type="radio" + name="popup"/> + <menuitem id="popup_allow" + label="&popupAllowCmd.label;" + accesskey="&popupAllowCmd.accesskey;" + title="&popupsMessageChangeTitle.label;" + msg="&popupAllowMsg.label;" + type="radio" + name="popup"/> + <menuseparator id="popupMenuSeparator"/> + <menuitem id="menuitem_PopupsManage" + label="&popupsManage.label;" + accesskey="&popupsManage.accesskey;" + oncommand="toDataManager(hostUrl() + '|permissions|add|popup'); + event.stopPropagation();"/> + </menupopup> + </menu> + <menuseparator id="navBeginGlobalItems"/> + <menuitem id="sanitizeItem" + label="&clearPrivateDataCmd.label;" + accesskey="&clearPrivateDataCmd.accesskey;" + key="key_sanitize" command="Tools:Sanitize"/> + </menupopup> + </menu> + + <menu id="windowMenu"/> + + <menu id="menu_Help"/> + </menubar> + +</overlay> diff --git a/comm/suite/browser/nsBrowserContentHandler.js b/comm/suite/browser/nsBrowserContentHandler.js new file mode 100644 index 0000000000..5e6c37380f --- /dev/null +++ b/comm/suite/browser/nsBrowserContentHandler.js @@ -0,0 +1,634 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const nsISupports = Ci.nsISupports; +const nsIBrowserDOMWindow = Ci.nsIBrowserDOMWindow; +const nsIBrowserHistory = Ci.nsIBrowserHistory; +const nsIBrowserSearchService = Ci.nsIBrowserSearchService; +const nsIChannel = Ci.nsIChannel; +const nsICommandLine = Ci.nsICommandLine; +const nsICommandLineHandler = Ci.nsICommandLineHandler; +const nsICommandLineValidator = Ci.nsICommandLineValidator; +const nsIComponentRegistrar = Ci.nsIComponentRegistrar; +const nsIContentHandler = Ci.nsIContentHandler; +const nsIDOMWindow = Ci.nsIDOMWindow; +const nsIFactory = Ci.nsIFactory; +const nsIFileURL = Ci.nsIFileURL; +const nsIHttpProtocolHandler = Ci.nsIHttpProtocolHandler; +const nsINetUtil = Ci.nsINetUtil; +const nsIPrefService = Ci.nsIPrefService; +const nsIPrefBranch = Ci.nsIPrefBranch; +const nsIPrefLocalizedString = Ci.nsIPrefLocalizedString; +const nsISupportsString = Ci.nsISupportsString; +const nsIWindowMediator = Ci.nsIWindowMediator; +const nsIWebNavigationInfo = Ci.nsIWebNavigationInfo; + +const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001; + +const URI_INHERITS_SECURITY_CONTEXT = nsIHttpProtocolHandler + .URI_INHERITS_SECURITY_CONTEXT; + +const NS_GENERAL_STARTUP_PREFIX = "@mozilla.org/commandlinehandler/general-startup;1?type="; + +function shouldLoadURI(aURI) +{ + if (aURI && !aURI.schemeIs("chrome")) + return true; + + dump("*** Preventing external load of chrome: URI into browser window\n"); + dump(" Use -chrome <uri> instead\n"); + return false; +} + +function resolveURIInternal(aCmdLine, aArgument) +{ + try { + var file = aCmdLine.resolveFile(aArgument); + if (file.exists()) { + return Services.io.newFileURI(file); + } + } catch (e) { + } + + // We have interpreted the argument as a relative file URI, but the file + // doesn't exist. Try URI fixup heuristics: see bug 290782. + + try { + return Services.uriFixup + .createFixupURI(aArgument, + Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP); + } catch (e) { + Cu.reportError(e); + } + + return null; +} + +function getHomePageGroup() +{ + var homePage = Services.prefs.getComplexValue("browser.startup.homepage", + nsIPrefLocalizedString).data; + + var count = 0; + try { + count = Services.prefs.getIntPref("browser.startup.homepage.count"); + } catch (e) { + } + + for (var i = 1; i < count; ++i) { + try { + homePage += '\n' + Services.prefs.getStringPref("browser.startup.homepage." + i); + } catch (e) { + } + } + return homePage; +} + +function needHomePageOverride() +{ + var savedmstone = null; + try { + savedmstone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone"); + if (savedmstone == "ignore") + return false; + } catch (e) { + } + + var mstone = Cc["@mozilla.org/network/protocol;1?name=http"] + .getService(nsIHttpProtocolHandler).misc; + + if (mstone == savedmstone) + return false; + + Services.prefs.setCharPref("browser.startup.homepage_override.mstone", mstone); + + return true; +} + +function getURLToLoad() +{ + if (needHomePageOverride()) { + try { + return Services.urlFormatter.formatURLPref("startup.homepage_override_url"); + } catch (e) { + } + } + + try { + var ss = Cc["@mozilla.org/suite/sessionstartup;1"] + .getService(Ci.nsISessionStartup); + // return about:blank if we are restoring previous session + if (ss.doRestore()) + return "about:blank"; + } catch (e) { + } + + try { + var st = Cc["@mozilla.org/suite/sessionstore;1"] + .getService(Ci.nsISessionStore); + // return about:blank if the last window was closed and should be restored + if (st.doRestoreLastWindow()) + return "about:blank"; + } catch (e) { + } + + try { + switch (Services.prefs.getIntPref("browser.startup.page")) { + case 1: + return getHomePageGroup(); + + case 2: + return Services.prefs.getStringPref("browser.history.last_page_visited"); + } + } catch (e) { + } + + return "about:blank"; +} + +function openWindow(parent, url, features, arg) +{ + var argstring = Cc["@mozilla.org/supports-string;1"] + .createInstance(nsISupportsString); + argstring.data = arg; + return Services.ww.openWindow(parent, url, "", features, argstring); +} + +function openPreferences() +{ + var win = Services.wm.getMostRecentWindow("mozilla:preferences"); + if (win) + win.focus(); + else + openWindow(null, "chrome://communicator/content/pref/preferences.xul", + "chrome,titlebar,dialog=no,resizable", ""); +} + +function getBrowserURL() +{ + try { + return Services.prefs.getCharPref("browser.chromeURL"); + } catch (e) { + } + return "chrome://navigator/content/navigator.xul"; +} + +function handURIToExistingBrowser(aUri, aLocation, aFeatures, aTriggeringPrincipal) +{ + if (!shouldLoadURI(aUri)) + return; + + var navWin = Services.wm.getMostRecentWindow("navigator:browser"); + if (!navWin) { + // if we couldn't load it in an existing window, open a new one + openWindow(null, getBrowserURL(), aFeatures, aUri.spec); + return; + } + + navWin.browserDOMWindow.openURI(aUri, null, aLocation, + nsIBrowserDOMWindow.OPEN_EXTERNAL, + aTriggeringPrincipal); +} + +function doSearch(aSearchTerm, aFeatures) { + var submission = Services.search.defaultEngine.getSubmission(aSearchTerm); + + // fill our nsIMutableArray with uri-as-wstring, null, null, postData + var sa = Cc["@mozilla.org/array;1"] + .createInstance(Ci.nsIMutableArray); + + var uristring = Cc["@mozilla.org/supports-string;1"] + .createInstance(nsISupportsString); + uristring.data = submission.uri.spec; + + sa.appendElement(uristring); + sa.appendElement(null); + sa.appendElement(null); + sa.appendElement(submission.postData); + + // XXXbsmedberg: use handURIToExistingBrowser to obey tabbed-browsing + // preferences, but need nsIBrowserDOMWindow extensions + return Services.ww.openWindow(null, getBrowserURL(), "_blank", aFeatures, + sa); +} + +var nsBrowserContentHandler = { + get wrappedJSObject() { + return this; + }, + + /* nsISupports */ + QueryInterface: function QueryInterface(iid) { + if (iid.equals(nsISupports) || + iid.equals(nsICommandLineHandler) || + iid.equals(nsICommandLine) || + iid.equals(nsICommandLineValidator) || + iid.equals(nsIContentHandler) || + iid.equals(nsIFactory)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + _handledURI: null, + + /* nsICommandLineHandler */ + handle: function handle(cmdLine) { + var features = "chrome,all,dialog=no"; + try { + var width = cmdLine.handleFlagWithParam("width", false); + if (width != null) + features += ",width=" + width; + } catch (e) { + } + try { + var height = cmdLine.handleFlagWithParam("height", false); + if (height != null) + features += ",height=" + height; + } catch (e) { + } + + try { + var remote = cmdLine.handleFlagWithParam("remote", true); + if (/^\s*(\w+)\s*\(\s*([^\s,]+)\s*,?\s*([^\s]*)\s*\)\s*$/.test(remote)) { + switch (RegExp.$1.toLowerCase()) { + case "openurl": + case "openfile": + // openURL(<url>) + // openURL(<url>,new-window) + // openURL(<url>,new-tab) + + var uri = resolveURIInternal(cmdLine, RegExp.$2); + + var location = nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW; + if (RegExp.$3 == "new-window") + location = nsIBrowserDOMWindow.OPEN_NEWWINDOW; + else if (RegExp.$3 == "new-tab") + location = nsIBrowserDOMWindow.OPEN_NEWTAB; + + handURIToExistingBrowser(uri, location, features, + Services.scriptSecurityManager.getSystemPrincipal()); + break; + + case "mailto": + openWindow(null, "chrome://messenger/content/messengercompose/messengercompose.xul", features, RegExp.$2); + break; + + case "xfedocommand": + switch (RegExp.$2.toLowerCase()) { + case "openbrowser": + openWindow(null, getBrowserURL(), features, RegExp.$3 || getURLToLoad()); + break; + + case "openinbox": + openWindow(null, "chrome://messenger/content", features); + break; + + case "composemessage": + openWindow(null, "chrome://messenger/content/messengercompose/messengercompose.xul", features, RegExp.$3); + break; + + default: + throw Cr.NS_ERROR_ABORT; + } + break; + + default: + // Somebody sent us a remote command we don't know how to process: + // just abort. + throw Cr.NS_ERROR_ABORT; + } + + cmdLine.preventDefault = true; + } + } catch (e) { + // If we had a -remote flag but failed to process it, throw + // NS_ERROR_ABORT so that the xremote code knows to return a failure + // back to the handling code. + throw Cr.NS_ERROR_ABORT; + } + + try { + var browserParam = cmdLine.handleFlagWithParam("browser", false); + if (browserParam) { + openWindow(null, getBrowserURL(), features, browserParam); + cmdLine.preventDefault = true; + } + } catch (e) { + if (cmdLine.handleFlag("browser", false)) { + openWindow(null, getBrowserURL(), features, getURLToLoad()); + cmdLine.preventDefault = true; + } + } + + try { + var privateParam = cmdLine.handleFlagWithParam("private", false); + if (privateParam) { + openWindow(null, getBrowserURL(), "private," + features, privateParam); + cmdLine.preventDefault = true; + } + } catch (e) { + if (cmdLine.handleFlag("private", false)) { + openWindow(null, getBrowserURL(), "private," + features, "about:privatebrowsing"); + cmdLine.preventDefault = true; + } + } + + // If we don't have a profile selected yet (e.g. the Profile Manager is + // displayed) we will crash if we open an url and then select a profile. To + // prevent this handle all url command line flag and set the command line's + // preventDefault to true to prevent the display of the ui. The initial + // command line will be retained when nsAppRunner calls LaunchChild though + // urls launched after the initial launch will be lost. + try { + // This will throw when a profile has not been selected. + Services.dirsvc.get("ProfD", Ci.nsIFile); + } catch (e) { + cmdLine.preventDefault = true; + throw Cr.NS_ERROR_ABORT; + } + + try { + var urlParam = cmdLine.handleFlagWithParam("url", false); + if (urlParam) { + if (this._handledURI == urlParam) { + this._handledURI = null; + } else { + if (cmdLine.handleFlag("requestpending", false) && + cmdLine.state == nsICommandLine.STATE_INITIAL_LAUNCH) { + // A DDE request with the URL will follow and the DDE handling code + // will send it to the commandline handler via + // "mozilla -url http://www.foo.com". Store the URL so we can + // ignore this request later + this._handledURI = urlParam; + } + + urlParam = resolveURIInternal(cmdLine, urlParam); + handURIToExistingBrowser(urlParam, + nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, + features, + Services.scriptSecurityManager.getSystemPrincipal()); + } + cmdLine.preventDefault = true; + } + } catch (e) { + } + + var param; + try { + while ((param = cmdLine.handleFlagWithParam("new-window", false)) != null) { + var uri = resolveURIInternal(cmdLine, param); + handURIToExistingBrowser(uri, + nsIBrowserDOMWindow.OPEN_NEWWINDOW, + features, + Services.scriptSecurityManager.getSystemPrincipal()); + cmdLine.preventDefault = true; + } + } catch (e) { + } + + try { + while ((param = cmdLine.handleFlagWithParam("new-tab", false)) != null) { + var uri = resolveURIInternal(cmdLine, param); + handURIToExistingBrowser(uri, + nsIBrowserDOMWindow.OPEN_NEWTAB, + features, + Services.scriptSecurityManager.getSystemPrincipal()); + cmdLine.preventDefault = true; + } + } catch (e) { + } + + try { + var chromeParam = cmdLine.handleFlagWithParam("chrome", false); + if (chromeParam) { + // only load URIs which do not inherit chrome privs + var uri = resolveURIInternal(cmdLine, chromeParam); + if (!Services.netUtils.URIChainHasFlags(uri, URI_INHERITS_SECURITY_CONTEXT)) { + openWindow(null, uri.spec, features); + cmdLine.preventDefault = true; + } + } + } catch (e) { + } + + try { + var fileParam = cmdLine.handleFlagWithParam("file", false); + if (fileParam) { + fileParam = resolveURIInternal(cmdLine, fileParam); + handURIToExistingBrowser(fileParam, + nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, + features, + Services.scriptSecurityManager.getSystemPrincipal()); + cmdLine.preventDefault = true; + } + } catch (e) { + } + + var searchParam = cmdLine.handleFlagWithParam("search", false); + if (searchParam) { + doSearch(searchParam, features); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("preferences", false)) { + openPreferences(); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("silent", false)) + cmdLine.preventDefault = true; + + if (!cmdLine.preventDefault && cmdLine.length) { + var arg = cmdLine.getArgument(0); + if (!/^-/.test(arg)) { + try { + arg = resolveURIInternal(cmdLine, arg); + handURIToExistingBrowser(arg, + nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, + features, + Services.scriptSecurityManager.getSystemPrincipal()); + cmdLine.preventDefault = true; + } catch (e) { + } + } + } + + if (!cmdLine.preventDefault) { + this.realCmdLine = cmdLine; + + var prefBranch = Services.prefs.getBranch("general.startup."); + + var startupArray = prefBranch.getChildList(""); + + for (var i = 0; i < startupArray.length; ++i) { + this.currentArgument = startupArray[i]; + var contract = NS_GENERAL_STARTUP_PREFIX + this.currentArgument; + if (contract in Cc) { + // Ignore any exceptions - we can't do anything about them here. + try { + if (prefBranch.getBoolPref(this.currentArgument)) { + var handler = Cc[contract].getService(nsICommandLineHandler); + if (handler.wrappedJSObject) + handler.wrappedJSObject.handle(this); + else + handler.handle(this); + } + } catch (e) { + Cu.reportError(e); + } + } + } + + this.realCmdLine = null; + } + + if (!cmdLine.preventDefault) { + var homePage = getURLToLoad(); + if (!/\n/.test(homePage)) { + try { + let uri = Services.uriFixup.createFixupURI(homePage, 0); + handURIToExistingBrowser(uri, nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, features); + cmdLine.preventDefault = true; + } catch (e) { + } + } + + if (!cmdLine.preventDefault) { + openWindow(null, getBrowserURL(), features, homePage); + cmdLine.preventDefault = true; + } + } + + }, + + /* nsICommandLineValidator */ + validate: function validate(cmdLine) { + var osintFlagIdx = cmdLine.findFlag("osint", false); + + // If the osint flag is not present and we are not called by DDE then we're safe + if (cmdLine.state != nsICommandLine.STATE_REMOTE_EXPLICIT && + cmdLine.findFlag("osint", false) == -1) + return; + + // Other handlers may use osint so only handle the osint flag if a + // flag is also present and the command line is valid. + ["url", "news", "compose"].forEach(function(value) { + var flagIdx = cmdLine.findFlag(value, false); + + if (flagIdx > -1) { + var testExpr = new RegExp("seamonkey" + value + ":"); + if (cmdLine.length != flagIdx + 2 || + testExpr.test(cmdLine.getArgument(flagIdx + 1))) + throw Cr.NS_ERROR_ABORT; + cmdLine.handleFlag("osint", false); + } + }); + }, + + helpInfo: " -browser <url> Open a browser window.\n" + + " -private <url> Open a private window.\n" + + " -new-window <url> Open <url> in a new browser window.\n" + + " -new-tab <url> Open <url> in a new browser tab.\n" + + " -url <url> Open the specified url.\n" + + " -chrome <url> Open the specified chrome.\n" + + " -search <term> Search <term> with your default search engine.\n" + + " -preferences Open Preferences dialog.\n", + + /* nsICommandLine */ + length: 1, + + getArgument: function getArgument(index) { + if (index == 0) + return this.currentArgument; + + throw Cr.NS_ERROR_INVALID_ARG; + }, + + findFlag: function findFlag(flag, caseSensitive) { + if (caseSensitive) + return flag == this.currentArgument ? 0 : -1; + return flag.toLowerCase() == this.currentArgument.toLowerCase() ? 0 : -1; + }, + + removeArguments: function removeArguments(start, end) { + // do nothing + }, + + handleFlag: function handleFlag(flag, caseSensitive) { + if (caseSensitive) + return flag == this.currentArgument; + return flag.toLowerCase() == this.currentArgument.toLowerCase(); + }, + + handleFlagWithParam : function handleFlagWithParam(flag, caseSensitive) { + if (this.handleFlag(flag, caseSensitive)) + throw Cr.NS_ERROR_INVALID_ARG; + }, + + get state() { + return this.realCmdLine.state; + }, + + get preventDefault() { + return this.realCmdLine.preventDefault; + }, + + set preventDefault(preventDefault) { + return this.realCmdLine.preventDefault = preventDefault; + }, + + get workingDirectory() { + return this.realCmdLine.workingDirectory; + }, + + get windowContext() { + return this.realCmdLine.windowContext; + }, + + resolveFile: function resolveFile(arg) { + return this.realCmdLine.resolveFile(arg); + }, + + resolveURI: function resolveURI(arg) { + return this.realCmdLine.resolveURI(arg); + }, + + /* nsIContentHandler */ + handleContent: function handleContent(contentType, context, request) { + var webNavInfo = Cc["@mozilla.org/webnavigation-info;1"] + .getService(nsIWebNavigationInfo); + if (!webNavInfo.isTypeSupported(contentType, null)) + throw NS_ERROR_WONT_HANDLE_CONTENT; + + request.QueryInterface(nsIChannel); + handURIToExistingBrowser(request.URI, + nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, + "chrome,all,dialog=no", + request.loadInfo.triggeringPrincipal); + request.cancel(Cr.NS_BINDING_ABORTED); + }, + + /* nsIFactory */ + createInstance: function createInstance(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + + return this.QueryInterface(iid); + }, + + lockFactory: function lockFactory(lock) { + /* no-op */ + } +}; + +const BROWSER_CID = Components.ID("{c2343730-dc2c-11d3-98b3-001083010e9b}"); + +function NSGetFactory(cid) { + if (cid.number == BROWSER_CID) + return nsBrowserContentHandler; + throw Cr.NS_ERROR_FACTORY_NOT_REGISTERED; +} diff --git a/comm/suite/browser/nsBrowserContentListener.js b/comm/suite/browser/nsBrowserContentListener.js new file mode 100644 index 0000000000..7e618f4f85 --- /dev/null +++ b/comm/suite/browser/nsBrowserContentListener.js @@ -0,0 +1,138 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const nsIWebBrowserChrome = Ci.nsIWebBrowserChrome; + +function nsBrowserContentListener(toplevelWindow, contentWindow) +{ + // this one is not as easy as you would hope. + // need to convert toplevelWindow to an XPConnected object, instead + // of a DOM-based object, to be able to QI() it to nsIXULWindow + + this.init(toplevelWindow, contentWindow); +} + +/* implements nsIURIContentListener */ + +nsBrowserContentListener.prototype = +{ + init: function(toplevelWindow, contentWindow) + { + this.toplevelWindow = toplevelWindow; + this.contentWindow = contentWindow; + + // hook up the whole parent chain thing + var windowDocShell = this.convertWindowToDocShell(toplevelWindow); + if (windowDocShell) { + windowDocshell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIURIContentListener) + .parentContentListener = this; + } + + var registerWindow = false; + try { + var treeItem = contentWindow.docShell.QueryInterface(Ci.nsIDocShellTreeItem); + var treeOwner = treeItem.treeOwner; + var interfaceRequestor = treeOwner.QueryInterface(Ci.nsIInterfaceRequestor); + var webBrowserChrome = interfaceRequestor.getInterface(nsIWebBrowserChrome); + if (webBrowserChrome) + { + var chromeFlags = webBrowserChrome.chromeFlags; + var res = chromeFlags & nsIWebBrowserChrome.CHROME_ALL; + var res2 = chromeFlags & nsIWebBrowserChrome.CHROME_DEFAULT; + if ( res == nsIWebBrowserChrome.CHROME_ALL || res2 == nsIWebBrowserChrome.CHROME_DEFAULT) + { + registerWindow = true; + } + } + } catch (ex) {} + + // register ourselves + if (registerWindow) + { + var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader); + uriLoader.registerContentListener(this); + } + }, + close: function() + { + this.contentWindow = null; + var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader); + + uriLoader.unRegisterContentListener(this); + }, + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIURIContentListener) || + iid.equals(Ci.nsISupportsWeakReference) || + iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + doContent: function(contentType, isContentPreferred, request, contentHandler) + { + // forward the doContent to our content area webshell + var docShell = this.contentWindow.docShell; + if (Services.prefs.getIntPref("browser.link.open_external") == nsIBrowserDOMWindow.OPEN_NEWTAB) { + var newTab = gBrowser.loadOneTab("about:blank", { + inBackground: Services.prefs.getBoolPref("browser.tabs.loadDivertedInBackground")}); + docShell = gBrowser.getBrowserForTab(newTab).docShell; + } + + var contentListener; + try { + contentListener = + docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIURIContentListener); + } catch (ex) { + dump(ex); + } + + if (!contentListener) return false; + + return contentListener.doContent(contentType, isContentPreferred, request, contentHandler); + + }, + + isPreferred: function(contentType, desiredContentType) + { + if (Services.prefs.getIntPref("browser.link.open_external") == nsIBrowserDOMWindow.OPEN_NEWWINDOW) + return false; + + try { + var webNavInfo = + Cc["@mozilla.org/webnavigation-info;1"] + .getService(Ci.nsIWebNavigationInfo); + return webNavInfo.isTypeSupported(contentType, null); + } catch (e) { + // XXX propagate failures other than "NS_ERROR_NOT_AVAILABLE"? + // This seems to never get called, so not like it matters.... + return false; + } + }, + canHandleContent: function(contentType, isContentPreferred, desiredContentType) + { + var docShell = this.contentWindow.docShell; + var contentListener; + try { + contentListener = + docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIURIContentListener); + } catch (ex) { + dump(ex); + } + if (!contentListener) return false; + + return contentListener.canHandleContent(contentType, isContentPreferred, desiredContentType); + }, + convertWindowToDocShell: function(win) { + // don't know how to do this + return null; + }, + loadCookie: null, + parentContentListener: null +} diff --git a/comm/suite/browser/nsBrowserStatusHandler.js b/comm/suite/browser/nsBrowserStatusHandler.js new file mode 100644 index 0000000000..b4699f8f9c --- /dev/null +++ b/comm/suite/browser/nsBrowserStatusHandler.js @@ -0,0 +1,473 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function nsBrowserStatusHandler() +{ + this.init(); +} + +nsBrowserStatusHandler.prototype = +{ + // Stored Status, Link and Loading values + status : "", + defaultStatus : "", + jsStatus : "", + jsDefaultStatus : "", + overLink : "", + feeds : [], + + QueryInterface : function(aIID) + { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsIXULBrowserWindow) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + }, + + init : function() + { + this.urlBar = document.getElementById("urlbar"); + this.throbberElement = document.getElementById("navigator-throbber"); + this.statusMeter = document.getElementById("statusbar-icon"); + this.statusPanel = document.getElementById("statusbar-progresspanel"); + this.stopButton = document.getElementById("stop-button"); + this.stopMenu = document.getElementById("menuitem-stop"); + this.stopContext = document.getElementById("context-stop"); + this.statusTextField = document.getElementById("statusbar-display"); + this.isImage = document.getElementById("isImage"); + this.securityButton = document.getElementById("security-button"); + this.evButton = document.getElementById("ev-button"); + this.feedsMenu = document.getElementById("feedsMenu"); + this.feedsButton = document.getElementById("feedsButton"); + + // Initialize the security button's state and tooltip text + const nsIWebProgressListener = Ci.nsIWebProgressListener; + this.onSecurityChange(null, null, nsIWebProgressListener.STATE_IS_INSECURE); + }, + + destroy : function() + { + // XXXjag to avoid leaks :-/, see bug 60729 + this.urlBar = null; + this.throbberElement = null; + this.statusMeter = null; + this.statusPanel = null; + this.stopButton = null; + this.stopMenu = null; + this.stopContext = null; + this.statusTextField = null; + this.isImage = null; + this.securityButton = null; + this.evButton = null; + this.feedsButton = null; + this.feedsMenu = null; + }, + + // nsIXULBrowserWindow + setJSStatus : function(status) + { + this.jsStatus = status; + this.updateStatusField(); + }, + + // nsIXULBrowserWindow + setJSDefaultStatus : function(status) + { + this.jsDefaultStatus = status; + this.updateStatusField(); + }, + + setDefaultStatus : function(status) + { + this.defaultStatus = status; + this.updateStatusField(); + }, + + // nsIXULBrowserWindow + setOverLink : function(link, context) + { + this.overLink = link; + // clear out 'Done' (or other message) on first hover + if (this.defaultStatus) + this.defaultStatus = ""; + this.updateStatusField(); + if (link) + this.statusTextField.setAttribute('crop', 'center'); + else + this.statusTextField.setAttribute('crop', 'end'); + }, + + // nsIXULBrowserWindow + // Called before links are navigated to to allow us to retarget them if needed. + onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) { + return originalTarget; + }, + + updateStatusField : function() + { + var text = this.overLink || this.status || this.jsStatus || this.jsDefaultStatus || this.defaultStatus; + + // check the current value so we don't trigger an attribute change + // and cause needless (slow!) UI updates + if (this.statusTextField.label != text) + this.statusTextField.label = text; + }, + +/** + * Returns true if |aMimeType| is text-based, false otherwise. + * + * @param aMimeType + * The MIME type to check. + * + * If adding types to this function, please also check the similar + * function in mozilla/toolkit/content/widgets/findbar.xml. + */ + mimeTypeIsTextBased : function(contentType) + { + return /^text\/|\+xml$/.test(contentType) || + contentType == "application/x-javascript" || + contentType == "application/javascript" || + contentType == "application/xml" || + contentType == "mozilla.application/cached-xul"; + }, + + populateFeeds : function(popup) + { + // First clear out any old items + while (popup.hasChildNodes()) + popup.lastChild.remove(); + + for (var i = 0; i < this.feeds.length; i++) { + var link = this.feeds[i]; + var menuitem = document.createElement("menuitem"); + menuitem.className = "menuitem-iconic bookmark-item"; + menuitem.statusText = link.href; + menuitem.setAttribute("label", link.title || link.href); + popup.appendChild(menuitem); + } + }, + + onFeedAvailable : function(aLink) + { + this.feeds.push(aLink); + this.feedsMenu.removeAttribute("disabled"); + this.feedsButton.hidden = false; + }, + + onLinkIconAvailable : function(aHref) + { + if (aHref && gProxyFavIcon && + Services.prefs.getBoolPref("browser.chrome.site_icons")) { + var browser = getBrowser(); + if (browser.userTypedValue === null) + gProxyFavIcon.setAttribute("src", aHref); + } + }, + + onProgressChange : function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) + { + if (aMaxTotalProgress > 0) { + // This is highly optimized. Don't touch this code unless + // you are intimately familiar with the cost of setting + // attrs on XUL elements. -- hyatt + var percentage = (aCurTotalProgress * 100) / aMaxTotalProgress; + this.statusMeter.value = percentage; + } + }, + + onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) + { + const nsIWebProgressListener = Ci.nsIWebProgressListener; + const nsIChannel = Ci.nsIChannel; + var ctype; + if (aStateFlags & nsIWebProgressListener.STATE_START) { + // This (thanks to the filter) is a network start or the first + // stray request (the first request outside of the document load), + // initialize the throbber and his friends. + + // Call start document load listeners (only if this is a network load) + if (aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK && + aRequest && aWebProgress.isTopLevel) + this.startDocumentLoad(aRequest); + + if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { + // Show the progress meter + this.statusPanel.collapsed = false; + // Turn the throbber on. + this.throbberElement.setAttribute("busy", "true"); + } + + // XXX: These need to be based on window activity... + this.stopButton.disabled = false; + this.stopMenu.removeAttribute('disabled'); + this.stopContext.removeAttribute('disabled'); + } + else if (aStateFlags & nsIWebProgressListener.STATE_STOP) { + if (aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + if (aRequest) { + if (aWebProgress.isTopLevel) + this.endDocumentLoad(aRequest, aStatus); + } + } + + // This (thanks to the filter) is a network stop or the last + // request stop outside of loading the document, stop throbbers + // and progress bars and such + if (aRequest) { + var msg = ""; + // Get the channel if the request is a channel + if (aRequest instanceof nsIChannel) { + var location = aRequest.URI.spec; + if (location != "about:blank") { + switch (aStatus) { + case Cr.NS_BINDING_ABORTED: + msg = gNavigatorBundle.getString("nv_stopped"); + break; + case Cr.NS_ERROR_NET_TIMEOUT: + msg = gNavigatorBundle.getString("nv_timeout"); + break; + } + } + } + // If msg is false then we did not have an error (channel may have + // been null, in the case of a stray image load). + if (!msg) { + msg = gNavigatorBundle.getString("nv_done"); + } + this.status = ""; + this.setDefaultStatus(msg); + + // Disable menu entries for images, enable otherwise + if (content.document && this.mimeTypeIsTextBased(content.document.contentType)) + this.isImage.removeAttribute('disabled'); + else + this.isImage.setAttribute('disabled', 'true'); + } + + // Turn the progress meter and throbber off. + this.statusPanel.collapsed = true; + this.statusMeter.value = 0; // be sure to clear the progress bar + this.throbberElement.removeAttribute("busy"); + + // XXX: These need to be based on window activity... + // XXXjag: <command id="cmd_stop"/> ? + this.stopButton.disabled = true; + this.stopMenu.setAttribute('disabled', 'true'); + this.stopContext.setAttribute('disabled', 'true'); + + ZoomListeners.onLocationChange(getBrowser().currentURI); + } + }, + + onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags) + { + const nsIWebProgressListener = Ci.nsIWebProgressListener; + if (gContextMenu) { + // Optimise for the common case + if (aWebProgress.isTopLevel) + document.getElementById("contentAreaContextMenu").hidePopup(); + else { + for (var contextWindow = gContextMenu.target.ownerDocument.defaultView; + contextWindow != contextWindow.parent; + contextWindow = contextWindow.parent) { + if (contextWindow == aWebProgress.DOMWindow) { + document.getElementById("contentAreaContextMenu").hidePopup(); + break; + } + } + } + } + + if (document.tooltipNode) { + // Optimise for the common case + if (aWebProgress.isTopLevel) { + document.getElementById("aHTMLTooltip").hidePopup(); + document.tooltipNode = null; + } else { + for (var tooltipWindow = document.tooltipNode.ownerDocument.defaultView; + tooltipWindow != tooltipWindow.parent; + tooltipWindow = tooltipWindow.parent) { + if (tooltipWindow == aWebProgress.DOMWindow) { + document.getElementById("aHTMLTooltip").hidePopup(); + document.tooltipNode = null; + break; + } + } + } + } + + // Hide the form invalid popup. + if (gFormSubmitObserver.panel) { + gFormSubmitObserver.panel.hidePopup(); + } + + // XXX temporary hack for bug 104532. + // Depends heavily on setOverLink implementation + if (!aRequest) + this.status = this.jsStatus = this.jsDefaultStatus = ""; + + this.setOverLink(""); + + // Disable menu entries for images, enable otherwise + if (content.document && this.mimeTypeIsTextBased(content.document.contentType)) + this.isImage.removeAttribute('disabled'); + else + this.isImage.setAttribute('disabled', 'true'); + + // We should probably not do this if the value has changed since the user + // searched + // Update urlbar only if a new page was loaded on the primary content area + // Do not update urlbar if there was a subframe navigation + + var browser = getBrowser().selectedBrowser; + if (aWebProgress.isTopLevel) { + var userTypedValue = browser.userTypedValue; + if (userTypedValue === null) { + URLBarSetURI(aLocation, true); + } else { + this.urlBar.value = userTypedValue; + SetPageProxyState("invalid", null); + } + + BookmarkingUI.updateStarState(); + + this.feedsMenu.setAttribute("disabled", "true"); + this.feedsButton.hidden = true; + this.feeds = []; + + // When background tab comes into foreground or loading a new page + // (aRequest set), might want to update zoom. + if (FullZoom.updateBackgroundTabs || aRequest){ + FullZoom.onLocationChange(getBrowser().currentURI, !aRequest, browser); + ZoomListeners.onLocationChange(getBrowser().currentURI); + } + } + UpdateBackForwardButtons(); + + UpdateStatusBarPopupIcon(); + + BrowserSearch.updateSearchButton(); + }, + + onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) + { + this.status = aMessage; + this.updateStatusField(); + }, + + onSecurityChange : function(aWebProgress, aRequest, aState) + { + const wpl = Ci.nsIWebProgressListener; + const wpl_security_bits = wpl.STATE_IS_SECURE | + wpl.STATE_IS_BROKEN | + wpl.STATE_IS_INSECURE; + + var highlightSecure = + Services.prefs.getBoolPref("browser.urlbar.highlight.secure"); + + /* aState is defined as a bitmask that may be extended in the future. + * We filter out any unknown bits before testing for known values. + */ + switch (aState & wpl_security_bits) { + case wpl.STATE_IS_SECURE: + const nsISSLStatusProvider = Ci.nsISSLStatusProvider; + var cert = getBrowser().securityUI.QueryInterface(nsISSLStatusProvider) + .SSLStatus.serverCert; + var issuerName = cert.issuerOrganization || + cert.issuerCommonName || cert.issuerName; + this.securityButton.setAttribute("tooltiptext", + gNavigatorBundle.getFormattedString("securityButtonTooltipSecure", + [issuerName])); + this.securityButton.setAttribute("level", "high"); + if (highlightSecure) + this.urlBar.setAttribute("level", "high"); + else + this.urlBar.removeAttribute("level"); + break; + case wpl.STATE_IS_BROKEN: + this.securityButton.setAttribute("tooltiptext", + gNavigatorBundle.getString("securityButtonTooltipMixedContent")); + this.securityButton.setAttribute("level", "broken"); + if (highlightSecure) + this.urlBar.setAttribute("level", "broken"); + else + this.urlBar.removeAttribute("level"); + break; + case wpl.STATE_IS_INSECURE: + default: + this.securityButton.setAttribute("tooltiptext", + gNavigatorBundle.getString("securityButtonTooltipInsecure")); + this.securityButton.removeAttribute("level"); + this.urlBar.removeAttribute("level"); + break; + } + + if (aState & wpl.STATE_IDENTITY_EV_TOPLEVEL) { + var organization = + getBrowser().securityUI + .QueryInterface(Ci.nsISSLStatusProvider) + .SSLStatus + .QueryInterface(Ci.nsISSLStatus) + .serverCert.organization; + this.securityButton.setAttribute("label", organization); + this.evButton.setAttribute("tooltiptext", organization); + this.evButton.hidden = false; + } else { + this.securityButton.removeAttribute("label"); + this.evButton.hidden = true; + } + }, + + startDocumentLoad : function(aRequest) + { + var uri = aRequest.QueryInterface(Ci.nsIChannel).originalURI; + + // clear out search-engine data + getBrowser().selectedBrowser.engines = null; + + // Set the URI now if it isn't already set, so that the user can tell which + // site is loading. Only do this if user requested the load via chrome UI, + // to minimise spoofing risk. + if (!content.opener && + !gURLBar.value && + getWebNavigation().currentURI.spec == "about:blank") + URLBarSetURI(uri); + + try { + Services.obs.notifyObservers(content, "StartDocumentLoad", uri.spec); + } catch (e) { + } + }, + + endDocumentLoad : function(aRequest, aStatus) + { + const nsIChannel = Ci.nsIChannel; + var urlStr = aRequest.QueryInterface(nsIChannel).originalURI.spec; + + if (Components.isSuccessCode(aStatus)) + dump("Document "+urlStr+" loaded successfully\n"); // per QA request + else { + // per QA request + var e = new Components.Exception("", aStatus); + var name = e.name; + dump("Error loading URL "+urlStr+" : "+ + Number(aStatus).toString(16)); + if (name) + dump(" ("+name+")"); + dump('\n'); + } + + var notification = Components.isSuccessCode(aStatus) ? "EndDocumentLoad" : "FailDocumentLoad"; + try { + Services.obs.notifyObservers(content, notification, urlStr); + } catch (e) { + } + } +} + diff --git a/comm/suite/browser/nsTypeAheadFind.js b/comm/suite/browser/nsTypeAheadFind.js new file mode 100644 index 0000000000..aa5e0b9fba --- /dev/null +++ b/comm/suite/browser/nsTypeAheadFind.js @@ -0,0 +1,416 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const kSpace = " ".charCodeAt(0); +const kSlash = "/".charCodeAt(0); +const kApostrophe = "'".charCodeAt(0); + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +function findTypeController(aTypeAheadFind, aWindow) +{ + this.mTypeAheadFind = aTypeAheadFind; + this.mWindow = aWindow; +} + +findTypeController.prototype = { + /* nsIController */ + supportsCommand: function(aCommand) { + return aCommand == "cmd_findTypeText" || aCommand == "cmd_findTypeLinks"; + }, + + isCommandEnabled: function(aCommand) { + // We can always find if there's a primary content window in which to find. + if (this.mWindow.content) + return true; + + // We can also find if the focused window is a content window. + // Note: this gets called during a focus change + // so the new window might not have focus yet. + var commandDispatcher = this.mWindow.document.commandDispatcher; + var e = commandDispatcher.focusedElement; + var w = e ? e.ownerDocument.defaultView : commandDispatcher.focusedWindow; + return w.top != this.mWindow; + }, + + doCommand: function(aCommand) { + this.mTypeAheadFind.startFind(this.mWindow, aCommand != "cmd_findTypeText"); + }, + + onEvent: function(aEvent) { + } +} + +function typeAheadFind() +{ +} + +typeAheadFind.prototype = { + /* properties required for XPCOMUtils */ + classID: Components.ID("{45c8f75b-a299-4178-a461-f63690389055}"), + + /* members */ + mBadKeysSinceMatch: 0, + mBundle: null, + mCurrentWindow: null, + mEventTarget: null, + mFind: null, + mFindService: null, + mFound: null, + mLinks: false, + mSearchString: "", + mSelection: null, + mTimer: null, + mXULBrowserWindow: null, + + /* nsISupports */ + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsISupportsWeakReference, + Ci.nsIObserver, + Ci.nsITimerCallback, + Ci.nsIDOMEventListener, + Ci.nsISelectionListener]), + + /* nsIObserver */ + observe: function(aSubject, aTopic, aData) { + if (aTopic == "app-startup") { + // It's now safe to get our pref branch. + this.mPrefs = Services.prefs.getBranch("accessibility.typeaheadfind."); + // We need to add our event listeners to all windows. + Services.ww.registerNotification(this); + // We also need to listen for find again commands + Services.obs.addObserver(this, "nsWebBrowserFind_FindAgain", true); + } + if (aTopic == "domwindowopened") { + // Add our listeners. They get automatically removed on window teardown. + aSubject.controllers.appendController(new findTypeController(this, aSubject)); + Services.els.addSystemEventListener(aSubject, "keypress", this, false); + } + if (aTopic == "nsWebBrowserFind_FindAgain" && + aSubject instanceof Ci.nsISupportsInterfacePointer && + aSubject.data instanceof Ci.nsIDOMWindow && + aSubject.data.top == this.mCurrentWindow && + this.mSearchString) { + // It's a find again. Was it one that we just searched for? + var w = aSubject.data; + var find = w.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebBrowserFind); + if (find.searchString.toLowerCase() == this.mSearchString) { + var reverse = aData == "up"; + this.stopFind(false); + var result = Ci.nsITypeAheadFind.FIND_NOTFOUND; + if (!this.mBadKeysSinceMatch) + result = this.mFind.findAgain(reverse, this.mLinks); + this.showStatusMatch(result, reverse ? "prevmatch" : "nextmatch"); + // Don't let anyone else try to find again. + aSubject.data = null; + } + } + }, + + /* nsITimerCallback */ + notify: function(aTimer) { + this.stopFind(false); + }, + + /* nsIDOMEventListener */ + handleEvent: function(aEvent) { + if (!aEvent.type.startsWith("key")) { + this.stopFind(false); + return true; + } + + // We don't care about these keys. + if (aEvent.altKey || aEvent.ctrlKey || aEvent.metaKey) + return true; + + if (aEvent.type != "keypress") { + aEvent.stopPropagation(); + return true; + } + + // Are we already in a find? + if (aEvent.eventPhase == Ci.nsIDOMEvent.CAPTURING_PHASE) + return this.processKey(aEvent); + + // Check whether we want to start a new find. + if (aEvent.defaultPrevented) + return true; + + // We don't want to start a find on a control character. + // We also don't want to start on a space, since that scrolls the page. + if (aEvent.keyCode || aEvent.charCode <= kSpace) + return true; + + // Don't start a find if the focus is an editable element. + var window = aEvent.currentTarget; + var element = window.document.commandDispatcher.focusedElement; + if (element.nodeType == element.ELEMENT_NODE && + element.namespaceURI == "http://www.w3.org/1999/xhtml" && + element.isContentEditable) + return true; + + // Don't start a find if the focus is on a form element. + if ((element.nodeType == element.ELEMENT_NODE && + element.namespaceURI == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul") || + ChromeUtils.getClassName(element) === "HTMLEmbedElement" || + ChromeUtils.getClassName(element) === "HTMLObjectElement" || + ChromeUtils.getClassName(element) === "HTMLSelectElement" || + ChromeUtils.getClassName(element) === "HTMLTextAreaElement") + return true; + + // Don't start a find if the focus is on an editable field + if (ChromeUtils.getClassName(element) === "HTMLInputElement" && + element.mozIsTextField(false)) + return true; + + // Don't start a find if the focus isn't or can't be set to content + var w = window.document.commandDispatcher.focusedWindow; + if (w.top == window) + w = window.content; + if (!w) + return true; + + var webNav = w.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation); + try { + // Don't start a find if the window is in design mode + if (webNav.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession) + .windowIsEditable(w)) + return true; + } catch (e) { + } + + switch (aEvent.charCode) { + // Start finding text as you type + case kSlash: + aEvent.preventDefault(); + this.startFind(window, false); + break; + + // Start finding links as you type + case kApostrophe: + aEvent.preventDefault(); + this.startFind(window, true); + break; + + default: + // Don't start if typeahead find is disabled + if (!this.mPrefs.getBoolPref("autostart")) + return true; + // Don't start in windows that don't want autostart + if (webNav.QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.getAttribute("autofind") == "false") + return true; + this.startFind(window, this.mPrefs.getBoolPref("linksonly")); + this.processKey(aEvent); + } + return false; + }, + + /* nsISelectionListener */ + notifySelectionChanged: function(aDoc, aSelection, aReason) { + this.stopFind(false); + }, + + /* private methods */ + showStatus: function(aText) { + if (this.mXULBrowserWindow) + this.mXULBrowserWindow.setOverLink(aText, null); + }, + showStatusString: function(aString) { + // Set the status text from a localised string + this.showStatus(aString && this.mBundle.GetStringFromName(aString)); + }, + showStatusMatch: function(aResult, aExtra) { + // Set the status text from a find result + // link|text "..." [not] found [next|previous match] [(href)] + if (aExtra) + aExtra = " " + this.mBundle.GetStringFromName(aExtra); + var url = ""; + var string = this.mLinks ? "link" : "text"; + if (aResult == Ci.nsITypeAheadFind.FIND_NOTFOUND) + string += "not"; + else if (this.mFind.foundLink && this.mFind.foundLink.href) + url = " " + this.mBundle.GetStringFromName("openparen") + + this.mFind.foundLink.href + + this.mBundle.GetStringFromName("closeparen"); + string += "found"; + this.showStatus(this.mBundle.GetStringFromName(string) + + this.mSearchString + + this.mBundle.GetStringFromName("closequote") + + aExtra + url); + }, + startTimer: function() { + if (this.mPrefs.getBoolPref("enabletimeout")) { + if (!this.mTimer) + this.mTimer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + this.mTimer.initWithCallback(this, + this.mPrefs.getIntPref("timeout"), + Ci.nsITimer.TYPE_ONE_SHOT); + } + }, + processKey: function(aEvent) { + // Escape always cancels the find. + if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { + aEvent.preventDefault(); + aEvent.stopPropagation(); + this.stopFind(false); + return false; + } + + var result = Ci.nsITypeAheadFind.FIND_NOTFOUND; + if (aEvent.keyCode == aEvent.DOM_VK_BACK_SPACE) { + aEvent.preventDefault(); + aEvent.stopPropagation(); + this.mSearchString = this.mSearchString.slice(0, -1); + // Backspacing past the start of the string cancels the find. + if (!this.mSearchString) { + this.stopFind(true); + return false; + } + this.startTimer(); + // The find will change the selection, so stop listening for changes + this.mEventTarget.removeEventListener("blur", this, true); + if (this.mSelection) + this.mSelection.removeSelectionListener(this); + // Don't bother finding until we get back to a working string + if (!this.mBadKeysSinceMatch || !--this.mBadKeysSinceMatch) + result = this.mFind.find(this.mSearchString, this.mLinks); + } else { + // Ignore control characters. + if (aEvent.keyCode || aEvent.charCode < kSpace) + return true; + + this.startTimer(); + aEvent.preventDefault(); + aEvent.stopPropagation(); + + // It looks as if the cat walked on the keyboard. + if (this.mBadKeysSinceMatch >= 3) + return false; + + // The find will change the selection/focus, so stop listening for changes + this.mEventTarget.removeEventListener("blur", this, true); + if (this.mSelection) + this.mSelection.removeSelectionListener(this); + var previousString = this.mSearchString; + this.mSearchString += String.fromCharCode(aEvent.charCode).toLowerCase(); + if (!this.mBadKeysSinceMatch) { + result = this.mFind.find(this.mSearchString, this.mLinks); + if (previousString && + result == Ci.nsITypeAheadFind.FIND_NOTFOUND) + // Use a separate find instance to rehighlight the previous match + // until bug 463294 is fixed. + this.mFound.find(previousString, this.mLinks); + } + if (result == Ci.nsITypeAheadFind.FIND_NOTFOUND) + this.mBadKeysSinceMatch++; + } + + // Ensure that the correct frame is focused (work around for bug 485213). + if (this.mFind.currentWindow) + this.mFind.currentWindow.focus(); + + this.showStatusMatch(result, ""); + if (!this.mFindService) + this.mFindService = Cc["@mozilla.org/find/find_service;1"] + .getService(Ci.nsIFindService); + this.mFindService.searchString = this.mSearchString; + // Watch for blur changes in case the cursor leaves the current field. + this.mEventTarget.addEventListener("blur", this, true); + // Also watch for the cursor moving within the current field or window. + var commandDispatcher = this.mEventTarget.ownerDocument.commandDispatcher; + var editable = commandDispatcher.focusedElement; + if (editable && + ["HTMLInputElement", "HTMLTextAreaElement"] + .includes(ChromeUtils.getClassName(editable))) + this.mSelection = editable.editor.selection; + else + this.mSelection = commandDispatcher.focusedWindow.getSelection(); + this.mSelection.addSelectionListener(this); + return false; + }, + startFind: function(aWindow, aLinks) { + if (this.mEventTarget) + this.stopFind(true); + // Try to get the status bar for the specified window + this.mXULBrowserWindow = + aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow) + .XULBrowserWindow; + + // If the current window is chrome then focus content instead + var w = aWindow.document.commandDispatcher.focusedWindow.top; + if (w == aWindow) + (w = aWindow.content).focus(); + + // Get two toolkit typeaheadfind instances if we don't have them already. + var docShell = w.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + if (!this.mFind) { + this.mFind = Cc["@mozilla.org/typeaheadfind;1"] + .createInstance(Ci.nsITypeAheadFind); + this.mFind.init(docShell); + this.mFound = Cc["@mozilla.org/typeaheadfind;1"] + .createInstance(Ci.nsITypeAheadFind); + this.mFound.init(docShell); + } + + // Get the string bundle if we don't have it already. + if (!this.mBundle) + this.mBundle = Services.strings + .createBundle("chrome://communicator/locale/typeaheadfind.properties"); + + // Set up all our properties + this.mFind.setDocShell(docShell); + this.mFound.setDocShell(docShell); + this.mEventTarget = docShell.chromeEventHandler; + this.mEventTarget.addEventListener("keypress", this, true); + this.mEventTarget.addEventListener("keydown", this, true); + this.mEventTarget.addEventListener("keyup", this, true); + this.mEventTarget.addEventListener("pagehide", this, true); + this.mCurrentWindow = w; + this.mBadKeysSinceMatch = 0; + this.mSearchString = ""; + this.mLinks = aLinks; + this.showStatusString(this.mLinks ? "startlinkfind" : "starttextfind"); + this.startTimer(); + }, + stopFind: function(aClear) { + if (this.mTimer) + this.mTimer.cancel(); + if (this.mFind) + this.mFind.setSelectionModeAndRepaint( + Ci.nsISelectionController.SELECTION_ON); + if (this.mEventTarget) { + this.mEventTarget.removeEventListener("blur", this, true); + this.mEventTarget.removeEventListener("pagehide", this, true); + this.mEventTarget.removeEventListener("keypress", this, true); + this.mEventTarget.removeEventListener("keydown", this, true); + this.mEventTarget.removeEventListener("keyup", this, true); + } + this.mEventTarget = null; + if (this.mSelection) + this.mSelection.removeSelectionListener(this); + this.mSelection = null; + this.showStatusString(aClear ? "" : "stopfind"); + if (aClear) + this.mSearchString = ""; + if (aClear && this.mFind) + this.mFind.collapseSelection(); + }, +}; + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([typeAheadFind]); diff --git a/comm/suite/browser/pageinfo/feeds.js b/comm/suite/browser/pageinfo/feeds.js new file mode 100644 index 0000000000..194f436d23 --- /dev/null +++ b/comm/suite/browser/pageinfo/feeds.js @@ -0,0 +1,31 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function initFeedTab(feeds) +{ + for (let feed of feeds) { + let [name, type, url] = feed; + addRow(name, type, url); + } + + var feedListbox = document.getElementById("feedListbox"); + document.getElementById("feedTab").hidden = feedListbox.getRowCount() == 0; +} + +function onSubscribeFeed(event) +{ + var listbox = document.getElementById("feedListbox"); + subscribeToFeed(listbox.selectedItem.getAttribute("feedURL"), event); +} + +function addRow(name, type, url) +{ + var item = document.createElement("richlistitem"); + item.setAttribute("feed", "true"); + item.setAttribute("name", name); + item.setAttribute("type", type); + item.setAttribute("feedURL", url); + document.getElementById("feedListbox").appendChild(item); +} diff --git a/comm/suite/browser/pageinfo/feeds.xml b/comm/suite/browser/pageinfo/feeds.xml new file mode 100644 index 0000000000..c487e30f55 --- /dev/null +++ b/comm/suite/browser/pageinfo/feeds.xml @@ -0,0 +1,40 @@ +<?xml version="1.0"?><!-- -*- Mode: nXML; indent-tabs-mode: nil -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings [ + <!ENTITY % pageInfoDTD SYSTEM "chrome://navigator/locale/pageInfo.dtd"> + %pageInfoDTD; +]> + +<bindings id="feedBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="feed" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content> + <xul:vbox flex="1"> + <xul:hbox flex="1"> + <xul:textbox flex="1" readonly="true" xbl:inherits="value=name" + class="feedTitle"/> + <xul:label xbl:inherits="value=type"/> + </xul:hbox> + <xul:vbox align="start" flex="1"> + <xul:hbox> + <xul:label xbl:inherits="value=feedURL,tooltiptext=feedURL" + class="text-link" + flex="1" + onclick="openUILink(this.value, event);" crop="end"/> + </xul:hbox> + </xul:vbox> + <xul:hbox flex="1" class="feed-subscribe"> + <xul:spacer flex="1"/> + <xul:button label="&feedSubscribe;" accesskey="&feedSubscribe.accesskey;" + oncommand="onSubscribeFeed(event)"/> + </xul:hbox> + </xul:vbox> + </content> + </binding> +</bindings> diff --git a/comm/suite/browser/pageinfo/pageInfo.css b/comm/suite/browser/pageinfo/pageInfo.css new file mode 100644 index 0000000000..749c01a434 --- /dev/null +++ b/comm/suite/browser/pageinfo/pageInfo.css @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +richlistitem[feed] { + -moz-binding: url("chrome://navigator/content/pageinfo/feeds.xml#feed"); +} + +#thepreviewimage { + display: block; +/* This following entry can be removed when Bug 522850 is fixed. */ + min-width: 1px; +} + +.urltext:-moz-locale-dir(rtl) { + direction: ltr !important; + text-align: end !important; +} diff --git a/comm/suite/browser/pageinfo/pageInfo.js b/comm/suite/browser/pageinfo/pageInfo.js new file mode 100644 index 0000000000..d4f59be55c --- /dev/null +++ b/comm/suite/browser/pageinfo/pageInfo.js @@ -0,0 +1,1177 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + Downloads: "resource://gre/modules/Downloads.jsm", + FileUtils: "resource://gre/modules/FileUtils.jsm", +}); + +//******** define a js object to implement nsITreeView +function pageInfoTreeView(treeid, copycol) +{ + /* copycol is the index number for the column that we want to add to + * the copy-n-paste buffer when the user hits accel-c. + */ + this.treeid = treeid; + this.copycol = copycol; + this.rows = 0; + this.tree = null; + this.data = [ ]; + this.selection = null; + this.sortcol = -1; + this.sortdir = false; +} + +pageInfoTreeView.prototype = { + get rowCount() { return this.rows; }, + + setTree: function(tree) + { + this.tree = tree; + }, + + getCellText: function(row, column) + { + // row can be null, but js arrays are 0-indexed. + return this.data[row][column.index] || ""; + }, + + setCellValue: function(row, column, value) + { + }, + + setCellText: function(row, column, value) + { + this.data[row][column.index] = value; + }, + + addRow: function(row) + { + this.rows = this.data.push(row); + this.rowCountChanged(this.rows - 1, 1); + if (this.selection.count == 0 && this.rowCount && !gImageElement) { + this.selection.select(0); + } + }, + + addRows: function(rows) + { + for (let row of rows) { + this.addRow(row); + } + }, + + rowCountChanged: function(index, count) + { + this.tree.rowCountChanged(index, count); + }, + + invalidate: function() + { + this.tree.invalidate(); + }, + + clear: function() + { + if (this.tree) + this.tree.rowCountChanged(0, -this.rows); + this.rows = 0; + this.data = []; + }, + + cycleHeader: function cycleHeader(col) + { + this.doSort(col, col.index); + }, + + doSort: function doSort(col, index, comparator) + { + var tree = document.getElementById(this.treeid); + if (!comparator) { + comparator = function comparator(a, b) { + return (a || "").toLowerCase().localeCompare((b || "").toLowerCase()); + }; + } + + this.sortdir = gTreeUtils.sort(tree, this, this.data, index, + comparator, this.sortcol, this.sortdir); + + Array.from(this.tree.columns).forEach(function(treecol) { + treecol.element.removeAttribute("sortActive"); + treecol.element.removeAttribute("sortDirection"); + }); + col.element.setAttribute("sortActive", true); + col.element.setAttribute("sortDirection", this.sortdir ? + "ascending" : "descending"); + + this.sortcol = index; + }, + + getRowProperties: function(row) { return ""; }, + getCellProperties: function(row, column) { return ""; }, + getColumnProperties: function(column) { return ""; }, + isContainer: function(index) { return false; }, + isContainerOpen: function(index) { return false; }, + isSeparator: function(index) { return false; }, + isSorted: function() { return this.sortcol > -1 }, + canDrop: function(index, orientation) { return false; }, + drop: function(row, orientation) { return false; }, + getParentIndex: function(index) { return -1; }, + hasNextSibling: function(index, after) { return false; }, + getLevel: function(index) { return 0; }, + getImageSrc: function(row, column) { }, + getProgressMode: function(row, column) { }, + getCellValue: function(row, column) { + let col = (column != null) ? column : this.copycol; + return (row < 0 || col < 0) ? "" : (this.data[row][col] || ""); + }, + toggleOpenState: function(index) { }, + selectionChanged: function() { }, + cycleCell: function(row, column) { }, + isEditable: function(row, column) { return false; }, + isSelectable: function(row, column) { return false; }, +}; + +// mmm, yummy. global variables. +var gDocInfo = null; +var gImageElement = null; + +// column number to help using the data array +const COL_IMAGE_ADDRESS = 0; +const COL_IMAGE_TYPE = 1; +const COL_IMAGE_SIZE = 2; +const COL_IMAGE_ALT = 3; +const COL_IMAGE_COUNT = 4; +const COL_IMAGE_NODE = 5; +const COL_IMAGE_BG = 6; +const COL_IMAGE_SIZENUM = 7; +const COL_IMAGE_PERSIST = 8; +const COL_IMAGE_MIME = 9; + +// column number to copy from, second argument to pageInfoTreeView's constructor +const COPYCOL_NONE = -1; +const COPYCOL_META_CONTENT = 1; +const COPYCOL_FORM_ACTION = 2; +const COPYCOL_FIELD_VALUE = 3; +const COPYCOL_LINK_ADDRESS = 1; +const COPYCOL_IMAGE = COL_IMAGE_ADDRESS; + +// one nsITreeView for each tree in the window +var gMetaView = new pageInfoTreeView("metatree", COPYCOL_META_CONTENT); +var gFormView = new pageInfoTreeView("formtree", COPYCOL_FORM_ACTION); +var gFieldView = new pageInfoTreeView("formpreview", COPYCOL_FIELD_VALUE); +var gLinkView = new pageInfoTreeView("linktree", COPYCOL_LINK_ADDRESS); +var gImageView = new pageInfoTreeView("imagetree", COPYCOL_IMAGE); + +gImageView.getCellProperties = function(row, col) { + var data = gImageView.data[row]; + var item = gImageView.data[row][COL_IMAGE_NODE]; + var properties = col.id == "image-address" ? "ltr" : ""; + if (!checkProtocol(data) || item.HTMLEmbedElement || + (item.HTMLObjectElement && !item.type.startsWith("image/"))) + properties += " broken"; + + return properties; +}; + +gFormView.getCellProperties = function(row, col) { + return col.id == "form-action" ? "ltr" : ""; +}; + +gLinkView.getCellProperties = function(row, col) { + return col.id == "link-address" ? "ltr" : ""; +}; + +gImageView.cycleHeader = function(col) +{ + var index = col.index; + var comparator; + switch (col.index) { + case COL_IMAGE_SIZE: + index = COL_IMAGE_SIZENUM; + case COL_IMAGE_COUNT: + comparator = function numComparator(a, b) { return a - b; }; + break; + } + + this.doSort(col, index, comparator); +}; + +var gImageHash = { }; + +// localized strings (will be filled in when the document is loaded) +// this isn't all of them, these are just the ones that would otherwise have been loaded inside a loop +var gStrings = { }; +var gBundle; + +const DRAGSERVICE_CONTRACTID = "@mozilla.org/widget/dragservice;1"; +const TRANSFERABLE_CONTRACTID = "@mozilla.org/widget/transferable;1"; +const STRING_CONTRACTID = "@mozilla.org/supports-string;1"; + +var loadContextInfo = Services.loadContextInfo.fromLoadContext( + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext), false); +var diskStorage = Services.cache2.diskCacheStorage(loadContextInfo, false); + +const nsICertificateDialogs = Ci.nsICertificateDialogs; +const CERTIFICATEDIALOGS_CONTRACTID = "@mozilla.org/nsCertificateDialogs;1" + +/* Overlays register functions here. + * These arrays are used to hold callbacks that Page Info will call at + * various stages. Use them by simply appending a function to them. + * For example, add a function to onLoadRegistry by invoking + * "onLoadRegistry.push(XXXLoadFunc);" + * The XXXLoadFunc should be unique to the overlay module, and will be + * invoked as "XXXLoadFunc();" + */ + +// These functions are called to build the data displayed in the Page +// Info window. +var onLoadRegistry = [ ]; + +// These functions are called to remove old data still displayed in +// the window when the document whose information is displayed +// changes. For example, the list of images in the Media tab +// is cleared. +var onResetRegistry = [ ]; + +// These functions are called once when all the elements in all of the target +// document (and all of its subframes, if any) have been processed +var onFinished = [ ]; + +// These functions are called once when the Page Info window is closed. +var onUnloadRegistry = [ ]; + +/* Called when PageInfo window is loaded. Arguments are: + * window.arguments[0] - (optional) an object consisting of + * - doc: (optional) document to use for source. if not provided, + * the calling window's document will be used + * - initialTab: (optional) id of the inital tab to display + */ +function onLoadPageInfo() +{ + gBundle = document.getElementById("pageinfobundle"); + var strNames = ["unknown", "notSet", "mediaImg", "mediaBGImg", + "mediaBorderImg", "mediaListImg", "mediaCursor", + "mediaObject", "mediaEmbed", "mediaLink", "mediaInput", + "mediaVideo", "mediaAudio", + "formTitle", "formUntitled", "formDefaultTarget", + "formChecked", "formUnchecked", "formPassword", "linkAnchor", + "linkArea", "linkSubmission", "linkSubmit", "linkRel", + "linkStylesheet", "linkRev", "linkX", "linkScript", + "linkScriptInline", "yes"]; + strNames.forEach(function(n) { gStrings[n] = gBundle.getString(n); }); + + var args = "arguments" in window && + window.arguments.length >= 1 && + window.arguments[0]; + + // init views + function initView(treeid, view) + { + document.getElementById(treeid).view = view; + } + + initView("imagetree", gImageView); + initView("formtree", gFormView); + initView("formpreview", gFieldView); + initView("linktree", gLinkView); + initPermission(); + + /* Select the requested tab, if the name is specified */ + loadTab(args); + Services.obs.notifyObservers(window, "page-info-dialog-loaded"); +} + +function loadPageInfo(frameOuterWindowID, imageElement, browser) +{ + browser = browser || window.opener.gBrowser.selectedBrowser; + let mm = browser.messageManager; + + gStrings["application/rss+xml"] = gBundle.getString("feedRss"); + gStrings["application/atom+xml"] = gBundle.getString("feedAtom"); + gStrings["text/xml"] = gBundle.getString("feedXML"); + gStrings["application/xml"] = gBundle.getString("feedXML"); + gStrings["application/rdf+xml"] = gBundle.getString("feedXML"); + + // Look for pageInfoListener in content.js. + // Sends message to listener with arguments. + mm.sendAsyncMessage("PageInfo:getData", {strings: gStrings, + frameOuterWindowID: frameOuterWindowID}, + { imageElement }); + + let pageInfoData; + + // Get initial pageInfoData needed to display the general, feeds, permission + // and security tabs. + mm.addMessageListener("PageInfo:data", function onmessage(message) { + mm.removeMessageListener("PageInfo:data", onmessage); + pageInfoData = message.data; + let docInfo = pageInfoData.docInfo; + let windowInfo = pageInfoData.windowInfo; + let uri = makeURI(docInfo.documentURIObject.spec); + let principal = docInfo.principal; + gDocInfo = docInfo; + + gImageElement = pageInfoData.imageInfo; + + var titleFormat = windowInfo.isTopWindow ? "pageInfo.page.title" + : "pageInfo.frame.title"; + document.title = gBundle.getFormattedString(titleFormat, + [docInfo.location]); + + document.getElementById("main-window").setAttribute("relatedUrl", + docInfo.location); + + makeGeneralTab(pageInfoData.metaViewRows, docInfo); + initFeedTab(pageInfoData.feeds); + onLoadPermission(uri, principal); + securityOnLoad(uri, windowInfo); + }); + + // Get the media elements from content script to setup the media tab. + mm.addMessageListener("PageInfo:mediaData", function onmessage(message) { + // Page info window was closed. + if (window.closed) { + mm.removeMessageListener("PageInfo:mediaData", onmessage); + return; + } + + // The page info media fetching has been completed. + if (message.data.isComplete) { + mm.removeMessageListener("PageInfo:mediaData", onmessage); + onFinished.forEach(function(func) { func(pageInfoData); }); + return; + } + + if (message.data.imageItems) { + for (let item of message.data.imageItems) { + addImage(item); + } + selectImage(); + } + + if (message.data.linkItems) { + gLinkView.addRows(message.data.linkItems); + } + + if (message.data.formItems) { + gFormView.addRows(message.data.formItems); + } + }); + + /* Call registered overlay init functions */ + onLoadRegistry.forEach(function(func) { func(); }); +} + +function resetPageInfo(args) +{ + /* Reset Media tab */ + // Remove the observer, only if there is at least 1 image. + if (gImageView.data.length != 0) { + Services.obs.removeObserver(imagePermissionObserver, "perm-changed"); + } + + /* Reset tree views */ + gMetaView.clear(); + gFormView.clear(); + gFieldView.clear(); + gLinkView.clear(); + gImageView.clear(); + gImageHash = {}; + + /* Reset Feeds Tab */ + var feedListbox = document.getElementById("feedListbox"); + while (feedListbox.hasChildNodes()) + feedListbox.lastChild.remove(); + + /* Call registered overlay reset functions */ + onResetRegistry.forEach(function(func) { func(); }); + + /* Rebuild the data */ + loadTab(args); + + Services.obs.notifyObservers(window, "page-info-dialog-reset"); +} + +function onUnloadPageInfo() +{ + // Remove the observer, only if there is at least 1 image. + if (gImageView.data.length != 0) { + Services.obs.removeObserver(imagePermissionObserver, "perm-changed"); + } + + /* Call registered overlay unload functions */ + onUnloadRegistry.forEach(function(func) { func(); }); +} + +function doHelpButton() +{ + const helpTopics = { + "generalTab": "pageinfo_general", + "mediaTab": "pageinfo_media", + // "feedTab": "pageinfo_feed", + // "permTab": "pageinfo_permissions", + "formsTab": "pageinfo_forms", + "linksTab": "pageinfo_links", + "securityTab": "pageinfo_security" + }; + + var tabbox = document.getElementById("tabbox"); + var helpdoc = helpTopics[tabbox.selectedTab.id] || "nav-page-info"; + openHelp(helpdoc, "chrome://communicator/locale/help/suitehelp.rdf"); +} + +function showTab(id) +{ + var tabbox = document.getElementById("tabbox"); + var selectedTab = document.getElementById(id) || + document.getElementById(id + "Tab") || // Firefox compatibility sillyness + document.getElementById("generalTab"); + tabbox.selectedTab = selectedTab; + selectedTab.focus(); +} + +function loadTab(args) +{ + // If the "View Image Info" context menu item was used, the related image + // element is provided as an argument. This can't be a background image. + let imageElement = args && args.imageElement; + let frameOuterWindowID = args && args.frameOuterWindowID; + let browser = args && args.browser; + + /* Load the page info */ + loadPageInfo(frameOuterWindowID, imageElement, browser); + + /* Select the requested tab, if the name is specified */ + var initialTab = (args && args.initialTab) || "generalTab"; + showTab(initialTab); +} + +function onClickMore() +{ + showTab("securityTab"); +} + +function openCacheEntry(key, cb) +{ + var checkCacheListener = { + onCacheEntryCheck: function(entry, appCache) { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + onCacheEntryAvailable: function(entry, isNew, appCache, status) { + cb(entry); + } + }; + diskStorage.asyncOpenURI(Services.io.newURI(key, null, null), "", + Ci.nsICacheStorage.OPEN_READONLY, + checkCacheListener); +} + +function makeGeneralTab(metaViewRows, docInfo) +{ + var title = (docInfo.title) ? docInfo.title : gBundle.getString("noPageTitle"); + document.getElementById("titletext").value = title; + + var url = docInfo.location.toString(); + setItemValue("urltext", url); + + var referrer = ("referrer" in docInfo && docInfo.referrer); + setItemValue("refertext", referrer); + + var mode = ("compatMode" in docInfo && docInfo.compatMode == "BackCompat") ? "generalQuirksMode" : "generalStrictMode"; + document.getElementById("modetext").value = gBundle.getString(mode); + + // find out the mime type + var mimeType = docInfo.contentType; + setItemValue("typetext", mimeType); + + // get the document characterset + var encoding = docInfo.characterSet; + document.getElementById("encodingtext").value = encoding; + + var length = metaViewRows.length; + + var metaGroup = document.getElementById("metaTags"); + if (!length) { + metaGroup.collapsed = true; + } + else { + var metaTagsCaption = document.getElementById("metaTagsCaption"); + if (length == 1) + metaTagsCaption.label = gBundle.getString("generalMetaTag"); + else + metaTagsCaption.label = gBundle.getFormattedString("generalMetaTags", [length]); + var metaTree = document.getElementById("metatree"); + metaTree.view = gMetaView; + + // Add the metaViewRows onto the general tab's meta info tree. + gMetaView.addRows(metaViewRows); + + metaGroup.collapsed = false; + } + + // get the date of last modification + var modifiedText = formatDate(docInfo.lastModified, gStrings.notSet); + document.getElementById("modifiedtext").value = modifiedText; + + // get cache info + var cacheKey = url.replace(/#.*$/, ""); + openCacheEntry(cacheKey, function(cacheEntry) { + var sizeText; + if (cacheEntry) { + var pageSize = cacheEntry.dataSize; + var kbSize = formatNumber(Math.round(pageSize / 1024 * 100) / 100); + sizeText = gBundle.getFormattedString("generalSize", [kbSize, formatNumber(pageSize)]); + } + setItemValue("sizetext", sizeText); + }); +} + +function ensureSelection(view) +{ + // only select something if nothing is currently selected + // and if there's anything to select + if (view.selection && view.selection.count == 0 && view.rowCount) + view.selection.select(0); +} + +function addImage(imageViewRow) +{ + let [url, type, alt, elem, isBg] = imageViewRow; + + if (!url) + return; + + if (!gImageHash.hasOwnProperty(url)) + gImageHash[url] = { }; + if (!gImageHash[url].hasOwnProperty(type)) + gImageHash[url][type] = { }; + if (!gImageHash[url][type].hasOwnProperty(alt)) { + gImageHash[url][type][alt] = gImageView.data.length; + var row = [url, type, gStrings.unknown, alt, 1, elem, isBg, -1, null, null]; + gImageView.addRow(row); + + // Fill in cache data asynchronously + openCacheEntry(url, function(cacheEntry) { + if (cacheEntry) { + // Update the corresponding data entries from the cache. + var imageSize = cacheEntry.dataSize; + // If it is not -1 then replace with actual value, else keep as unknown. + if (imageSize && imageSize != -1) { + var kbSize = Math.round(imageSize / 1024 * 100) / 100; + row[2] = gBundle.getFormattedString("mediaFileSize", + [formatNumber(kbSize)]); + row[7] = imageSize; + } + row[8] = cacheEntry.persistent; + row[9] = getContentTypeFromHeaders(cacheEntry); + // Invalidate the row to trigger a repaint. + gImageView.tree.invalidateRow(gImageView.data.indexOf(row)); + } + }); + + // Add the observer, only once. + if (gImageView.data.length == 1) { + Services.obs.addObserver(imagePermissionObserver, "perm-changed"); + } + } + else { + var i = gImageHash[url][type][alt]; + gImageView.data[i][COL_IMAGE_COUNT]++; + // The same image can occur several times on the page at different sizes. + // If the "View Image Info" context menu item was used, ensure we select + // the correct element. + if (!gImageView.data[i][COL_IMAGE_BG] && + gImageElement && url == gImageElement.currentSrc && + gImageElement.width == elem.width && + gImageElement.height == elem.height && + gImageElement.imageText == elem.imageText) { + gImageView.data[i][COL_IMAGE_NODE] = elem; + } + } +} + +//******** Form Stuff +function onFormSelect() +{ + if (gFormView.selection.count == 1) + { + var formPreview = document.getElementById("formpreview"); + gFieldView.clear(); + formPreview.view = gFieldView; + + var clickedRow = gFormView.selection.currentIndex; + // form-node; + var form = gFormView.data[clickedRow][3]; + + var ft = null; + if (form.name) + ft = gBundle.getFormattedString("formTitle", [form.name]); + + setItemValue("formenctype", form.encoding, gStrings.default); + setItemValue("formtarget", form.target, gStrings.formDefaultTarget); + document.getElementById("formname").value = ft || gStrings.formUntitled; + + gFieldView.addRows(form.formfields); + } +} + +//******** Link Stuff +function onBeginLinkDrag(event,urlField,descField) +{ + if (event.originalTarget.localName != "treechildren") + return; + + var tree = event.target; + if (!("treeBoxObject" in tree)) + tree = tree.parentNode; + + var row = tree.treeBoxObject.getRowAt(event.clientX, event.clientY); + if (row == -1) + return; + + // Adding URL flavor + var col = tree.columns[urlField]; + var url = tree.view.getCellText(row, col); + col = tree.columns[descField]; + var desc = tree.view.getCellText(row, col); + + var dataTransfer = event.dataTransfer; + dataTransfer.setData("text/x-moz-url", url + "\n" + desc); + dataTransfer.setData("text/url-list", url); + dataTransfer.setData("text/plain", url); +} + +//******** Image Stuff +function getSelectedRows(tree) { + var start = { }; + var end = { }; + var numRanges = tree.view.selection.getRangeCount(); + + var rowArray = [ ]; + for (var t = 0; t < numRanges; t++) { + tree.view.selection.getRangeAt(t, start, end); + for (var v = start.value; v <= end.value; v++) + rowArray.push(v); + } + + return rowArray; +} + +function getSelectedRow(tree) { + var rows = getSelectedRows(tree); + return (rows.length == 1) ? rows[0] : -1; +} + +function selectSaveFolder(aCallback) { + return selectSaveFolderTask(aCallback).catch(Cu.reportError); +} + +async function selectSaveFolderTask(aCallback) { + let titleText = gBundle.getString("mediaSelectFolder"); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + + fp.init(window, titleText, Ci.nsIFilePicker.modeGetFolder); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + try { + let initialDir = Services.prefs.getComplexValue("browser.download.dir", + Ci.nsIFile); + if (!initialDir) { + let downloadsDir = await Downloads.getSystemDownloadsDirectory(); + initialDir = new FileUtils.File(downloadsDir); + } + + fp.displayDirectory = initialDir; + } catch (ex) { + } + + let result = await new Promise(resolve => fp.open(resolve)); + + if (result == Ci.nsIFilePicker.returnOK) { + aCallback(fp.file.QueryInterface(Ci.nsIFile)); + } else { + aCallback(null); + } +} + +function saveMedia() +{ + var tree = document.getElementById("imagetree"); + var rowArray = getSelectedRows(tree); + if (rowArray.length == 1) { + let row = rowArray[0]; + let item = gImageView.data[row][COL_IMAGE_NODE]; + let url = gImageView.data[row][COL_IMAGE_ADDRESS]; + + if (url) { + let titleKey = "SaveImageTitle"; + + if (item instanceof HTMLVideoElement) + titleKey = "SaveVideoTitle"; + else if (item instanceof HTMLAudioElement) + titleKey = "SaveAudioTitle"; + + saveURL(url, null, titleKey, false, false, makeURI(item.baseURI), + null, gDocInfo.isContentWindowPrivate, + gDocument.nodePrincipal); + } + } else { + selectSaveFolder(function(aDirectory) { + if (aDirectory) { + var saveAnImage = function(aURIString, aChosenData, aBaseURI) { + uniqueFile(aChosenData.file); + internalSave(aURIString, null, null, null, null, false, + "SaveImageTitle", aChosenData, aBaseURI, null, false, + null, gDocInfo.isContentWindowPrivate, + gDocument.nodePrincipal); + }; + + for (var i = 0; i < rowArray.length; i++) { + let v = rowArray[i]; + let dir = aDirectory.clone(); + let item = gImageView.data[v][COL_IMAGE_NODE]; + let uriString = gImageView.data[v][COL_IMAGE_ADDRESS]; + let uri = makeURI(uriString); + + try { + uri.QueryInterface(Ci.nsIURL); + dir.append(decodeURIComponent(uri.fileName)); + } catch (ex) { + // data:/blob: uris + // Supply a dummy filename, otherwise Download Manager + // will try to delete the base directory on failure. + dir.append(gImageView.data[v][COL_IMAGE_TYPE]); + } + + if (i == 0) { + saveAnImage(uriString, new AutoChosen(dir, uri), makeURI(item.baseURI)); + } else { + // This delay is a hack which prevents the download manager + // from opening many times. See bug 377339. + setTimeout(saveAnImage, 200, uriString, new AutoChosen(dir, uri), + makeURI(item.baseURI)); + } + } + } + }); + } +} + +function onBlockImage(aChecked) +{ + var uri = makeURI(document.getElementById("imageurltext").value); + if (aChecked) + Services.perms.add(uri, "image", Services.perms.DENY_ACTION); + else + Services.perms.remove(uri, "image"); +} + +function onImageSelect() +{ + var previewBox = document.getElementById("mediaPreviewBox"); + var mediaSaveBox = document.getElementById("mediaSaveBox"); + var mediaSaveButton = document.getElementById("imagesaveasbutton"); + var splitter = document.getElementById("mediaSplitter"); + var tree = document.getElementById("imagetree"); + var count = tree.view.selection.count; + + if (count == 0) + { + previewBox.collapsed = true; + mediaSaveBox.collapsed = true; + mediaSaveButton.disabled = true; + splitter.collapsed = true; + tree.flex = 1; + } + else if (count > 1) + { + previewBox.collapsed = true; + mediaSaveBox.collapsed = false; + mediaSaveButton.disabled = false; + splitter.collapsed = true; + tree.flex = 1; + } + else + { + previewBox.collapsed = false; + mediaSaveBox.collapsed = true; + mediaSaveButton.disabled = false; + splitter.collapsed = false; + tree.flex = 0; + makePreview(tree.view.selection.currentIndex); + } +} + +// Makes the media preview (image, video, etc) for the selected row on +// the media tab. +function makePreview(row) +{ + var [url, type, sizeText, alt, count, item, isBG, imageSize, persistent, cachedType] = gImageView.data[row]; + var isAudio = false; + + setItemValue("imageurltext", url); + setItemValue("imagetext", item.imageText); + setItemValue("imagelongdesctext", item.longDesc); + + // get cache info + var sourceText; + switch (persistent) { + case true: + sourceText = gBundle.getString("generalDiskCache"); + break; + case false: + sourceText = gBundle.getString("generalMemoryCache"); + break; + default: + sourceText = gBundle.getString("generalNotCached"); + break; + } + setItemValue("imagesourcetext", sourceText); + + // find out the file size + var sizeText; + if (imageSize && imageSize != -1) { + var kbSize = Math.round(imageSize / 1024 * 100) / 100; + sizeText = gBundle.getFormattedString("generalSize", + [formatNumber(kbSize), + formatNumber(imageSize)]); + } + else + sizeText = gBundle.getString("mediaUnknownNotCached"); + setItemValue("imagesizetext", sizeText); + + var mimeType = item.mimeType || cachedType; + var numFrames = item.numFrames; + + var imageType; + if (mimeType) { + // We found the type, try to display it nicely + let imageMimeType = /^image\/(.*)/i.exec(mimeType); + if (imageMimeType) { + imageType = imageMimeType[1].toUpperCase(); + if (numFrames > 1) + imageType = gBundle.getFormattedString("mediaAnimatedImageType", + [imageType, numFrames]); + else + imageType = gBundle.getFormattedString("mediaImageType", [imageType]); + } + else { + // the MIME type doesn't begin with image/, display the raw type + imageType = mimeType; + } + } + else { + // We couldn't find the type, fall back to the value in the treeview + imageType = type; + } + + setItemValue("imagetypetext", imageType); + + var imageContainer = document.getElementById("theimagecontainer"); + var oldImage = document.getElementById("thepreviewimage"); + + var isProtocolAllowed = checkProtocol(gImageView.data[row]); + var isImageType = mimeType && mimeType.startsWith("image/"); + + var newImage = new Image; + newImage.id = "thepreviewimage"; + var physWidth = 0, physHeight = 0; + var width = 0, height = 0; + + if ((item.HTMLLinkElement || item.HTMLInputElement || + item.HTMLImageElement || item.SVGImageElement || + (item.HTMLObjectElement && isImageType) || + (item.HTMLEmbedElement && isImageType) || + isBG) && isProtocolAllowed) { + // We need to wait for the image to finish loading before + // using width & height. + newImage.addEventListener("loadend", function() { + physWidth = newImage.width || 0; + physHeight = newImage.height || 0; + + // "width" and "height" attributes must be set to newImage, + // even if there is no "width" or "height attribute in item; + // otherwise, the preview image cannot be displayed correctly. + // Since the image might have been loaded out-of-process, we expect + // the item to tell us its width / height dimensions. Failing that + // the item should tell us the natural dimensions of the image. Finally + // failing that, we'll assume that the image was never loaded in the + // other process (this can be true for favicons, for example), and so + // we'll assume that we can use the natural dimensions of the newImage + // we just created. If the natural dimensions of newImage are not known + // then the image is probably broken. + if (!isBG) { + newImage.width = ("width" in item && item.width) || + newImage.naturalWidth; + newImage.height = ("height" in item && item.height) || + newImage.naturalHeight; + } + else { + // The width and height of an HTML tag should not be used for its + // background image (for example, "table" can have "width" or "height" + // attributes). + newImage.width = item.naturalWidth || newImage.naturalWidth; + newImage.height = item.naturalHeight || newImage.naturalHeight; + } + + if (item.SVGImageElement) { + newImage.width = item.SVGImageElementWidth; + newImage.height = item.SVGImageElementHeight; + } + + width = newImage.width; + height = newImage.height; + + document.getElementById("theimagecontainer").collapsed = false + document.getElementById("brokenimagecontainer").collapsed = true; + + let imageSize = ""; + if (url) { + if (width != physWidth || height != physHeight) { + imageSize = gBundle.getFormattedString("mediaDimensionsScaled", + [formatNumber(physWidth), + formatNumber(physHeight), + formatNumber(width), + formatNumber(height)]); + } else { + imageSize = gBundle.getFormattedString("mediaDimensions", + [formatNumber(width), + formatNumber(height)]); + } + } + setItemValue("imagedimensiontext", imageSize); + }, {once: true}); + + newImage.setAttribute("src", url); + } + else { + // Handle the case where newImage is not used for width & height. + if (item.HTMLVideoElement && isProtocolAllowed) { + newImage = document.createElementNS("http://www.w3.org/1999/xhtml", "video"); + newImage.id = "thepreviewimage"; + newImage.src = url; + newImage.controls = true; + width = physWidth = item.videoWidth; + height = physHeight = item.videoHeight; + + document.getElementById("theimagecontainer").collapsed = false + document.getElementById("brokenimagecontainer").collapsed = true; + } + else if (item.HTMLAudioElement && isProtocolAllowed) { + newImage = new Audio; + newImage.id = "thepreviewimage"; + newImage.src = url; + newImage.controls = true; + newImage.preload = "metadata"; + isAudio = true; + + document.getElementById("theimagecontainer").collapsed = false + document.getElementById("brokenimagecontainer").collapsed = true; + } + else { + // fallback image for protocols not allowed (e.g., javascript:) + // or elements not [yet] handled (e.g., object, embed). + document.getElementById("brokenimagecontainer").collapsed = false; + document.getElementById("theimagecontainer").collapsed = true; + } + + let imageSize = ""; + if (url && !isAudio) { + imageSize = gBundle.getFormattedString("mediaDimensions", + [formatNumber(width), + formatNumber(height)]); + } + setItemValue("imagedimensiontext", imageSize); + } + + makeBlockImage(url); + + oldImage.remove(); + imageContainer.appendChild(newImage); +} + +function makeBlockImage(url) +{ + var checkbox = document.getElementById("blockImage"); + var imagePref = Services.prefs.getIntPref("permissions.default.image"); + if (!(/^https?:/.test(url)) || imagePref == 2) + // We can't block the images from this host because either is is not + // for http(s) or we don't load images at all + checkbox.hidden = true; + else { + var uri = makeURI(url); + if (uri.host) { + checkbox.hidden = false; + checkbox.label = gBundle.getFormattedString("mediaBlockImage", [uri.host]); + var perm = Services.perms.testPermission(uri, "image"); + checkbox.checked = perm == Services.perms.DENY_ACTION; + } + else + checkbox.hidden = true; + } +} + +var imagePermissionObserver = { + observe: function (aSubject, aTopic, aData) + { + if (document.getElementById("mediaPreviewBox").collapsed) + return; + + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface(Ci.nsIPermission); + if (permission.type == "image") { + var imageTree = document.getElementById("imagetree"); + var row = imageTree.currentIndex; + var item = gImageView.data[row][COL_IMAGE_NODE]; + var url = gImageView.data[row][COL_IMAGE_ADDRESS]; + if (permission.matchesURI(makeURI(url), true)) + makeBlockImage(url); + } + } + } +} + +function getContentTypeFromHeaders(cacheEntryDescriptor) +{ + if (!cacheEntryDescriptor) + return null; + + let headers = cacheEntryDescriptor.getMetaDataElement("response-head"); + let type = /^Content-Type:\s*(.*?)\s*(?:\;|$)/mi.exec(headers); + return type && type[1]; +} + +function setItemValue(id, value, defaultString = gStrings.notSet) +{ + var item = document.getElementById(id); + if (value) { + item.disabled = false; + item.value = value; + } + else + { + item.value = defaultString; + item.disabled = true; + } +} + +function formatNumber(number) +{ + return (+number).toLocaleString(); // coerce number to a numeric value before calling toLocaleString() +} + +function formatDate(datestr, unknown) +{ + var date = new Date(datestr); + if (!date.valueOf()) + return unknown; + + const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "full", timeStyle: "long"}); + return dateTimeFormatter.format(date); +} + +function getSelectedItems(linksMode) +{ + // linksMode is a boolean that is used to determine + // whether the getSelectedItems() function needs to + // run with urlSecurityCheck() or not. + + var elem = document.commandDispatcher.focusedElement; + + var view = elem.view; + var selection = view.selection; + var text = [], tmp = ''; + var min = {}, max = {}; + + var count = selection.getRangeCount(); + + for (var i = 0; i < count; i++) { + selection.getRangeAt(i, min, max); + + for (var row = min.value; row <= max.value; row++) { + tmp = view.getCellValue(row, null); + if (tmp) + { + try { + if (linksMode) + urlSecurityCheck(tmp, gDocInfo.principal); + text.push(tmp); + } + catch (e) { + } + } + } + } + + return text; +} + +function doCopy(isLinkMode) +{ + var text = getSelectedItems(isLinkMode); + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(text.join("\n")); +} + +function doSelectAllMedia() +{ + var tree = document.getElementById("imagetree"); + + if (tree) + tree.view.selection.selectAll(); +} + +function doSelectAll() +{ + var elem = document.commandDispatcher.focusedElement; + + if (elem && "treeBoxObject" in elem) + elem.view.selection.selectAll(); +} + +function selectImage() { + if (!gImageElement) + return; + + var tree = document.getElementById("imagetree"); + for (var i = 0; i < tree.view.rowCount; i++) { + // If the image row element is the image selected from + // the "View Image Info" context menu item. + let image = gImageView.data[i][COL_IMAGE_NODE]; + if (!gImageView.data[i][COL_IMAGE_BG] && + gImageElement.currentSrc == gImageView.data[i][COL_IMAGE_ADDRESS] && + gImageElement.width == image.width && + gImageElement.height == image.height && + gImageElement.imageText == image.imageText) { + tree.view.selection.select(i); + tree.treeBoxObject.ensureRowIsVisible(i); + tree.focus(); + return; + } + } +} + +function checkProtocol(img) +{ + var url = img[COL_IMAGE_ADDRESS]; + return /^data:image\//i.test(url) || + /^(https?|ftp|file|about|chrome|resource):/.test(url); +} + +function onOpenIn(mode) +{ + var linkList = getSelectedItems(true); + + if (linkList.length) + openUILinkArrayIn(linkList, mode); +} diff --git a/comm/suite/browser/pageinfo/pageInfo.xul b/comm/suite/browser/pageinfo/pageInfo.xul new file mode 100644 index 0000000000..8f4dc2ae96 --- /dev/null +++ b/comm/suite/browser/pageinfo/pageInfo.xul @@ -0,0 +1,531 @@ +<?xml version="1.0"?><!-- -*- Mode: nXML; indent-tabs-mode: nil -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://navigator/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://navigator/content/pageinfo/pageInfo.css" type="text/css"?> +<?xml-stylesheet href="chrome://navigator/skin/pageInfo.css" type="text/css"?> + +<!DOCTYPE window [ + <!ENTITY % pageInfoDTD SYSTEM "chrome://navigator/locale/pageInfo.dtd"> + %pageInfoDTD; +]> + +<window id="main-window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + windowtype="Browser:page-info" + onload="onLoadPageInfo()" + onunload="onUnloadPageInfo()" + align="stretch" style="&pageInfoWindow.dimensions;" + persist="screenX screenY width height sizemode"> + + <script src="chrome://global/content/globalOverlay.js"/> + <script src="chrome://global/content/contentAreaUtils.js"/> + <script src="chrome://global/content/treeUtils.js"/> + <script src="chrome://communicator/content/utilityOverlay.js"/> + <script src="chrome://navigator/content/pageinfo/pageInfo.js"/> + <script src="chrome://navigator/content/pageinfo/feeds.js"/> + <script src="chrome://navigator/content/pageinfo/permissions.js"/> + <script src="chrome://navigator/content/pageinfo/security.js"/> + <script src="chrome://help/content/contextHelp.js"/> + <script src="chrome://communicator/content/tasksOverlay.js"/> + + <stringbundleset id="pageinfobundleset"> + <stringbundle id="pageinfobundle" src="chrome://navigator/locale/pageInfo.properties"/> + <stringbundle id="pkiBundle" src="chrome://pippki/locale/pippki.properties"/> + </stringbundleset> + + <commandset id="pageInfoCommandSet"> + <command id="cmd_close" oncommand="window.close();"/> + <command id="cmd_help" oncommand="doHelpButton();"/> + <command id="cmd_copy" oncommand="doCopy(false);"/> + <command id="cmd_selectall" oncommand="doSelectAll();"/> + + <!-- links tab --> + <command id="cmd_openInNewTab" oncommand="onOpenIn('tab');"/> + <command id="cmd_openInNewWindow" oncommand="onOpenIn('window');"/> + <command id="cmd_copyLinks" oncommand="doCopy(true);"/> + </commandset> + + <keyset> + <key key="&closeWindow.key;" modifiers="accel" command="cmd_close"/> + <key keycode="VK_ESCAPE" command="cmd_close"/> + <key key="." modifiers="meta" command="cmd_close"/> + <key keycode="VK_F1" command="cmd_help"/> + <key key="&openHelpMac.key;" modifiers="meta" command="cmd_help"/> + <key key="©.key;" modifiers="accel" command="cmd_copy"/> + <key key="&selectall.key;" modifiers="accel" command="cmd_selectall"/> + <key key="&selectall.key;" modifiers="alt" command="cmd_selectall"/> + </keyset> + + <menupopup id="picontext"> + <menuitem id="menu_selectall" label="&selectall.label;" command="cmd_selectall" accesskey="&selectall.accesskey;"/> + <menuitem id="menu_copy" label="©.label;" command="cmd_copy" accesskey="©.accesskey;"/> + </menupopup> + + <menupopup id="piLinksContext"> + <menuitem id="menu_openInNewTab" + label="&openInNewTab.label;" + command="cmd_openInNewTab" + accesskey="&openInNewTab.accesskey;"/> + <menuitem id="menu_openInNewWindow" + label="&openInNewWindow.label;" + command="cmd_openInNewWindow" + accesskey="&openInNewWindow.accesskey;"/> + <menuitem id="menu_selectall_links" + label="&selectall.label;" + command="cmd_selectall" + accesskey="&selectall.accesskey;"/> + <menuitem id="menu_copyLinks" + label="©Links.label;" + command="cmd_copyLinks" + accesskey="©Links.accesskey;"/> + </menupopup> + + <tabbox id="tabbox" flex="1"> + <vbox id="dragbox"> + <tabs id="tabs" + onselect="[gImageView, gFormView, gLinkView].forEach(ensureSelection);"> + <tab id="generalTab" + label="&generalTab;" + accesskey="&generalTab.accesskey;"/> + <tab id="mediaTab" + label="&mediaTab;" + accesskey="&mediaTab.accesskey;"/> + <tab id="feedTab" + label="&feedTab;" + accesskey="&feedTab.accesskey;"/> + <tab id="permTab" + label="&permTab;" + accesskey="&permTab.accesskey;"/> + <tab id="formsTab" + label="&formsTab;" + accesskey="&formsTab.accesskey;"/> + <tab id="linksTab" + label="&linksTab;" + accesskey="&linksTab.accesskey;"/> + <tab id="securityTab" + label="&securityTab;" + accesskey="&securityTab.accesskey;"/> + <!-- Others added by overlay --> + </tabs> + </vbox> + + <tabpanels id="mainDeck" flex="1"> + <!-- General page information --> + <vbox id="generalPanel"> + <grid> + <columns> + <column/> + <column class="gridSeparator"/> + <column flex="1"/> + </columns> + <rows> + <row id="generalTitle"> + <label control="titletext" value="&generalTitle;"/> + <separator/> + <textbox readonly="true" id="titletext"/> + </row> + <row> + <label control="urltext" value="&generalURL;"/> + <separator/> + <textbox readonly="true" id="urltext" class="urltext"/> + </row> + <row> + <separator class="thin"/> + </row> + <row> + <label control="typetext" value="&generalType;"/> + <separator/> + <textbox readonly="true" id="typetext"/> + </row> + <row> + <label control="modetext" value="&generalMode;"/> + <separator/> + <textbox readonly="true" crop="end" id="modetext"/> + </row> + <row> + <label control="encodingtext" value="&generalEncoding2;"/> + <separator/> + <textbox readonly="true" id="encodingtext"/> + </row> + <row> + <label control="sizetext" value="&generalSize;"/> + <separator/> + <textbox readonly="true" id="sizetext"/> + </row> + <row> + <label control="refertext" value="&generalReferrer;"/> + <separator/> + <textbox readonly="true" id="refertext" class="urltext"/> + </row> + <row> + <separator class="thin"/> + </row> + <row> + <label control="modifiedtext" value="&generalModified;"/> + <separator/> + <textbox readonly="true" id="modifiedtext"/> + </row> + </rows> + </grid> + <separator class="thin"/> + <groupbox id="metaTags" flex="1"> + <caption id="metaTagsCaption"/> + <tree id="metatree" flex="1" hidecolumnpicker="true" contextmenu="picontext"> + <treecols> + <treecol id="meta-name" label="&generalMetaName;" + persist="width" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="meta-content" label="&generalMetaContent;" + persist="width" flex="4"/> + </treecols> + <treechildren flex="1"/> + </tree> + </groupbox> + <groupbox id="securityBox"> + <caption id="securityBoxCaption" label="&securityHeader;"/> + <description id="general-security-identity" class="indent header"/> + <description id="general-security-privacy" class="indent header"/> + <hbox align="right"> + <button id="security-view-details" label="&generalSecurityDetails;" + accesskey="&generalSecurityDetails.accesskey;" + oncommand="onClickMore();"/> + </hbox> + </groupbox> + </vbox> + + <!-- Media information --> + <vbox id="mediaPanel"> + <tree id="imagetree" onselect="onImageSelect();" contextmenu="picontext" + ondragstart="onBeginLinkDrag(event,'image-address','image-alt')"> + <treecols> + <treecol sortSeparators="true" primary="true" persist="width" flex="10" + width="10" id="image-address" label="&mediaAddress;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="2" + width="2" id="image-type" label="&mediaType;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="2" + width="2" id="image-size" label="&mediaSize;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="4" + width="4" id="image-alt" label="&mediaAltHeader;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="1" + width="1" id="image-count" label="&mediaCount;"/> + </treecols> + <treechildren flex="1"/> + </tree> + <splitter orient="vertical" id="mediaSplitter"/> + <vbox flex="1" id="mediaPreviewBox"> + <grid id="mediaGrid"> + <columns> + <column id="mediaLabelColumn"/> + <column class="gridSeparator"/> + <column flex="1"/> + </columns> + <rows> + <row> + <label control="imageurltext" value="&mediaLocation;"/> + <separator/> + <textbox readonly="true" id="imageurltext" class="urltext"/> + </row> + <row> + <label control="imagetypetext" value="&generalType;"/> + <separator/> + <textbox readonly="true" id="imagetypetext"/> + </row> + <row> + <label control="imagesourcetext" value="&generalSource;"/> + <separator/> + <textbox readonly="true" id="imagesourcetext"/> + </row> + <row> + <label control="imagesizetext" value="&generalSize;"/> + <separator/> + <textbox readonly="true" id="imagesizetext"/> + </row> + <row> + <label control="imagedimensiontext" value="&mediaDimension;"/> + <separator/> + <textbox readonly="true" id="imagedimensiontext"/> + </row> + <row> + <label control="imagetext" value="&mediaText;"/> + <separator/> + <textbox readonly="true" id="imagetext"/> + </row> + <row> + <label control="imagelongdesctext" value="&mediaLongdesc;"/> + <separator/> + <textbox readonly="true" id="imagelongdesctext"/> + </row> + </rows> + </grid> + <hbox align="end"> + <vbox> + <checkbox id="blockImage" + hidden="true" + oncommand="onBlockImage(this.checked);" + accesskey="&mediaBlockImage.accesskey;"/> + <label control="thepreviewimage" value="&mediaPreview;" class="header"/> + </vbox> + <spacer flex="1"/> + <button label="&selectall.label;" accesskey="&selectall.accesskey;" + id="selectallbutton" + oncommand="doSelectAllMedia();"/> + <button label="&mediaSaveAs;" accesskey="&mediaSaveAs.accesskey;" + icon="save" id="imagesaveasbutton" disabled="true" + oncommand="saveMedia();"/> + </hbox> + <vbox class="inset iframe" flex="1" pack="center"> + <hbox id="theimagecontainer" pack="center"> + <image id="thepreviewimage"/> + </hbox> + <hbox id="brokenimagecontainer" pack="center" collapsed="true"> + <image id="brokenimage" src="resource://gre-resources/broken-image.png"/> + </hbox> + </vbox> + </vbox> + <hbox id="mediaSaveBox" collapsed="true"> + <spacer flex="1"/> + <button label="&mediaSaveAs;" accesskey="&mediaSaveAs2.accesskey;" + icon="save" oncommand="saveMedia();"/> + </hbox> + </vbox> + + <!-- Feeds --> + <vbox id="feedPanel"> + <richlistbox id="feedListbox" flex="1"/> + </vbox> + + <!-- Permissions --> + <vbox id="permPanel"> + <hbox> + <label control="hostText" value="&permissionsFor;"/> + <textbox id="hostText" class="header" readonly="true" + crop="end" flex="1"/> + </hbox> + + <!-- + The richlist below is generated by permissions.js. + The labels point to the radio groups to give the radio buttons + an accessible context. The accessible context for the preceeding + checkbox is already taken care of through the richlistitem grouping. + --> + <richlistbox id="permList" flex="1"/> + </vbox> + + <!-- Form information --> + <vbox> + <tree id="formtree" class="fixedsize" onselect="onFormSelect();" contextmenu="picontext"> + <treecols> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" primary="true" persist="width" flex="1" + width="1" id="form-name" label="&formName;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="3" + width="3" id="form-method" label="&formMethod;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="2" + width="2" id="form-action" label="&formAction;"/> + </treecols> + <treechildren flex="1"/> + </tree> + <splitter orient="vertical"/> + <vbox flex="1"> + <textbox readonly="true" class="header" id="formname"/> + <grid> + <columns> + <column/> + <column style="width: .5em;"/> + <column flex="1"/> + </columns> + <rows> + <row> + <label control="formenctype" value="&formEncoding;"/> + <separator/> + <textbox readonly="true" id="formenctype"/> + </row> + <row> + <label control="formtarget" value="&formTarget;"/> + <separator/> + <textbox readonly="true" class="label" id="formtarget"/> + </row> + </rows> + </grid> + <label control="formpreview" class="header" value="&formFields;"/> + <tree id="formpreview" flex="1" contextmenu="picontext"> + <treecols> + <treecol sortSeparators="true" primary="true" persist="width" flex="3" + width="3" id="field-label" label="&formLabel;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="3" + width="3" id="field-field" label="&formFName;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="1" + width="1" id="field-type" label="&formType;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="3" + width="3" id="field-value" label="&formCValue;"/> + </treecols> + <treechildren flex="1"/> + </tree> + </vbox> + </vbox> + + <!-- Link info --> + <vbox> + <tree id="linktree" + flex="1" + ondragstart="onBeginLinkDrag(event,'link-address','link-name')" + contextmenu="piLinksContext"> + <treecols> + <treecol sortSeparators="true" primary="true" persist="width" flex="5" + width="5" id="link-name" label="&linkName;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="7" + width="7" id="link-address" label="&linkAddress;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="2" + width="2" id="link-type" label="&linkType;"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="2" + width="2" id="link-target" label="&linkTarget;" hidden="true"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="1" + width="1" id="link-accesskey" label="&linkAccessKey;" hidden="true"/> + </treecols> + <treechildren flex="1"/> + </tree> + </vbox> + + <!-- Security & Privacy --> + <vbox id="securityPanel"> + <!-- Identity Section --> + <groupbox id="security-identity-groupbox" flex="1"> + <caption id="security-identity" label="&securityView.identity.header;"/> + <hbox> + <image id="identity-icon"/> + <grid flex="1"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row><!-- Domain --> + <label control="security-identity-domain-value" + id="security-identity-domain-label" + class="fieldLabel" + value="&securityView.identity.domain;"/> + <textbox id="security-identity-domain-value" + class="fieldValue" readonly="true"/> + </row> + <row><!-- Owner --> + <label control="security-identity-owner-value" + id="security-identity-owner-label" + class="fieldLabel" + value="&securityView.identity.owner;"/> + <textbox id="security-identity-owner-value" + class="fieldValue" readonly="true"/> + </row> + <row><!-- Verifier --> + <label control="security-identity-verifier-value" + id="security-identity-verifier-label" + class="fieldLabel" + value="&securityView.identity.verifier;"/> + <textbox id="security-identity-verifier-value" + class="fieldValue" readonly="true"/> + </row> + <!-- Certificate Validity --> + <row id="security-identity-validity-row"> + <label id="security-identity-validity-label" + class="fieldLabel" + value="&securityView.identity.validity;" + control="security-identity-validity-value"/> + <textbox id="security-identity-validity-value" + class="fieldValue" readonly="true" /> + </row> + </rows> + </grid> + </hbox> + <spacer flex="1"/> + <hbox pack="end"><!-- Cert button --> + <button id="security-view-cert" label="&securityView.certView;" + accesskey="&securityView.accesskey;" + oncommand="security.viewCert();"/> + </hbox> + </groupbox> + + <!-- Privacy & History section --> + <groupbox id="security-privacy-groupbox" flex="1"> + <caption id="security-privacy" label="&securityView.privacy.header;" /> + <grid> + <columns> + <column flex="1"/> + <column flex="1"/> + </columns> + <rows> + <row align="center"><!-- History --> + <label id="security-privacy-history-label" + control="security-privacy-history-value" + class="fieldLabel">&securityView.privacy.history;</label> + <textbox id="security-privacy-history-value" + class="fieldValue" + value="&securityView.unknown;" + readonly="true"/> + </row> + <row align="center"><!-- Cookies --> + <label id="security-privacy-cookies-label" + control="security-privacy-cookies-value" + class="fieldLabel">&securityView.privacy.cookies;</label> + <hbox align="center"> + <textbox id="security-privacy-cookies-value" + class="fieldValue" + value="&securityView.unknown;" + flex="1" + readonly="true"/> + <button id="security-view-cookies" + label="&securityView.privacy.viewCookies;" + accesskey="&securityView.privacy.viewCookies.accessKey;" + oncommand="security.viewCookies();"/> + </hbox> + </row> + <row align="center"><!-- Passwords --> + <label id="security-privacy-passwords-label" + control="security-privacy-passwords-value" + class="fieldLabel">&securityView.privacy.passwords;</label> + <hbox align="center"> + <textbox id="security-privacy-passwords-value" + class="fieldValue" + value="&securityView.unknown;" + flex="1" + readonly="true"/> + <button id="security-view-password" + label="&securityView.privacy.viewPasswords;" + accesskey="&securityView.privacy.viewPasswords.accessKey;" + oncommand="security.viewPasswords();"/> + </hbox> + </row> + </rows> + </grid> + </groupbox> + + <!-- Technical Details section --> + <groupbox id="security-technical-groupbox" flex="1"> + <caption id="security-technical" label="&securityView.technical.header;" /> + <vbox flex="1"> + <label id="security-technical-shortform" class="fieldValue"/> + <description id="security-technical-longform1" class="fieldLabel"/> + <description id="security-technical-longform2" class="fieldLabel"/> + <description id="security-technical-certificate-transparency" class="fieldLabel"/> + </vbox> + </groupbox> + </vbox> + + <!-- Others added by overlay --> + </tabpanels> + </tabbox> +</window> diff --git a/comm/suite/browser/pageinfo/permissions.js b/comm/suite/browser/pageinfo/permissions.js new file mode 100644 index 0000000000..7d126d6ea7 --- /dev/null +++ b/comm/suite/browser/pageinfo/permissions.js @@ -0,0 +1,204 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. */ + +const { SitePermissions } = ChromeUtils.import("resource:///modules/SitePermissions.jsm"); +const { BrowserUtils } = ChromeUtils.import("resource://gre/modules/BrowserUtils.jsm""); + +var gPermPrincipal; + +// Array of permissionIDs sorted alphabetically by label. +var gPermissions = SitePermissions.listPermissions().sort((a, b) => { + let firstLabel = SitePermissions.getPermissionLabel(a); + let secondLabel = SitePermissions.getPermissionLabel(b); + return firstLabel.localeCompare(secondLabel); +}); + +var permissionObserver = { + observe: function (aSubject, aTopic, aData) + { + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface(Ci.nsIPermission); + if (permission.matches(gPermPrincipal, true) && + gPermissions.includes(permission.type)) { + initRow(permission.type); + } + } + } +}; + +function initPermission() +{ + onUnloadRegistry.push(onUnloadPermission); + onResetRegistry.push(onUnloadPermission); +} + +function onLoadPermission(uri, principal) +{ + var permTab = document.getElementById("permTab"); + if (!SitePermissions.isSupportedPrincipal(principal)) { + permTab.hidden = true; + return; + } + + gPermPrincipal = principal; + if (gPermPrincipal && !gPermPrincipal.isSystemPrincipal) { + var hostText = document.getElementById("hostText"); + hostText.value = gPermPrincipal.origin; + Services.obs.addObserver(permissionObserver, "perm-changed"); + } + for (var i of gPermissions) + initRow(i); + permTab.hidden = false; +} + +function onUnloadPermission() +{ + if (gPermPrincipal && !gPermPrincipal.isSystemPrincipal) { + Services.obs.removeObserver(permissionObserver, "perm-changed"); + } +} + +function initRow(aPartId) +{ + createRow(aPartId); + + var checkbox = document.getElementById(aPartId + "Def"); + var command = document.getElementById("cmd_" + aPartId + "Toggle"); + if (gPermPrincipal && gPermPrincipal.isSystemPrincipal) { + checkbox.checked = false; + checkbox.setAttribute("disabled", "true"); + command.setAttribute("disabled", "true"); + document.getElementById(aPartId + "RadioGroup").selectedItem = null; + return; + } + checkbox.removeAttribute("disabled"); + var {state} = SitePermissions.getForPrincipal(gPermPrincipal, aPartId); + let defaultState = SitePermissions.getDefault(aPartId); + + // Since cookies preferences have many different possible configuration states + // we don't consider any permission except "no permission" to be default. + if (aPartId == "cookie") { + state = Services.perms.testPermissionFromPrincipal(gPermPrincipal, "cookie"); + + if (state == SitePermissions.UNKNOWN) { + checkbox.checked = true; + command.setAttribute("disabled", "true"); + // Don't select any item in the radio group, as we can't + // confidently say that all cookies on the site will be allowed. + let radioGroup = document.getElementById("cookieRadioGroup"); + radioGroup.selectedItem = null; + } else { + checkbox.checked = false; + command.removeAttribute("disabled"); + } + + setRadioState(aPartId, state); + return; + } + + if (state != defaultState) { + checkbox.checked = false; + command.removeAttribute("disabled"); + } + else { + checkbox.checked = true; + command.setAttribute("disabled", "true"); + } + setRadioState(aPartId, state); +} + +function createRow(aPartId) { + let rowId = "perm-" + aPartId + "-row"; + if (document.getElementById(rowId)) + return; + + let commandId = "cmd_" + aPartId + "Toggle"; + let labelId = "perm-" + aPartId + "-label"; + let radiogroupId = aPartId + "RadioGroup"; + + let command = document.createElement("command"); + command.setAttribute("id", commandId); + command.setAttribute("oncommand", "onRadioClick('" + aPartId + "');"); + document.getElementById("pageInfoCommandSet").appendChild(command); + + let row = document.createElement("richlistitem"); + row.setAttribute("id", rowId); + row.setAttribute("class", "permission"); + row.setAttribute("orient", "vertical"); + + let label = document.createElement("label"); + label.setAttribute("id", labelId); + label.setAttribute("control", radiogroupId); + label.setAttribute("value", SitePermissions.getPermissionLabel(aPartId)); + label.setAttribute("class", "permissionLabel"); + row.appendChild(label); + + let controls = document.createElement("hbox"); + controls.setAttribute("role", "group"); + controls.setAttribute("aria-labelledby", labelId); + + let checkbox = document.createElement("checkbox"); + checkbox.setAttribute("id", aPartId + "Def"); + checkbox.setAttribute("oncommand", "onCheckboxClick('" + aPartId + "');"); + checkbox.setAttribute("label", gBundle.getString("permissions.useDefault")); + controls.appendChild(checkbox); + + let spacer = document.createElement("spacer"); + spacer.setAttribute("flex", "1"); + controls.appendChild(spacer); + + let radiogroup = document.createElement("radiogroup"); + radiogroup.setAttribute("id", radiogroupId); + radiogroup.setAttribute("orient", "horizontal"); + for (let state of SitePermissions.getAvailableStates(aPartId)) { + let radio = document.createElement("radio"); + radio.setAttribute("id", aPartId + "#" + state); + radio.setAttribute("label", SitePermissions.getMultichoiceStateLabel(aPartId, state)); + radio.setAttribute("command", commandId); + radiogroup.appendChild(radio); + } + controls.appendChild(radiogroup); + + row.appendChild(controls); + + document.getElementById("permList").appendChild(row); +} + +function onCheckboxClick(aPartId) +{ + var command = document.getElementById("cmd_" + aPartId + "Toggle"); + var checkbox = document.getElementById(aPartId + "Def"); + if (checkbox.checked) { + SitePermissions.removeFromPrincipal(gPermPrincipal, aPartId); + command.setAttribute("disabled", "true"); + } + else { + onRadioClick(aPartId); + command.removeAttribute("disabled"); + } +} + +function onRadioClick(aPartId) +{ + var radioGroup = document.getElementById(aPartId + "RadioGroup"); + let permission; + if (radioGroup.selectedItem) { + permission = parseInt(radioGroup.selectedItem.id.split("#")[1]); + } else { + permission = SitePermissions.getDefault(aPartId); + } + SitePermissions.setForPrincipal(gPermPrincipal, aPartId, permission); +} + +function setRadioState(aPartId, aValue) +{ + var radio = document.getElementById(aPartId + "#" + aValue); + if (radio) { + radio.radioGroup.selectedItem = radio; + } +} diff --git a/comm/suite/browser/pageinfo/security.js b/comm/suite/browser/pageinfo/security.js new file mode 100644 index 0000000000..f6357c9ac4 --- /dev/null +++ b/comm/suite/browser/pageinfo/security.js @@ -0,0 +1,354 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +ChromeUtils.defineModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +var security = { + init: function(uri, windowInfo) { + this.uri = uri; + this.windowInfo = windowInfo; + }, + + // Display the server certificate (static) + viewCert : function() { + var cert = security._cert; + viewCertHelper(window, cert); + }, + + _getSecurityInfo : function() { + // We don't have separate info for a frame, return null until further notice + // (see bug 138479) + if (!this.windowInfo.isTopWindow) + return null; + + var hostName = this.windowInfo.hostName; + + var ui = security._getSecurityUI(); + if (!ui) + return null; + + var isBroken = + (ui.state & Ci.nsIWebProgressListener.STATE_IS_BROKEN); + var isMixed = + (ui.state & (Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT | + Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT)); + var isInsecure = + (ui.state & Ci.nsIWebProgressListener.STATE_IS_INSECURE); + var isEV = + (ui.state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL); + var status = ui.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus; + + if (!isInsecure && status) { + status.QueryInterface(Ci.nsISSLStatus); + var cert = status.serverCert; + var issuerName = cert.issuerOrganization || cert.issuerName; + + var retval = { + hostName : hostName, + cAName : issuerName, + encryptionAlgorithm : undefined, + encryptionStrength : undefined, + version: undefined, + isBroken : isBroken, + isMixed : isMixed, + isEV : isEV, + cert : cert, + certificateTransparency : undefined, + }; + + var version; + try { + retval.encryptionAlgorithm = status.cipherName; + retval.encryptionStrength = status.secretKeyLength; + version = status.protocolVersion; + } + catch (e) { + } + + switch (version) { + case Ci.nsISSLStatus.SSL_VERSION_3: + retval.version = "SSL 3"; + break; + case Ci.nsISSLStatus.TLS_VERSION_1: + retval.version = "TLS 1.0"; + break; + case Ci.nsISSLStatus.TLS_VERSION_1_1: + retval.version = "TLS 1.1"; + break; + case Ci.nsISSLStatus.TLS_VERSION_1_2: + retval.version = "TLS 1.2"; + break; + case Ci.nsISSLStatus.TLS_VERSION_1_3: + retval.version = "TLS 1.3"; + break; + } + + // Select the status text to display for Certificate Transparency. + // Since we do not yet enforce the CT Policy on secure connections, + // we must not complain on policy discompliance (it might be viewed + // as a security issue by the user). + switch (status.certificateTransparencyStatus) { + case Ci.nsISSLStatus.CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE: + case Ci.nsISSLStatus.CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS: + case Ci.nsISSLStatus.CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS: + retval.certificateTransparency = null; + break; + case Ci.nsISSLStatus.CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT: + retval.certificateTransparency = "Compliant"; + break; + } + + return retval; + } + return { + hostName : hostName, + cAName : "", + encryptionAlgorithm : "", + encryptionStrength : 0, + version: "", + isBroken : isBroken, + isMixed : isMixed, + isEV : isEV, + cert : null, + certificateTransparency : null, + }; + }, + + // Find the secureBrowserUI object (if present) + _getSecurityUI : function() { + if (window.opener.gBrowser) + return window.opener.gBrowser.securityUI; + return null; + }, + + /** + * Open the cookie manager window + */ + viewCookies : function() + { + var eTLD; + try { + eTLD = Services.eTLD.getBaseDomain(this.uri); + } catch (e) { + // getBaseDomain will fail if the host is an IP address or is empty + eTLD = this.uri.asciiHost; + } + + toDataManager(eTLD + '|cookies'); + }, + + /** + * Open the login manager window + */ + viewPasswords : function() + { + toDataManager(this._getSecurityInfo().hostName + '|passwords'); + }, + + _cert : null +}; + +function securityOnLoad(uri, windowInfo) { + security.init(uri, windowInfo); + + var info = security._getSecurityInfo(); + if (!info || uri.scheme === "about") { + document.getElementById("securityTab").hidden = true; + document.getElementById("securityBox").hidden = true; + return; + } + + document.getElementById("securityTab").hidden = false; + document.getElementById("securityBox").hidden = false; + + const pageInfoBundle = document.getElementById("pageinfobundle"); + + /* Set Identity section text */ + setText("security-identity-domain-value", info.hostName); + + var owner; + var verifier; + var generalPageIdentityString; + var identityClass; + var validity; + if (info.cert && !info.isBroken) { + validity = info.cert.validity.notAfterLocalDay; + + // Try to pull out meaningful values. Technically these fields are optional + // so we'll employ fallbacks where appropriate. The EV spec states that Org + // fields must be specified for subject and issuer so that case is simpler. + if (info.isEV) { + owner = info.cert.organization; + verifier = info.cAName; + generalPageIdentityString = + pageInfoBundle.getFormattedString("generalSiteIdentity", + [owner, verifier]); + identityClass = "verifiedIdentity"; + } else { + // Technically, a non-EV cert might specify an owner in the O field or + // not, depending on the CA's issuing policies. However we don't have any + // programmatic way to tell those apart, and no policy way to establish + // which organization vetting standards are good enough (that's what EV is + // for) so we default to treating these certs as domain-validated only. + owner = pageInfoBundle.getString("securityNoOwner"); + verifier = info.cAName || info.cert.issuerCommonName || info.cert.issuerName; + generalPageIdentityString = owner; + identityClass = "verifiedDomain"; + } + } else { + // We don't have valid identity credentials. + owner = pageInfoBundle.getString("securityNoOwner"); + verifier = pageInfoBundle.getString("notSet"); + generalPageIdentityString = owner; + identityClass = ""; + } + + setText("security-identity-owner-value", owner); + setText("security-identity-verifier-value", verifier); + setText("general-security-identity", generalPageIdentityString); + document.getElementById("identity-icon").className = identityClass; + if (validity) { + setText("security-identity-validity-value", validity); + } else { + document.getElementById("security-identity-validity-row").hidden = true; + } + + /* Manage the View Cert button*/ + if (info.cert) + security._cert = info.cert; + document.getElementById("security-view-cert").collapsed = !info.cert; + + /* Set Privacy & History section text */ + var yesStr = pageInfoBundle.getString("yes"); + var noStr = pageInfoBundle.getString("no"); + + var hasCookies = hostHasCookies(uri); + setText("security-privacy-cookies-value", hasCookies ? yesStr : noStr); + document.getElementById("security-view-cookies").disabled = !hasCookies; + var hasPasswords = realmHasPasswords(uri); + setText("security-privacy-passwords-value", hasPasswords ? yesStr : noStr); + document.getElementById("security-view-password").disabled = !hasPasswords; + + var visitCount = previousVisitCount(info.hostName); + let visitCountStr = visitCount > 0 + ? PluralForm.get(visitCount, pageInfoBundle.getString("securityVisitsNumber")) + .replace("#1", visitCount.toLocaleString()) + : pageInfoBundle.getString("securityNoVisits"); + setText("security-privacy-history-value", visitCountStr); + + /* Set the Technical Detail section messages */ + const pkiBundle = document.getElementById("pkiBundle"); + var hdr; + var msg1; + var msg2; + + if (info.isBroken) { + if (info.isMixed) { + hdr = pkiBundle.getString("pageInfo_MixedContent"); + msg1 = pkiBundle.getString("pageInfo_MixedContent2"); + } else { + hdr = pkiBundle.getFormattedString("pageInfo_BrokenEncryption", + [info.encryptionAlgorithm, + info.encryptionStrength + "", + info.version]); + msg1 = pkiBundle.getString("pageInfo_WeakCipher"); + } + msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } else if (info.encryptionStrength > 0) { + hdr = pkiBundle.getFormattedString("pageInfo_EncryptionWithBitsAndProtocol", + [info.encryptionAlgorithm, + info.encryptionStrength + "", + info.version]); + msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1"); + msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2"); + security._cert = info.cert; + } else { + hdr = pkiBundle.getString("pageInfo_NoEncryption"); + if (info.hostName != null) { + msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [info.hostName]); + } else { + msg1 = pkiBundle.getString("pageInfo_Privacy_None4"); + } + msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } + setText("security-technical-shortform", hdr); + setText("security-technical-longform1", msg1); + setText("security-technical-longform2", msg2); + setText("general-security-privacy", hdr); + + const ctStatus = + document.getElementById("security-technical-certificate-transparency"); + if (info.certificateTransparency) { + ctStatus.hidden = false; + ctStatus.value = pkiBundle.getString( + "pageInfo_CertificateTransparency_" + info.certificateTransparency); + } else { + ctStatus.hidden = true; + } +} + +function setText(id, value) +{ + var element = document.getElementById(id); + if (!element) + return; + if (element.localName == "textbox" || element.localName == "label") + element.value = value; + else + element.textContent = value; +} + +function viewCertHelper(parent, cert) +{ + if (!cert) + return; + + var cd = Cc[CERTIFICATEDIALOGS_CONTRACTID].getService(nsICertificateDialogs); + cd.viewCert(parent, cert); +} + +/** + * Return true iff we have cookies for uri. + */ +function hostHasCookies(aUri) { + return Services.cookies.countCookiesFromHost(aUri.asciiHost) > 0; +} + +/** + * Return true iff realm (proto://host:port) (extracted from uri) has + * saved passwords + */ +function realmHasPasswords(aUri) { + return Services.logins.countLogins(aUri.prePath, "", "") > 0; +} + +/** + * Return the number of previous visits recorded for host before today. + * + * @param host - the domain name to look for in history + */ +function previousVisitCount(host, endTimeReference) { + if (!host) + return false; + + var historyService = Cc["@mozilla.org/browser/nav-history-service;1"] + .getService(Ci.nsINavHistoryService); + + var options = historyService.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_VISIT; + + // Search for visits to this host before today + var query = historyService.getNewQuery(); + query.endTimeReference = query.TIME_RELATIVE_TODAY; + query.endTime = 0; + query.domain = host; + + var result = historyService.executeQuery(query, options); + result.root.containerOpen = true; + var cc = result.root.childCount; + result.root.containerOpen = false; + return cc; +} diff --git a/comm/suite/browser/safeBrowsingOverlay.js b/comm/suite/browser/safeBrowsingOverlay.js new file mode 100644 index 0000000000..3e92234472 --- /dev/null +++ b/comm/suite/browser/safeBrowsingOverlay.js @@ -0,0 +1,75 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gSafeBrowsing = { + initMenuItems: function initMenuItems() { + // A blocked page will have a specific about:blocked content documentURI. + var docURI = content.document.documentURI; + + // "reason" isn't currently used but it's here to make porting + // from Firefox easier and may also be useful in the future + // for further testing and setting menu items. + let reason; + + // Show/hide the appropriate menu item. + // Initially allow report url and disallow reporting phishing error. + document.getElementById("reportPhishing").hidden = false; + document.getElementById("reportPhishingError").hidden = true; + + if (docURI.startsWith("about:blocked")) { + // It's blocked so don't allow reporting again. + document.getElementById("reportPhishing").hidden = true; + // Test for blocked page. + if (/e=malwareBlocked/.test(docURI)) { + reason = "malware"; + } else if (/e=unwantedBlocked/.test(docURI)) { + reason = "unwanted"; + } else if (/e=deceptiveBlocked/.test(docURI)) { + reason = "phishing"; + document.getElementById("reportPhishingError").hidden = false; + } + } + + var broadcaster = document.getElementById("safeBrowsingBroadcaster"); + var uri = getBrowser().currentURI; + if (uri && (uri.schemeIs("http") || uri.schemeIs("https"))) + broadcaster.removeAttribute("disabled"); + else + broadcaster.setAttribute("disabled", true); + }, + + /** + * Used to report a phishing page or a false positive + * + * @param name + * String One of "PhishMistake", "MalwareMistake", or "Phish" + * @param info + * Information about the reasons for blocking the resource. + * In the case false positive, it may contain SafeBrowsing + * matching list and provider of the list + * @return String the report phishing URL. + */ + getReportURL(name, info) { + let reportInfo = info; + if (!reportInfo) { + let pageUri = getBrowser().currentURI.clone(); + + // Remove the query to avoid including potentially sensitive data + if (pageUri instanceof Ci.nsIURL) { + pageUri = pageUri.mutate().setQuery("").finalize(); + } + + reportInfo = { uri: pageUri.asciiSpec }; + } + return SafeBrowsing.getReportURL(name, reportInfo); + }, + + initOverlay: function initOverlay(aEvent) { + var popup = document.getElementById("helpPopup"); + popup.addEventListener("popupshowing", gSafeBrowsing.initMenuItems); + } +} + +window.addEventListener("load", gSafeBrowsing.initOverlay); diff --git a/comm/suite/browser/safeBrowsingOverlay.xul b/comm/suite/browser/safeBrowsingOverlay.xul new file mode 100644 index 0000000000..e034fd5891 --- /dev/null +++ b/comm/suite/browser/safeBrowsingOverlay.xul @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay SYSTEM "chrome://communicator/locale/safeBrowsing.dtd"> + +<overlay id="safeBrowsingOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://navigator/content/safeBrowsingOverlay.js"/> + + <broadcasterset id="navBroadcasters"> + <broadcaster id="safeBrowsingBroadcaster" disabled="true"/> + </broadcasterset> + + <menupopup id="helpPopup"> + <menuitem id="reportPhishing" + label="&reportDeceptiveSite.label;" + accesskey="&reportDeceptiveSite.accesskey;" + insertbefore="menu_HelpAboutSeparator" + observes="safeBrowsingBroadcaster" + oncommand="openUILink(gSafeBrowsing.getReportURL('Phish'), event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="reportPhishingError" + label="¬ADeceptiveSite.label;" + accesskey="¬ADeceptiveSite.accesskey;" + insertbefore="menu_HelpAboutSeparator" + observes="safeBrowsingBroadcaster" + oncommand="openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tabfocused');"/> + </menupopup> +</overlay> diff --git a/comm/suite/browser/sessionHistoryUI.js b/comm/suite/browser/sessionHistoryUI.js new file mode 100644 index 0000000000..f8fdcc79aa --- /dev/null +++ b/comm/suite/browser/sessionHistoryUI.js @@ -0,0 +1,172 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var {AppConstants} = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +function toggleTabsFromOtherComputers() + { + // enable/disable the Tabs From Other Computers menu + let menuitem = document.getElementById("sync-tabs-menuitem"); + + // If Sync isn't configured yet, then don't show the menuitem. + if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || + Weave.Svc.Prefs.get("firstSync", "") == "notReady") { + menuitem.hidden = true; + return; + } + + // The tabs engine might never be inited (if services.sync.registerEngines + // is modified), so make sure we avoid undefined errors. + let enabled = Weave.Service.isLoggedIn && + Weave.Service.engineManager.get("tabs") && + Weave.Service.engineManager.get("tabs").enabled; + menuitem.setAttribute("disabled", !enabled); + menuitem.hidden = false; + } + +function FillHistoryMenu(aParent, aMenu) +{ + // Remove old entries if any + deleteHistoryItems(aParent); + + var sessionHistory = getWebNavigation().sessionHistory; + + var count = sessionHistory.count; + var index = sessionHistory.index; + var end; + const MAX_HISTORY_MENU_ITEMS = 15; + + switch (aMenu) + { + case "back": + end = index > MAX_HISTORY_MENU_ITEMS ? index - MAX_HISTORY_MENU_ITEMS + : 0; + if (index <= end) + return false; + for (let j = index - 1; j >= end; j--) + { + let entry = sessionHistory.getEntryAtIndex(j); + if (entry) + createHistoryMenuItem(aParent, j, entry); + } + break; + case "forward": + end = count - index > MAX_HISTORY_MENU_ITEMS ? index + MAX_HISTORY_MENU_ITEMS + : count - 1; + if (index >= end) + return false; + for (let j = index + 1; j <= end; j++) + { + let entry = sessionHistory.getEntryAtIndex(j); + if (entry) + createHistoryMenuItem(aParent, j, entry); + } + break; + case "go": + var startHistory = document.getElementById("startHistorySeparator"); + var endHistory = document.getElementById("endHistorySeparator"); + // var syncMenuItem = document.getElementById("sync-tabs-menuitem"); + startHistory.hidden = (count == 0); + end = count > MAX_HISTORY_MENU_ITEMS ? count - MAX_HISTORY_MENU_ITEMS + : 0; + for (let j = count - 1; j >= end; j--) + { + let entry = sessionHistory.getEntryAtIndex(j); + if (entry) + createHistoryMenuItem(aParent, j, entry, endHistory, j == index); + } + // toggleTabsFromOtherComputers(); + // endHistory.hidden = (endHistory == aParent.lastChild || syncMenuItem.hidden); + endHistory.hidden = (endHistory == aParent.lastChild); + break; + } + return true; +} + +function executeUrlBarHistoryCommand( aTarget ) + { + var index = aTarget.getAttribute("index"); + var label = aTarget.getAttribute("label"); + if (index != "nothing_available" && label) + { + gURLBar.value = label; + UpdatePageProxyState(); + handleURLBarCommand(); + } + } + +function createUBHistoryMenu( aParent ) + { + while (aParent.hasChildNodes()) + aParent.lastChild.remove(); + + var file = GetUrlbarHistoryFile(); + if (file.exists()) { + var connection = Services.storage.openDatabase(file); + try { + if (connection.tableExists("urlbarhistory")) { + var statement = connection.createStatement( + "SELECT url FROM urlbarhistory ORDER BY ROWID DESC"); + while (statement.executeStep()) + aParent.appendChild(document.createElement("menuitem")) + .setAttribute("label", statement.getString(0)); + statement.reset(); + statement.finalize(); + return; + } + } finally { + connection.close(); + } + } + //Create the "Nothing Available" Menu item and disable it. + var na = aParent.appendChild(document.createElement("menuitem")); + na.setAttribute("label", gNavigatorBundle.getString("nothingAvailable")); + na.setAttribute("disabled", "true"); + } + +function createHistoryMenuItem(aParent, aIndex, aEntry, aAnchor, aChecked) +{ + var menuitem = document.createElement("menuitem"); + menuitem.setAttribute("label", aEntry.title); + menuitem.setAttribute("index", aIndex); + if (aChecked) + { + menuitem.setAttribute("type", "radio"); + menuitem.setAttribute("checked", "true"); + } + + if (!aChecked || AppConstants.platform == "macosx") + { + menuitem.className = "menuitem-iconic bookmark-item menuitem-with-favicon"; + PlacesUtils.favicons.getFaviconURLForPage(aEntry.URI, + function faviconURLCallback(aURI) { + if (aURI) { + menuitem.setAttribute("image", + PlacesUtils.favicons + .getFaviconLinkForIcon(aURI).spec); + } + } + ); + } + aParent.insertBefore(menuitem, aAnchor); +} + +function deleteHistoryItems(aParent) +{ + var children = aParent.childNodes; + for (let i = children.length - 1; i >= 0; --i) + { + if (children[i].hasAttribute("index")) + children[i].remove(); + } +} + +function updateGoMenu(event) + { + FillHistoryMenu(event.target, "go"); + updateRecentMenuItems(); + } diff --git a/comm/suite/browser/tabbrowser.xml b/comm/suite/browser/tabbrowser.xml new file mode 100644 index 0000000000..390fbaa0cf --- /dev/null +++ b/comm/suite/browser/tabbrowser.xml @@ -0,0 +1,3707 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings [ +<!ENTITY % tabBrowserDTD SYSTEM "chrome://navigator/locale/tabbrowser.dtd" > +%tabBrowserDTD; +]> + +<bindings id="tabBrowserBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="tabbrowser"> + <resources> + <stylesheet src="chrome://navigator/skin/tabbrowser.css"/> + </resources> + + <content> + <xul:stringbundle anonid="tbstringbundle" src="chrome://navigator/locale/tabbrowser.properties"/> + <xul:tabbox anonid="tabbox" flex="1" eventnode="document"> + <xul:hbox class="tab-drop-indicator-bar" collapsed="true"> + <xul:image class="tab-drop-indicator" mousethrough="always"/> + </xul:hbox> + <xul:hbox class="tabbrowser-strip" collapsed="true" tooltip="_child" context="_child" + anonid="strip" + ondragstart="this.parentNode.parentNode._onDragStart(event);" + ondragover="this.parentNode.parentNode._onDragOver(event);" + ondrop="this.parentNode.parentNode._onDrop(event);" + ondragleave="this.parentNode.parentNode._onDragLeave(event);"> + <xul:tooltip onpopupshowing="event.stopPropagation(); return this.parentNode.parentNode.parentNode.doPreview(this);" + onpopuphiding="this.parentNode.parentNode.parentNode.resetPreview(this);" orient="vertical"> + <xul:label class="tooltip-label" crop="right"/> + <xul:label class="tooltip-label" hidden="true"><html:canvas class="tab-tooltip-canvas"/></xul:label> + </xul:tooltip> + <xul:menupopup anonid="tabContextMenu" onpopupshowing="return document.getBindingParent(this).updatePopupMenu(this);"> + <xul:menuitem label="&closeTab.label;" accesskey="&closeTab.accesskey;" + tbattr="tabbrowser-tab" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.removeTab(tabbrowser.mContextTab);"/> + <xul:menuitem label="&closeOtherTabs.label;" accesskey="&closeOtherTabs.accesskey;" + tbattr="tabbrowser-multiple tabbrowser-tab" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.removeAllTabsBut(tabbrowser.mContextTab);"/> + <xul:menuitem label="&closeTabsToTheEnd.label;" + accesskey="&closeTabsToTheEnd.accesskey;" + tbattr="tabbrowser-totheend tabbrowser-tab" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.removeTabsToTheEndFrom(tabbrowser.mContextTab);"/> + <xul:menuseparator/> + <xul:menuitem label="&newTab.label;" accesskey="&newTab.accesskey;" + xbl:inherits="oncommand=onnewtab"/> + <xul:menuitem label="&undoCloseTab.label;" accesskey="&undoCloseTab.accesskey;" + tbattr="tabbrowser-undoclosetab" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.undoCloseTab(0);"/> + <xul:menuseparator/> + <xul:menuitem label="&bookmarkGroup.label;" accesskey="&bookmarkGroup.accesskey;" + tbattr="tabbrowser-multiple" + xbl:inherits="oncommand=onbookmarkgroup"/> + <xul:menuseparator/> + <xul:menuitem label="&reloadTab.label;" accesskey="&reloadTab.accesskey;" + tbattr="tabbrowser-tab" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.reloadTab(tabbrowser.mContextTab);"/> + <xul:menuitem label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;" + tbattr="tabbrowser-multiple" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.reloadAllTabs();"/> + </xul:menupopup> + + <xul:tabs class="tabbrowser-tabs" flex="1" + anonid="tabcontainer" + tooltiptextnew="&newTabButton.tooltip;" + tooltiptextclose="&closeTabButton.tooltip;" + tooltiptextalltabs="&listAllTabs.tooltip;" + setfocus="false" + onclick="this.parentNode.parentNode.parentNode.onTabClick(event);" + xbl:inherits="onnewtab,onnewtabclick" + onclosetab="var node = this.parentNode; + while (node.localName != 'tabbrowser') + node = node.parentNode; + node.removeCurrentTab();"> + <xul:tab selected="true" validate="never" + onerror="this.parentNode.parentNode.parentNode.parentNode.addToMissedIconCache(this.getAttribute('image')); + this.removeAttribute('image');" + width="0" flex="100" + class="tabbrowser-tab" label="&untitledTab;" crop="end"/> + </xul:tabs> + </xul:hbox> + <xul:tabpanels flex="1" class="plain" selectedIndex="0" anonid="panelcontainer"> + <xul:notificationbox class="browser-notificationbox" xbl:inherits="popupnotification"> + <xul:stack flex="1" anonid="browserStack"> + <xul:browser flex="1" type="content" primary="true" + xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,datetimepicker"/> + </xul:stack> + </xul:notificationbox> + </xul:tabpanels> + </xul:tabbox> + <children/> + </content> + <implementation implements="nsIDOMEventListener, nsIObserver"> + <field name="closingTabsEnum" readonly="true"> + ({ ALL: 0, OTHER: 1, TO_END: 2 }); + </field> + <field name="mSessionStore" readonly="true"> + Cc["@mozilla.org/suite/sessionstore;1"].getService(Ci.nsISessionStore); + </field> + <field name="mTabBox" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "tabbox"); + </field> + <field name="mStrip" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "strip"); + </field> + <field name="tabContainer" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "tabcontainer"); + </field> + <field name="mPanelContainer" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "panelcontainer"); + </field> + <field name="tabs" readonly="true"> + this.tabContainer.childNodes + </field> + <field name="mStringBundle"> + document.getAnonymousElementByAttribute(this, "anonid", "tbstringbundle"); + </field> + <field name="mCurrentTab"> + null + </field> + <field name="mPreviousTab"> + null + </field> + <field name="mCurrentBrowser"> + null + </field> + <field name="mProgressListeners"> + [] + </field> + <field name="mTabsProgressListeners"> + [] + </field> + <field name="mTabListeners"> + new Array() + </field> + <field name="mTabFilters"> + new Array() + </field> + <field name="mLastRelatedIndex"> + 0 + </field> + <field name="usePrivateBrowsing" readonly="true"> + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext) + .usePrivateBrowsing; + </field> + <field name="mContextTab"> + null + </field> + + <method name="_handleKeyEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!aEvent.isTrusted) { + // Don't let untrusted events mess with tabs. + return; + } + + if (aEvent.altKey) + return; + + if (AppConstants.platform == "macosx") { + if (!aEvent.metaKey) + return; + + var offset = 1; + switch (aEvent.charCode) { + case '}'.charCodeAt(0): + offset = -1; + case '{'.charCodeAt(0): + if (window.getComputedStyle(this, null).direction == "ltr") + offset *= -1; + this.tabContainer.advanceSelectedTab(offset, true); + aEvent.stopPropagation(); + aEvent.preventDefault(); + return; + } + } else { + if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey && + aEvent.keyCode == KeyEvent.DOM_VK_F4 && + this.getStripVisibility()) { + this.removeCurrentTab(); + aEvent.stopPropagation(); + aEvent.preventDefault(); + return; + } + + if (aEvent.target == this) { + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_UP: + this.moveTabBackward(); + break; + case KeyEvent.DOM_VK_DOWN: + this.moveTabForward(); + break; + case KeyEvent.DOM_VK_RIGHT: + case KeyEvent.DOM_VK_LEFT: + this.moveTabOver(aEvent); + break; + case KeyEvent.DOM_VK_HOME: + this.moveTabToStart(); + break; + case KeyEvent.DOM_VK_END: + this.moveTabToEnd(); + break; + default: + // Stop the keypress event for the above keyboard + // shortcuts only. + return; + } + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + } + ]]></body> + </method> + + <field name="arrowKeysShouldWrap"> + null + </field> + <field name="nextTabNumber"> + 0 + </field> + <field name="_browsers"> + null + </field> + <field name="savedBrowsers"> + new Array() + </field> + <field name="referenceTab"> + null + </field> + + <method name="doPreview"> + <parameter name="aPopup"/> + <body> + <![CDATA[ + var tab = document.tooltipNode; + if (tab.localName != "tab") + return false; + var b = tab.linkedBrowser; + if (!b) + return false; + + var label = aPopup.firstChild; + label.setAttribute("value", tab.getAttribute("label")); + + var canvas = aPopup.lastChild.firstChild; + canvas.parentNode.hidden = true; + + var win = b.contentWindow; + var w = win.innerWidth; + var h = win.innerHeight; + + if (tab == this.mCurrentTab || h == 0 || + !Services.prefs.getBoolPref("browser.tabs.tooltippreview.enable")) { + return true; + } + + var ctx; + try { + ctx = canvas.getContext("2d"); + } catch (e) { + return true; + } + + label.width = 0; + aPopup.setAttribute("tabpreview", "true"); + + var canvasW = Services.prefs.getIntPref("browser.tabs.tooltippreview.width"); + var canvasH = Math.round(canvasW * h / w); + canvas.width = canvasW; + canvas.height = canvasH; + canvas.parentNode.hidden = false; + + var bgColor = Services.prefs.getBoolPref("browser.display.use_system_colors") ? + "Window" : + Services.prefs.getCharPref("browser.display.background_color"); + if (b.contentDocument instanceof ImageDocument && + !(b.contentDocument.imageRequest.imageStatus & + Ci.imgIRequest.STATUS_ERROR)) { + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, canvasW, canvasH); + var img = b.contentDocument.body.firstChild; + var ratio = img.naturalHeight / img.naturalWidth; + if (img.naturalHeight <= canvasH && img.naturalWidth <= canvasW) { + ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight); + } + else if (ratio * canvasW > canvasH) { + ctx.drawImage(img, 0, 0, canvasH / ratio, canvasH); + } + else { + ctx.drawImage(img, 0, 0, canvasW, ratio * canvasW); + } + } + else { + ctx.save(); + ctx.scale(canvasW / w, canvasH / h); + ctx.drawWindow(win, win.pageXOffset, win.pageYOffset, w, h, bgColor); + ctx.restore(); + } + return true; + ]]> + </body> + </method> + + <!-- XXXcst This should not be needed, but it seems that the tooltip + sizing is happening too early when we want to stop showing the + preview. This clears the label's width early (i.e. when the + previous preview disappears) so that when the next tooltip appears, + it doesn't start with a bad size. For now, I blame Gecko. --> + <method name="resetPreview"> + <parameter name="aPopup"/> + <body> + <![CDATA[ + var label = aPopup.firstChild; + // If this function is removed, these two lines need to be restored + // to the non-preview codepath above. + label.removeAttribute("width"); + aPopup.removeAttribute("tabpreview"); + ]]> + </body> + </method> + + <method name="previewTab"> + <parameter name="aTab"/> + <parameter name="aCallback"/> + <body> + <![CDATA[ + let currentTab = this.selectedTab; + try { + // Suppress focus, ownership and selected tab changes. + this._previewMode = true; + this.selectedTab = aTab; + aCallback(); + } finally { + this.selectedTab = currentTab; + this._previewMode = false; + } + ]]> + </body> + </method> + + <method name="getBrowserAtIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + return this.browsers[aIndex]; + ]]> + </body> + </method> + + <method name="getBrowserIndexForDocument"> + <parameter name="aDocument"/> + <body> + <![CDATA[ + var browsers = this.browsers; + for (var i = 0; i < browsers.length; i++) + if (browsers[i].contentDocument == aDocument) + return i; + return -1; + ]]> + </body> + </method> + + <method name="getBrowserForDocument"> + <parameter name="aDocument"/> + <body> + <![CDATA[ + var browsers = this.browsers; + for (var i = 0; i < browsers.length; i++) + if (browsers[i].contentDocument == aDocument) + return browsers[i]; + return null; + ]]> + </body> + </method> + + <method name="getBrowserForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + const browsers = this.browsers; + for (let browser of browsers) { + if (browser.contentWindow == aWindow) + return browser; + } + return null; + ]]> + </body> + </method> + + <method name="getNotificationBox"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return aBrowser ? aBrowser.parentNode.parentNode + : this.mCurrentBrowser.parentNode.parentNode; + ]]> + </body> + </method> + + <method name="_callProgressListeners"> + <parameter name="aBrowser"/> + <parameter name="aMethod"/> + <parameter name="aArguments"/> + <parameter name="aCallGlobalListeners"/> + <parameter name="aCallTabsListeners"/> + <body><![CDATA[ + if (!aBrowser) + aBrowser = this.mCurrentBrowser; + + if (aCallGlobalListeners != false && + aBrowser == this.mCurrentBrowser) { + this.mProgressListeners.forEach(function (p) { + if (aMethod in p) { + try { + p[aMethod].apply(p, aArguments); + } catch (e) { + // don't inhibit other listeners + Cu.reportError(e); + } + } + }); + } + + if (aCallTabsListeners != false) { + aArguments.unshift(aBrowser); + + this.mTabsProgressListeners.forEach(function (p) { + if (aMethod in p) { + try { + p[aMethod].apply(p, aArguments); + } catch (e) { + // don't inhibit other listeners + Cu.reportError(e); + } + } + }); + } + ]]></body> + </method> + + <!-- A web progress listener object definition for a given tab. --> + <method name="mTabProgressListener"> + <parameter name="aTab"/> + <parameter name="aBrowser"/> + <parameter name="aStartsBlank"/> + <body> + <![CDATA[ + return ({ + mTabBrowser: this, + mTab: aTab, + mBrowser: aBrowser, + mBlank: aStartsBlank, + mFeeds: [], + mRequest: null, + mStateFlags: 0, + mStatus: 0, + mMessage: "", + + // cache flags for correct status UI update after tab switching + mTotalProgress: 0, + + // count of open requests (should always be 0 or 1) + mRequestCount: 0, + + _callProgressListeners: function () { + Array.prototype.unshift.call(arguments, this.mBrowser); + return this.mTabBrowser._callProgressListeners.apply(this.mTabBrowser, arguments); + }, + + _shouldShowProgress: function (aRequest) { + if (this.mBlank) + return false; + + // Don't show progress indicators in tabs for about: URIs + // pointing to local resources. + if ((aRequest instanceof Ci.nsIChannel) && + aRequest.originalURI.schemeIs("about") && + (aRequest.URI.schemeIs("jar") || aRequest.URI.schemeIs("file"))) + return false; + + return true; + }, + + onProgressChange: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0; + + if (!this._shouldShowProgress(aRequest)) + return; + + if (this.mTotalProgress && this.mTab.hasAttribute("busy")) + this.mTab.setAttribute("progress", "true"); + + this._callProgressListeners("onProgressChange", + [aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress]); + }, + + onProgressChange64: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + return this.onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress); + }, + + onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) { + if (!aRequest) + return; + + var oldBlank = this.mBlank; + + let location; + let originalLocation; + try { + aRequest.QueryInterface(Ci.nsIChannel) + location = aRequest.URI; + originalLocation = aRequest.originalURI; + } catch (ex) {} + + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { + this.mRequestCount++; + } + else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + const NS_ERROR_UNKNOWN_HOST = 2152398878; + if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) { + // to prevent bug 235825: wait for the request handled + // by the automatic keyword resolver + return; + } + // since we (try to) only handle STATE_STOP of the last request, + // the count of open requests should now be 0 + this.mRequestCount = 0; + } + + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + if (aWebProgress.isTopLevel) { + this.mFeeds = []; + // Need to use originalLocation rather than location because things + // like about:privatebrowsing arrive with nsIRequest pointing to + // their resolved jar: or file: URIs. + if (!(originalLocation && gInitialPages.has(originalLocation.spec) && + originalLocation != "about:blank" && + this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) { + // This will trigger clearing the location bar. Don't do it if + // we loaded off a blank browser and this is an initial page load + // (e.g. about:privatebrowsing, etc.) so we avoid clearing the + // location bar in case the user is typing in it. + // Loading about:blank shouldn't trigger this, either, because its + // loads are "special". + this.mBrowser.urlbarChangeTracker.startedLoad(); + } + // If the browser is loading it must not be crashed anymore. + this.mTab.removeAttribute("crashed"); + } + + if (this._shouldShowProgress(aRequest)) { + if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) && + aWebProgress && aWebProgress.isTopLevel) { + this.mTab.setAttribute("busy", "true"); + + // Do the following only for the top frame not any subframes. + // Remove favicon. This shows busy and progress indicators even during a reload. + this.mTab.removeAttribute("image"); + + if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD)) + this.mTabBrowser.setTabTitleLoading(this.mTab); + } + + if (this.mTab.selected) + this.mTabBrowser.mIsBusy = true; + } + } + else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + + if (this.mTab.hasAttribute("busy")) { + this.mTab.removeAttribute("busy"); + this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]); + if (!this.mTab.selected) + this.mTab.setAttribute("unread", "true"); + } + this.mTab.removeAttribute("progress"); + + if (aWebProgress.isTopLevel) { + let isSuccessful = Components.isSuccessCode(aStatus); + if (!isSuccessful && !isTabEmpty(this.mTab)) { + // Restore the current document's location in case the + // request was stopped (possibly from a content script) + // before the location changed. + this.mBrowser.userTypedValue = null; + + let inLoadURI = this.mBrowser.inLoadURI; + if (this.mTab.selected && gURLBar && !inLoadURI) + URLBarSetURI(); + } else if (isSuccessful) { + this.mBrowser.urlbarChangeTracker.finishedLoad(); + } + + if (!this.mBrowser.mIconURL) + this.mTabBrowser.useDefaultIcon(this.mTab); + } + + if (this.mBlank) + this.mBlank = false; + + // For keyword URIs clear the user typed value since they will be changed into real URIs. + if (location && location.scheme == "keyword") + this.mBrowser.userTypedValue = null; + + if (this.mTab.label == this.mTabBrowser.mStringBundle.getString("tabs.loading")) + this.mTabBrowser.setTabTitle(this.mTab); + + if (this.mTab.selected) + this.mTabBrowser.mIsBusy = false; + } + + if (oldBlank) { + this._callProgressListeners("onUpdateCurrentBrowser", + [aStateFlags, aStatus, "", 0], + true, false); + } else { + this._callProgressListeners("onStateChange", + [aWebProgress, aRequest, aStateFlags, aStatus], + true, false); + } + + this._callProgressListeners("onStateChange", + [aWebProgress, aRequest, aStateFlags, aStatus], + false); + + if (aStateFlags & (Ci.nsIWebProgressListener.STATE_START | + Ci.nsIWebProgressListener.STATE_STOP)) { + // reset cached temporary values at beginning and end + this.mMessage = ""; + this.mTotalProgress = 0; + } + this.mStateFlags = aStateFlags; + this.mStatus = aStatus; + }, + + onLocationChange: function (aWebProgress, aRequest, aLocation, aFlags) { + // OnLocationChange is called for both the top-level content + // and the subframes. + let topLevel = aWebProgress.isTopLevel; + + if (topLevel) { + let isSameDocument = + !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); + // We need to clear the typed value + // if the document failed to load, to make sure the urlbar reflects the + // failed URI (particularly for SSL errors). However, don't clear the value + // if the error page's URI is about:blank, because that causes complete + // loss of urlbar contents for invalid URI errors (see bug 867957). + // Another reason to clear the userTypedValue is if this was an anchor + // navigation initiated by the user. + if (this.mBrowser.didStartLoadSinceLastUserTyping() || + ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) && + aLocation.spec != "about:blank") || + (isSameDocument && this.mBrowser.inLoadURI)) + this.mBrowser.userTypedValue = null; + + // Don't clear the favicon if this onLocationChange was + // triggered by a pushState or a replaceState (bug 550565) or + // a hash change (bug 408415). + if (aWebProgress.isLoadingDocument && !isSameDocument) + this.mBrowser.mIconURL = null; + } + + if (!this.mBlank) + this._callProgressListeners("onLocationChange", + [aWebProgress, aRequest, aLocation, + aFlags]); + + if (topLevel) { + this.mBrowser.lastURI = aLocation; + this.mBrowser.lastLocationChange = Date.now(); + } + }, + + onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) { + if (this.mBlank) + return; + + this.mMessage = aMessage; + + this.mTabBrowser._callProgressListeners(this.mBrowser, "onStatusChange", + [aWebProgress, aRequest, aStatus, aMessage]); + }, + + onSecurityChange: function (aWebProgress, aRequest, aState) { + this.mTabBrowser._callProgressListeners(this.mBrowser, "onSecurityChange", + [aWebProgress, aRequest, aState]); + }, + + onRefreshAttempted: function(aWebProgress, aURI, aDelay, aSameURI) + { + var allowRefresh = true; + if (this.mTabBrowser.mCurrentTab == this.mTab) { + this.mTabBrowser.mProgressListeners.forEach( + function notifyRefreshAttempted(element) { + if (element && "onRefreshAttempted" in element) { + try { + if (!element.onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI)) + allowRefresh = false; + } catch (e) { + Cu.reportError(e); + } + } + } + ); + } + + this.mTabBrowser.mTabsProgressListeners.forEach( + function notifyRefreshAttempted(element) { + if (element && "onRefreshAttempted" in element) { + try { + if (!element.onRefreshAttempted(this.mBrowser, aWebProgress, aURI, aDelay, aSameURI)) + allowRefresh = false; + } catch (e) { + Cu.reportError(e); + } + } + } + , this); + return allowRefresh; + }, + + addFeed: function(aLink) + { + this.mFeeds.push(aLink); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsIWebProgressListener2) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + } + }); + ]]> + </body> + </method> + + <method name="mInstallSH"> + <parameter name="aBrowser"/> + <parameter name="aSH"/> + <body> + <![CDATA[ + return ({ + mBrowser: aBrowser, + mSH: aSH, + + onProgressChange : function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) + { + }, + + onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) + { + if ((aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) && + (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)) { + function refresh(closure) { + closure.mBrowser.webNavigation.sessionHistory = closure.mSH; + closure.mBrowser.webProgress.removeProgressListener(closure); + delete closure.mBrowser._SHListener; + closure.mSH.QueryInterface(Ci.nsIWebNavigation) + .gotoIndex(closure.mSH.index); + } + setTimeout(refresh, 0, this); + } + }, + + onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags) + { + }, + + onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) + { + }, + + onSecurityChange : function(aWebProgress, aRequest, aState) + { + }, + + QueryInterface : function(aIID) + { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + } + }); + ]]> + </body> + </method> + + <method name="setIcon"> + <parameter name="aTab"/> + <parameter name="aURI"/> + <parameter name="aLoadingPrincipal"/> + <body> + <![CDATA[ + let browser = this.getBrowserForTab(aTab); + browser.mIconURL = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + + if (aURI) { + if (!(aURI instanceof Ci.nsIURI)) { + aURI = makeURI(aURI); + } + + // We do not serialize the principal from within nsSessionStore.js, + // hence if aLoadingPrincipal is null we default to the + // systemPrincipal which will allow the favicon to load. + let loadingPrincipal = aLoadingPrincipal || + Services.scriptSecurityManager.getSystemPrincipal(); + + PlacesUIUtils.loadFavicon(browser, loadingPrincipal, aURI); + } + + let sizedIconUrl = browser.mIconURL || ""; + if (sizedIconUrl != aTab.getAttribute("image")) { + if (sizedIconUrl) + aTab.setAttribute("image", sizedIconUrl); + else + aTab.removeAttribute("image"); + this._tabAttrModified(aTab, ["image"]); + } + + this._callProgressListeners(browser, "onLinkIconAvailable", [browser.mIconURL]); + ]]> + </body> + </method> + + <method name="getIcon"> + <parameter name="aTab"/> + <body> + <![CDATA[ + let browser = aTab ? aTab.linkedBrowser : this.selectedBrowser; + return browser.mIconURL; + ]]> + </body> + </method> + + <method name="buildFavIconString"> + <parameter name="aURI"/> + <body> + <![CDATA[ + try { + aURI = Services.uriFixup.createExposableURI(aURI); + } catch (e) { + } + return aURI.resolve("/favicon.ico"); + ]]> + </body> + </method> + + <method name="shouldLoadFavIcon"> + <parameter name="aURI"/> + <body> + <![CDATA[ + try { + aURI = Services.uriFixup.createExposableURI(aURI); + } catch (e) { + } + return (aURI && Services.prefs.getBoolPref("browser.chrome.site_icons") && + Services.prefs.getBoolPref("browser.chrome.favicons") && + ("schemeIs" in aURI) && (aURI.schemeIs("http") || aURI.schemeIs("https"))); + ]]> + </body> + </method> + + <method name="useDefaultIcon"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var browser = this.getBrowserForTab(aTab); + var documentURI = browser.documentURI; + var icon = null; + + if (browser.imageDocument) { + if (Services.prefs.getBoolPref("browser.chrome.site_icons")) { + let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size"); + if (browser.imageDocument.width <= sz && + browser.imageDocument.height <= sz) { + icon = browser.currentURI; + } + } + } + + // Use documentURIObject in the check for shouldLoadFavIcon so that we + // do the right thing with about:-style error pages. Bug 453442 + if (!icon && this.shouldLoadFavIcon(documentURI)) { + let url = documentURI.prePath + "/favicon.ico"; + if (!this.isFailedIcon(url)) + icon = url; + } + this.setIcon(aTab, icon, browser.contentPrincipal); + ]]> + </body> + </method> + + <method name="isFailedIcon"> + <parameter name="aURI"/> + <body> + <![CDATA[ + if (!(aURI instanceof Ci.nsIURI)) + aURI = makeURI(aURI); + return PlacesUtils.favicons.isFailedFavicon(aURI); + ]]> + </body> + </method> + + <method name="loadFavIcon"> + <parameter name="aURI"/> + <parameter name="aAttr"/> + <parameter name="aElt"/> + <parameter name="aLoadingPrincipal"/> + <body> + <![CDATA[ + let iconURL = this.buildFavIconString(aURI); + let iconURI = Services.io.newURI(iconURL); + let faviconFlags = this.usePrivateBrowsing ? + PlacesUtils.favicons.FAVICON_LOAD_PRIVATE + : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE; + let loadingPrincipal = aLoadingPrincipal || + Services.scriptSecurityManager.getSystemPrincipal(); + + PlacesUtils.favicons + .setAndFetchFaviconForPage(aURI, iconURI, false, faviconFlags, + null, + loadingPrincipal); + if (PlacesUtils.favicons.isFailedFavicon(aURI)) { + return; + } + + aElt.setAttribute(aAttr, iconURL); + ]]> + </body> + </method> + + <method name="addToMissedIconCache"> + <parameter name="aURI"/> + <body> + <![CDATA[ + let uri = Services.io.newURI(aURI); + PlacesUtils.favicons.addFailedFavicon(uri); + ]]> + </body> + </method> + + <method name="getTitleForURI"> + <parameter name="aURI"/> + <body> + <![CDATA[ + try { + aURI = Services.uriFixup.createExposableURI(aURI).spec; + } catch (e) { + aURI = aURI.spec; + } + + if (aURI == "about:blank") + return ""; + + // We have a URI. Let's try to unescape it using a character set + // in case the URI is not ASCII. + try { + let characterSet = this.mCurrentBrowser.contentDocument.characterSet; + let textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"] + .getService(Ci.nsITextToSubURI); + aURI = textToSubURI.unEscapeNonAsciiURI(characterSet, aURI); + } catch (e) { + // Do nothing. + } + return aURI; + ]]> + </body> + </method> + + <method name="updateUrlBar"> + <parameter name="aWebProgress"/> + <parameter name="aRequest"/> + <parameter name="aLocation"/> + <parameter name="aFlags"/> + <parameter name="aSecurityUI"/> + <parameter name="aBrowser"/> + <parameter name="aFeeds"/> + <body> + <![CDATA[ + this.mProgressListeners.forEach( + function notifyUrlBar(element) { + try { + if ("onLocationChange" in element) + element.onLocationChange(aWebProgress, aRequest, aLocation, aFlags); + // If switching tabs, the security may have changed. + if (aSecurityUI && "onSecurityChange" in element) + element.onSecurityChange(aWebProgress, null, aSecurityUI.state); + // If the document already exists, just resend cached data. + if (!aRequest && aWebProgress.isTopLevel) { + if (aBrowser.mIconURL && "onLinkIconAvailable" in element) + element.onLinkIconAvailable(aBrowser.mIconURL); + if ("onFeedAvailable" in element) { + aFeeds.forEach( + function notifyFeedAvailable(feed) { + element.onFeedAvailable(feed); + } + ); + } + } + } catch (e) { + Cu.reportError(e); + } + } + ); + ]]> + </body> + </method> + + <method name="getWindowTitleForBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + var newTitle = ""; + var docTitle; + var docElement = this.ownerDocument.documentElement; + var sep = docElement.getAttribute("titlemenuseparator"); + var modifier = docElement.getAttribute("titlemodifier"); + + // Strip out any null bytes in the content title, since the + // underlying widget implementations of nsWindow::SetTitle pass + // null-terminated strings to system APIs. + if (aBrowser.docShell.contentViewer) + docTitle = aBrowser.contentTitle.replace(/\0/g, ""); + + if (!docTitle && !modifier) { + docTitle = this.getTitleForURI(aBrowser.currentURI); + if (!docTitle) { + // Here we actually override contenttitlesetting, because we + // don't want the titledefault value. + docTitle = this.mStringBundle.getString("tabs.untitled"); + } + } + + if (docTitle) { + newTitle += docElement.getAttribute("titlepreface") + docTitle; + if (modifier) + newTitle += sep; + } + newTitle += modifier; + + // If location bar is hidden and the URL type supports a host, + // add the scheme and host to the title to prevent spoofing. + // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239 + // (only for schemes that support a host) + try { + if (docElement.getAttribute("chromehidden").includes("location")) { + let uri = Services.uriFixup.createExposableURI( + aBrowser.currentURI); + if (uri.schemeIs("about")) + newTitle = uri.spec + sep + newTitle; + else if (uri.host) + newTitle = uri.prePath + sep + newTitle; + } + } catch (e) { + } + + return newTitle; + ]]> + </body> + </method> + + <method name="updateTitlebar"> + <body> + <![CDATA[ + var newTitle = this.getWindowTitleForBrowser(this.mCurrentBrowser); + document.title = newTitle; + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIBaseWindow).title = newTitle; + ]]> + </body> + </method> + + <method name="updatePopupMenu"> + <parameter name="aPopupMenu"/> + <body> + <![CDATA[ + this.mContextTab = aPopupMenu.triggerNode; + // The user might right-click on a tab or an empty part of the tabbar. + var isTab = this.mContextTab.localName == "tab"; + var isMultiple = this.tabs.length > 1; + var isAtEnd = this.getTabsToTheEndFrom(this.mContextTab).length == 0; + var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "*"); + for (let menuitem of menuItems) { + let tbattr = menuitem.getAttribute("tbattr"); + + if (tbattr.includes("tabbrowser-undoclosetab")) { + menuitem.disabled = (this.usePrivateBrowsing ? + this.savedBrowsers.length : + this.mSessionStore.getClosedTabCount(window)) == 0; + menuitem.hidden = (this.usePrivateBrowsing || + Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo") <= 0) && + Services.prefs.getIntPref("browser.tabs.max_tabs_undo") <= 0; + } + else + menuitem.disabled = + (tbattr.includes("tabbrowser-totheend") && isAtEnd) || + (tbattr.includes("tabbrowser-multiple") && !isMultiple) || + (tbattr.includes("tabbrowser-tab") && !isTab); + } + ]]> + </body> + </method> + + <field name="mAeroPeek">false</field> + + <method name="updateCurrentBrowser"> + <body> + <![CDATA[ + var newBrowser = this.mPanelContainer.selectedPanel.firstChild.firstChild; + var oldBrowser = this.mCurrentBrowser; + + // Transfer the dropped link handler to the new browser. + // Note: closing the current tab sets mCurrentBrowser to null + // so we use mCurrentTab.linkedBrowser instead. + newBrowser.droppedLinkHandler = this.mCurrentTab.linkedBrowser.droppedLinkHandler; + newBrowser.showWindowResizer = this.mCurrentTab.linkedBrowser.showWindowResizer; + newBrowser.docShellIsActive = this.mCurrentTab.linkedBrowser.docShellIsActive; + if (this.mCurrentBrowser) { + this.mCurrentBrowser.droppedLinkHandler = null; + this.mCurrentBrowser.docShellIsActive = false; + this.mCurrentBrowser.removeAttribute("primary"); + this.finder.mListeners.forEach(l => this.mCurrentBrowser.finder.removeResultListener(l)); + } + + let oldTab = this.mCurrentTab; + + // Preview mode should not reset the owner. + if (!this._previewMode && !oldTab.selected) + oldTab.owner = null; + + let lastRelatedTab = this.mLastRelatedIndex ? this.tabs[this.mLastRelatedIndex] : null; + if (lastRelatedTab && !lastRelatedTab.selected) { + lastRelatedTab.owner = null; + } + + newBrowser.setAttribute("primary", "true"); + this.mCurrentBrowser = newBrowser; + this.mCurrentTab = this.selectedTab; + this.mCurrentTab.removeAttribute("unread"); + this.finder.mListeners.forEach(l => this.mCurrentBrowser.finder.addResultListener(l)); + + var tabListener = this.mTabListeners[this.tabContainer.selectedIndex]; + + if (!oldBrowser || + (!oldBrowser.blockedPopups != !newBrowser.blockedPopups)) + this.mCurrentBrowser.updateBlockedPopups(); + + // Update the URL bar. + this.updateUrlBar(newBrowser.webProgress, + null, + newBrowser.currentURI, + 0, + newBrowser.securityUI, + newBrowser, + tabListener.mFeeds); + + // Send the state, status and progress to all progress listeners. + var flags = tabListener.mStateFlags & + (Ci.nsIWebProgressListener.STATE_START | + Ci.nsIWebProgressListener.STATE_STOP); + this._callProgressListeners(null, "onStateChange", + [this.mCurrentBrowser.webProgress, + tabListener.mRequest, + flags, + tabListener.mStatus], + true, false); + + this._callProgressListeners(null, "onStatusChange", + [this.mCurrentBrowser.webProgress, + tabListener.mRequest, + tabListener.mStatus, + tabListener.mMessage], + true, false); + + // Also send the onUpdateCurrentBrowser event for compatibility + this._callProgressListeners(null, "onUpdateCurrentBrowser", + [tabListener.mStateFlags, + tabListener.mStatus, + tabListener.mMessage, + tabListener.mTotalProgress], + true, false); + + if (this.mAeroPeek) + return; + + // we only want to return to the parent tab if no other + // tabs have been opened and the user hasn't switched tabs + this.mPreviousTab = null; + this.mLastRelatedIndex = 0; + + // Update the window title. + this.updateTitlebar(); + + // FAYT + this.fastFind.setDocShell(this.mCurrentBrowser.docShell); + + // We've selected the new tab, so go ahead and notify listeners + this.mCurrentTab.dispatchEvent(new Event("TabSelect", + { bubbles: true, cancelable: false })); + + if (!document.commandDispatcher.focusedElement || + document.commandDispatcher.focusedElement.parentNode != + this.mCurrentTab.parentNode) { + // The focus was not on one of our tabs, so focus the new browser. + newBrowser.focus(); + } + ]]> + </body> + </method> + + <method name="onTabClick"> + <parameter name="event"/> + <body> + <![CDATA[ + // A middle mouse button click on a tab is a short cut for + // closing that tab. + if (event.button != 1 || event.target.localName != 'tab') + return; + + this.removeTab(event.target); + event.stopPropagation(); + event.preventDefault(); + ]]> + </body> + </method> + + <method name="onLinkEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + var link = aEvent.originalTarget; + var href = link.href; + if (!href) + return; + + var targetDoc = link.ownerDocument; + var index = this.getBrowserIndexForDocument(targetDoc); + if (index < 0) + return; + + var rel = link.rel; + var type = link.type; + var isIcon = /(?:^|\s)icon(?:\s|$)/i.test(rel) && + Services.prefs.getBoolPref("browser.chrome.site_icons"); + if (isIcon) { + var iconUri = this.getLinkIconURI(link); + if (iconUri) + this.setIcon(this.tabs[index], iconUri, + link.nodePrincipal); + return; + } + + if (aEvent.type == "DOMLinkChanged") + return; + + var isFeed = /(?:^|\s)feed(?:\s|$)/i.test(rel) || + (/(?:^|\s)alternate(?:\s|$)/i.test(rel) && + !/(?:^|\s)stylesheet(?:\s|$)/i.test(rel) && + /^\s*application\/(?:atom|rss)\+xml\s*$/i.test(type)); + + if (!isFeed) + return; + + try { + let feedURI = Services.io.newURI(href, targetDoc.characterSet); + if (!/^https?$/.test(feedURI.scheme)) { + return; + } + urlSecurityCheck(feedURI, targetDoc.nodePrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + } catch(e) { + return; + } + + this.mTabListeners[index].addFeed(link); + if (this.browsers[index] == this.mCurrentBrowser) { + this.mProgressListeners.forEach( + function notifyFeedAvailable(element) { + if ("onFeedAvailable" in element) { + try { + element.onFeedAvailable(link); + } catch (e) { + Cu.reportError(e); + } + } + } + ); + } + ]]> + </body> + </method> + + <method name="getLinkIconURI"> + <parameter name="aLink"/> + <body><![CDATA[ + var targetDoc = aLink.ownerDocument; + // Make a URI out of our href. + var uri = Services.io.newURI(aLink.href, targetDoc.characterSet); + + // Verify that the load of this icon is legal. + // Some error or special pages can load their favicon. + // To be on the safe side, only allow chrome:// favicons. + const re = /^about:(neterror|certerror|blocked)\?/; + var isAllowedPage = re.test(targetDoc.documentURI); + + if (!isAllowedPage || !uri.schemeIs("chrome")) { + try { + urlSecurityCheck(uri,targetDoc.nodePrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + } catch(e) { + return null; + } + } + + // Security says okay, now ask content policy + try { + var contentPolicy = + Cc['@mozilla.org/layout/content-policy;1'] + .getService(Ci.nsIContentPolicy); + } catch (e) { + return null; // Refuse to load if we can't do a security check. + } + + if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE, + uri, targetDoc.documentURIObject, + aLink, aLink.type, + null) != Ci.nsIContentPolicy.ACCEPT) { + return null; + } + return uri; + ]]></body> + </method> + + <method name="_tabAttrModified"> + <parameter name="aTab"/> + <parameter name="aChanged"/> + <body><![CDATA[ + if (aTab.closing) + return; + + let event = new CustomEvent("TabAttrModified", { + bubbles: true, + cancelable: false, + detail: { + changed: aChanged, + } + }); + aTab.dispatchEvent(event); + ]]></body> + </method> + + <method name="setTabTitleLoading"> + <parameter name="aTab"/> + <body> + <![CDATA[ + aTab.label = this.mStringBundle.getString("tabs.loading"); + aTab.crop = "end"; + this._tabAttrModified(aTab, ["label", "crop"]); + ]]> + </body> + </method> + + <method name="setTabTitle"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var browser = aTab.linkedBrowser; + var title = browser.contentTitle; + var crop = "end"; + + if (!title) { + title = this.getTitleForURI(browser.currentURI); + + if (title) + crop = "center"; + else + title = this.mStringBundle.getString("tabs.untitled"); + } + aTab.label = title; + aTab.crop = crop; + ]]> + </body> + </method> + + <method name="setStripVisibilityTo"> + <parameter name="aShow"/> + <body> + <![CDATA[ + this.mStrip.collapsed = !aShow; + ]]> + </body> + </method> + + <method name="getStripVisibility"> + <body> + return !this.mStrip.collapsed; + </body> + </method> + + <method name="loadOneTab"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <parameter name="aLoadInBackground"/> + <parameter name="aAllowThirdPartyFixup"/> + <body> + <![CDATA[ + var params = aReferrerURI; + if (!params || params instanceof Ci.nsIURI) { + params = { + triggeringPrincipal: Services.scriptSecurityManager + .getSystemPrincipal(), + referrerURI: aReferrerURI, + charset: aCharset, + postData: aPostData, + inBackground: aLoadInBackground, + allowThirdPartyFixup: aAllowThirdPartyFixup, + allowMixedContent: false, + userContextId: null, + opener: null, + }; + } + + params.focusNewTab = params.inBackground != null ? + !params.inBackground : + !Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + + if (params.focusNewTab) + params.ownerTab = this.selectedTab; + + return this.addTab(aURI, params); + ]]> + </body> + </method> + + <method name="loadTabs"> + <parameter name="aURIs"/> + <parameter name="aLoadInBackground"/> + <parameter name="aReplace"/> + <body><![CDATA[ + let aAllowThirdPartyFixup; + let aPostDatas = []; + let aUserContextId; + let aTriggeringPrincipal; + + // Additional parameters are in a params object. + // Firefox uses additional parameters not supported here. + if (arguments.length == 2 && + typeof arguments[1] == "object") { + let params = arguments[1]; + aLoadInBackground = params.inBackground; + aReplace = params.replace; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aPostDatas = params.postDatas || aPostDatas; + aUserContextId = params.userContextId; + aTriggeringPrincipal = params.triggeringPrincipal; + } + + if (!aURIs.length) + return; + + // The tab selected after this new tab is closed (i.e. the new tab's + // "owner") is the next adjacent tab (i.e. not the previously viewed tab) + // when several urls are opened here (i.e. closing the first should select + // the next of many URLs opened) or if the pref to have UI links opened in + // the background is set (i.e. the link is not being opened modally) + // + // i.e. + // Number of URLs Load UI Links in BG Focus Last Viewed? + // == 1 false YES + // == 1 true NO + // > 1 false/true NO + var multiple = aURIs.length > 1; + var owner = multiple || aLoadInBackground ? null : this.selectedTab; + var firstTabAdded = null; + var targetTabIndex = -1; + + if (aReplace) { + let browser; + browser = this.mCurrentBrowser; + targetTabIndex = this.tabContainer.selectedIndex; + + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + if (aAllowThirdPartyFixup) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | + Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + try { + browser.loadURIWithFlags(aURIs[0], { + flags, + postData: aPostDatas[0], + triggeringPrincipal : aTriggeringPrincipal, + }); + } catch (e) { + // Ignore failure in case a URI is wrong, so we can continue + // opening the next ones. + } + } else { + firstTabAdded = this.addTab(aURIs[0], { + ownerTab: owner, + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostDatas[0], + userContextId: aUserContextId, + triggeringPrincipal: aTriggeringPrincipal, + }); + } + + let tabNum = targetTabIndex; + for (let i = 1; i < aURIs.length; ++i) { + let tab = this.addTab(aURIs[i], { + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostDatas[i], + userContextId: aUserContextId, + triggeringPrincipal: aTriggeringPrincipal, + }); + if (targetTabIndex !== -1) + this.moveTabTo(tab, ++tabNum); + } + + if (!aLoadInBackground) { + if (firstTabAdded) { + // .selectedTab setter focuses the content area + this.selectedTab = firstTabAdded; + } else + this.selectedBrowser.focus(); + } + ]]></body> + </method> + + <method name="addTab"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <parameter name="aFocusNewTab"/> + <parameter name="aAllowThirdPartyFixup"/> + <body> + <![CDATA[ + var aTriggeringPrincipal; + var aReferrerPolicy; + var aFromExternal; + var aOwner; + var aRelatedToCurrent; + var aAllowMixedContent; + var aNoReferrer; + var aUserContextId; + var aOriginPrincipal; + var aOpener; + if (arguments.length == 2 && + arguments[1] != null && + typeof arguments[1] == "object" && + !(arguments[1] instanceof Ci.nsIURI)) { + let params = arguments[1]; + aTriggeringPrincipal = params.triggeringPrincipal; + aReferrerURI = params.referrerURI; + aReferrerPolicy = params.referrerPolicy; + aCharset = params.charset; + aPostData = params.postData; + aOwner = params.ownerTab; + aFocusNewTab = params.focusNewTab; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aFromExternal = params.fromExternal; + aRelatedToCurrent = params.relatedToCurrent; + aAllowMixedContent = params.allowMixedContent; + aNoReferrer = params.noReferrer; + aUserContextId = params.userContextId; + aOriginPrincipal = params.originPrincipal; + aOpener = params.opener; + } + + // If we're adding tabs, we're past interrupt mode, ditch the owner. + if (this.mCurrentTab.owner) + this.mCurrentTab.owner = null; + + this._browsers = null; // invalidate cache + + var t = this.referenceTab.cloneNode(true); + + var blank = !aURI || aURI == "about:blank"; + + if (!blank) + t.setAttribute("label", aURI); + + this.tabContainer.appendChild(t); + + var b = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "browser"); + b.setAttribute("type", "content"); + b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu")); + b.setAttribute("tooltip", this.getAttribute("contenttooltip")); + b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup")); + if (this.hasAttribute("datetimepicker")) { + b.setAttribute("datetimepicker", this.getAttribute("datetimepicker")); + } + + // Check if we have a "parent" window which we need to set as our opener + if (aOpener) { + b.presetOpenerWindow(aOpener); + } + + // Create the browserStack container + var stack = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "stack"); + stack.setAttribute("anonid", "browserStack"); + stack.appendChild(b); + stack.setAttribute("flex", "1"); + + // Add the Message and the Browser to the box + var n = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "notificationbox"); + n.setAttribute("class", "browser-notificationbox"); + n.setAttribute("popupnotification", this.getAttribute("popupnotification")); + n.appendChild(stack); + + var uniqueId = "panel" + this.nextTabNumber++; + n.id = uniqueId; + t.linkedPanel = uniqueId; + t.linkedBrowser = b; + if (t.previousSibling.selected) + t.setAttribute("afterselected", true); + + // Prevent the superfluous initial load of a blank document + // if we're going to load something other than about:blank. + if (!blank) + b.setAttribute("nodefaultsrc", "true"); + + // NB: this appendChild call causes us to run constructors for the + // browser element, which fires off a bunch of notifications. Some + // of those notifications can cause code to run that inspects our + // state, so it is important that the tab element is fully + // initialized by this point. + this.mPanelContainer.appendChild(n); + + // We start our browsers out as inactive. + b.docShellIsActive = false; + + this.mStrip.collapsed = false; + + Services.prefs.setBoolPref("browser.tabs.forceHide", false); + + // If this new tab is owned by another, assert that relationship. + if (aOwner) + t.owner = aOwner; + + // wire up a progress listener for the new browser object. + var position = this.tabs.length - 1; + var tabListener = this.mTabProgressListener(t, b, blank); + const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); + b.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL); + this.mTabListeners[position] = tabListener; + this.mTabFilters[position] = filter; + + if (!blank) { + // pretend the user typed this so it'll be available till + // the document successfully loads + b.userTypedValue = aURI; + + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + if (aAllowThirdPartyFixup) + flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | + Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + if (aFromExternal) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL; + if (aAllowMixedContent) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT; + try { + b.loadURIWithFlags(aURI, { + flags, + triggeringPrincipal : aTriggeringPrincipal, + referrerURI: aNoReferrer ? null : aReferrerURI, + charset: aCharset, + referrerPolicy: aReferrerPolicy, + postData: aPostData, + }); + } + catch (ex) { } + } + + t.dispatchEvent(new Event("TabOpen", + { bubbles: true, cancelable: false })); + + // Check if we're opening a tab related to the current tab and + // move it to after the current tab. + // aReferrerURI is null or undefined if the tab is opened from + // an external application or bookmark, i.e. somewhere other + // than the current tab. + if ((aRelatedToCurrent || aReferrerURI || + Services.prefs.getBoolPref("browser.tabs.insertAllTabsAfterCurrent")) && + Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) { + var lastRelatedIndex = this.mLastRelatedIndex || + this.tabContainer.selectedIndex; + if (this.mLastRelatedIndex) + this.tabs[this.mLastRelatedIndex].owner = null; + else + t.owner = this.selectedTab; + this.moveTabTo(t, ++lastRelatedIndex); + this.mLastRelatedIndex = lastRelatedIndex; + } + + if (aFocusNewTab) { + var parentTab = this.selectedTab; + this.selectedTab = t; + this.mPreviousTab = parentTab; + } + else { + // The user opened a background tab, so updateCurrentBrowser + // won't be called. Explicitly clear the previous tab. + this.mPreviousTab = null; + } + this.tabContainer._handleNewTab(t); + + return t; + ]]> + </body> + </method> + + <method name="warnAboutClosingTabs"> + <parameter name="aCloseTabs"/> + <parameter name="aTab"/> + <body> + <![CDATA[ + var tabsToClose; + switch (aCloseTabs) { + case this.closingTabsEnum.ALL: + tabsToClose = this.tabs.length; + break; + case this.closingTabsEnum.OTHER: + tabsToClose = this.tabs.length - 1; + break; + case this.closingTabsEnum.TO_END: + if (!aTab) + throw new Error("Required argument missing: aTab"); + + tabsToClose = this.getTabsToTheEndFrom(aTab).length; + break; + default: + throw new Error("Invalid argument: " + aCloseTabs); + } + + if (tabsToClose <= 1) + return true; + + const pref = aCloseTabs == this.closingTabsEnum.ALL ? + "browser.tabs.warnOnClose" : + "browser.tabs.warnOnCloseOther"; + if (!Services.prefs.getBoolPref(pref)) + return true; + + //default to true: if it were false, we wouldn't get this far + var warnOnClose = { value:true }; + var bundle = this.mStringBundle; + + // Focus the window before prompting. This will raise any minimized + // window, which will make it obvious which window the prompt is + // for and will solve the problem of windows "obscuring" the + // prompt. See bug #350299 for more details. + window.focus(); + var warningTitle; + var warningMessage; + var closeButton; + var promptMessage; + switch (aCloseTabs) { + case this.closingTabsEnum.ALL: + warningTitle = "tabs.closeWarningTitleAll"; + warningMessage = + PluralForm.get(tabsToClose, + bundle.getString("tabs.closeWarningAll")); + closeButton = "tabs.closeButtonAll"; + promptMessage = "tabs.closeWarningPromptMeAll"; + break; + case this.closingTabsEnum.OTHER: + // fall through + case this.closingTabsEnum.TO_END: + // fall through + default: + warningTitle = "tabs.closeWarningTitle"; + warningMessage = + PluralForm.get(tabsToClose, + bundle.getString("tabs.closeWarningOther")); + closeButton = "tabs.closeButton"; + promptMessage = "tabs.closeWarningPromptMe"; + break; + } + + var ps = Services.prompt; + var buttonPressed = + ps.confirmEx(window, + bundle.getString(warningTitle), + warningMessage.replace("#1", tabsToClose), + (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) + + (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1), + bundle.getString(closeButton), + null, null, + bundle.getString(promptMessage), + warnOnClose); + var reallyClose = (buttonPressed == 0); + // Don't set the pref unless they press OK and it's false + if (reallyClose && !warnOnClose.value) + Services.prefs.setBoolPref(pref, false); + + return reallyClose; + ]]> + </body> + </method> + + <method name="getTabsToTheEndFrom"> + <parameter name="aTab"/> + <body> + <![CDATA[ + let tabsToEnd = []; + let tabs = this.tabs; + for (let i = tabs.length - 1; tabs[i] != aTab && i >= 0; --i) { + tabsToEnd.push(tabs[i]); + } + return tabsToEnd.reverse(); + ]]> + </body> + </method> + + <method name="removeTabsToTheEndFrom"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (this.warnAboutClosingTabs(this.closingTabsEnum.TO_END, aTab)) { + let tabs = this.getTabsToTheEndFrom(aTab); + for (let i = tabs.length - 1; i >= 0; --i) { + this.removeTab(tabs[i]); + } + } + ]]> + </body> + </method> + + <method name="removeAllTabsBut"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (this.warnAboutClosingTabs(this.closingTabsEnum.OTHER)) { + this.selectedTab = aTab; + + for (let i = this.tabs.length - 1; i >= 0; --i) { + if (this.tabs[i] != aTab) + this.removeTab(this.tabs[i]); + } + } + ]]> + </body> + </method> + + <method name="removeCurrentTab"> + <parameter name="aParams"/> + <body> + <![CDATA[ + return this.removeTab(this.mCurrentTab, aParams); + ]]> + </body> + </method> + + <method name="isBrowserEmpty"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return aBrowser.sessionHistory.count < 2 && + aBrowser.currentURI.spec == "about:blank" && + !aBrowser.contentDocument.body.hasChildNodes(); + ]]> + </body> + </method> + + <method name="getUndoList"> + <body> + <![CDATA[ + var tabData = this.usePrivateBrowsing ? this.savedBrowsers : + JSON.parse(this.mSessionStore.getClosedTabData(window)); + return tabData.map(function(aTabData) { return aTabData.title; }); + ]]> + </body> + </method> + + <method name="undoCloseTab"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + if (this.usePrivateBrowsing) + return this.savedBrowsers.length ? this.restoreTab(aIndex) : null; + + return this.mSessionStore.getClosedTabCount(window) ? + this.mSessionStore.undoCloseTab(window, aIndex) : null; + ]]> + </body> + </method> + + <method name="restoreTab"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + if (aIndex >= this.savedBrowsers.length || aIndex < 0) + return null; + + this._browsers = null; + + var savedData = this.savedBrowsers.splice(aIndex, 1)[0]; + var t = savedData.browserData.tab; + var b = savedData.browserData.browser; + var hist = savedData.browserData.history; + + this.tabContainer.appendChild(t); + if (t.previousSibling.selected) + t.setAttribute("afterselected", true); + + // navigate back to the proper page from the light page + b.stop(); + b.webNavigation.gotoIndex(0); + + // reattach the old history + b.webNavigation.sessionHistory = hist; + + // add back the filters, security first (bug 313335) + var secFlags = Ci.nsIWebProgress.NOTIFY_STATE_ALL | + Ci.nsIWebProgress.NOTIFY_LOCATION | + Ci.nsIWebProgress.NOTIFY_SECURITY; + b.webProgress.addProgressListener(b.securityUI, secFlags); + + var position = this.tabs.length - 1; + var tabListener = this.mTabProgressListener(t, b, false); + const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); + b.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL); + this.mTabListeners[position] = tabListener; + this.mTabFilters[position] = filter; + + t.dispatchEvent(new Event("TabOpen", + { bubbles: true, cancelable: false })); + + if (savedData.pos < position) + this.moveTabTo(t, savedData.pos); + + if (this.tabs.length == 2 && this.isBrowserEmpty(this)) + this.removeCurrentTab({ disableUndo: true }); + else { + this.selectedTab = t; + this.mStrip.collapsed = false; + } + this.tabContainer._handleNewTab(t); + + return t; + ]]> + </body> + </method> + + <method name="removeBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + let panel = this.getNotificationBox(aBrowser); + panel.destroy(); + aBrowser.destroy(); + + // The pagehide event that this removal triggers is safe + // because the browser is no longer current at this point. + panel.remove(); + + // Fix up the selected panel. + panel = this.getNotificationBox(this.selectedTab.linkedBrowser); + this.mTabBox.selectedPanel = panel; + ]]> + </body> + </method> + + <method name="removeTab"> + <parameter name="aTab"/> + <parameter name="aParams"/> + <body> + <![CDATA[ + this.mLastRelatedIndex = 0; + + if (!aParams) { + aParams = { + animate: false, + disableUndo: false + }; + } + + var oldBrowser = aTab.linkedBrowser; + + var ds = oldBrowser.docShell; + + if (ds.contentViewer && !ds.contentViewer.permitUnload()) + return; + + // We're committed to closing the tab now. + var l = this.tabs.length; + switch (l) { + case 1: + // add a new blank tab to replace the one we're about to close + // (this ensures that the remaining tab is as good as new) + this.addTab("about:blank"); + l++; + // fall through + case 2: + if (Services.prefs.getBoolPref("browser.tabs.autoHide")) + this.mStrip.collapsed = true; + } + + // Dispatch a notification. + // We dispatch it before any teardown so that event listeners can + // inspect the tab that's about to close. + aTab.dispatchEvent(new UIEvent("TabClose", + { bubbles: true, cancelable: false, view: window, + detail: !!aParams.disableUndo })); + var tabData = aTab.tabData || {}; + tabData.pos = this.getTabIndex(aTab); + tabData.panel = this.getNotificationBox(oldBrowser).id; + tabData.title = oldBrowser.contentDocument.title || + this.getTitleForURI(oldBrowser.currentURI) || + this.mStringBundle.getString("tabs.untitled"); + + var index = this.getTabIndex(aTab); + + // Remove SSL listener + oldBrowser.webProgress.removeProgressListener(oldBrowser.securityUI); + + // Remove the tab's filter and progress listener. + const filter = this.mTabFilters[index]; + oldBrowser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(this.mTabListeners[index]); + this.mTabFilters.splice(index, 1); + this.mTabListeners.splice(index, 1); + + // We are no longer the primary content area + oldBrowser.removeAttribute("primary"); + + // Remove this tab as the owner of any other tabs, since it's going away. + for (let tab of this.tabs) { + if ("owner" in tab && tab.owner == aTab) + // |tab| is a child of the tab we're removing, make it an orphan. + tab.owner = null; + } + + // Now select the new tab before nuking the old one. + var currentIndex = this.tabContainer.selectedIndex; + + var newIndex = -1; + if (currentIndex > index) + newIndex = currentIndex - 1; + else if (currentIndex < index) + newIndex = currentIndex; + else if (index == l - 1) + newIndex = index - 1; + else + newIndex = index; + + if (oldBrowser == this.mCurrentBrowser) + this.mCurrentBrowser = null; + + // Invalidate browsers cache, as the tab is removed from the + // tab container. + this._browsers = null; + + let owner = ("owner" in aTab) ? aTab.owner : null; + + // Clean up before/after selected attributes before removing the + // tab. + aTab._selected = false; + aTab.remove(); + + // When the current tab is removed select a new tab + // and fire select events on tabpanels and tabs + if (owner && !owner.hidden && !owner.closing && + Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) { + this.selectedTab = owner; + } + else if (this.mPreviousTab && (aTab == this.mCurrentTab)) + this.selectedTab = this.mPreviousTab; + else { + this.tabContainer.selectedIndex = newIndex; + + // We need to explicitly clear this, because updateCurrentBrowser + // doesn't get called for a background tab + this.mPreviousTab = null; + } + + // Save the tab for undo. + // Even though we navigate to about:blank, it costs more RAM than + // really closing the tab. The pref controls how far you can undo + var maxUndoDepth = Services.prefs.getIntPref("browser.tabs.max_tabs_undo"); + var oldSH = oldBrowser.webNavigation.sessionHistory; + var inOnLoad = oldBrowser.docShell.isExecutingOnLoadHandler; + var isPopup = oldBrowser.contentWindow.opener && + !Services.prefs.getBoolPref("browser.tabs.cache_popups"); + if (maxUndoDepth <= 0 || aParams.disableUndo || inOnLoad || isPopup || this.isBrowserEmpty(oldBrowser)) { + // Undo is disabled/tab is blank. Kill the browser for real. + // Because of the way XBL works (fields just set JS + // properties on the element) and the code we have in place + // to preserve the JS objects for any elements that have + // JS properties set on them, the browser element won't be + // destroyed until the document goes away. So we force a + // cleanup ourselves. Also fix up the selected panel in the case + // the removed browser was to the left of the current browser. + this.removeBrowser(oldBrowser); + return; + } + + // preserve a pointer to the browser for undoing the close + // 1. save a copy of the session history (oldSH) + // 2. hook up a new history + // 3. add the last history entry from the old history the new + // history so we'll be able to go back from about:blank + // 4. load a light URL in the browser, pushing the current page + // into bfcache - allows for saving of JS modifications + // and also saves RAM by allowing bfcache to evict the full page + + tabData.browserData = { + tab: aTab, + browser: oldBrowser, + history: oldSH, + toJSON: function() {} // hides this object from JSON.stringify + }; + this.savedBrowsers.unshift(tabData); + + var newSH = Cc["@mozilla.org/browser/shistory;1"] + .createInstance(Ci.nsISHistory); + oldBrowser.webNavigation.sessionHistory = newSH; + var entry = oldSH.getEntryAtIndex(oldSH.index) + .QueryInterface(Ci.nsISHEntry) + .clone(); + // The bfcache entry is tightly coupled to the original shistory it + // belongs to, better to drop it. + entry.abandonBFCacheEntry(); + // don't try to repost data when restoring the tab + entry.postData = null; + newSH.addEntry(entry, true); + + // about:blank is light + oldBrowser.loadURI("about:blank"); + + // remove overflow from the undo stack + if (this.savedBrowsers.length > maxUndoDepth) { + tabData = this.savedBrowsers.pop(); + var deadBrowser = tabData.browserData.browser; + delete tabData.browserData; + this.removeBrowser(deadBrowser); + } + ]]> + </body> + </method> + + <method name="forgetSavedBrowser"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + if (aIndex >= this.savedBrowsers.length || aIndex < 0) + return false; + + var tabData = this.savedBrowsers.splice(aIndex, 1)[0]; + var deadBrowser = tabData.browserData.browser; + delete tabData.browserData; + this.removeBrowser(deadBrowser); + return true; + ]]> + </body> + </method> + + <method name="reloadAllTabs"> + <body> + <![CDATA[ + var l = this.tabs.length; + for (var i = 0; i < l; i++) { + try { + this.tabs[i].linkedBrowser.reload(); + } catch (e) { + // ignore failure to reload so others will be reloaded + } + } + ]]> + </body> + </method> + + <method name="reloadTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + this.getBrowserForTab(aTab).reload(); + ]]> + </body> + </method> + + <method name="addProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (!aListener) + throw Cr.NS_ERROR_INVALID_ARG; + + if (this.mProgressListeners.includes(aListener)) + throw Cr.NS_ERROR_FAILURE; + + // push() does not disturb possibly ongoing iterations. + this.mProgressListeners.push(aListener); + ]]> + </body> + </method> + + <method name="removeProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (!this.mProgressListeners.includes(aListener)) + throw Cr.NS_ERROR_FAILURE; + + // Create a new array, not to disturb possibly ongoing iterations. + this.mProgressListeners = + this.mProgressListeners.filter( + function removeListener(element) { + return element != aListener; + } + ); + ]]> + </body> + </method> + + <method name="addTabsProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (!aListener) + throw Cr.NS_ERROR_INVALID_ARG; + + if (this.mTabsProgressListeners.includes(aListener)) + throw Cr.NS_ERROR_FAILURE; + + // push() does not disturb possibly ongoing iterations. + this.mTabsProgressListeners.push(aListener); + ]]> + </body> + </method> + + <method name="removeTabsProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (!this.mTabsProgressListeners.includes(aListener)) + throw Cr.NS_ERROR_FAILURE; + + // Create a new array, not to disturb possibly ongoing iterations. + this.mTabsProgressListeners = + this.mTabsProgressListeners.filter( + function removeListener(element) { + return element != aListener; + } + ); + ]]> + </body> + </method> + + <method name="_getTabForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + const browsers = this.browsers; + for (var i = 0; i < browsers.length; ++i) + if (browsers[i].contentWindow == aWindow) + return this.tabs[i]; + + return null; + ]]> + </body> + </method> + + <method name="getTabForBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + for (var tab of this.tabs) + if (tab.linkedBrowser == aBrowser) + return tab; + + return null; + ]]> + </body> + </method> + + <method name="getBrowserForTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + return aTab.linkedBrowser; + ]]> + </body> + </method> + + <method name="getBrowserForOuterWindowID"> + <parameter name="aID"/> + <body> + <![CDATA[ + for (var browser of this.browsers) + if (browser.outerWindowID == aID) + return browser; + + return null; + ]]> + </body> + </method> + + <method name="getTabIndex"> + <parameter name="aTab"/> + <body> + <![CDATA[ + for (var i = 0; i < this.tabs.length; ++i) + if (this.tabs[i] == aTab) + return i; + + throw Cr.NS_ERROR_ILLEGAL_VALUE; + ]]> + </body> + </method> + + <property name="popupAnchor" readonly="true"> + <getter><![CDATA[ + if (this.mCurrentTab._popupAnchor) { + return this.mCurrentTab._popupAnchor; + } + // Actually the notificationbox not the browserstack. + let stack = this.mCurrentBrowser.parentNode; + // Create an anchor for the popup + const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let popupAnchor = document.createElementNS(NS_XUL, "hbox"); + popupAnchor.className = "popup-anchor"; + popupAnchor.hidden = true; + stack.appendChild(popupAnchor); + return this.mCurrentTab._popupAnchor = popupAnchor; + ]]></getter> + </property> + + <method name="selectTabAtIndex"> + <parameter name="aIndex"/> + <parameter name="aEvent"/> + <body> + <![CDATA[ + // count backwards for aIndex < 0 + if (aIndex < 0) + aIndex += this.tabs.length; + + if (aIndex >= 0 && + aIndex < this.tabs.length && + aIndex != this.tabContainer.selectedIndex) + this.selectedTab = this.tabs[aIndex]; + + if (aEvent) { + aEvent.preventDefault(); + aEvent.stopPropagation(); + } + ]]> + </body> + </method> + + <property name="selectedTab"> + <getter> + return this.mTabBox.selectedTab; + </getter> + <setter> + <![CDATA[ + // Update the tab + this.mTabBox.selectedTab = val; + return val; + ]]> + </setter> + </property> + + <property name="selectedBrowser" + onget="return this.mCurrentBrowser;" + readonly="true"/> + + <property name="browsers" readonly="true"> + <getter> + <![CDATA[ + return this._browsers || + (this._browsers = Array.from(this.tabs, tab => tab.linkedBrowser)); + ]]> + </getter> + </property> + + <!-- Drag and drop observer API --> + <method name="_onDragStart"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + var target = aEvent.target; + if (target.localName == "tab") { + var URI = target.linkedBrowser.currentURI; + var spec = URI.spec; + var title = target.linkedBrowser.contentTitle || spec; + var dt = aEvent.dataTransfer; + dt.mozSetDataAt("text/x-moz-url", spec + "\n" + title, 0); + dt.mozSetDataAt("text/uri-list", spec, 0); + dt.mozSetDataAt("text/plain", spec, 0); + dt.mozSetDataAt("text/html", '<a href="' + spec + '">' + title + '</a>', 0); + } + aEvent.stopPropagation(); + ]]> + </body> + </method> + + <method name="_onDragOver"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + aEvent.preventDefault(); + aEvent.stopPropagation(); + + var ib = document.getAnonymousElementByAttribute(this, "class", "tab-drop-indicator-bar"); + + // autoscroll the tab strip if we drag over the scroll buttons, + // even if we aren't dragging a tab + var pixelsToScroll = 0; + var tabStrip = this.tabContainer.arrowScrollbox; + var ltr = window.getComputedStyle(this, null).direction == "ltr"; + if (this.tabContainer.getAttribute("overflow") == "true") { + var targetAnonid = aEvent.originalTarget.getAttribute("anonid"); + switch (targetAnonid) { + case "scrollbutton-up": + pixelsToScroll = -tabStrip.scrollIncrement; + break; + case "scrollbutton-down": + case "alltabs-button": + pixelsToScroll = tabStrip.scrollIncrement; + break; + } + if (pixelsToScroll) + tabStrip.scrollByPixels((ltr ? 1 : -1) * pixelsToScroll); + } + + var ind = document.getAnonymousElementByAttribute(this, "class", "tab-drop-indicator"); + + var draggedTab = aEvent.dataTransfer.mozSourceNode; + var within = draggedTab && + draggedTab.parentNode == this.tabContainer; + var newIndexOn = within ? -1 : this.getDropOnIndex(aEvent); + + var ltr = window.getComputedStyle(this, null).direction == "ltr"; + var arrowX, tabBoxObject; + if (newIndexOn != -1) { + tabBoxObject = this.tabs[newIndexOn].boxObject; + arrowX = tabBoxObject.screenX + tabBoxObject.width / 2; + } + else { + var newIndexBetween = this.getDropIndex(aEvent); + if (within) { + var tabIndex = this.getTabIndex(draggedTab); + if (newIndexBetween == tabIndex || + newIndexBetween == tabIndex + 1) { + ib.collapsed = true; + return; + } + } + + if (newIndexBetween == this.tabs.length) { + tabBoxObject = this.tabs[this.tabs.length - 1].boxObject; + arrowX = tabBoxObject.x; + arrowX = tabBoxObject.screenX; + if (ltr) // for LTR "after" is on the right-hand side of the tab + arrowX += tabBoxObject.width; + } + else { + tabBoxObject = this.tabs[newIndexBetween].boxObject; + arrowX = tabBoxObject.screenX; + if (!ltr) // for RTL "before" is on the right-hand side of the tab + arrowX += tabBoxObject.width; + } + } + + var boxObject = tabStrip.scrollBoxObject; + // Check pixelsToScroll as well to prevent noticable judder. + if (pixelsToScroll > 0 || arrowX >= boxObject.screenX + boxObject.width) + arrowX = boxObject.screenX + boxObject.width; + else if (pixelsToScroll < 0 || arrowX < boxObject.screenX) + arrowX = boxObject.screenX; + + if (ltr) + ind.style.marginLeft = (arrowX - this.boxObject.screenX) + "px"; + else + ind.style.marginRight = (this.boxObject.screenX + this.boxObject.width - arrowX) + "px"; + + ib.collapsed = false; + ]]> + </body> + </method> + + <method name="_onDrop"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + document.getAnonymousElementByAttribute(this, "class", + "tab-drop-indicator-bar") + .collapsed = true; + aEvent.stopPropagation(); + + var newIndex = this.getDropIndex(aEvent); + var dt = aEvent.dataTransfer; + var draggedTab = dt.mozSourceNode; + if (draggedTab && draggedTab.parentNode == this.tabContainer) { + if (newIndex > this.getTabIndex(draggedTab)) + newIndex--; + this.moveTabTo(draggedTab, newIndex); + return; + } + + var url; + try { + // Pass true to disallow dropping javascript: or data: urls. + url = Services.droppedLinkHandler.dropLink(aEvent, {}, true); + } catch (ex) {} + + // Valid urls don't contain spaces ' '; if we have a space + // it isn't a valid url. + if (!url || url.includes(" ")) + return; + + getShortcutOrURIAndPostData(url).then(data => { + var bgLoad = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + if (aEvent.shiftKey) + bgLoad = !bgLoad; + + let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(aEvent); + + var tab = null; + tabIndex = this.getDropOnIndex(aEvent); + if (tabIndex != -1) { + // Load in an existing tab. + tab = this.tabs[tabIndex]; + tab.linkedBrowser.loadURI(data.url, { + allowThirdPartyFixup: true, + triggeringPrincipal, + }); + if (this.mCurrentTab != tab && !bgLoad) + this.selectedTab = tab; + } + else if (dt.mozSourceDocument && + dt.mozSourceDocument.defaultView.top == content) { + // We're adding a new tab, and we may want parent-tab tracking. + + tab = this.loadOneTab(data.url, { + inBackground: bgLoad, + allowThirdPartyFixup: true, + triggeringPrincipal, + }); + + this.moveTabTo(tab, newIndex); + } + else { + // We're adding a new tab, but do not want parent-tab tracking. + tab = this.addTab(data.url, { + allowThirdPartyFixup: true, + triggeringPrincipal, + }); + + this.moveTabTo(tab, newIndex); + if (this.mCurrentTab != tab && !bgLoad) + this.selectedTab = tab; + } + }); + ]]> + </body> + </method> + + <method name="_onDragLeave"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + var target = aEvent.relatedTarget; + while (target && target != this.mStrip) + target = target.parentNode; + + if (target) + return; + + document.getAnonymousElementByAttribute(this, "class", + "tab-drop-indicator-bar") + .collapsed = true; + aEvent.stopPropagation(); + ]]> + </body> + </method> + + <method name="moveTabTo"> + <parameter name="aTab"/> + <parameter name="aIndex"/> + <body> + <![CDATA[ + let oldPosition; + // for compatibility with extensions + if (typeof(aTab) == "number") { + oldPosition = aTab; + aTab = this.tabs[oldPosition]; + } else { + oldPosition = this.getTabIndex(aTab); + } + + if (oldPosition == aIndex) + return; + + this.mLastRelatedIndex = 0; + + this.mTabFilters.splice(aIndex, 0, this.mTabFilters.splice(oldPosition, 1)[0]); + this.mTabListeners.splice(aIndex, 0, this.mTabListeners.splice(oldPosition, 1)[0]); + + let wasFocused = (document.activeElement == this.mCurrentTab); + + if (aIndex >= oldPosition) + ++aIndex; + this.mCurrentTab._selected = false; + + // invalidate cache + this._browsers = null; + + // Use .item() instead of [] because dragging to the end of the + // strip goes out of bounds: .item() returns null (so it acts like + // appendChild), but [] throws. + var tab = this.tabContainer.insertBefore(aTab, this.tabs.item(aIndex)); + + this.mCurrentTab._selected = true; + + if (wasFocused) + this.mCurrentTab.focus(); + + this.tabContainer._handleTabSelect(false); + + tab.dispatchEvent(new UIEvent("TabMove", + { bubbles: true, cancelable: false, view: window, + detail: oldPosition })); + ]]> + </body> + </method> + + <method name="getDropIndex"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + for (var i = 0; i < this.tabs.length; ++i) { + var coord = this.tabs[i].boxObject.screenX + + this.tabs[i].boxObject.width / 2; + if (window.getComputedStyle(this, null).direction == "ltr") { + if (aEvent.screenX < coord) + return i; + } else { + if (aEvent.screenX > coord) + return i; + } + } + + return this.tabs.length; + ]]> + </body> + </method> + + <method name="getDropOnIndex"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + for (var i = 0; i < this.tabs.length; ++i) { + var tabBoxObject = this.tabs[i].boxObject; + if (aEvent.screenX > tabBoxObject.screenX + tabBoxObject.width * .25 && + aEvent.screenX < tabBoxObject.screenX + tabBoxObject.width * .75) + return i; + } + + return -1; + ]]> + </body> + </method> + + <!-- moveTabLeft and moveTabRight methods have been kept for backwards + compatibility for extensions. Internally moveTabOver is used. --> + <method name="moveTabLeft"> + <body> + <![CDATA[ + if (window.getComputedStyle(this, null).direction == "ltr") + this.moveTabBackward(); + else + this.moveTabForward(); + ]]> + </body> + </method> + + <method name="moveTabRight"> + <body> + <![CDATA[ + if (window.getComputedStyle(this, null).direction == "ltr") + this.moveTabForward(); + else + this.moveTabBackward(); + ]]> + </body> + </method> + + <method name="moveTabForward"> + <body> + <![CDATA[ + var tabPos = this.tabContainer.selectedIndex; + if (tabPos < this.browsers.length - 1) { + this.moveTabTo(this.mCurrentTab, tabPos + 1); + } + else if (this.arrowKeysShouldWrap) + this.moveTabToStart(); + ]]> + </body> + </method> + + <method name="moveTabBackward"> + <body> + <![CDATA[ + var tabPos = this.tabContainer.selectedIndex; + if (tabPos > 0) { + this.moveTabTo(this.mCurrentTab, tabPos - 1); + } + else if (this.arrowKeysShouldWrap) + this.moveTabToEnd(); + ]]> + </body> + </method> + + <method name="moveTabToStart"> + <body> + <![CDATA[ + if (this.tabContainer.selectedIndex > 0) { + this.moveTabTo(this.mCurrentTab, 0); + } + ]]> + </body> + </method> + + <method name="moveTabToEnd"> + <body> + <![CDATA[ + if (this.tabContainer.selectedIndex < this.browsers.length - 1) { + this.moveTabTo(this.mCurrentTab, this.browsers.length - 1); + } + ]]> + </body> + </method> + + <method name="moveTabOver"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + var direction = window.getComputedStyle(this, null).direction; + var keyCode = aEvent.keyCode; + if ((direction == "ltr" && keyCode == KeyEvent.DOM_VK_RIGHT) || + (direction == "rtl" && keyCode == KeyEvent.DOM_VK_LEFT)) + this.moveTabForward(); + else + this.moveTabBackward(); + ]]> + </body> + </method> + + <!-- BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT + MAKE SURE TO ADD IT HERE AS WELL. --> + <property name="canGoBack" + onget="return this.mCurrentBrowser.canGoBack;" + readonly="true"/> + + <property name="canGoForward" + onget="return this.mCurrentBrowser.canGoForward;" + readonly="true"/> + + <method name="goBack"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goBack(); + ]]> + </body> + </method> + + <method name="goForward"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goForward(); + ]]> + </body> + </method> + + <method name="reload"> + <body> + <![CDATA[ + return this.mCurrentBrowser.reload(); + ]]> + </body> + </method> + + <method name="reloadWithFlags"> + <parameter name="aFlags"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.reloadWithFlags(aFlags); + ]]> + </body> + </method> + + <method name="stop"> + <body> + <![CDATA[ + return this.mCurrentBrowser.stop(); + ]]> + </body> + </method> + + <!-- throws exception for unknown schemes --> + <method name="loadURI"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.loadURI(aURI, aReferrerURI, aCharset); + ]]> + </body> + </method> + + <!-- throws exception for unknown schemes --> + <method name="loadURIWithFlags"> + <parameter name="aURI"/> + <parameter name="aFlags"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <body> + <![CDATA[ + // Note - the callee understands both: + // (a) loadURIWithFlags(aURI, aFlags, ...) + // (b) loadURIWithFlags(aURI, { flags: aFlags, ... }) + // Forwarding it as (a) here actually supports both (a) and (b), + // so you can call us either way too. + return this.mCurrentBrowser.loadURIWithFlags(aURI, aFlags, aReferrerURI, aCharset, aPostData); + ]]> + </body> + </method> + + <method name="goHome"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goHome(); + ]]> + </body> + </method> + + <property name="homePage"> + <getter> + <![CDATA[ + return this.mCurrentBrowser.homePage; + ]]> + </getter> + <setter> + <![CDATA[ + this.mCurrentBrowser.homePage = val; + return val; + ]]> + </setter> + </property> + + <method name="gotoIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.gotoIndex(aIndex); + ]]> + </body> + </method> + + <property name="currentURI" + onget="return this.mCurrentBrowser.currentURI;" + readonly="true"/> + + <field name="finder"><![CDATA[ + ({ + mTabBrowser: this, + mListeners: new Set(), + get finder() { + return this.mTabBrowser.mCurrentBrowser.finder; + }, + addResultListener: function(aListener) { + this.mListeners.add(aListener); + this.finder.addResultListener(aListener); + }, + removeResultListener: function(aListener) { + this.mListeners.delete(aListener); + this.finder.removeResultListener(aListener); + }, + get searchString() { + return this.finder.searchString; + }, + get clipboardSearchString() { + return this.finder.clipboardSearchString; + }, + set clipboardSearchString(val) { + return this.finder.clipboardSearchString = val; + }, + set caseSensitive(val) { + return this.finder.caseSensitive = val; + }, + set entireWord(val) { + return this.finder.entireWord = val; + }, + get highlighter() { + return this.finder.highlighter; + }, + get matchesCountLimit() { + return this.finder.matchesCountLimit; + }, + fastFind: function(aSearchString, aLinksOnly, aDrawOutline) { + this.finder.fastFind(aSearchString, aLinksOnly, aDrawOutline); + }, + findAgain: function(aFindBackwards, aLinksOnly, aDrawOutline) { + this.finder.findAgain(aFindBackwards, aLinksOnly, aDrawOutline); + }, + setSearchStringToSelection: function() { + return this.finder.setSearchStringToSelection(); + }, + highlight: function(...args) { + this.finder.highlight(...args); + }, + getInitialSelection: function() { + this.finder.getInitialSelection(); + }, + getActiveSelectionText: function() { + return this.finder.getActiveSelectionText(); + }, + enableSelection: function() { + this.finder.enableSelection(); + }, + removeSelection: function() { + this.finder.removeSelection(); + }, + focusContent: function() { + this.finder.focusContent(); + }, + onFindbarClose: function() { + this.finder.onFindbarClose(); + }, + onFindbarOpen: function() { + this.finder.onFindbarOpen(); + }, + onModalHighlightChange: function(...args) { + return this.finder.onModalHighlightChange(...args); + }, + onHighlightAllChange: function(...args) { + return this.finder.onHighlightAllChange(...args); + }, + keyPress: function(aEvent) { + this.finder.keyPress(aEvent); + }, + requestMatchesCount: function(...args) { + this.finder.requestMatchesCount(...args); + } + }) + ]]></field> + + <property name="docShell" + onget="return this.mCurrentBrowser.docShell" + readonly="true"/> + + <property name="webNavigation" + onget="return this.mCurrentBrowser.webNavigation" + readonly="true"/> + + <property name="webBrowserFind" + readonly="true" + onget="return this.mCurrentBrowser.webBrowserFind"/> + + <property name="webProgress" + readonly="true" + onget="return this.mCurrentBrowser.webProgress"/> + + <property name="contentWindow" + readonly="true" + onget="return this.mCurrentBrowser.contentWindow"/> + + <property name="contentWindowAsCPOW" + readonly="true" + onget="return this.mCurrentBrowser.contentWindowAsCPOW"/> + + <property name="sessionHistory" + onget="return this.mCurrentBrowser.sessionHistory;" + readonly="true"/> + + <property name="markupDocumentViewer" + onget="return this.mCurrentBrowser.markupDocumentViewer;" + readonly="true"/> + + <property name="contentDocument" + onget="return this.mCurrentBrowser.contentDocument;" + readonly="true"/> + + <property name="contentTitle" + onget="return this.mCurrentBrowser.contentTitle;" + readonly="true"/> + + <property name="securityUI" + onget="return this.mCurrentBrowser.securityUI;" + readonly="true"/> + + <property name="userTypedValue" + onget="return this.mCurrentBrowser.userTypedValue;" + onset="return this.mCurrentBrowser.userTypedValue = val;"/> + + <property name="droppedLinkHandler" + onget="return this.mCurrentBrowser.droppedLinkHandler;" + onset="return this.mCurrentBrowser.droppedLinkHandler = val;"/> + + <property name="showWindowResizer" + onget="return this.mCurrentBrowser.showWindowResizer;" + onset="return this.mCurrentBrowser.showWindowResizer = val;"/> + + <property name="docShellIsActive" + onget="return this.mCurrentBrowser.docShellIsActive;" + onset="return this.mCurrentBrowser.docShellIsActive = val;"/> + + <property name="fullZoom" + onget="return this.mCurrentBrowser.fullZoom;" + onset="return this.mCurrentBrowser.fullZoom = val;"/> + + <property name="textZoom" + onget="return this.mCurrentBrowser.textZoom;" + onset="return this.mCurrentBrowser.textZoom = val;"/> + + <property name="isSyntheticDocument" + onget="return this.mCurrentBrowser.isSyntheticDocument;" + readonly="true"/> + + <property name="messageManager" + onget="return window.messageManager;" + readonly="true"/> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body> + <![CDATA[ + var maxUndoDepth = 0; + switch (aTopic) { + case "browser:purge-session-history": + break; + + case "nsPref:changed": + if (aData == "browser.tabs.max_tabs_undo") { + maxUndoDepth = Math.max(0, Services.prefs.getIntPref(aData)); + break; + } + + default: + return; + } + + // Wipe out savedBrowsers since history is gone + while (this.savedBrowsers.length > maxUndoDepth) { + var tabData = this.savedBrowsers.pop(); + var deadBrowser = tabData.browserData.browser; + delete tabData.browserData; + this.removeBrowser(deadBrowser); + } + ]]> + </body> + </method> + + <field name="_fastFind">null</field> + <property name="fastFind" readonly="true"> + <getter> + <![CDATA[ + if (!this._fastFind) { + this._fastFind = Cc["@mozilla.org/typeaheadfind;1"] + .createInstance(Ci.nsITypeAheadFind); + this._fastFind.init(this.docShell); + } + return this._fastFind; + ]]> + </getter> + </property> + + <field name="_lastSearchString">null</field> + <field name="_lastSearchHighlight">false</field> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "keypress": + this._handleKeyEvent(aEvent); + break; + } + ]]></body> + </method> + + <constructor> + <![CDATA[ + document.addEventListener("keypress", this); + this.arrowKeysShouldWrap = AppConstants.platform == "macosx"; + // Bail out early if we are in tabmail. See Bug 521803. + if (!this.mPanelContainer) + return; + + this.mCurrentBrowser = this.mPanelContainer.firstChild.firstChild.firstChild; + this.mCurrentTab = this.tabContainer.firstChild; + + var uniqueId = "panel" + this.nextTabNumber++; + this.mPanelContainer.childNodes[0].id = uniqueId; + this.tabs[0].linkedPanel = uniqueId; + this.tabs[0].linkedBrowser = this.mCurrentBrowser; + + // Ensure the browser's session history and security UI are wired up + // note that toolkit browser automatically inits its security UI + // when you get it but for xpfe you need to init it explicitly + if (!this.mCurrentBrowser.securityUI) + this.mCurrentBrowser.init(); + + // Wire up the tab's progress listener and filter. + var tabListener = this.mTabProgressListener(this.mCurrentTab, + this.mCurrentBrowser, + false); + var filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); + this.webProgress.addProgressListener(filter, + Ci.nsIWebProgress.NOTIFY_ALL); + this.mTabListeners[0] = tabListener; + this.mTabFilters[0] = filter; + + if (!Services.prefs.getBoolPref("browser.tabs.autoHide") && + !Services.prefs.getBoolPref("browser.tabs.forceHide") && + window.toolbar.visible) + this.mStrip.collapsed = false; + + var t = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "tab"); + t.setAttribute("label", this.mStringBundle.getString("tabs.untitled")); + t.setAttribute("crop", "end"); + t.className = "tabbrowser-tab"; + t.style.maxWidth = Services.prefs.getIntPref("browser.tabs.tabMaxWidth") + "px"; + t.style.minWidth = Services.prefs.getIntPref("browser.tabs.tabMinWidth") + "px"; + t.width = 0; + t.flex = 100; + t.setAttribute("validate", "never"); + t.setAttribute("onerror", "this.parentNode.parentNode.parentNode.parentNode.addToMissedIconCache(this.getAttribute('image')); this.removeAttribute('image');"); + this.referenceTab = t; + + Services.obs.addObserver(this, "browser:purge-session-history"); + Services.prefs.addObserver("browser.tabs.max_tabs_undo", this); + + var onclick = this.getAttribute("oncontentclick"); + if (onclick) + this.onContentClick = new Function("event", onclick); + ]]> + </constructor> + + <destructor> + <![CDATA[ + document.removeEventListener("keypress", this); + // Bail out early if we are in tabmail. See Bug 521803. + if (!this.mPanelContainer) + return; + + for (var i = 0; i < this.mTabListeners.length; ++i) { + this.browsers[i].webProgress.removeProgressListener(this.mTabFilters[i]); + this.mTabFilters[i].removeProgressListener(this.mTabListeners[i]); + this.mTabFilters[i] = null; + this.mTabListeners[i] = null; + } + Services.obs.removeObserver(this, "browser:purge-session-history"); + Services.prefs.removeObserver("browser.tabs.max_tabs_undo", this); + this.savedBrowsers.forEach(function(aTabData) { + delete aTabData.browserData; + }); + ]]> + </destructor> + + <!-- Deprecated stuff, implemented for backwards compatibility. --> + <property name="mTabContainer" readonly="true" + onget="return this.tabContainer;"/> + <property name="mTabs" readonly="true" + onget="return this.tabs;"/> + </implementation> + + <handlers> + <handler event="select" action="if (event.originalTarget == this.mPanelContainer) this.updateCurrentBrowser();"/> + + <handler event="DOMLinkAdded" phase="capturing" action="this.onLinkEvent(event);"/> + <handler event="DOMLinkChanged" phase="capturing" action="this.onLinkEvent(event);"/> + + <handler event="DOMWindowClose" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + if (this.tabs.length == 1) + return; + + this.removeTab(this._getTabForContentWindow(event.target)); + event.preventDefault(); + ]]> + </handler> + + <handler event="DOMWebNotificationClicked" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + // The user clicked a desktop notification; make sure its + // tab is brought to the front and then raise the window. + this.selectedTab = this._getTabForContentWindow(event.target.top); + window.focus(); + ]]> + </handler> + + <handler event="DOMWillOpenModalDialog" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + // We're about to open a modal dialog, make sure the opening + // tab is brought to the front. + this.selectedTab = this._getTabForContentWindow(event.target.top); + ]]> + </handler> + + <handler event="DOMTitleChanged"> + <![CDATA[ + if (!event.isTrusted) + return; + + var contentWin = event.target.defaultView; + if (contentWin != contentWin.top) + return; + + var tab = this._getTabForContentWindow(contentWin); + if (!tab) + return; + + this.setTabTitle(tab); + if (tab == this.mCurrentTab) + this.updateTitlebar(); + ]]> + </handler> + + <handler event="click" phase="capturing" group="system"> + <![CDATA[ + if (this.onContentClick) + this.onContentClick(event); + ]]> + </handler> + + </handlers> + </binding> + + <binding id="tabbrowser-arrowscrollbox" + extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll"> + <implementation> + <!-- Override scrollbox.xml method, since our scrollbox's children are + inherited from the binding parent --> + <method name="_getScrollableElements"> + <body> + <![CDATA[ + return Array.from(document.getBindingParent(this).childNodes) + .filter(this._canScrollToElement, + this); + ]]> + </body> + </method> + <method name="_canScrollToElement"> + <parameter name="aTab"/> + <body> + <![CDATA[ + return !aTab.pinned && !aTab.hidden; + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="underflow"> + <![CDATA[ + if (event.detail == 0) + return; // Ignore vertical events + + var tabs = document.getBindingParent(this); + tabs.removeAttribute("overflow"); + ]]> + </handler> + <handler event="overflow"> + <![CDATA[ + if (event.detail == 0) + return; // Ignore vertical events + + var tabs = document.getBindingParent(this); + tabs.setAttribute("overflow", true); + tabs._handleTabSelect(false); + ]]> + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-tabs" + extends="chrome://global/content/bindings/tabbox.xml#tabs"> + <content> + <xul:stack flex="1" class="tabs-stack"> + <xul:vbox> + <xul:spacer flex="1"/> + <xul:hbox class="tabs-bottom" align="center"/> + </xul:vbox> + <xul:vbox> + <xul:hbox> + <xul:stack> + <xul:spacer class="tabs-left"/> + <xul:toolbarbutton class="tabs-newbutton" context="" + anonid="tabstrip-newbutton" + xbl:inherits="oncommand=onnewtab,onclick=onnewtabclick,tooltiptext=tooltiptextnew"/> + </xul:stack> + <xul:arrowscrollbox anonid="arrowscrollbox" + class="tabbrowser-arrowscrollbox" + flex="1" + xbl:inherits="smoothscroll" + orient="horizontal" + style="min-width: 1px;"> + <children includes="tab"/> + <xul:spacer class="tabs-right" flex="1"/> + </xul:arrowscrollbox> + <children/> + <xul:stack> + <xul:spacer class="tabs-right"/> + <xul:hbox class="tabs-closebutton-box" align="stretch" pack="end"> + <xul:toolbarbutton class="tabs-alltabs-button" context="" + anonid="alltabs-button" + type="menu" + xbl:inherits="tooltiptext=tooltiptextalltabs"> + <xul:menupopup class="tabs-alltabs-popup" + anonid="alltabs-popup" + position="after_end"/> + </xul:toolbarbutton> + <xul:hbox align="center"> + <xul:toolbarbutton class="tabs-closebutton close-button" context="" + anonid="tabstrip-closebutton" + xbl:inherits="disabled=disableclose,oncommand=onclosetab,tooltiptext=tooltiptextclose"/> + </xul:hbox> + </xul:hbox> + </xul:stack> + </xul:hbox> + <xul:spacer class="tabs-bottom-spacer"/> + </xul:vbox> + </xul:stack> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor> + <![CDATA[ + var tab = this.firstChild; + // set the tabstrip's minWidth too, otherwise it immediately overflows + this.arrowScrollbox.style.minWidth = + tab.style.minWidth = Services.prefs.getIntPref("browser.tabs.tabMinWidth") + "px"; + tab.style.maxWidth = Services.prefs.getIntPref("browser.tabs.tabMaxWidth") + "px"; + window.addEventListener("resize", this); + ]]> + </constructor> + + <destructor> + <![CDATA[ + window.removeEventListener("resize", this); + ]]> + </destructor> + + <field name="arrowScrollboxWidth">0</field> + + <field name="arrowScrollbox"> + document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox"); + </field> + + <method name="_handleTabSelect"> + <parameter name="aSmoothScroll"/> + <body> + <![CDATA[ + if (this.getAttribute("overflow") == "true") + this.arrowScrollbox.ensureElementIsVisible(this.selectedItem, + aSmoothScroll); + ]]> + </body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + switch (aEvent.type) + { + case "resize": + if (aEvent.target != window) + break; + var width = this.arrowScrollbox.boxObject.width; + if (width != this.arrowScrollboxWidth) + { + this._handleTabSelect(false); + this.arrowScrollboxWidth = width; + } + break; + } + ]]> + </body> + </method> + + <field name="mAllTabsPopup"> + document.getAnonymousElementByAttribute(this, "anonid", "alltabs-popup"); + </field> + + <field name="_animateElement"> + this.arrowScrollbox._scrollButtonDown; + </field> + + <method name="_notifyBackgroundTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var scrollRect = this.arrowScrollbox.scrollClientRect; + var tab = aTab.getBoundingClientRect(); + + // Is the new tab already completely visible? + if (scrollRect.left <= tab.left && tab.right <= scrollRect.right) + return; + + if (this.arrowScrollbox.smoothScroll) { + let selected = this.selectedItem.getBoundingClientRect(); + + /* Can we make both the new tab and the selected tab completely + visible? */ + if (!selected || + Math.max(tab.right - selected.left, selected.right - tab.left) <= scrollRect.width) { + this.arrowScrollbox.ensureElementIsVisible(aTab); + return; + } + + this.arrowScrollbox.scrollByPixels(this.arrowScrollbox._isRTLScrollbox ? + selected.right - scrollRect.right : + selected.left - scrollRect.left); + } + + if (!this._animateElement.hasAttribute("notifybgtab")) { + this._animateElement.setAttribute("notifybgtab", "true"); + setTimeout(function(ele) { + ele.removeAttribute("notifybgtab"); + }, 150, this._animateElement); + } + ]]> + </body> + </method> + + <method name="_handleNewTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (aTab.parentNode != this) + return; + + if (aTab.getAttribute("selected") == "true") { + this._handleTabSelect(); + } else { + this._notifyBackgroundTab(aTab); + } + + /* XXXmano: this is a temporary workaround for bug 345399 + * We need to manually update the scroll buttons disabled state + * if a tab was inserted to the overflow area or removed from it + * without any scrolling and when the tabbar has already + * overflowed. + */ + this.arrowScrollbox._updateScrollButtonsDisabledState(); + ]]> + </body> + </method> + + <method name="_handleMouseScroll"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + // Javascript does not have a logical XOR operator. + if (aEvent.shiftKey != Services.prefs.getBoolPref("browser.tabs.mouseScrollAdvancesTab")) { + this.advanceSelectedTab(aEvent.detail < 0 ? -1 : 1); + aEvent.stopPropagation(); + } + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="TabSelect" action="this._handleTabSelect();"/> + + <handler event="transitionend"> + <![CDATA[ + if (event.propertyName == "max-width") + this._handleNewTab(event.target); + ]]> + </handler> + + <handler event="DOMMouseScroll" phase="capturing"> + <![CDATA[ + this._handleMouseScroll(event); + ]]> + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-alltabs-popup" + extends="chrome://global/content/bindings/popup.xml#popup"> + <implementation implements="nsIDOMEventListener"> + <method name="_tabOnTabClose"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + let menuItem = aEvent.target.mCorrespondingMenuitem; + if (menuItem) + menuItem.remove(); + ]]> + </body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + switch (aEvent.type) + { + case "TabClose": + this._tabOnTabClose(aEvent); + break; + case "TabOpen": + this._createTabMenuItem(aEvent.originalTarget); + break; + case "scroll": + this._updateTabsVisibilityStatus(); + break; + } + ]]> + </body> + </method> + + <method name="_updateTabsVisibilityStatus"> + <body> + <![CDATA[ + let tabContainer = document.getBindingParent(this); + let tabstripBO = tabContainer.arrowScrollbox.scrollBoxObject; + + for (let i = 0; i < this.childNodes.length; i++) + { + let curTabBO = this.childNodes[i].tab.boxObject; + if (curTabBO.screenX >= tabstripBO.screenX && + curTabBO.screenX + curTabBO.width <= tabstripBO.screenX + tabstripBO.width) + this.childNodes[i].removeAttribute("tabIsScrolled"); + else + this.childNodes[i].setAttribute("tabIsScrolled", "true"); + } + ]]> + </body> + </method> + + <method name="_createTabMenuItem"> + <parameter name="aTabNode"/> + <body> + <![CDATA[ + let menuItem = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "menuitem"); + menuItem.setAttribute("class", "menuitem-iconic alltabs-item icon-holder"); + menuItem.setAttribute("label", aTabNode.label); + menuItem.setAttribute("crop", aTabNode.getAttribute("crop")); + + ["busy", "image", "selected"].forEach( + function(attribute) + { + if (aTabNode.hasAttribute(attribute)) + { + menuItem.setAttribute(attribute, aTabNode.getAttribute(attribute)); + } + } + ); + + // Keep some attributes of the menuitem in sync with its + // corresponding tab (e.g. the tab label) + aTabNode.mCorrespondingMenuitem = menuItem; + document.addBroadcastListenerFor(aTabNode, menuItem, "label"); + document.addBroadcastListenerFor(aTabNode, menuItem, "crop"); + document.addBroadcastListenerFor(aTabNode, menuItem, "image"); + document.addBroadcastListenerFor(aTabNode, menuItem, "busy"); + document.addBroadcastListenerFor(aTabNode, menuItem, "selected"); + aTabNode.addEventListener("TabClose", this); + menuItem.tab = aTabNode; + this.appendChild(menuItem); + return menuItem; + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="popupshowing"> + <![CDATA[ + // set up the menu popup + let tabcontainer = document.getBindingParent(this); + let tabs = tabcontainer.childNodes; + + // Listen for changes in the tab bar. + let tabbrowser = document.getBindingParent(tabcontainer); + tabbrowser.addEventListener("TabOpen", this); + tabcontainer.arrowScrollbox.addEventListener("scroll", this); + + for (let i = 0; i < tabs.length; i++) + this._createTabMenuItem(tabs[i]); + this._updateTabsVisibilityStatus(); + ]]> + </handler> + + <handler event="popuphiding"> + <![CDATA[ + // clear out the menu popup and remove the listeners + while (this.hasChildNodes()) + { + let menuItem = this.lastChild; + document.removeBroadcastListenerFor(menuItem.tab, menuItem, "label"); + document.removeBroadcastListenerFor(menuItem.tab, menuItem, "crop"); + document.removeBroadcastListenerFor(menuItem.tab, menuItem, "image"); + document.removeBroadcastListenerFor(menuItem.tab, menuItem, "busy"); + document.removeBroadcastListenerFor(menuItem.tab, menuItem, "selected"); + menuItem.tab.removeEventListener("TabClose", this); + menuItem.tab.mCorrespondingMenuitem = null; + menuItem.tab = null; + menuItem.remove(); + } + let tabcontainer = document.getBindingParent(this); + tabcontainer.arrowScrollbox.removeEventListener("scroll", this); + document.getBindingParent(tabcontainer).removeEventListener("TabOpen", this); + ]]> + </handler> + + <handler event="command"> + <![CDATA[ + let tabcontainer = document.getBindingParent(this); + let tab = event.target.tab; + if (tabcontainer.selectedItem == tab) + tabcontainer._handleTabSelect(); + else + tabcontainer.selectedItem = tab; + ]]> + </handler> + + <handler event="DOMMenuItemActive"> + <![CDATA[ + var tab = event.target.tab; + if (tab) { + let overLink = tab.linkedBrowser.currentURI.spec; + if (overLink == "about:blank") + overLink = ""; + XULBrowserWindow.setOverLink(overLink, null); + } + ]]></handler> + + <handler event="DOMMenuItemInactive"> + <![CDATA[ + XULBrowserWindow.setOverLink("", null); + ]]></handler> + + </handlers> + </binding> +</bindings> diff --git a/comm/suite/browser/test/browser/alltabslistener.html b/comm/suite/browser/test/browser/alltabslistener.html new file mode 100644 index 0000000000..166c31037a --- /dev/null +++ b/comm/suite/browser/test/browser/alltabslistener.html @@ -0,0 +1,8 @@ +<html> +<head> +<title>Test page for bug 463387</title> +</head> +<body> +<p>Test page for bug 463387</p> +</body> +</html> diff --git a/comm/suite/browser/test/browser/authenticate.sjs b/comm/suite/browser/test/browser/authenticate.sjs new file mode 100644 index 0000000000..2f8c85adc1 --- /dev/null +++ b/comm/suite/browser/test/browser/authenticate.sjs @@ -0,0 +1,210 @@ +function handleRequest(request, response) +{ + try { + reallyHandleRequest(request, response); + } catch (e) { + response.setStatusLine("1.0", 200, "AlmostOK"); + response.write("Error handling request: " + e); + } +} + + +function reallyHandleRequest(request, response) { + var match; + var requestAuth = true, requestProxyAuth = true; + + // Allow the caller to drive how authentication is processed via the query. + // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar + // The extra ? allows the user/pass/realm checks to succeed if the name is + // at the beginning of the query string. + var query = "?" + request.queryString; + + var expected_user = "", expected_pass = "", realm = "mochitest"; + var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy"; + var huge = false, anonymous = false; + var authHeaderCount = 1; + // user=xxx + match = /[^_]user=([^&]*)/.exec(query); + if (match) + expected_user = match[1]; + + // pass=xxx + match = /[^_]pass=([^&]*)/.exec(query); + if (match) + expected_pass = match[1]; + + // realm=xxx + match = /[^_]realm=([^&]*)/.exec(query); + if (match) + realm = match[1]; + + // proxy_user=xxx + match = /proxy_user=([^&]*)/.exec(query); + if (match) + proxy_expected_user = match[1]; + + // proxy_pass=xxx + match = /proxy_pass=([^&]*)/.exec(query); + if (match) + proxy_expected_pass = match[1]; + + // proxy_realm=xxx + match = /proxy_realm=([^&]*)/.exec(query); + if (match) + proxy_realm = match[1]; + + // huge=1 + match = /huge=1/.exec(query); + if (match) + huge = true; + + // multiple=1 + match = /multiple=([^&]*)/.exec(query); + if (match) + authHeaderCount = match[1]+0; + + // anonymous=1 + match = /anonymous=1/.exec(query); + if (match) + anonymous = true; + + // Look for an authentication header, if any, in the request. + // + // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + // + // This test only supports Basic auth. The value sent by the client is + // "username:password", obscured with base64 encoding. + + var actual_user = "", actual_pass = "", authHeader, authPresent = false; + if (request.hasHeader("Authorization")) { + authPresent = true; + authHeader = request.getHeader("Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw "Couldn't parse auth header: " + authHeader; + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw "Couldn't decode auth header: " + userpass; + actual_user = match[1]; + actual_pass = match[2]; + } + + var proxy_actual_user = "", proxy_actual_pass = ""; + if (request.hasHeader("Proxy-Authorization")) { + authHeader = request.getHeader("Proxy-Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw "Couldn't parse auth header: " + authHeader; + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw "Couldn't decode auth header: " + userpass; + proxy_actual_user = match[1]; + proxy_actual_pass = match[2]; + } + + // Don't request authentication if the credentials we got were what we + // expected. + if (expected_user == actual_user && + expected_pass == actual_pass) { + requestAuth = false; + } + if (proxy_expected_user == proxy_actual_user && + proxy_expected_pass == proxy_actual_pass) { + requestProxyAuth = false; + } + + if (anonymous) { + if (authPresent) { + response.setStatusLine("1.0", 400, "Unexpected authorization header found"); + } else { + response.setStatusLine("1.0", 200, "Authorization header not found"); + } + } else { + if (requestProxyAuth) { + response.setStatusLine("1.0", 407, "Proxy authentication required"); + for (i = 0; i < authHeaderCount; ++i) + response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true); + } else if (requestAuth) { + response.setStatusLine("1.0", 401, "Authentication required"); + for (i = 0; i < authHeaderCount; ++i) + response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true); + } else { + response.setStatusLine("1.0", 200, "OK"); + } + } + + response.setHeader("Content-Type", "application/xhtml+xml", false); + response.write("<html xmlns='http://www.w3.org/1999/xhtml'>"); + response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n"); + response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n"); + response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n"); + response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n"); + response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n"); + + if (huge) { + response.write("<div style='display: none'>"); + for (i = 0; i < 100000; i++) { + response.write("123456789\n"); + } + response.write("</div>"); + response.write("<span id='footnote'>This is a footnote after the huge content fill</span>"); + } + + response.write("</html>"); +} + + +// base64 decoder +// +// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa() +// doesn't seem to exist. :-( +/* Convert Base64 data to a string */ +const toBinaryTable = [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 +]; +const base64Pad = '='; + +function base64ToString(data) { + + var result = ''; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + + // Convert one by one. + for (var i = 0; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data[i] == base64Pad); + // Skip illegal characters and whitespace + if (c == -1) continue; + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) + result += String.fromCharCode((leftdata >> leftbits) & 0xff); + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) + throw Components.Exception('Corrupted base64 string'); + + return result; +} diff --git a/comm/suite/browser/test/browser/browser.ini b/comm/suite/browser/test/browser/browser.ini new file mode 100644 index 0000000000..a6b00157c6 --- /dev/null +++ b/comm/suite/browser/test/browser/browser.ini @@ -0,0 +1,38 @@ +[DEFAULT] +support-files = + head.js + +[browser_alltabslistener.js] +support-files = alltabslistener.html +[browser_bug329212.js] +support-files = title_test.svg +[browser_bug409624.js] +[browser_bug413915.js] +[browser_bug427559.js] +[browser_bug435325.js] +[browser_bug462289.js] +skip-if = toolkit == "cocoa" +[browser_bug519216.js] +[browser_bug561636.js] +[browser_bug562649.js] +[browser_bug581947.js] +[browser_bug585511.js] +[browser_bug595507.js] +[browser_bug623155.js] +support-files = redirect_bug623155.sjs +[browser_fayt.js] +[browser_page_style_menu.js] +support-files = page_style_sample.html +[browser_notification_tab_switching.js] +support-files = file_dom_notifications.html +[browser_pageInfo.js] +support-files = feed_tab.html +[browser_popupNotification.js] +[browser_privatebrowsing_protocolhandler.js] +support-files = browser_privatebrowsing_protocolhandler_page.html +[browser_relatedTabs.js] +[browser_scope.js] +[browser_selectTabAtIndex.js] +[browser_urlbarCopying.js] +support-files = + authenticate.sjs diff --git a/comm/suite/browser/test/browser/browser_alltabslistener.js b/comm/suite/browser/test/browser/browser_alltabslistener.js new file mode 100644 index 0000000000..609a4e66e8 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_alltabslistener.js @@ -0,0 +1,201 @@ +const gCompleteState = Ci.nsIWebProgressListener.STATE_STOP + + Ci.nsIWebProgressListener.STATE_IS_NETWORK; + +var gFrontProgressListener = { + onProgressChange: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + }, + + onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) { + var state = "onStateChange"; + info("FrontProgress: " + state + " 0x" + aStateFlags.toString(16)); + ok(gFrontNotificationsPos < gFrontNotifications.length, "Got an expected notification for the front notifications listener"); + is(state, gFrontNotifications[gFrontNotificationsPos], "Got a notification for the front notifications listener"); + gFrontNotificationsPos++; + }, + + onLocationChange: function (aWebProgress, aRequest, aLocationURI, aFlags) { + var state = "onLocationChange"; + info("FrontProgress: " + state + " " + aLocationURI.spec); + ok(gFrontNotificationsPos < gFrontNotifications.length, "Got an expected notification for the front notifications listener"); + is(state, gFrontNotifications[gFrontNotificationsPos], "Got a notification for the front notifications listener"); + gFrontNotificationsPos++; + }, + + onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) { + }, + + onSecurityChange: function (aWebProgress, aRequest, aState) { + var state = "onSecurityChange"; + info("FrontProgress: " + state + " 0x" + aState.toString(16)); + ok(gFrontNotificationsPos < gFrontNotifications.length, "Got an expected notification for the front notifications listener"); + is(state, gFrontNotifications[gFrontNotificationsPos], "Got a notification for the front notifications listener"); + gFrontNotificationsPos++; + } +} + +var gAllProgressListener = { + onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + var state = "onStateChange"; + info("AllProgress: " + state + " 0x" + aStateFlags.toString(16)); + is(aBrowser, gTestBrowser, state + " notification came from the correct browser"); + ok(gAllNotificationsPos < gAllNotifications.length, "Got an expected notification for the all notifications listener"); + is(state, gAllNotifications[gAllNotificationsPos], "Got a notification for the all notifications listener"); + gAllNotificationsPos++; + + if ((aStateFlags & gCompleteState) == gCompleteState) { + is(gAllNotificationsPos, gAllNotifications.length, "Saw the expected number of notifications"); + is(gFrontNotificationsPos, gFrontNotifications.length, "Saw the expected number of frontnotifications"); + executeSoon(gNextTest); + } + }, + + onLocationChange: function (aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) { + var state = "onLocationChange"; + info("AllProgress: " + state + " " + aLocationURI.spec); + is(aBrowser, gTestBrowser, state + " notification came from the correct browser"); + ok(gAllNotificationsPos < gAllNotifications.length, "Got an expected notification for the all notifications listener"); + is(state, gAllNotifications[gAllNotificationsPos], "Got a notification for the all notifications listener"); + gAllNotificationsPos++; + }, + + onStatusChange: function (aBrowser, aWebProgress, aRequest, aStatus, aMessage) { + var state = "onStatusChange"; + is(aBrowser, gTestBrowser, state + " notification came from the correct browser"); + }, + + onSecurityChange: function (aBrowser, aWebProgress, aRequest, aState) { + var state = "onSecurityChange"; + info("AllProgress: " + state + " 0x" + aState.toString(16)); + is(aBrowser, gTestBrowser, state + " notification came from the correct browser"); + ok(gAllNotificationsPos < gAllNotifications.length, "Got an expected notification for the all notifications listener"); + is(state, gAllNotifications[gAllNotificationsPos], "Got a notification for the all notifications listener"); + gAllNotificationsPos++; + } +} + +var gFrontNotifications, gAllNotifications, gFrontNotificationsPos, gAllNotificationsPos; +var gBackgroundTab, gForegroundTab, gBackgroundBrowser, gForegroundBrowser, gTestBrowser; +var gTestPage = "/browser/suite/browser/test/browser/alltabslistener.html"; +var gNextTest; + +function test() { + waitForExplicitFinish(); + + gBackgroundTab = getBrowser().addTab("about:blank"); + gForegroundTab = getBrowser().addTab("about:blank"); + gBackgroundBrowser = getBrowser().getBrowserForTab(gBackgroundTab); + gForegroundBrowser = getBrowser().getBrowserForTab(gForegroundTab); + getBrowser().selectedTab = gForegroundTab; + + // We must wait until the about:blank page has completed loading before + // starting tests or we get notifications from that + gForegroundBrowser.addEventListener("load", startTests, true); +} + +function runTest(browser, url, next) { + gFrontNotificationsPos = 0; + gAllNotificationsPos = 0; + gNextTest = next; + gTestBrowser = browser; + browser.loadURI(url); +} + +function startTests() { + gForegroundBrowser.removeEventListener("load", startTests, true); + executeSoon(startTest1); +} + +function startTest1() { + info("\nTest 1"); + getBrowser().addProgressListener(gFrontProgressListener); + getBrowser().addTabsProgressListener(gAllProgressListener); + + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = gAllNotifications; + runTest(gForegroundBrowser, "http://example.org" + gTestPage, startTest2); +} + +function startTest2() { + info("\nTest 2"); + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = gAllNotifications; + runTest(gForegroundBrowser, "https://example.com" + gTestPage, startTest3); +} + +function startTest3() { + info("\nTest 3"); + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = []; + runTest(gBackgroundBrowser, "http://example.org" + gTestPage, startTest4); +} + +function startTest4() { + info("\nTest 4"); + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = []; + runTest(gBackgroundBrowser, "https://example.com" + gTestPage, startTest5); +} + +function startTest5() { + info("\nTest 5"); + // Switch the foreground browser + [gForegroundBrowser, gBackgroundBrowser] = [gBackgroundBrowser, gForegroundBrowser]; + [gForegroundTab, gBackgroundTab] = [gBackgroundTab, gForegroundTab]; + // Avoid the onLocationChange this will fire + getBrowser().removeProgressListener(gFrontProgressListener); + getBrowser().selectedTab = gForegroundTab; + getBrowser().addProgressListener(gFrontProgressListener); + + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = gAllNotifications; + runTest(gForegroundBrowser, "http://example.org" + gTestPage, startTest6); +} + +function startTest6() { + info("\nTest 6"); + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = []; + runTest(gBackgroundBrowser, "http://example.org" + gTestPage, finishTest); +} + +function finishTest() { + getBrowser().removeProgressListener(gFrontProgressListener); + getBrowser().removeTabsProgressListener(gAllProgressListener); + getBrowser().removeTab(gBackgroundTab); + getBrowser().removeTab(gForegroundTab); + finish(); +} diff --git a/comm/suite/browser/test/browser/browser_bug329212.js b/comm/suite/browser/test/browser/browser_bug329212.js new file mode 100644 index 0000000000..86925a4cb8 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug329212.js @@ -0,0 +1,42 @@ +function test() { + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("load", loadListener, true); + + let doc = gBrowser.contentDocument; + let tooltip = document.getElementById("aHTMLTooltip"); + + ok(FillInHTMLTooltip(doc.getElementById("svg1")), "should get title"); + is(tooltip.getAttribute("label"), "This is a non-root SVG element title"); + + ok(FillInHTMLTooltip(doc.getElementById("text1")), "should get title"); + is(tooltip.getAttribute("label"), "\n\n\n This is a title\n\n "); + + ok(!FillInHTMLTooltip(doc.getElementById("text2")), "should not get title"); + + ok(!FillInHTMLTooltip(doc.getElementById("text3")), "should not get title"); + + ok(FillInHTMLTooltip(doc.getElementById("link1")), "should get title"); + is(tooltip.getAttribute("label"), "\n This is a title\n "); + ok(FillInHTMLTooltip(doc.getElementById("text4")), "should get title"); + is(tooltip.getAttribute("label"), "\n This is a title\n "); + + ok(!FillInHTMLTooltip(doc.getElementById("link2")), "should not get title"); + + ok(FillInHTMLTooltip(doc.getElementById("link3")), "should get title"); + isnot(tooltip.getAttribute("label"), ""); + + ok(FillInHTMLTooltip(doc.getElementById("link4")), "should get title"); + is(tooltip.getAttribute("label"), "This is an xlink:title attribute"); + + ok(!FillInHTMLTooltip(doc.getElementById("text5")), "should not get title"); + + gBrowser.removeCurrentTab(); + finish(); + }, true); + + content.location = + "http://mochi.test:8888/browser/suite/browser/test/browser/title_test.svg"; +} + diff --git a/comm/suite/browser/test/browser/browser_bug409624.js b/comm/suite/browser/test/browser/browser_bug409624.js new file mode 100644 index 0000000000..c3a800dffa --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug409624.js @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +ChromeUtils.defineModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); + +function test() { + waitForExplicitFinish(); + + // This test relies on the form history being empty to start with delete + // all the items first. + FormHistory.update({ op: "remove" }, + { handleError: function (error) { + do_throw("Error occurred updating form history: " + error); + }, + handleCompletion: function (reason) { if (!reason) test2(); }, + }); +} + +function test2() +{ + let prefService = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + + let findBar = document.getElementById("FindToolbar"); + let textbox = findBar.getElement("findbar-textbox"); + + let temp = {}; + ChromeUtils.import("resource:///modules/Sanitizer.jsm", temp); + let s = temp.Sanitizer; + let prefBranch = prefService.getBranch("privacy.clearOnShutdown."); + + prefBranch.setBoolPref("cache", false); + prefBranch.setBoolPref("cookies", false); + prefBranch.setBoolPref("downloads", false); + prefBranch.setBoolPref("formdata", true); + prefBranch.setBoolPref("history", false); + prefBranch.setBoolPref("offlineApps", false); + prefBranch.setBoolPref("passwords", false); + prefBranch.setBoolPref("sessions", false); + prefBranch.setBoolPref("siteSettings", false); + + prefService.setBoolPref("privacy.sanitize.promptOnSanitize", false); + + // Sanitize now so we can test the baseline point. + s.sanitize(); + ok(!gFindBar.hasTransactions, "pre-test baseline for sanitizer"); + + gFindBar.getElement("findbar-textbox").value = "m"; + ok(gFindBar.hasTransactions, "formdata can be cleared after input"); + + s.sanitize(); + is(gFindBar.getElement("findbar-textbox").value, "", "findBar textbox should be empty after sanitize"); + ok(!gFindBar.hasTransactions, "No transactions after sanitize"); + + finish(); +} diff --git a/comm/suite/browser/test/browser/browser_bug413915.js b/comm/suite/browser/test/browser/browser_bug413915.js new file mode 100644 index 0000000000..98b968a1b3 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug413915.js @@ -0,0 +1,70 @@ +ChromeUtils.defineModuleGetter(this, "Feeds", + "resource:///modules/Feeds.jsm"); + +function test() { + var exampleUri = Services.io.newURI("http://example.com/"); + var secman = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + var principal = secman.createCodebasePrincipal(exampleUri, {}); + + function testIsFeed(aTitle, aHref, aType, aKnown) { + var link = { + title: aTitle, + href: aHref, + type: aType, + ownerDocument: { + characterSet: "UTF-8" + } + }; + return Feeds.isValidFeed(link, principal, aKnown); + } + + var href = "http://example.com/feed/"; + var atomType = "application/atom+xml"; + var funkyAtomType = " aPPLICAtion/Atom+XML "; + var rssType = "application/rss+xml"; + var funkyRssType = " Application/RSS+XML "; + var rdfType = "application/rdf+xml"; + var texmlType = "text/xml"; + var appxmlType = "application/xml"; + var noRss = "Foo"; + var rss = "RSS"; + + // things that should be valid + ok(testIsFeed(noRss, href, atomType, false) == atomType, + "detect Atom feed"); + ok(testIsFeed(noRss, href, funkyAtomType, false) == atomType, + "clean up and detect Atom feed"); + ok(testIsFeed(noRss, href, rssType, false) == rssType, + "detect RSS feed"); + ok(testIsFeed(noRss, href, funkyRssType, false) == rssType, + "clean up and detect RSS feed"); + + // things that should not be feeds + ok(testIsFeed(noRss, href, rdfType, false) == null, + "should not detect RDF non-feed"); + ok(testIsFeed(rss, href, rdfType, false) == null, + "should not detect RDF feed from type and title"); + ok(testIsFeed(noRss, href, texmlType, false) == null, + "should not detect text/xml non-feed"); + ok(testIsFeed(rss, href, texmlType, false) == null, + "should not detect text/xml feed from type and title"); + ok(testIsFeed(noRss, href, appxmlType, false) == null, + "should not detect application/xml non-feed"); + ok(testIsFeed(rss, href, appxmlType, false) == null, + "should not detect application/xml feed from type and title"); + + // security check only, returns cleaned up type or "application/rss+xml" + ok(testIsFeed(noRss, href, atomType, true) == atomType, + "feed security check should return Atom type"); + ok(testIsFeed(noRss, href, funkyAtomType, true) == atomType, + "feed security check should return cleaned up Atom type"); + ok(testIsFeed(noRss, href, rssType, true) == rssType, + "feed security check should return RSS type"); + ok(testIsFeed(noRss, href, funkyRssType, true) == rssType, + "feed security check should return cleaned up RSS type"); + ok(testIsFeed(noRss, href, "", true) == rssType, + "feed security check without type should return RSS type"); + ok(testIsFeed(noRss, href, "garbage", true) == "garbage", + "feed security check with garbage type should return garbage"); +} diff --git a/comm/suite/browser/test/browser/browser_bug427559.js b/comm/suite/browser/test/browser/browser_bug427559.js new file mode 100644 index 0000000000..bd34d89f5c --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug427559.js @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test bug 427559 to make sure focused elements that are no longer on the page + * will have focus transferred to the window when changing tabs back to that + * tab with the now-gone element. + */ + +// Default focus on a button and have it kill itself on blur +var testPage = 'data:text/html,<body><button onblur="this.remove();"><script>document.body.firstChild.focus();</script></body>'; + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + + gBrowser.selectedBrowser.addEventListener("load", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("load", loadListener, true); + executeSoon(function () { + + // The test page loaded, so open an empty tab, select it, then restore + // the test tab. This causes the test page's focused element to be removed + // from its document. + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.removeCurrentTab(); + + // Make sure focus is given to the window because the element is now gone. + is(document.commandDispatcher.focusedWindow, window.content, + "content window is focused"); + + gBrowser.removeCurrentTab(); + finish(); + }); + }, true); + + content.location = testPage; +} diff --git a/comm/suite/browser/test/browser/browser_bug435325.js b/comm/suite/browser/test/browser/browser_bug435325.js new file mode 100644 index 0000000000..dca45a6001 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug435325.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Ensure that clicking the button in the Offline mode neterror page makes the browser go online. See bug 435325. */ + +var proxyPrefValue; + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + window.addEventListener("DOMContentLoaded", checkPage); + + + // Tests always connect to localhost, and per bug 87717, localhost is now + // reachable in offline mode. To avoid this, disable any proxy. + proxyPrefValue = Services.prefs.getIntPref("network.proxy.type"); + Services.prefs.setIntPref("network.proxy.type", 0); + + // Go offline and disable the proxy and cache, then try to load the test URL. + Services.io.offline = true; + Services.prefs.setBoolPref("browser.cache.disk.enable", false); + Services.prefs.setBoolPref("browser.cache.memory.enable", false); + content.location = "http://example.com/"; +} + +function checkPage() { + if(content.location == "about:blank") { + info("got about:blank, which is expected once, so return"); + return; + } + + window.removeEventListener("DOMContentLoaded", checkPage); + + ok(Services.io.offline, "Setting Services.io.offline to true."); + is(gBrowser.contentDocument.documentURI.substring(0,27), + "about:neterror?e=netOffline", "Loading the Offline mode neterror page."); + + // Now press the "Try Again" button + ok(gBrowser.contentDocument.getElementById("errorTryAgain"), + "The error page has got a #errorTryAgain element"); + gBrowser.contentDocument.getElementById("errorTryAgain").click(); + + ok(!Services.io.offline, "After clicking the Try Again button, we're back " + + "online."); + + finish(); +} + +registerCleanupFunction(function() { + Services.prefs.setIntPref("network.proxy.type", proxyPrefValue); + Services.prefs.setBoolPref("browser.cache.disk.enable", true); + Services.prefs.setBoolPref("browser.cache.memory.enable", true); + Services.io.offline = false; + gBrowser.removeCurrentTab(); +}); diff --git a/comm/suite/browser/test/browser/browser_bug462289.js b/comm/suite/browser/test/browser/browser_bug462289.js new file mode 100644 index 0000000000..c61b9a0403 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug462289.js @@ -0,0 +1,87 @@ +// Wanted delay (in ms) to let UI fully update. +// 375: hopefully enough (on slow test environments). +var gDelay = 375; + +var tab1, tab2; + +function focus_in_navbar() +{ + var parent = document.activeElement.parentNode; + while (parent && parent.id != "nav-bar") + parent = parent.parentNode; + + return parent != null; +} + +function test() +{ + waitForExplicitFinish(); + + // Ftr, SeaMonkey doesn't support animation (yet). + tab1 = gBrowser.addTab("about:blank"); + tab2 = gBrowser.addTab("about:blank"); + + EventUtils.synthesizeMouseAtCenter(tab1, {}); + setTimeout(step2, gDelay); +} + +function step2() +{ + is(gBrowser.selectedTab, tab1, "1st click on tab1 selects tab"); + isnot(document.activeElement, tab1, "1st click on tab1 does not activate tab"); + + EventUtils.synthesizeMouseAtCenter(tab1, {}); + setTimeout(step3, gDelay); +} + +function step3() +{ + is(gBrowser.selectedTab, tab1, "2nd click on selected tab1 keeps tab selected"); + // SeaMonkey differs from Firefox. + is(document.activeElement, tab1, "2nd click on selected tab1 activates tab"); + + // Ftr, SeaMonkey doesn't support tabsontop (yet). + ok(true, "focusing URLBar then sending Tab(s) until out of nav-bar."); + document.getElementById("urlbar").focus(); + while (focus_in_navbar()) + EventUtils.synthesizeKey("VK_TAB", { }); + is(gBrowser.selectedTab, tab1, "tab key to selected tab1 keeps tab selected"); + is(document.activeElement, tab1, "tab key to selected tab1 activates tab"); + + EventUtils.synthesizeMouseAtCenter(tab1, {}); + setTimeout(step4, gDelay); +} + +function step4() +{ + is(gBrowser.selectedTab, tab1, "3rd click on activated tab1 keeps tab selected"); + is(document.activeElement, tab1, "3rd click on activated tab1 keeps tab activated"); + + EventUtils.synthesizeMouseAtCenter(tab2, {}); + setTimeout(step5, gDelay); +} + +function step5() +{ + // The tabbox selects a tab within a setTimeout in a bubbling mousedown event + // listener, and focuses the current tab if another tab previously had focus. + is(gBrowser.selectedTab, tab2, "click on tab2 while tab1 is activated selects tab"); + is(document.activeElement, tab2, "click on tab2 while tab1 is activated activates tab"); + + ok(true, "focusing content then sending middle-button mousedown to tab2."); + content.focus(); + EventUtils.synthesizeMouseAtCenter(tab2, {button: 1, type: "mousedown"}); + setTimeout(step6, gDelay); +} + +function step6() +{ + is(gBrowser.selectedTab, tab2, "middle-button mousedown on selected tab2 keeps tab selected"); + // SeaMonkey differs from Firefox. + is(document.activeElement, tab2, "middle-button mousedown on selected tab2 activates tab"); + + gBrowser.removeTab(tab2); + gBrowser.removeTab(tab1); + + finish(); +} diff --git a/comm/suite/browser/test/browser/browser_bug519216.js b/comm/suite/browser/test/browser/browser_bug519216.js new file mode 100644 index 0000000000..a924f7a091 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug519216.js @@ -0,0 +1,50 @@ +function test() { + waitForExplicitFinish(); + gBrowser.stop(); + gBrowser.addProgressListener(progressListener1); + gBrowser.addProgressListener(progressListener2); + gBrowser.addProgressListener(progressListener3); + gBrowser.loadURI("data:text/plain,bug519216"); +} + +var calledListener1 = false; +var progressListener1 = { + onLocationChange: function onLocationChange() { + calledListener1 = true; + gBrowser.removeProgressListener(this); + } +}; + +var calledListener2 = false; +var progressListener2 = { + onLocationChange: function onLocationChange() { + ok(calledListener1, "called progressListener1 before progressListener2"); + calledListener2 = true; + gBrowser.removeProgressListener(this); + } +}; + +var progressListener3 = { + onLocationChange: function onLocationChange() { + ok(calledListener2, "called progressListener2 before progressListener3"); + gBrowser.removeProgressListener(this); + gBrowser.addProgressListener(progressListener4); + executeSoon(function () { + expectListener4 = true; + gBrowser.reload(); + }); + } +}; + +var expectListener4 = false; +var progressListener4 = { + onLocationChange: function onLocationChange() { + ok(expectListener4, "didn't call progressListener4 for the first location change"); + gBrowser.removeProgressListener(this); + executeSoon(function () { + gBrowser.addTab(); + gBrowser.removeCurrentTab(); + finish(); + }); + } +}; diff --git a/comm/suite/browser/test/browser/browser_bug561636.js b/comm/suite/browser/test/browser/browser_bug561636.js new file mode 100644 index 0000000000..8f41629253 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug561636.js @@ -0,0 +1,459 @@ +var gInvalidFormPopup = document.getElementById('invalid-form-popup'); +ok(gInvalidFormPopup, + "The browser should have a popup to show when a form is invalid"); + +function checkPopupShow() +{ + ok(gInvalidFormPopup.state == 'showing' || gInvalidFormPopup.state == 'open', + "The invalid form popup should be shown"); +} + +function checkPopupHide() +{ + ok(gInvalidFormPopup.state != 'showing' && gInvalidFormPopup.state != 'open', + "The invalid form popup should not be shown"); +} + +function checkPopupMessage(doc) +{ + is(gInvalidFormPopup.firstChild.textContent, + doc.getElementById('i').validationMessage, + "The panel should show the message from validationMessage"); +} + +var gObserver = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), + + notifyInvalidSubmit : function (aFormElement, aInvalidElements) + { + } +}; + +function test() +{ + waitForExplicitFinish(); + + test1(); +} + +/** + * In this test, we check that no popup appears if the form is valid. + */ +function test1() { + let uri = "data:text/html,<html><body><iframe name='t'></iframe><form target='t' action='data:text/html,'><input><input id='s' type='submit'></form></body></html>"; + let tab = gBrowser.addTab(); + + tab.linkedBrowser.addEventListener("load", function test1TabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test1TabLBLoad, true); + let doc = gBrowser.contentDocument; + + doc.getElementById('s').click(); + + executeSoon(function() { + checkPopupHide(); + + // Clean-up + gBrowser.removeTab(gBrowser.selectedTab); + + // Next test + executeSoon(test2); + }); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} + +/** + * In this test, we check that, when an invalid form is submitted, + * the invalid element is focused and a popup appears. + */ +function test2() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input required id='i'><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function test2gIpopupShown() { + gInvalidFormPopup.removeEventListener("popupshown", test2gIpopupShown); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(test3); + }); + + tab.linkedBrowser.addEventListener("load", function test2TabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test2TabLBLoad, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} + +/** + * In this test, we check that, when an invalid form is submitted, + * the first invalid element is focused and a popup appears. + */ +function test3() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input><input id='i' required><input required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function test3gIPopupShown() { + gInvalidFormPopup.removeEventListener("popupshown", test3gIPopupShown); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(test4a); + }); + + tab.linkedBrowser.addEventListener("load", function test3TabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test3TabLBLoad, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} + +/** + * In this test, we check that, we hide the popup by interacting with the + * invalid element if the element becomes valid. + */ +function test4a() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function test4agIPopupShown() { + gInvalidFormPopup.removeEventListener("popupshown", test4agIPopupShown); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + EventUtils.synthesizeKey("a", {}); + + executeSoon(function () { + checkPopupHide(); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(test4b); + }); + }); + + tab.linkedBrowser.addEventListener("load", function test4aTabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test4aTabLBLoad, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} + +/** + * In this test, we check that, we don't hide the popup by interacting with the + * invalid element if the element is still invalid. + */ +function test4b() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input type='email' id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function test4bgIpopupShown() { + gInvalidFormPopup.removeEventListener("popupshown", test4bgIpopupShown); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + EventUtils.synthesizeKey("a", {}); + + executeSoon(function () { + checkPopupShow(); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(test5); + }); + }); + + tab.linkedBrowser.addEventListener("load", function test4bTabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test4bTabLBLoad, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} + +/** + * In this test, we check that we can hide the popup by blurring the invalid + * element. + */ +function test5() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function test5gIpopupShown() { + gInvalidFormPopup.removeEventListener("popupshown", test5gIpopupShown); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + doc.getElementById('i').blur(); + + executeSoon(function () { + checkPopupHide(); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(test6); + }); + }); + + tab.linkedBrowser.addEventListener("load", function test5TabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test5TabLBLoad, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} + +/** + * In this test, we check that we can hide the popup by pressing TAB. + */ +function test6() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function test6gIpopupShown() { + gInvalidFormPopup.removeEventListener("popupshown", test6gIpopupShown); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + EventUtils.synthesizeKey("VK_TAB", {}); + + executeSoon(function () { + checkPopupHide(); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(test7); + }); + }); + + tab.linkedBrowser.addEventListener("load", function test6TabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test6TabLBLoad, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} + +/** + * In this test, we check that the popup will hide if we move to another tab. + */ +function test7() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function test7gIpopupShown() { + gInvalidFormPopup.removeEventListener("popupshown", test7gIpopupShown); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + // Create a new tab and move to it. + // Ftr, SeaMonkey doesn't support animation (yet). + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + + executeSoon(function() { + checkPopupHide(); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(test8); + }); + }); + + tab.linkedBrowser.addEventListener("load", function test7TabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test7TabLBLoad, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} + +/** + * In this test, we check that nothing happen (no focus nor popup) if the + * invalid form is submitted in another tab than the current focused one + * (submitted in background). + */ +function test8() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gObserver.notifyInvalidSubmit = function() { + executeSoon(function() { + let doc = tab.linkedBrowser.contentDocument; + isnot(doc.activeElement, doc.getElementById('i'), + "We should not focus the invalid element when the form is submitted in background"); + + checkPopupHide(); + + // Clean-up + Services.obs.removeObserver(gObserver, "invalidformsubmit"); + gObserver.notifyInvalidSubmit = function () {}; + gBrowser.removeTab(tab); + + // Next test + executeSoon(test9); + }); + }; + + Services.obs.addObserver(gObserver, "invalidformsubmit"); + + tab.linkedBrowser.addEventListener("load", function test8TabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test8TabLBLoad, true); + + isnot(gBrowser.selectedTab, tab, + "This tab should have been loaded in background"); + + tab.linkedBrowser.contentDocument.getElementById('s').click(); + }, true); + + tab.linkedBrowser.loadURI(uri); +} + +/** + * In this test, we check that the author defined error message is shown. + */ +function test9() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input x-moz-errormessage='foo' required id='i'><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function test9gIpopupShown() { + gInvalidFormPopup.removeEventListener("popupshown", test9gIpopupShown); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + + is(gInvalidFormPopup.firstChild.textContent, "foo", + "The panel should show the author defined error message"); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(test10); + }); + + tab.linkedBrowser.addEventListener("load", function test9TabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test9TabLBLoad, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} + +/** + * In this test, we check that the message is correctly updated when it changes. + */ +function test10() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input type='email' required id='i'><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function test10gIpopupShown() { + gInvalidFormPopup.removeEventListener("popupshown", test10gIpopupShown); + + let doc = gBrowser.contentDocument; + let input = doc.getElementById('i'); + is(doc.activeElement, input, "First invalid element should be focused"); + + checkPopupShow(); + + is(gInvalidFormPopup.firstChild.textContent, input.validationMessage, + "The panel should show the current validation message"); + + input.addEventListener('input', function test10InputInput() { + input.removeEventListener('input', test10InputInput); + + executeSoon(function() { + // Now, the element suffers from another error, the message should have + // been updated. + is(gInvalidFormPopup.firstChild.textContent, input.validationMessage, + "The panel should show the current validation message"); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(finish); + }); + }); + + EventUtils.synthesizeKey('f', {}); + }); + + tab.linkedBrowser.addEventListener("load", function test10TabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", test10TabLBLoad, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} diff --git a/comm/suite/browser/test/browser/browser_bug562649.js b/comm/suite/browser/test/browser/browser_bug562649.js new file mode 100644 index 0000000000..57524a4b82 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug562649.js @@ -0,0 +1,26 @@ +function test() { + const URI = "data:text/plain,bug562649"; + browserDOMWindow.openURI(makeURI(URI), + null, + Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, + Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + + is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI"); + is(gURLBar.value, URI, "location bar value matches test URI"); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.removeCurrentTab(); + is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI after switching tabs"); + is(gURLBar.value, URI, "location bar value matches test URI after switching tabs"); + + waitForExplicitFinish(); + gBrowser.selectedBrowser.addEventListener("load", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("load", loadListener, true); + + is(gBrowser.userTypedValue, null, "userTypedValue is null as the page has loaded"); + is(gURLBar.value, URI, "location bar value matches test URI as the page has loaded"); + + gBrowser.removeCurrentTab(); + finish(); + }, true); +} diff --git a/comm/suite/browser/test/browser/browser_bug581947.js b/comm/suite/browser/test/browser/browser_bug581947.js new file mode 100644 index 0000000000..5ac81b1218 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug581947.js @@ -0,0 +1,95 @@ +function check(aElementName, aBarred, aType) { + let doc = gBrowser.contentDocument; + let tooltip = document.getElementById("aHTMLTooltip"); + let content = doc.getElementById('content'); + + let e = doc.createElement(aElementName); + content.appendChild(e); + + if (aType) { + e.type = aType; + } + + ok(!FillInHTMLTooltip(e), + "No tooltip should be shown when the element is valid"); + + e.setCustomValidity('foo'); + if (aBarred) { + ok(!FillInHTMLTooltip(e), + "No tooltip should be shown when the element is barred from constraint validation"); + } else { + ok(FillInHTMLTooltip(e), + e.tagName + " " +"A tooltip should be shown when the element isn't valid"); + } + + e.setAttribute('title', ''); + ok (!FillInHTMLTooltip(e), + "No tooltip should be shown if the title attribute is set"); + + e.removeAttribute('title'); + content.setAttribute('novalidate', ''); + ok (!FillInHTMLTooltip(e), + "No tooltip should be shown if the novalidate attribute is set on the form owner"); + content.removeAttribute('novalidate'); + + e.remove(); +} + +function todo_check(aElementName, aBarred) { + let doc = gBrowser.contentDocument; + let tooltip = document.getElementById("aHTMLTooltip"); + let content = doc.getElementById('content'); + + let e = doc.createElement(aElementName); + content.appendChild(e); + + let caught = false; + try { + e.setCustomValidity('foo'); + } catch (e) { + caught = true; + } + + todo(!caught, "setCustomValidity should exist for " + aElementName); + + e.remove(); +} + +function test() { + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("load", loadListener, true); + + let testData = [ + /* element name, barred */ + [ 'input', false, null], + [ 'textarea', false, null], + [ 'button', true, 'button'], + [ 'button', false, 'submit'], + [ 'select', false, null], + [ 'output', true, null], + [ 'fieldset', true, null], + [ 'object', true, null] + ]; + + for (let data of testData) { + check(data[0], data[1], data[2]); + } + + let todo_testData = [ + [ 'keygen', 'false' ] + ]; + + for (let data of todo_testData) { + todo_check(data[0], data[1]); + } + + gBrowser.removeCurrentTab(); + finish(); + }, true); + + content.location = + "data:text/html,<!DOCTYPE html><html><body><form id='content'></form></body></html>"; +} + diff --git a/comm/suite/browser/test/browser/browser_bug585511.js b/comm/suite/browser/test/browser/browser_bug585511.js new file mode 100644 index 0000000000..f6513cac5a --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug585511.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function test() { + is(getBrowser().tabs.length, 1, "one tab is open initially"); + is(getBrowser().browsers.length, getBrowser().tabs.length, ".browsers is in sync"); + + // Add several new tabs + let tab1 = getBrowser().addTab("http://mochi.test:8888/#1"); + let tab2 = getBrowser().addTab("http://mochi.test:8888/#2"); + let tab3 = getBrowser().addTab("http://mochi.test:8888/#3"); + is(getBrowser().tabs.length, 4, "four tabs are open"); + is(getBrowser().browsers.length, getBrowser().tabs.length, ".browsers is in sync"); + getBrowser().removeTab(tab2); + is(getBrowser().tabs.length, 3, "three tabs are open"); + is(getBrowser().browsers.length, getBrowser().tabs.length, ".browsers is in sync"); + getBrowser().removeTab(tab1); + is(getBrowser().tabs.length, 2, "two tabs are open"); + is(getBrowser().browsers.length, getBrowser().tabs.length, ".browsers is in sync"); + getBrowser().removeTab(tab3); + is(getBrowser().tabs.length, 1, "we've closed all our tabs"); + is(getBrowser().browsers.length, getBrowser().tabs.length, ".browsers is in sync"); +} diff --git a/comm/suite/browser/test/browser/browser_bug595507.js b/comm/suite/browser/test/browser/browser_bug595507.js new file mode 100644 index 0000000000..0bea8cabc6 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug595507.js @@ -0,0 +1,39 @@ +var gInvalidFormPopup = document.getElementById('invalid-form-popup'); +ok(gInvalidFormPopup, + "The browser should have a popup to show when a form is invalid"); + +/** + * Make sure that the form validation error message shows even if the form is in an iframe. + */ +function test() +{ + waitForExplicitFinish(); + + let uri = "data:text/html,<iframe src=\"data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input required id='i'><input id='s' type='submit'></form>\"</iframe>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function testgIpopupShown() { + gInvalidFormPopup.removeEventListener("popupshown", testgIpopupShown); + + let doc = gBrowser.contentDocument.getElementsByTagName('iframe')[0].contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + ok(gInvalidFormPopup.state == 'showing' || gInvalidFormPopup.state == 'open', + "The invalid form popup should be shown"); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab, {animate: false}); + executeSoon(finish); + }); + + tab.linkedBrowser.addEventListener("load", function testTabLBLoad(aEvent) { + tab.linkedBrowser.removeEventListener("load", testTabLBLoad, true); + + gBrowser.contentDocument.getElementsByTagName('iframe')[0].contentDocument + .getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} diff --git a/comm/suite/browser/test/browser/browser_bug623155.js b/comm/suite/browser/test/browser/browser_bug623155.js new file mode 100644 index 0000000000..df8197a565 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_bug623155.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const REDIRECT_FROM = "https://example.com/browser/suite/browser/test/browser/" + + "redirect_bug623155.sjs"; + +const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host. + +function isRedirectedURISpec(aURISpec) { + return isRedirectedURI(Services.io.newURI(aURISpec)); +} + +function isRedirectedURI(aURI) { + // Compare only their before-hash portion. + return Services.io.newURI(REDIRECT_TO) + .equalsExceptRef(aURI); +} + +/* + Test. + +1. Load +https://example.com/browser/browser/base/content/test/redirect_bug623155.sjs#BG + in a background tab. + +2. The redirected URI is <https://www.bank1.com/#BG>, which displayes a cert + error page. + +3. Switch the tab to foreground. + +4. Check the URLbar's value, expecting <https://www.bank1.com/#BG> + +5. Load +https://example.com/browser/browser/base/content/test/redirect_bug623155.sjs#FG + in the foreground tab. + +6. The redirected URI is <https://www.bank1.com/#FG>. And this is also + a cert-error page. + +7. Check the URLbar's value, expecting <https://www.bank1.com/#FG> + +8. End. + + */ + +var gNewTab; + +function test() { + waitForExplicitFinish(); + + // Load a URI in the background. + gNewTab = gBrowser.addTab(REDIRECT_FROM + "#BG"); + gBrowser.getBrowserForTab(gNewTab) + .webProgress + .addProgressListener(gWebProgressListener, + Ci.nsIWebProgress + .NOTIFY_LOCATION); +} + +var gWebProgressListener = { + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + }, + + // --------------------------------------------------------------------------- + // NOTIFY_LOCATION mode should work fine without these methods. + // + //onStateChange: function() {}, + //onStatusChange: function() {}, + //onProgressChange: function() {}, + //onSecurityChange: function() {}, + //---------------------------------------------------------------------------- + + onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { + if (!aRequest) { + // This is bug 673752, or maybe initial "about:blank". + return; + } + + ok(gNewTab, "There is a new tab."); + ok(isRedirectedURI(aLocation), + "onLocationChange catches only redirected URI."); + + if (aLocation.ref == "BG") { + // This is background tab's request. + isnot(gNewTab, gBrowser.selectedTab, "This is a background tab."); + } else if (aLocation.ref == "FG") { + // This is foreground tab's request. + is(gNewTab, gBrowser.selectedTab, "This is a foreground tab."); + } + else { + // We shonuld not reach here. + ok(false, "This URI hash is not expected:" + aLocation.ref); + } + + let isSelectedTab = (gNewTab == gBrowser.selectedTab); + setTimeout(delayed, 0, isSelectedTab); + } +}; + +function delayed(aIsSelectedTab) { + // Switch tab and confirm URL bar. + if (!aIsSelectedTab) { + gBrowser.selectedTab = gNewTab; + } + + ok(isRedirectedURISpec(content.location.href), + "The content area is redirected. aIsSelectedTab:" + aIsSelectedTab); + is(gURLBar.value, content.location.href, + "The URL bar shows the content URI. aIsSelectedTab:" + aIsSelectedTab); + + if (!aIsSelectedTab) { + // If this was a background request, go on a foreground request. + content.location = REDIRECT_FROM + "#FG"; + } + else { + // Othrewise, nothing to do remains. + finish(); + } +} + +/* Cleanup */ +registerCleanupFunction(function() { + if (gNewTab) { + gBrowser.getBrowserForTab(gNewTab) + .webProgress + .removeProgressListener(gWebProgressListener); + + gBrowser.removeTab(gNewTab); + } + gNewTab = null; +}); diff --git a/comm/suite/browser/test/browser/browser_ctrlTab.js b/comm/suite/browser/test/browser/browser_ctrlTab.js new file mode 100644 index 0000000000..aea07917a3 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_ctrlTab.js @@ -0,0 +1,197 @@ +function test() { + waitForExplicitFinish(); + + gBrowser.addTab(); + gBrowser.addTab(); + gBrowser.addTab(); + + assertTabs(4); + + ctrlTabTest([2] , 1, 0); + ctrlTabTest([2, 3, 1], 2, 2); + ctrlTabTest([] , 4, 2); + + { + let selectedIndex = gBrowser.tabContainer.selectedIndex; + pressCtrlTab(); + pressCtrlTab(true); + releaseCtrl(); + is(gBrowser.tabContainer.selectedIndex, selectedIndex, + "Ctrl+Tab -> Ctrl+Shift+Tab keeps the selected tab"); + } + + { // test for bug 445369 + let tabs = gBrowser.tabs.length; + pressCtrlTab(); + EventUtils.synthesizeKey("w", { ctrlKey: true }); + is(gBrowser.tabs.length, tabs - 1, "Ctrl+Tab -> Ctrl+W removes one tab"); + releaseCtrl(); + } + assertTabs(3); + + ctrlTabTest([2, 1, 0], 7, 1); + + { // test for bug 445369 + selectTabs([1, 2, 0]); + + let selectedTab = gBrowser.selectedTab; + let tabToRemove = gBrowser.tabs[1]; + + pressCtrlTab(); + pressCtrlTab(); + EventUtils.synthesizeKey("w", { ctrlKey: true }); + ok(!tabToRemove.parentNode, + "Ctrl+Tab*2 -> Ctrl+W removes the second most recently selected tab"); + + pressCtrlTab(true); + pressCtrlTab(true); + releaseCtrl(); + ok(gBrowser.selectedTab == selectedTab, + "Ctrl+Tab*2 -> Ctrl+W -> Ctrl+Shift+Tab*2 keeps the selected tab"); + } + assertTabs(2); + + ctrlTabTest([1], 1, 0); + + gBrowser.removeTab(gBrowser.tabContainer.lastChild); + + assertTabs(1); + + { // test for bug 445768 + let focusedWindow = document.commandDispatcher.focusedWindow; + let eventConsumed = true; + let detectKeyEvent = function (event) { + eventConsumed = event.defaultPrevented; + }; + document.addEventListener("keypress", detectKeyEvent); + pressCtrlTab(); + document.removeEventListener("keypress", detectKeyEvent); + ok(eventConsumed, "Ctrl+Tab consumed by the tabbed browser if one tab is open"); + is(focusedWindow.location, document.commandDispatcher.focusedWindow.location, + "Ctrl+Tab doesn't change focus if one tab is open"); + } + + gBrowser.addTab(); + gBrowser.addTab(); + gBrowser.addTab(); + + assertTabs(4); + selectTabs([0, 1, 2, 3]); + pressCtrlTab(); + ctrlTab.panel.addEventListener("popupshown", stickyTests); + + function stickyTests() { + ctrlTab.panel.removeEventListener("popupshown", stickyTests); + + EventUtils.synthesizeKey("f", { ctrlKey: true }); + is(document.activeElement, ctrlTab.searchField.inputField, + "Ctrl+Tab -> Ctrl+F focuses the panel's search field"); + + releaseCtrl(); + ok(isOpen(), + "panel is sticky after focusing the search field and releasing the Ctrl key"); + + EventUtils.synthesizeKey("f", {}); + EventUtils.synthesizeKey("o", {}); + EventUtils.synthesizeKey("o", {}); + is(ctrlTab.searchField.value, "foo", + "text entered into search field"); + + EventUtils.synthesizeKey("VK_RETURN", {}); + ok(isOpen(), + "Enter key kicks pending search off; the panel stays open as there's no match"); + is(ctrlTab.searchField.value, "foo", + "search field value persists after Enter pressed"); + + EventUtils.synthesizeKey("VK_ESCAPE", {}); + is(ctrlTab.searchField.value, "", + "ESC key clears the search field"); + ok(isOpen(), + "Clearing the search field with ESC keeps the panel open"); + + // blur the search field + EventUtils.synthesizeKey("VK_TAB", {}); + isnot(document.activeElement, ctrlTab.searchField.inputField, + "Tab key blurs the panel's search field"); + + // advance selection and close panel + EventUtils.synthesizeKey("VK_TAB", {}); + EventUtils.synthesizeKey("VK_TAB", {}); + EventUtils.synthesizeKey("VK_RETURN", {}); + ok(!isOpen(), + "Enter key closes the panel"); + is(gBrowser.tabContainer.selectedIndex, 1, + "Tab key advances the selection while the panel is sticky"); + + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); + assertTabs(1); + finish(); + } + + + /* private utility functions */ + + function pressCtrlTab(aShiftKey) { + EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: !!aShiftKey }); + } + + function releaseCtrl() { + EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }); + } + + function isOpen() { + return ctrlTab.panel.state == "showing" || ctrlTab.panel.state == "open"; + } + + function assertTabs(aTabs) { + var tabs = gBrowser.tabs.length; + if (tabs != aTabs) { + while (gBrowser.tabs.length > 1) + gBrowser.removeCurrentTab(); + throw "expected " + aTabs + " open tabs, got " + tabs; + } + } + + function selectTabs(tabs) { + tabs.forEach(function (index) { + gBrowser.selectedTab = gBrowser.tabs[index]; + }); + } + + function ctrlTabTest(tabsToSelect, tabTimes, expectedIndex) { + selectTabs(tabsToSelect); + + var indexStart = gBrowser.tabContainer.selectedIndex; + var tabCount = gBrowser.tabs.length; + var normalized = tabTimes % tabCount; + var where = normalized == 1 ? "back to the previously selected tab" : + normalized + " tabs back in most-recently-selected order"; + + for (let i = 0; i < tabTimes; i++) { + pressCtrlTab(); + + if (tabCount > 2) + is(gBrowser.tabContainer.selectedIndex, indexStart, + "Selected tab doesn't change while tabbing"); + } + + if (tabCount > 2) { + ok(isOpen(), + "With " + tabCount + " tabs open, Ctrl+Tab opens the preview panel"); + + releaseCtrl(); + + ok(!isOpen(), + "Releasing Ctrl closes the preview panel"); + } else { + ok(!isOpen(), + "With " + tabCount + " tabs open, Ctrl+Tab doesn't open the preview panel"); + } + + is(gBrowser.tabContainer.selectedIndex, expectedIndex, + "With "+ tabCount +" tabs open and tab " + indexStart + + " selected, Ctrl+Tab*" + tabTimes + " goes " + where); + } +} diff --git a/comm/suite/browser/test/browser/browser_fayt.js b/comm/suite/browser/test/browser/browser_fayt.js new file mode 100644 index 0000000000..8522bc366b --- /dev/null +++ b/comm/suite/browser/test/browser/browser_fayt.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function test() { + var tab1 = gBrowser.addTab("data:text/html;charset=utf-8,<p>this is some dummy text</p>"); + var tab2 = gBrowser.addTab("data:text/html;charset=utf-8,<p>this is some random text</p>"); + + gBrowser.getBrowserForTab(tab2).addEventListener("load", runTest, true); + waitForExplicitFinish(); + + function runTest() { + gBrowser.getBrowserForTab(tab2).removeEventListener("load", runTest, true); + + gBrowser.selectedTab = tab2; + is(gBrowser.fastFind.find("random", false), Ci.nsITypeAheadFind.FIND_FOUND, "FAYT found the random text"); + gBrowser.selectedTab = tab1; + is(gBrowser.fastFind.find("dummy", false), Ci.nsITypeAheadFind.FIND_FOUND, "FAYT found the dummy text"); + + gBrowser.removeTab(tab2); + gBrowser.removeTab(tab1); + finish(); + } +} + diff --git a/comm/suite/browser/test/browser/browser_notification_tab_switching.js b/comm/suite/browser/test/browser/browser_notification_tab_switching.js new file mode 100644 index 0000000000..2cbdaa3147 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_notification_tab_switching.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +var tab; +var notification; +var notificationURL = "http://example.org/browser/suite/browser/test/browser/file_dom_notifications.html"; +var newWindowOpenedFromTab; + +function test () { + waitForExplicitFinish(); + + let pm = Services.perms; + registerCleanupFunction(function() { + pm.remove(makeURI(notificationURL), "desktop-notification"); + gBrowser.removeTab(tab); + window.restore(); + }); + + pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION); + + tab = gBrowser.addTab(notificationURL); + tab.linkedBrowser.addEventListener("load", onLoad, true); +} + +function onLoad() { + isnot(gBrowser.selectedTab, tab, "Notification page loaded as a background tab"); + tab.linkedBrowser.removeEventListener("load", onLoad, true); + let win = tab.linkedBrowser.contentWindow.wrappedJSObject; + win.newWindow = win.open("about:blank", "", "height=100,width=100"); + newWindowOpenedFromTab = win.newWindow; + win.newWindow.addEventListener("load", function() { + info("new window loaded"); + win.newWindow.addEventListener("blur", function b() { + info("new window got blur"); + win.newWindow.removeEventListener("blur", b); + notification = win.showNotification1(); + win.newWindow.addEventListener("focus", onNewWindowFocused); + notification.addEventListener("show", onAlertShowing); + }); + + function waitUntilNewWindowHasFocus() { + if (!win.newWindow.document.hasFocus()) { + setTimeout(waitUntilNewWindowHasFocus, 50); + } else { + // Focus another window so that new window gets blur event. + gBrowser.selectedBrowser.contentWindow.focus(); + } + } + win.newWindow.focus(); + waitUntilNewWindowHasFocus(); + }); +} + +function onAlertShowing() { + info("Notification alert showing"); + notification.removeEventListener("show", onAlertShowing); + + let alertWindow = findChromeWindowByURI("chrome://global/content/alerts/alert.xul"); + if (!alertWindow) { + todo(false, "Notifications don't use XUL windows on all platforms."); + notification.close(); + newWindowOpenedFromTab.close(); + finish(); + return; + } + gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect); + EventUtils.synthesizeMouseAtCenter(alertWindow.document.getElementById("alertTitleLabel"), {}, alertWindow); + info("Clicked on notification"); + alertWindow.close(); +} + +function onNewWindowFocused(event) { + event.target.close(); + isnot(gBrowser.selectedTab, tab, "Notification page loaded as a background tab"); + // Using timeout to test that something do *not* happen! + setTimeout(openSecondNotification, 50); +} + +function openSecondNotification() { + isnot(gBrowser.selectedTab, tab, "Notification page loaded as a background tab"); + let win = tab.linkedBrowser.contentWindow.wrappedJSObject; + notification = win.showNotification2(); + notification.addEventListener("show", onAlertShowing); +} + +function onTabSelect() { + gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect); + is(gBrowser.selectedBrowser.contentWindow.location.href, notificationURL, + "Notification tab should be selected."); + + finish(); +} diff --git a/comm/suite/browser/test/browser/browser_pageInfo.js b/comm/suite/browser/test/browser/browser_pageInfo.js new file mode 100644 index 0000000000..399301a784 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_pageInfo.js @@ -0,0 +1,38 @@ +function test() { + waitForExplicitFinish(); + + var pageInfo; + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("load", loadListener, true); + + Services.obs.addObserver(observer, "page-info-dialog-loaded"); + pageInfo = BrowserPageInfo(); + }, true); + content.location = + "https://example.com/browser/suite/browser/test/browser/feed_tab.html"; + + function observer(win, topic, data) { + Services.obs.removeObserver(observer, "page-info-dialog-loaded"); + handlePageInfo(); + } + + function handlePageInfo() { + ok(pageInfo.document.getElementById("feedPanel"), "Feed panel"); + let feedListbox = pageInfo.document.getElementById("feedListbox"); + ok(feedListbox, "Feed list"); + + var feedRowsNum = feedListbox.getRowCount(); + is(feedRowsNum, 3, "Number of feeds listed"); + + for (var i = 0; i < feedRowsNum; i++) { + let feedItem = feedListbox.getItemAtIndex(i); + is(feedItem.getAttribute("name"), i + 1, "Feed name"); + } + + pageInfo.close(); + gBrowser.removeCurrentTab(); + finish(); + } +} diff --git a/comm/suite/browser/test/browser/browser_page_style_menu.js b/comm/suite/browser/test/browser/browser_page_style_menu.js new file mode 100644 index 0000000000..88ce97da0b --- /dev/null +++ b/comm/suite/browser/test/browser/browser_page_style_menu.js @@ -0,0 +1,67 @@ +function test() { + waitForExplicitFinish(); + + var tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + tab.linkedBrowser.addEventListener("load", function loadListener() { + tab.linkedBrowser.removeEventListener("load", loadListener, true); + checkPageStyleMenu(); + }, true); + let rootDir = getRootDirectory(gTestPath); + content.location = rootDir + "page_style_sample.html"; +} + +function checkPageStyleMenu() { + var menupopup = document.getElementById("menu_UseStyleSheet") + .getElementsByTagName("menupopup")[0]; + stylesheetFillPopup(menupopup); + + var items = []; + var current = menupopup.getElementsByTagName("menuitem")[1]; + while (current.nextSibling) { + current = current.nextSibling; + items.push(current); + } + + var validLinks = 0; + Array.from(content.document.getElementsByTagName("link")).forEach(function (link) { + var title = link.getAttribute("title"); + var rel = link.getAttribute("rel"); + var media = link.getAttribute("media"); + var idstring = "link " + (title ? title : "without title and") + + " with rel=\"" + rel + "\"" + + (media ? " and media=\"" + media + "\"" : ""); + + var item = items.filter(item => item.getAttribute("label") == title); + var found = item.length == 1; + var checked = found && (item[0].getAttribute("checked") == "true"); + + switch (link.getAttribute("data-state")) { + case "0": + ok(!found, idstring + " does not show up in page style menu"); + break; + case "0-todo": + validLinks++; + todo(!found, idstring + " should not show up in page style menu"); + ok(!checked, idstring + " is not selected"); + break; + case "1": + validLinks++; + ok(found, idstring + " shows up in page style menu"); + ok(!checked, idstring + " is not selected"); + break; + case "2": + validLinks++; + ok(found, idstring + " shows up in page style menu"); + ok(checked, idstring + " is selected"); + break; + default: + throw "data-state attribute is missing or has invalid value"; + } + }); + + is(validLinks, items.length, "all valid links found"); + + gBrowser.removeCurrentTab(); + finish(); +} diff --git a/comm/suite/browser/test/browser/browser_popupNotification.js b/comm/suite/browser/test/browser/browser_popupNotification.js new file mode 100644 index 0000000000..da88243e66 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_popupNotification.js @@ -0,0 +1,782 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + ok(PopupNotifications, "PopupNotifications object exists"); + ok(PopupNotifications.panel, "PopupNotifications panel exists"); + + registerCleanupFunction(cleanUp); + + runNextTest(); +} + +function cleanUp() { + for (var topic in gActiveObservers) + Services.obs.removeObserver(gActiveObservers[topic], topic); + for (var eventName in gActiveListeners) + PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName]); +} + +var gActiveListeners = {}; +var gActiveObservers = {}; +var gShownState = {}; + +function runNextTest() { + let nextTest = tests[gTestIndex]; + + function goNext() { + if (++gTestIndex == tests.length) + executeSoon(finish); + else + executeSoon(runNextTest); + } + + function addObserver(topic) { + function observer() { + Services.obs.removeObserver(observer, "PopupNotifications-" + topic); + delete gActiveObservers["PopupNotifications-" + topic]; + + info("[Test #" + gTestIndex + "] observer for " + topic + " called"); + nextTest[topic](); + goNext(); + } + Services.obs.addObserver(observer, "PopupNotifications-" + topic); + gActiveObservers["PopupNotifications-" + topic] = observer; + } + + if (nextTest.backgroundShow) { + addObserver("backgroundShow"); + } else if (nextTest.updateNotShowing) { + addObserver("updateNotShowing"); + } else { + doOnPopupEvent("popupshowing", function () { + info("[Test #" + gTestIndex + "] popup showing"); + }); + doOnPopupEvent("popupshown", function () { + gShownState[gTestIndex] = true; + info("[Test #" + gTestIndex + "] popup shown"); + nextTest.onShown(this); + }); + + // We allow multiple onHidden functions to be defined in an array. They're + // called in the order they appear. + let onHiddenArray = nextTest.onHidden instanceof Array ? + nextTest.onHidden : + [nextTest.onHidden]; + doOnPopupEvent("popuphidden", function () { + if (!gShownState[gTestIndex]) { + // This is expected to happen for test 9, so let's not treat it as a failure. + info("Popup from test " + gTestIndex + " was hidden before its popupshown fired"); + } + + let onHidden = onHiddenArray.shift(); + info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)"); + executeSoon(function () { + onHidden.call(nextTest, this); + if (!onHiddenArray.length) + goNext(); + }); + }, onHiddenArray.length); + info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen); + } + + info("[Test #" + gTestIndex + "] running test"); + nextTest.run(); +} + +function doOnPopupEvent(eventName, callback, numExpected) { + gActiveListeners[eventName] = function (event) { + if (event.target != PopupNotifications.panel) + return; + if (typeof(numExpected) === "number") + numExpected--; + if (!numExpected) { + PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName]); + delete gActiveListeners[eventName]; + } + + callback.call(PopupNotifications.panel); + } + PopupNotifications.panel.addEventListener(eventName, gActiveListeners[eventName]); +} + +var gTestIndex = 0; +var gNewTab; + +function basicNotification() { + var self = this; + this.browser = gBrowser.selectedBrowser; + this.id = "test-notification-" + gTestIndex; + this.message = "This is popup notification " + this.id + " from test " + gTestIndex; + this.anchorID = null; + this.mainAction = { + label: "Main Action", + accessKey: "M", + callback: function () { + self.mainActionClicked = true; + } + }; + this.secondaryActions = [ + { + label: "Secondary Action", + accessKey: "S", + callback: function () { + self.secondaryActionClicked = true; + } + } + ]; + this.options = { + eventCallback: function (eventName) { + switch (eventName) { + case "dismissed": + self.dismissalCallbackTriggered = true; + break; + case "shown": + self.shownCallbackTriggered = true; + break; + case "removed": + self.removedCallbackTriggered = true; + break; + } + } + }; + this.addOptions = function(options) { + for (let [name, value] of Object.entries(options)) + self.options[name] = value; + } +} + +var wrongBrowserNotificationObject = new basicNotification(); +var wrongBrowserNotification; + +var tests = [ + { // Test #0 + run: function () { + this.notifyObj = new basicNotification(); + showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + triggerMainCommand(popup); + }, + onHidden: function (popup) { + ok(this.notifyObj.mainActionClicked, "mainAction was clicked"); + ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered"); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + { // Test #1 + run: function () { + this.notifyObj = new basicNotification(); + showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + triggerSecondaryCommand(popup, 0); + }, + onHidden: function (popup) { + ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked"); + ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered"); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + { // Test #2 + run: function () { + this.notifyObj = new basicNotification(); + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function (popup) { + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered"); + this.notification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // test opening a notification for a background browser + { // Test #3 + run: function () { + gNewTab = gBrowser.addTab("about:blank"); + isnot(gBrowser.selectedTab, gNewTab, "new tab isn't selected"); + wrongBrowserNotificationObject.browser = gBrowser.getBrowserForTab(gNewTab); + wrongBrowserNotification = showNotification(wrongBrowserNotificationObject); + }, + backgroundShow: function () { + is(PopupNotifications.isPanelOpen, false, "panel isn't open"); + ok(!wrongBrowserNotificationObject.mainActionClicked, "main action wasn't clicked"); + ok(!wrongBrowserNotificationObject.secondaryActionClicked, "secondary action wasn't clicked"); + ok(!wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback wasn't called"); + } + }, + // now select that browser and test to see that the notification appeared + { // Test #4 + run: function () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gNewTab; + }, + onShown: function (popup) { + checkPopup(popup, wrongBrowserNotificationObject); + is(PopupNotifications.isPanelOpen, true, "isPanelOpen getter doesn't lie"); + + // switch back to the old browser + gBrowser.selectedTab = this.oldSelectedTab; + }, + onHidden: function (popup) { + // actually remove the notification to prevent it from reappearing + ok(wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback triggered due to tab switch"); + wrongBrowserNotification.remove(); + ok(wrongBrowserNotificationObject.removedCallbackTriggered, "removed callback triggered"); + wrongBrowserNotification = null; + } + }, + // test that the removed notification isn't shown on browser re-select + { // Test #5 + run: function () { + gBrowser.selectedTab = gNewTab; + }, + updateNotShowing: function () { + is(PopupNotifications.isPanelOpen, false, "panel isn't open"); + gBrowser.removeTab(gNewTab); + } + }, + // Test that two notifications with the same ID result in a single displayed + // notification. + { // Test #6 + run: function () { + this.notifyObj = new basicNotification(); + // Show the same notification twice + this.notification1 = showNotification(this.notifyObj); + this.notification2 = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + this.notification2.remove(); + }, + onHidden: function (popup) { + ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered"); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test that two notifications with different IDs are displayed + { // Test #7 + run: function () { + this.testNotif1 = new basicNotification(); + this.testNotif1.message += " 1"; + showNotification(this.testNotif1); + this.testNotif2 = new basicNotification(); + this.testNotif2.message += " 2"; + this.testNotif2.id += "-2"; + showNotification(this.testNotif2); + }, + onShown: function (popup) { + is(popup.childNodes.length, 2, "two notifications are shown"); + // Trigger the main command for the first notification, and the secondary + // for the second. Need to do mainCommand first since the secondaryCommand + // triggering is async. + triggerMainCommand(popup); + is(popup.childNodes.length, 1, "only one notification left"); + triggerSecondaryCommand(popup, 0); + }, + onHidden: function (popup) { + ok(this.testNotif1.mainActionClicked, "main action #1 was clicked"); + ok(!this.testNotif1.secondaryActionClicked, "secondary action #1 wasn't clicked"); + ok(!this.testNotif1.dismissalCallbackTriggered, "dismissal callback #1 wasn't called"); + + ok(!this.testNotif2.mainActionClicked, "main action #2 wasn't clicked"); + ok(this.testNotif2.secondaryActionClicked, "secondary action #2 was clicked"); + ok(!this.testNotif2.dismissalCallbackTriggered, "dismissal callback #2 wasn't called"); + } + }, + // Test notification without mainAction + { // Test #8 + run: function () { + this.notifyObj = new basicNotification(); + this.notifyObj.mainAction = null; + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function (popup) { + this.notification.remove(); + } + }, + // Test two notifications with different anchors + { // Test #9 + run: function () { + this.notifyObj = new basicNotification(); + this.firstNotification = showNotification(this.notifyObj); + this.notifyObj2 = new basicNotification(); + this.notifyObj2.id += "-2"; + this.notifyObj2.anchorID = "addons-notification-icon"; + // Second showNotification() overrides the first + this.secondNotification = showNotification(this.notifyObj2); + }, + onShown: function (popup) { + // This also checks that only one element is shown. + checkPopup(popup, this.notifyObj2); + is(document.getElementById("geo-notification-icon").boxObject.width, 0, + "geo anchor shouldn't be visible"); + dismissNotification(popup); + }, + onHidden: [ + // The second showing triggers a popuphidden event that we should ignore. + function (popup) {}, + function (popup) { + // Remove the notifications + this.firstNotification.remove(); + this.secondNotification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered"); + } + ] + }, + // Test optional params + { // Test #10 + run: function () { + this.notifyObj = new basicNotification(); + this.notifyObj.secondaryActions = undefined; + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function (popup) { + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered"); + this.notification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test that icons appear + { // Test #11 + run: function () { + this.notifyObj = new basicNotification(); + this.notifyObj.id = "geolocation"; + this.notifyObj.anchorID = "geo-notification-icon"; + // Fake these options (with arbitrary values), + // to avoid strict warnings in SeaMonkey notification constructor. + this.notifyObj.addOptions({ + value: "Learn More...", + href: "http://www.seamonkey-project.org/doc/2.0/geolocation" + }); + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + isnot(document.getElementById("geo-notification-icon").boxObject.width, 0, + "geo anchor should be visible"); + dismissNotification(popup); + }, + onHidden: function (popup) { + let icon = document.getElementById("geo-notification-icon"); + isnot(icon.boxObject.width, 0, + "geo anchor should be visible after dismissal"); + this.notification.remove(); + is(icon.boxObject.width, 0, + "geo anchor should not be visible after removal"); + } + }, + // Test that persistence allows the notification to persist across reloads + { // Test #12 + run: function () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + + let self = this; + loadURI("http://example.com/", function() { + self.notifyObj = new basicNotification(); + self.notifyObj.addOptions({ + persistence: 2 + }); + self.notification = showNotification(self.notifyObj); + }); + }, + onShown: function (popup) { + this.complete = false; + + let self = this; + loadURI("http://example.org/", function() { + loadURI("http://example.com/", function() { + + // Next load will remove the notification + self.complete = true; + + loadURI("http://example.org/"); + }); + }); + }, + onHidden: function (popup) { + ok(this.complete, "Should only have hidden the notification after 3 page loads"); + ok(this.notifyObj.removedCallbackTriggered, "removal callback triggered"); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.selectedTab = this.oldSelectedTab; + } + }, + // Test that a timeout allows the notification to persist across reloads + { // Test #13 + run: function () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + + let self = this; + loadURI("http://example.com/", function() { + self.notifyObj = new basicNotification(); + // Set a timeout of 10 minutes that should never be hit + self.notifyObj.addOptions({ + timeout: Date.now() + 600000 + }); + self.notification = showNotification(self.notifyObj); + }); + }, + onShown: function (popup) { + this.complete = false; + + let self = this; + loadURI("http://example.org/", function() { + loadURI("http://example.com/", function() { + + // Next load will hide the notification + self.notification.options.timeout = Date.now() - 1; + self.complete = true; + + loadURI("http://example.org/"); + }); + }); + }, + onHidden: function (popup) { + ok(this.complete, "Should only have hidden the notification after the timeout was passed"); + this.notification.remove(); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.selectedTab = this.oldSelectedTab; + } + }, + // Test that setting persistWhileVisible allows a visible notification to + // persist across location changes + { // Test #14 + run: function () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + + let self = this; + loadURI("http://example.com/", function() { + self.notifyObj = new basicNotification(); + self.notifyObj.addOptions({ + persistWhileVisible: true + }); + self.notification = showNotification(self.notifyObj); + }); + }, + onShown: function (popup) { + this.complete = false; + + let self = this; + loadURI("http://example.org/", function() { + loadURI("http://example.com/", function() { + + // Notification should persist across location changes + self.complete = true; + dismissNotification(popup); + }); + }); + }, + onHidden: function (popup) { + ok(this.complete, "Should only have hidden the notification after it was dismissed"); + this.notification.remove(); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.selectedTab = this.oldSelectedTab; + } + }, + // Test that nested icon nodes correctly activate popups + { // Test #15 + run: function() { + // Add a temporary box as the anchor with a button + this.box = document.createElement("box"); + PopupNotifications.iconBox.appendChild(this.box); + + let button = document.createElement("button"); + button.setAttribute("label", "Please click me!"); + this.box.appendChild(button); + + // The notification should open up on the box + this.notifyObj = new basicNotification(); + this.notifyObj.anchorID = this.box.id = "nested-box"; + this.notifyObj.addOptions({dismissed: true}); + this.notification = showNotification(this.notifyObj); + + EventUtils.synthesizeMouse(button, 1, 1, {}); + }, + onShown: function(popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function(popup) { + this.notification.remove(); + this.box.remove(); + } + }, + // Test that popupnotifications without popups have anchor icons shown + { // Test #16 + run: function() { + let notifyObj = new basicNotification(); + notifyObj.anchorID = "geo-notification-icon"; + notifyObj.addOptions({neverShow: true}); + showNotification(notifyObj); + }, + updateNotShowing: function() { + isnot(document.getElementById("geo-notification-icon").boxObject.width, 0, + "geo anchor should be visible"); + } + }, + // Test notification "Not Now" menu item + { // Test #17 + run: function () { + this.notifyObj = new basicNotification(); + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + triggerSecondaryCommand(popup, 1); + }, + onHidden: function (popup) { + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered"); + this.notification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test notification close button + { // Test #18 + run: function () { + this.notifyObj = new basicNotification(); + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + let notification = popup.childNodes[0]; + EventUtils.synthesizeMouseAtCenter(notification.closebutton, {}); + }, + onHidden: function (popup) { + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered"); + this.notification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test notification when chrome is hidden + { // Test #19 + run: function () { + window.locationbar.visible = false; + this.notifyObj = new basicNotification(); + this.notification = showNotification(this.notifyObj); + window.locationbar.visible = true; + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + is(popup.anchorNode.className, "tabbrowser-tab", "notification anchored to tab"); + dismissNotification(popup); + }, + onHidden: function (popup) { + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered"); + this.notification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test notification is removed when dismissed if removeOnDismissal is true + { // Test #20 + run: function () { + this.notifyObj = new basicNotification(); + this.notifyObj.addOptions({ + removeOnDismissal: true + }); + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function (popup) { + ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered"); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test multiple notification icons are shown + { // Test #21 + run: function () { + this.notifyObj1 = new basicNotification(); + this.notifyObj1.id += "_1"; + this.notifyObj1.anchorID = "default-notification-icon"; + this.notification1 = showNotification(this.notifyObj1); + + this.notifyObj2 = new basicNotification(); + this.notifyObj2.id += "_2"; + this.notifyObj2.anchorID = "geo-notification-icon"; + this.notification2 = showNotification(this.notifyObj2); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj2); + + // check notifyObj1 anchor icon is showing + isnot(document.getElementById("default-notification-icon").boxObject.width, 0, + "default anchor should be visible"); + // check notifyObj2 anchor icon is showing + isnot(document.getElementById("geo-notification-icon").boxObject.width, 0, + "geo anchor should be visible"); + + dismissNotification(popup); + }, + onHidden: [ + function (popup) { + }, + function (popup) { + this.notification1.remove(); + ok(this.notifyObj1.removedCallbackTriggered, "removed callback triggered"); + + this.notification2.remove(); + ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered"); + } + ] + }, + // Test that multiple notification icons are removed when switching tabs + { // Test #22 + run: function () { + // show the notification on old tab. + this.notifyObjOld = new basicNotification(); + this.notifyObjOld.anchorID = "default-notification-icon"; + this.notificationOld = showNotification(this.notifyObjOld); + + // switch tab + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + + // show the notification on new tab. + this.notifyObjNew = new basicNotification(); + this.notifyObjNew.anchorID = "geo-notification-icon"; + this.notificationNew = showNotification(this.notifyObjNew); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObjNew); + + // check notifyObjOld anchor icon is removed + is(document.getElementById("default-notification-icon").boxObject.width, 0, + "default anchor shouldn't be visible"); + // check notifyObjNew anchor icon is showing + isnot(document.getElementById("geo-notification-icon").boxObject.width, 0, + "geo anchor should be visible"); + + dismissNotification(popup); + }, + onHidden: [ + function (popup) { + }, + function (popup) { + this.notificationNew.remove(); + gBrowser.removeTab(gBrowser.selectedTab); + + gBrowser.selectedTab = this.oldSelectedTab; + this.notificationOld.remove(); + } + ] + } +]; + +function showNotification(notifyObj) { + return PopupNotifications.show(notifyObj.browser, + notifyObj.id, + notifyObj.message, + notifyObj.anchorID, + notifyObj.mainAction, + notifyObj.secondaryActions, + notifyObj.options); +} + +function checkPopup(popup, notificationObj) { + info("[Test #" + gTestIndex + "] checking popup"); + + ok(notificationObj.shownCallbackTriggered, "shown callback was triggered"); + + let notifications = popup.childNodes; + is(notifications.length, 1, "only one notification displayed"); + let notification = notifications[0]; + let icon = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-icon"); + if (notificationObj.id == "geolocation") { + isnot(icon.boxObject.width, 0, "icon for geo displayed"); + is(popup.anchorNode.className, "notification-anchor-icon", "notification anchored to icon"); + } + is(notification.getAttribute("label"), notificationObj.message, "message matches"); + is(notification.id, notificationObj.id + "-notification", "id matches"); + if (notificationObj.mainAction) { + is(notification.getAttribute("buttonlabel"), notificationObj.mainAction.label, "main action label matches"); + is(notification.getAttribute("buttonaccesskey"), notificationObj.mainAction.accessKey, "main action accesskey matches"); + } + let actualSecondaryActions = notification.childNodes; + let secondaryActions = notificationObj.secondaryActions || []; + let actualSecondaryActionsCount = actualSecondaryActions.length; + if (secondaryActions.length) { + let lastChild = actualSecondaryActions.item(actualSecondaryActions.length - 1); + is(lastChild.tagName, "menuseparator", "menuseparator exists"); + actualSecondaryActionsCount--; + } + is(actualSecondaryActionsCount, secondaryActions.length, actualSecondaryActions.length + " secondary actions"); + secondaryActions.forEach(function (a, i) { + is(actualSecondaryActions[i].getAttribute("label"), a.label, "label for secondary action " + i + " matches"); + is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey, "accessKey for secondary action " + i + " matches"); + }); +} + +function triggerMainCommand(popup) { + info("[Test #" + gTestIndex + "] triggering main command"); + let notifications = popup.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + let notification = notifications[0]; + EventUtils.synthesizeMouseAtCenter(notification.button, {}); +} + +function triggerSecondaryCommand(popup, index) { + info("[Test #" + gTestIndex + "] triggering secondary command"); + let notifications = popup.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + let notification = notifications[0]; + info("Triggering secondary command for notification " + notification.id); + notification.button.nextSibling.nextSibling.focus(); + + popup.addEventListener("popupshown", function triggerPopupShown() { + popup.removeEventListener("popupshown", triggerPopupShown); + + // Press down until the desired command is selected + for (let i = 0; i <= index; i++) + EventUtils.synthesizeKey("VK_DOWN", {}); + + // Activate + EventUtils.synthesizeKey("VK_RETURN", {}); + }); + + // One down event to open the popup + EventUtils.synthesizeKey("VK_DOWN", { altKey: AppConstants.platform != "macosx" }); +} + +function loadURI(uri, callback) { + if (callback) { + gBrowser.addEventListener("load", function loadURIgBLoad() { + // Ignore the about:blank load + if (gBrowser.currentURI.spec != uri) + return; + + gBrowser.removeEventListener("load", loadURIgBLoad, true); + + callback(); + }, true); + } + gBrowser.loadURI(uri); +} + +function dismissNotification(popup) { + info("[Test #" + gTestIndex + "] dismissing notification"); + executeSoon(function () { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }); +} diff --git a/comm/suite/browser/test/browser/browser_privatebrowsing_protocolhandler.js b/comm/suite/browser/test/browser/browser_privatebrowsing_protocolhandler.js new file mode 100644 index 0000000000..2fc5392dca --- /dev/null +++ b/comm/suite/browser/test/browser/browser_privatebrowsing_protocolhandler.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This test makes sure that the web pages can't register protocol handlers +// inside the private browsing mode. + +function test() { + // initialization + waitForExplicitFinish(); + let windowsToClose = []; + let notificationValue = "Protocol Registration: testprotocol"; + let testURI = "http://example.com/browser/" + + "suite/browser/test/browser/browser_privatebrowsing_protocolhandler_page.html"; + + function doTest(aIsPrivateMode, aWindow, aCallback) { + aWindow.getBrowser().selectedBrowser.addEventListener("load", function onLoad() { + aWindow.getBrowser().selectedBrowser.removeEventListener("load", onLoad, true); + + setTimeout(function() { + let notificationBox = aWindow.getBrowser().getNotificationBox(); + let notification = notificationBox.getNotificationWithValue(notificationValue); + + if (aIsPrivateMode) { + // Make sure the notification is correctly displayed without a remember control + ok(!notification, "Notification box should not be displayed inside of private browsing mode"); + } else { + // Make sure the notification is correctly displayed with a remember control + ok(notification, "Notification box should be displaying outside of private browsing mode"); + } + + aCallback(); + }, 100); // remember control is added in a setTimeout(0) call + }, true); + + aWindow.getBrowser().selectedBrowser.loadURI(testURI); + } + + function testOnWindow(aOptions, aCallback) { + whenNewWindowLoaded(aOptions, function(aWin) { + windowsToClose.push(aWin); + // execute should only be called when need, like when you are opening + // web pages on the test. If calling executeSoon() is not necesary, then + // call whenNewWindowLoaded() instead of testOnWindow() on your test. + executeSoon(() => aCallback(aWin)); + }); + }; + + // this function is called after calling finish() on the test. + registerCleanupFunction(function() { + windowsToClose.forEach(function(aWin) { + aWin.close(); + }); + }); + + // test first when not on private mode + testOnWindow({}, function(aWin) { + doTest(false, aWin, function() { + // then test when on private mode + testOnWindow({private: true}, function(aWin) { + doTest(true, aWin, finish); + }); + }); + }); +} diff --git a/comm/suite/browser/test/browser/browser_privatebrowsing_protocolhandler_page.html b/comm/suite/browser/test/browser/browser_privatebrowsing_protocolhandler_page.html new file mode 100644 index 0000000000..800476fd03 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_privatebrowsing_protocolhandler_page.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> +<html> + <head> + <title>Protocol registrar page</title> + </head> + <body> + <script> + navigator.registerProtocolHandler("testprotocol", + "https://example.com/foobar?uri=%s", + "Test Protocol"); + </script> + </body> +</html> diff --git a/comm/suite/browser/test/browser/browser_relatedTabs.js b/comm/suite/browser/test/browser/browser_relatedTabs.js new file mode 100644 index 0000000000..f93e212391 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_relatedTabs.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + is(getBrowser().tabs.length, 1, "one tab is open initially"); + + // Add several new tabs in sequence, interrupted by selecting a + // different tab, moving a tab around and closing a tab, + // returning a list of opened tabs for verifying the expected order. + // The new tab behaviour is documented in bug 465673 + let tabs = []; + function addTab(aURL, aReferrer) { + tabs.push(getBrowser().addTab(aURL, {referrerURI: aReferrer})); + } + + addTab("http://mochi.test:8888/#0"); + getBrowser().selectedTab = tabs[0]; + addTab("http://mochi.test:8888/#1"); + addTab("http://mochi.test:8888/#2", getBrowser().currentURI); + addTab("http://mochi.test:8888/#3", getBrowser().currentURI); + getBrowser().selectedTab = tabs[tabs.length - 1]; + getBrowser().selectedTab = tabs[0]; + addTab("http://mochi.test:8888/#4", getBrowser().currentURI); + getBrowser().selectedTab = tabs[3]; + addTab("http://mochi.test:8888/#5", getBrowser().currentURI); + getBrowser().removeTab(tabs.pop()); + addTab("about:blank", getBrowser().currentURI); + getBrowser().moveTabTo(getBrowser().selectedTab, 1); + addTab("http://mochi.test:8888/#6", getBrowser().currentURI); + addTab(); + addTab("http://mochi.test:8888/#7"); + + function testPosition(tabNum, expectedPosition, msg) { + is(Array.from(getBrowser().tabs) + .indexOf(tabs[tabNum]), + expectedPosition, + msg); + } + + testPosition(0, 3, "tab without referrer was opened to the far right"); + testPosition(1, 7, "tab without referrer was opened to the far right"); + testPosition(2, 5, "tab with referrer opened immediately to the right"); + testPosition(3, 1, "next tab with referrer opened further to the right"); + testPosition(4, 4, "tab selection changed, tab opens immediately to the right"); + testPosition(5, 6, "blank tab with referrer opens to the right of 3rd original tab where removed tab was"); + testPosition(6, 2, "tab has moved, new tab opens immediately to the right"); + testPosition(7, 8, "blank tab without referrer opens at the end"); + testPosition(8, 9, "tab without referrer opens at the end"); + + tabs.forEach(getBrowser().removeTab, getBrowser()); +} diff --git a/comm/suite/browser/test/browser/browser_scope.js b/comm/suite/browser/test/browser/browser_scope.js new file mode 100644 index 0000000000..e4edac1e04 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_scope.js @@ -0,0 +1,4 @@ +function test() { + ok(!!gBrowser, "gBrowser exists"); + is(gBrowser, getBrowser(), "both ways of getting tabbrowser work"); +} diff --git a/comm/suite/browser/test/browser/browser_selectTabAtIndex.js b/comm/suite/browser/test/browser/browser_selectTabAtIndex.js new file mode 100644 index 0000000000..9da6c8d489 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_selectTabAtIndex.js @@ -0,0 +1,22 @@ +function test() { + for (let i = 0; i < 9; i++) + gBrowser.addTab(); + +/* This part tests accel keys, which are not implemented in Seamonkey yet. + * Commenting out for now ... + * var isLinux = AppConstants.platform == "linux"; + * for (let i = 9; i >= 1; i--) { + * EventUtils.synthesizeKey(i.toString(), { altKey: isLinux, accelKey: !isLinux }); + * + * is(gBrowser.tabContainer.selectedIndex, (i == 9 ? gBrowser.tabs.length : i) - 1, + * (isLinux ? "Alt" : "Accel") + "+" + i + " selects expected tab"); + * } + */ + + gBrowser.selectTabAtIndex(-3); + is(gBrowser.tabContainer.selectedIndex, gBrowser.tabs.length - 3, + "gBrowser.selectTabAtIndex(-3) selects expected tab"); + + for (let i = 0; i < 9; i++) + gBrowser.removeCurrentTab(); +} diff --git a/comm/suite/browser/test/browser/browser_urlbarCopying.js b/comm/suite/browser/test/browser/browser_urlbarCopying.js new file mode 100644 index 0000000000..cc51832996 --- /dev/null +++ b/comm/suite/browser/test/browser/browser_urlbarCopying.js @@ -0,0 +1,204 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const trimPref = "browser.urlbar.trimURLs"; +const phishyUserPassPref = "network.http.phishy-userpass-length"; + +function test() { + + let tab = gBrowser.selectedTab = gBrowser.addTab(); + + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + Services.prefs.clearUserPref(trimPref); + Services.prefs.clearUserPref(phishyUserPassPref); + URLBarSetURI(); + }); + + Services.prefs.setBoolPref(trimPref, true); + Services.prefs.setIntPref(phishyUserPassPref, 32); // avoid prompting about phishing + + waitForExplicitFinish(); + + nextTest(); +} + +var tests = [ + // pageproxystate="invalid" + { + setURL: "http://example.com/", + expectedURL: "example.com", + copyExpected: "example.com" + }, + { + copyVal: "<e>xample.com", + copyExpected: "e" + }, + + // pageproxystate="valid" from this point on (due to the load) + { + loadURL: "http://example.com/", + expectedURL: "example.com", + copyExpected: "http://example.com/" + }, + { + copyVal: "<example.co>m", + copyExpected: "example.co" + }, + { + copyVal: "e<x>ample.com", + copyExpected: "x" + }, + { + copyVal: "<e>xample.com", + copyExpected: "e" + }, + + { + loadURL: "http://example.com/foo", + expectedURL: "example.com/foo", + copyExpected: "http://example.com/foo" + }, + { + copyVal: "<example.com>/foo", + copyExpected: "http://example.com" + }, + { + copyVal: "<example>.com/foo", + copyExpected: "example" + }, + + // Test that userPass is stripped out + { + loadURL: "http://user:pass@mochi.test:8888/browser/browser/base/content/test/general/authenticate.sjs?user=user&pass=pass", + expectedURL: "mochi.test:8888/browser/browser/base/content/test/general/authenticate.sjs?user=user&pass=pass", + copyExpected: "http://mochi.test:8888/browser/browser/base/content/test/general/authenticate.sjs?user=user&pass=pass" + }, + + // Test escaping + { + loadURL: "http://example.com/()%28%29%C3%A9", + expectedURL: "example.com/()()\xe9", + copyExpected: "http://example.com/()%28%29%C3%A9" + }, + { + copyVal: "<example.com/(>)()\xe9", + copyExpected: "http://example.com/(" + }, + { + copyVal: "e<xample.com/(>)()\xe9", + copyExpected: "xample.com/(" + }, + + { + loadURL: "http://example.com/%C3%A9%C3%A9", + expectedURL: "example.com/\xe9\xe9", + copyExpected: "http://example.com/%C3%A9%C3%A9" + }, + { + copyVal: "e<xample.com/\xe9>\xe9", + copyExpected: "xample.com/\xe9" + }, + { + copyVal: "<example.com/\xe9>\xe9", + copyExpected: "http://example.com/\xe9" + }, + + { + loadURL: "http://example.com/?%C3%B7%C3%B7", + expectedURL: "example.com/?\xf7\xf7", + copyExpected: "http://example.com/?%C3%B7%C3%B7" + }, + { + copyVal: "e<xample.com/?\xf7>\xf7", + copyExpected: "xample.com/?\xf7" + }, + { + copyVal: "<example.com/?\xf7>\xf7", + copyExpected: "http://example.com/?\xf7" + }, + + // data: and javsacript: URIs shouldn't be encoded + { + loadURL: "javascript:('%C3%A9%20%25%50')", + expectedURL: "javascript:('%C3%A9 %25P')", + copyExpected: "javascript:('%C3%A9 %25P')" + }, + { + copyVal: "<javascript:(>'%C3%A9 %25P')", + copyExpected: "javascript:(" + }, + + { + loadURL: "data:text/html,(%C3%A9%20%25%50)", + expectedURL: "data:text/html,(%C3%A9 %25P)", + copyExpected: "data:text/html,(%C3%A9 %25P)", + }, + { + copyVal: "<data:text/html,(>%C3%A9 %25P)", + copyExpected: "data:text/html,(" + }, + { + copyVal: "<data:text/html,(%C3%A9 %25P>)", + copyExpected: "data:text/html,(%C3%A9 %25P", + } +]; + +function nextTest() { + let test = tests.shift(); + if (tests.length == 0) + runTest(test, finish); + else + runTest(test, nextTest); +} + +function runTest(test, cb) { + function doCheck() { + if (test.setURL || test.loadURL) { + gURLBar.valueIsTyped = !!test.setURL; + is(gURLBar.textValue, test.expectedURL, "url bar value set"); + } + + testCopy(test.copyVal, test.copyExpected, cb); + } + + if (test.loadURL) { + loadURL(test.loadURL, doCheck); + } else { + if (test.setURL) + gURLBar.value = test.setURL; + doCheck(); + } +} + +function testCopy(copyVal, targetValue, cb) { + info("Expecting copy of: " + targetValue); + waitForClipboard(targetValue, function () { + gURLBar.focus(); + if (copyVal) { + let startBracket = copyVal.indexOf("<"); + let endBracket = copyVal.indexOf(">"); + if (startBracket == -1 || endBracket == -1 || + startBracket > endBracket || + copyVal.replace("<", "").replace(">", "") != gURLBar.textValue) { + ok(false, "invalid copyVal: " + copyVal); + } + gURLBar.selectionStart = startBracket; + gURLBar.selectionEnd = endBracket - 1; + } else { + gURLBar.select(); + } + + goDoCommand("cmd_copy"); + }, cb, cb); +} + +function loadURL(aURL, aCB) { + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + is(gBrowser.currentURI.spec, aURL, "loaded expected URL"); + aCB(); + }, true); + + gBrowser.loadURI(aURL); +} diff --git a/comm/suite/browser/test/browser/feed_tab.html b/comm/suite/browser/test/browser/feed_tab.html new file mode 100644 index 0000000000..50903f48b6 --- /dev/null +++ b/comm/suite/browser/test/browser/feed_tab.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=458579 +--> + <head> + <title>Test for page info feeds tab</title> + + <!-- Straight up standard --> + <link rel="alternate" type="application/atom+xml" title="1" href="/1.atom" /> + <link rel="alternate" type="application/rss+xml" title="2" href="/2.rss" /> + <link rel="feed" title="3" href="/3.xml" /> + + </head> + <body> + </body> +</html> diff --git a/comm/suite/browser/test/browser/file_dom_notifications.html b/comm/suite/browser/test/browser/file_dom_notifications.html new file mode 100644 index 0000000000..078c94a42d --- /dev/null +++ b/comm/suite/browser/test/browser/file_dom_notifications.html @@ -0,0 +1,40 @@ +<html> +<head> +<script> +"use strict"; + +function showNotification1() { + var options = { + dir: undefined, + lang: undefined, + body: "Test body", + tag: "Test tag", + icon: undefined, + }; + var n = new Notification("Test title", options); + n.addEventListener("click", function(event) { + event.preventDefault(); + dump("Should focus new window."); + newWindow.focus(); + }); + return n; +} + +function showNotification2() { + var options = { + dir: undefined, + lang: undefined, + body: "Test body", + tag: "Test tag", + icon: undefined, + }; + return new Notification("Test title", options); +} +</script> +</head> +<body> +<form id="notificationForm" onsubmit="showNotification();"> + <input type="submit" value="Show notification" id="submit"/> +</form> +</body> +</html> diff --git a/comm/suite/browser/test/browser/head.js b/comm/suite/browser/test/browser/head.js new file mode 100644 index 0000000000..9a065930a2 --- /dev/null +++ b/comm/suite/browser/test/browser/head.js @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function findChromeWindowByURI(aURI) { + let windows = Services.wm.getEnumerator(null); + while (windows.hasMoreElements()) { + let win = windows.getNext(); + if (win.location.href == aURI) + return win; + } + return null; +} + +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + if (condition()) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); nextTest(); }; +} + +function whenNewWindowLoaded(aOptions, aCallback) { + var { private } = aOptions; + var features = private ? "private,chrome,all,dialog=no" : + "non-private,chrome,all,dialog=no"; + var win = window.openDialog(getBrowserURL(), "_blank", features, + "about:blank"); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad); + aCallback(win); + }); +} + +function updateBlocklist(aCallback) { + var blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"] + .getService(Ci.nsITimerCallback); + var observer = function() { + Services.obs.removeObserver(observer, "blocklist-updated"); + SimpleTest.executeSoon(aCallback); + }; + Services.obs.addObserver(observer, "blocklist-updated"); + blocklistNotifier.notify(null); +} + +var _originalTestBlocklistURL = null; +function setAndUpdateBlocklist(aURL, aCallback) { + if (!_originalTestBlocklistURL) + _originalTestBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url"); + Services.prefs.setCharPref("extensions.blocklist.url", aURL); + updateBlocklist(aCallback); +} + +function resetBlocklist() { + Services.prefs.setCharPref("extensions.blocklist.url", _originalTestBlocklistURL); +} diff --git a/comm/suite/browser/test/browser/page_style_sample.html b/comm/suite/browser/test/browser/page_style_sample.html new file mode 100644 index 0000000000..633956c4c7 --- /dev/null +++ b/comm/suite/browser/test/browser/page_style_sample.html @@ -0,0 +1,31 @@ +<html> + <head> + <title>Test for page style menu</title> + <!-- data-state values: + 0: should not appear in the page style menu + 0-todo: should not appear in the page style menu, but does + 1: should appear in the page style menu + 2: should appear in the page style menu as the selected stylesheet --> + <link data-state="1" href="404.css" title="1" rel="alternate stylesheet"> + <link data-state="0" title="2" rel="alternate stylesheet"> + <link data-state="0" href="404.css" rel="alternate stylesheet"> + <link data-state="0" href="404.css" title="" rel="alternate stylesheet"> + <link data-state="1" href="404.css" title="3" rel="stylesheet alternate"> + <link data-state="1" href="404.css" title="4" rel=" alternate stylesheet "> + <link data-state="1" href="404.css" title="5" rel="alternate stylesheet"> + <link data-state="2" href="404.css" title="6" rel="stylesheet"> + <link data-state="1" href="404.css" title="7" rel="foo stylesheet"> + <link data-state="0" href="404.css" title="8" rel="alternate"> + <link data-state="1" href="404.css" title="9" rel="alternate STYLEsheet"> + <link data-state="1" href="404.css" title="10" rel="alternate stylesheet" media=""> + <link data-state="1" href="404.css" title="11" rel="alternate stylesheet" media="all"> + <link data-state="1" href="404.css" title="12" rel="alternate stylesheet" media="ALL "> + <link data-state="1" href="404.css" title="13" rel="alternate stylesheet" media="screen"> + <link data-state="1" href="404.css" title="14" rel="alternate stylesheet" media=" Screen"> + <link data-state="1" href="404.css" title="15" rel="alternate stylesheet" media="screen foo"> + <link data-state="1" href="404.css" title="16" rel="alternate stylesheet" media="all screen"> + <link data-state="0-todo" href="404.css" title="17" rel="alternate stylesheet" media="allscreen"> + <link data-state="0-todo" href="404.css" title="18" rel="alternate stylesheet" media="_all"> + </head> + <body></body> +</html> diff --git a/comm/suite/browser/test/browser/redirect_bug623155.sjs b/comm/suite/browser/test/browser/redirect_bug623155.sjs new file mode 100644 index 0000000000..2e4681f240 --- /dev/null +++ b/comm/suite/browser/test/browser/redirect_bug623155.sjs @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host. + +function handleRequest(aRequest, aResponse) { + // Set HTTP Status + aResponse.setStatusLine(aRequest.httpVersion, 301, "Moved Permanently"); + + // Set redirect URI, mirroring the hash value. + let hash = (/\#.+/.test(aRequest.pathQueryRef))? + "#" + aRequest.pathQueryRef.split("#")[1]: + ""; + aResponse.setHeader("Location", REDIRECT_TO + hash); +} diff --git a/comm/suite/browser/test/browser/title_test.svg b/comm/suite/browser/test/browser/title_test.svg new file mode 100644 index 0000000000..6ab5b2f5c2 --- /dev/null +++ b/comm/suite/browser/test/browser/title_test.svg @@ -0,0 +1,59 @@ +<svg width="640px" height="480px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0"> + <title>This is a root SVG element's title</title> + <foreignObject> + <html xmlns="http://www.w3.org/1999/xhtml"> + <body> + <svg xmlns="http://www.w3.org/2000/svg" id="svg1"> + <title>This is a non-root SVG element title</title> + </svg> + </body> + </html> + </foreignObject> + <text id="text1" x="10px" y="32px" font-size="24px"> + This contains only <title> + <title> + + + This is a title + + </title> + </text> + <text id="text2" x="10px" y="96px" font-size="24px"> + This contains only <desc> + <desc>This is a desc</desc> + </text> + <text id="text3" x="10px" y="128px" font-size="24px"> + This contains nothing. + </text> + <a id="link1" xlink:href="#"> + This link contains <title> + <title> + This is a title + </title> + <text id="text4" x="10px" y="192px" font-size="24px"> + </text> + </a> + <a id="link2" xlink:href="#"> + <text x="10px" y="192px" font-size="24px"> + This text contains <title> + <title> + This is a title + </title> + </text> + </a> + <a id="link3" xlink:href="#" xlink:title="This is an xlink:title attribute"> + <text x="10px" y="224px" font-size="24px"> + This link contains <title> & xlink:title attr. + <title>This is a title</title> + </text> + </a> + <a id="link4" xlink:href="#" xlink:title="This is an xlink:title attribute"> + <text x="10px" y="256px" font-size="24px"> + This link contains xlink:title attr. + </text> + </a> + <text id="text5" x="10px" y="160px" font-size="24px" + xlink:title="This is an xlink:title attribute but it isn't on a link" > + This contains nothing. + </text> +</svg> diff --git a/comm/suite/browser/test/chrome/chrome.ini b/comm/suite/browser/test/chrome/chrome.ini new file mode 100644 index 0000000000..05c31c161a --- /dev/null +++ b/comm/suite/browser/test/chrome/chrome.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_maxSniffing.html] diff --git a/comm/suite/browser/test/chrome/test_maxSniffing.html b/comm/suite/browser/test/chrome/test_maxSniffing.html new file mode 100644 index 0000000000..8fb333e523 --- /dev/null +++ b/comm/suite/browser/test/chrome/test_maxSniffing.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=739040 +--> +<head> + <title>Test that we only sniff 512 bytes</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=739040">Mozilla Bug 739040</a> +<p id="display"> + <iframe id="validTestFrame" src="http://mochi.test:8888/tests/suite/browser/test/mochitest/valid-feed.xml"></iframe> + <iframe id="unsniffableTestFrame" src="http://mochi.test:8888/tests/suite/browser/test/mochitest/valid-unsniffable-feed.xml"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody"> + +/** Test for Bug 739040 **/ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + is($("validTestFrame").contentDocument.documentElement.id, "feedHandler", + "valid feed should be sniffed"); + isnot($("unsniffableTestFrame").contentDocument.documentElement.id, "feedHandler", + "unsniffable feed should not be sniffed"); +}); +addLoadEvent(SimpleTest.finish); + +</script> +</pre> +</body> +</html> diff --git a/comm/suite/browser/test/mochitest/audio.ogg b/comm/suite/browser/test/mochitest/audio.ogg Binary files differnew file mode 100644 index 0000000000..7e6ef77ec4 --- /dev/null +++ b/comm/suite/browser/test/mochitest/audio.ogg diff --git a/comm/suite/browser/test/mochitest/bug364677-data.xml b/comm/suite/browser/test/mochitest/bug364677-data.xml new file mode 100644 index 0000000000..b48915c050 --- /dev/null +++ b/comm/suite/browser/test/mochitest/bug364677-data.xml @@ -0,0 +1,5 @@ +<rss version="2.0"> + <channel> + <title>t</title> + </channel> +</rss> diff --git a/comm/suite/browser/test/mochitest/bug364677-data.xml^headers^ b/comm/suite/browser/test/mochitest/bug364677-data.xml^headers^ new file mode 100644 index 0000000000..f203c6368e --- /dev/null +++ b/comm/suite/browser/test/mochitest/bug364677-data.xml^headers^ @@ -0,0 +1 @@ +Content-Type: text/xml diff --git a/comm/suite/browser/test/mochitest/bug395533-data.txt b/comm/suite/browser/test/mochitest/bug395533-data.txt new file mode 100644 index 0000000000..e0ed39850f --- /dev/null +++ b/comm/suite/browser/test/mochitest/bug395533-data.txt @@ -0,0 +1,6 @@ +<rss version="2.0"> + <channel> + <link>http://example.org/</link> + <title>t</title> + </channel> +</rss> diff --git a/comm/suite/browser/test/mochitest/bug436801-data.xml b/comm/suite/browser/test/mochitest/bug436801-data.xml new file mode 100644 index 0000000000..0e45c7ed8e --- /dev/null +++ b/comm/suite/browser/test/mochitest/bug436801-data.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom" xml:base="http://www.example.com/"> + + <title type="xhtml" xml:base="/foo/bar/"> + <div xmlns="http://www.w3.org/1999/xhtml">Example of a <em>special</em> feed (<img height="20px" src="baz.png" alt="base test sprite"/>)</div> + </title> + + <subtitle type="html" xml:base="/foo/bar/"> + <![CDATA[ + With a <em>special</em> subtitle (<img height="20px" src="baz.png" alt="base test sprite"/>) + ]]> + </subtitle> + + <link href="http://example.org/"/> + + <updated>2010-09-02T18:30:02Z</updated> + + <author> + <name>John Doe</name> + </author> + + <id>urn:uuid:22906062-ecbd-46e2-b6a7-3039506a398f</id> + + <entry> + <title type="xhtml" xml:base="/foo/bar/"> + <div xmlns="http://www.w3.org/1999/xhtml">Some <abbr title="Extensible Hyper-text Mark-up Language">XHTML</abbr> examples (<img height="20px" src="baz.png" alt="base test sprite"/>)</div> + </title> + <id>urn:uuid:b48083a7-71a7-4c9c-8515-b7c0d22955e7</id> + <updated>2010-09-02T18:30:02Z</updated> + <summary>Some text.</summary> + </entry> + + <entry> + <title type="html" xml:base="/foo/bar/"> + <![CDATA[ + Some <abbr title="Hyper-text Mark-up Language">HTML</abbr> examples (<img height="20px" src="baz.png" alt="base test sprite"/>) + ]]> + </title> + <id>urn:uuid:1424967a-280a-414d-b0ab-8b11c4ac1bb7</id> + <updated>2010-09-02T18:30:02Z</updated> + <summary>Some text.</summary> + </entry> + +</feed> diff --git a/comm/suite/browser/test/mochitest/ctxmenu-image.png b/comm/suite/browser/test/mochitest/ctxmenu-image.png Binary files differnew file mode 100644 index 0000000000..4c3be50847 --- /dev/null +++ b/comm/suite/browser/test/mochitest/ctxmenu-image.png diff --git a/comm/suite/browser/test/mochitest/feed_discovery.html b/comm/suite/browser/test/mochitest/feed_discovery.html new file mode 100644 index 0000000000..80c35d19ab --- /dev/null +++ b/comm/suite/browser/test/mochitest/feed_discovery.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=377611 +--> + <head> + <title>Test for feed discovery</title> + + <!-- Straight up standard --> + <link rel="alternate" type="application/atom+xml" title="1" href="/1.atom" /> + <link rel="alternate" type="application/rss+xml" title="2" href="/2.rss" /> + <link rel="feed" title="3" href="/3.xml" /> + + <!-- rel is a space-separated list --> + <link rel=" alternate " type="application/atom+xml" title="4" href="/4.atom" /> + <link rel="foo alternate" type="application/atom+xml" title="5" href="/5.atom" /> + <link rel="alternate foo" type="application/atom+xml" title="6" href="/6.atom" /> + <link rel="foo alternate foo" type="application/atom+xml" title="7" href="/7.atom" /> + <link rel="meat feed cake" title="8" href="/8.atom" /> + + <!-- rel is case-insensitive --> + <link rel="ALTERNate" type="application/atom+xml" title="9" href="/9.atom" /> + <link rel="fEEd" title="10" href="/10.atom" /> + + <!-- type can have leading and trailing whitespace --> + <link rel="alternate" type=" application/atom+xml " title="11" href="/11.atom" /> + + <!-- type is case-insensitive --> + <link rel="alternate" type="aPPliCAtion/ATom+xML" title="12" href="/12.atom" /> + + <!-- "feed stylesheet" is a feed, though "alternate stylesheet" isn't --> + <link rel="feed stylesheet" title="13" href="/13.atom" /> + + <!-- hyphens or letters around rel not allowed --> + <link rel="disabled-alternate" type="application/atom+xml" title="Bogus1" href="/Bogus1" /> + <link rel="alternates" type="application/atom+xml" title="Bogus2" href="/Bogus2" /> + <link rel=" alternate-like" type="application/atom+xml" title="Bogus3" href="/Bogus3" /> + + <!-- don't tolerate text/xml if title includes 'rss' not as a word --> + <link rel="alternate" type="text/xml" title="Bogus4 scissorsshaped" href="/Bogus4" /> + + <!-- don't tolerate application/xml if title includes 'rss' not as a word --> + <link rel="alternate" type="application/xml" title="Bogus5 scissorsshaped" href="/Bogus5" /> + + <!-- don't tolerate application/rdf+xml if title includes 'rss' not as a word --> + <link rel="alternate" type="application/rdf+xml" title="Bogus6 scissorsshaped" href="/Bogus6" /> + + <!-- don't tolerate random types --> + <link rel="alternate" type="text/plain" title="Bogus7 rss" href="/Bogus7" /> + + <!-- don't find Atom by title --> + <link rel="foopy" type="application/atom+xml" title="Bogus8 Atom and RSS" href="/Bogus8" /> + + <!-- don't find application/rss+xml by title --> + <link rel="goats" type="application/rss+xml" title="Bogus9 RSS and Atom" href="/Bogus9" /> + + <!-- don't find application/rdf+xml by title --> + <link rel="alternate" type="application/rdf+xml" title="Bogus10 RSS and Atom" href="/Bogus10" /> + + <!-- don't find application/xml by title --> + <link rel="alternate" type="application/xml" title="Bogus11 RSS and Atom" href="/Bogus11" /> + + <!-- don't find text/xml by title --> + <link rel="alternate" type="text/xml" title="Bogus12 RSS and Atom" href="/Bogus12" /> + + <!-- alternate and stylesheet isn't a feed --> + <link rel="alternate stylesheet" type="application/rss+xml" title="Bogus13 RSS" href="/Bogus13" /> + </head> + <body> + <script> + window.onload = function() { + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + + var tests = new Array(); + + var currentWindow = + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + var browserSH = currentWindow.XULBrowserWindow; + + var discovered = browserSH.feeds; + tests.push({ check: discovered.length > 0, + message: "some feeds should be discovered" }); + + var feeds = []; + + for (var aFeed of discovered) { + feeds[aFeed.href] = true; + } + + for (var aLink of document.getElementsByTagName("link")) { + // ignore real stylesheets, and anything without an href property + if (aLink.type != "text/css" && aLink.href) { + if (/bogus/i.test(aLink.title)) { + tests.push({ check: !feeds[aLink.href], todo: /todo/i.test(aLink.title), + message: "don't discover " + aLink.href }); + } else { + tests.push({ check: feeds[aLink.href], todo: /todo/i.test(aLink.title), + message: "should discover " + aLink.href }); + } + } + } + window.arguments[0].tests = tests; + window.close(); + } + </script> + </body> +</html> diff --git a/comm/suite/browser/test/mochitest/mochitest.ini b/comm/suite/browser/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..99b25bba9d --- /dev/null +++ b/comm/suite/browser/test/mochitest/mochitest.ini @@ -0,0 +1,17 @@ +[DEFAULT] +support-files = + valid-feed.xml + valid-unsniffable-feed.xml + +[test_bug364677.html] +support-files = bug364677-data.xml bug364677-data.xml^headers^ +[test_bug395533.html] +support-files = bug395533-data.txt +[test_bug436801.html] +support-files = bug436801-data.xml +[test_contextmenu.html] +support-files = audio.ogg ctxmenu-image.png subtst_contextmenu.html video.ogg +skip-if = os != "win" # disabled on Linux due to bug 513558, on Mac after 10.6 due to bug 792304 +[test_feed_discovery.html] +support-files = feed_discovery.html +[test_registerHandler.html] diff --git a/comm/suite/browser/test/mochitest/subtst_contextmenu.html b/comm/suite/browser/test/mochitest/subtst_contextmenu.html new file mode 100644 index 0000000000..63cc70c84e --- /dev/null +++ b/comm/suite/browser/test/mochitest/subtst_contextmenu.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Subtest for browser context menu</title> +</head> +<body> +Browser context menu subtest. + +<div id="test-text">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div> +<a id="test-link" href="http://mozilla.com">Click the monkey!</a> +<a id="test-mailto" href="mailto:codemonkey@mozilla.com">Mail the monkey!</a><br> +<input id="test-input"><br> +<input id="test-input-select"><br> +<img id="test-image" src="ctxmenu-image.png"> +<canvas id="test-canvas" width="100" height="100" style="background-color: blue"></canvas> +<video controls id="test-video-ok" src="video.ogg" width="100" height="100" style="background-color: green"></video> +<video id="test-audio-in-video" src="audio.ogg" width="100" height="100" style="background-color: red"></video> +<video controls id="test-video-bad" src="bogus.duh" width="100" height="100" style="background-color: orange"></video> +<video controls id="test-video-bad2" width="100" height="100" style="background-color: yellow"> + <source src="bogus.duh" type="video/durrrr;"> +</video> +<iframe id="test-iframe" width="98" height="98" style="border: 1px solid black"></iframe> +<iframe id="test-video-in-iframe" src="video.ogg" width="98" height="98" style="border: 1px solid black"></iframe> +<iframe id="test-image-in-iframe" src="ctxmenu-image.png" width="98" height="98" style="border: 1px solid black"></iframe> +<textarea id="test-textarea">chssseesbbbie</textarea> <!-- a weird word which generates only one suggestion --> +<textarea id="test-textarea-sel">test</textarea> +<div id="test-contenteditable" contenteditable="true">chssseefsbbbie</div> <!-- a more weird word which generates no suggestions --> +<input id="test-input-spellcheck" type="text" spellcheck="true" autofocus value="prodkjfgigrty"> <!-- this one also generates one suggestion --> +<div contextmenu="myMenu"> + <p id="test-pagemenu" hopeless="true">I've got a context menu!</p> + <menu id="myMenu" type="context"> + <menuitem label="Plain item" onclick="document.getElementById('test-pagemenu').removeAttribute('hopeless');"></menuitem> + <menuitem label="Disabled item" disabled></menuitem> + <menuitem> Item w/ textContent</menuitem> + <menu> + <menuitem type="checkbox" label="Checkbox" checked></menuitem> + </menu> + <menu> + <menuitem type="radio" label="Radio1" checked></menuitem> + <menuitem type="radio" label="Radio2"></menuitem> + <menuitem type="radio" label="Radio3"></menuitem> + </menu> + <menu> + <menuitem label="Item w/ icon" icon="favicon.ico"></menuitem> + <menuitem label="Item w/ bad icon" icon="data://www.mozilla.org/favicon.ico"></menuitem> + </menu> + <menu label="Submenu"> + <menuitem type="radio" label="Radio1" radiogroup="rg"></menuitem> + <menuitem type="radio" label="Radio2" checked radiogroup="rg"></menuitem> + <menuitem type="radio" label="Radio3" radiogroup="rg"></menuitem> + <menu> + <menuitem type="checkbox" label="Checkbox"></menuitem> + </menu> + </menu> + <menu hidden> + <menuitem label="Bogus item"></menuitem> + </menu> + <menu> + </menu> + <menuitem label="Hidden item" hidden></menuitem> + <menuitem></menuitem> + </menu> +</div> + +<!-- SeaMonkey specific elements --> +<a href="http://mozilla.com"><img id="test-image-link" alt="Click the monkey!" src="ctxmenu-image.png"></a> +<a href="mailto:codemonkey@mozilla.com"><img id="test-image-mailto" alt="Mail the monkey!" src="ctxmenu-image.png"></a><br> + +</body> +</html> diff --git a/comm/suite/browser/test/mochitest/test_bug364677.html b/comm/suite/browser/test/mochitest/test_bug364677.html new file mode 100644 index 0000000000..e08b1403f6 --- /dev/null +++ b/comm/suite/browser/test/mochitest/test_bug364677.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=364677 +--> +<head> + <title>Test for Bug 364677</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=364677">Mozilla Bug 364677</a> +<p id="display"><iframe id="testFrame" src="bug364677-data.xml"></iframe></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody"> + +/** Test for Bug 364677 **/ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + is(SpecialPowers.wrap($("testFrame")).contentDocument.documentElement.id, "feedHandler", + "Feed served as text/xml without a channel/link should have been sniffed"); +}); +addLoadEvent(SimpleTest.finish); +</script> +</pre> +</body> +</html> + diff --git a/comm/suite/browser/test/mochitest/test_bug395533.html b/comm/suite/browser/test/mochitest/test_bug395533.html new file mode 100644 index 0000000000..5d3cfa0121 --- /dev/null +++ b/comm/suite/browser/test/mochitest/test_bug395533.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=395533 +--> +<head> + <title>Test for Bug 395533</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=395533">Mozilla Bug 395533</a> +<p id="display"><iframe id="testFrame" src="bug395533-data.txt"></iframe></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody"> + +/** Test for Bug 395533 **/ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // Need privs because the feed seems to have an about:feeds principal or some + // such. It's not same-origin with us in any case. + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + is($("testFrame").contentDocument.documentElement.id, "", + "Text got sniffed as a feed?"); +}); +addLoadEvent(SimpleTest.finish); + + + + +</script> +</pre> +</body> +</html> + diff --git a/comm/suite/browser/test/mochitest/test_bug436801.html b/comm/suite/browser/test/mochitest/test_bug436801.html new file mode 100644 index 0000000000..6ca1b83cf5 --- /dev/null +++ b/comm/suite/browser/test/mochitest/test_bug436801.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=436801 +--> +<head> + <title>Test feed preview subscribe UI</title> + <script src="/MochiKit/packed.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=436801">Mozilla Bug 436801</a> +<p id="display"><iframe id="testFrame" src="bug436801-data.xml"></iframe></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function () { + var doc = SpecialPowers.wrap($("testFrame")).contentDocument; + + checkNode(doc.getElementById("feedTitleText"), [ + "ELEMENT", "h1", { "xml:base": "http://www.example.com/foo/bar/" }, [ + ["TEXT", "Example of a "], + ["ELEMENT", "em", [ + ["TEXT", "special"], + ]], + ["TEXT", " feed ("], + ["ELEMENT", "img", { "src": "baz.png" }], + ["TEXT", ")"], + ] + ]); + + checkNode(doc.getElementById("feedSubtitleText"), [ + "ELEMENT", "h2", { "xml:base": "http://www.example.com/foo/bar/" }, [ + ["TEXT", "With a "], + ["ELEMENT", "em", [ + ["TEXT", "special"], + ]], + ["TEXT", " subtitle ("], + ["ELEMENT", "img", { "src": "baz.png" }], + ["TEXT", ")"], + ] + ]); + + checkNode(doc.querySelector(".entry").firstChild.firstChild.firstChild, [ + "ELEMENT", "span", { "xml:base": "http://www.example.com/foo/bar/" }, [ + ["TEXT", "Some "], + ["ELEMENT", "abbr", { title: "Extensible Hyper-text Mark-up Language" }, [ + ["TEXT", "XHTML"], + ]], + ["TEXT", " examples ("], + ["ELEMENT", "img", { "src": "baz.png" }], + ["TEXT", ")"], + ] + ]); + + checkNode(doc.querySelectorAll(".entry")[1].firstChild.firstChild.firstChild, [ + "ELEMENT", "span", { "xml:base": "http://www.example.com/foo/bar/" }, [ + ["TEXT", "Some "], + ["ELEMENT", "abbr", { title: "Hyper-text Mark-up Language" }, [ + ["TEXT", "HTML"], + ]], + ["TEXT", " examples ("], + ["ELEMENT", "img", { "src": "baz.png" }], + ["TEXT", ")"], + ] + ]); +}); + +addLoadEvent(SimpleTest.finish); + +function checkNode(node, schema) { + var typeName = schema.shift() + "_NODE"; + var type = Node[typeName]; + is(node.nodeType, type, "Node should be expected type " + typeName); + if (type == Node.TEXT_NODE) { + var text = schema.shift(); + is(node.data, text, "Text should match"); + return; + } + // type == Node.ELEMENT_NODE + var tag = schema.shift(); + is(node.localName, tag, "Element should have expected tag"); + while (schema.length) { + var val = schema.shift(); + if (Array.isArray(val)) + var childSchema = val; + else + var attrSchema = val; + } + if (attrSchema) { + var nsTable = { + xml: "http://www.w3.org/XML/1998/namespace", + }; + for (var name in attrSchema) { + var [ns, nsName] = name.split(":"); + var val = nsName ? node.getAttributeNS(nsTable[ns], nsName) : + node.getAttribute(name); + is(val, attrSchema[name], "Attribute " + name + " should match"); + } + } + if (childSchema) { + var numChildren = node.childNodes.length; + is(childSchema.length, numChildren, + "Element should have expected number of children"); + for (var i = 0; i < numChildren; i++) + checkNode(node.childNodes[i], childSchema[i]); + } +} + +</script> +</pre> +</body> +</html> diff --git a/comm/suite/browser/test/mochitest/test_contextmenu.html b/comm/suite/browser/test/mochitest/test_contextmenu.html new file mode 100644 index 0000000000..0dea49c925 --- /dev/null +++ b/comm/suite/browser/test/mochitest/test_contextmenu.html @@ -0,0 +1,931 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tests for browser context menu</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Browser context menu tests. +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody"> + +/** Test for Login Manager: multiple login autocomplete. **/ + +SpecialPowers.ChromeUtils.import("resource://gre/modules/InlineSpellChecker.jsm", window); +SpecialPowers.ChromeUtils.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", window); + +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; + +function openContextMenuFor(element, shiftkey, waitForSpellCheck) { + // Context menu should be closed before we open it again. + is(SpecialPowers.wrap(contextMenu).state, "closed", "checking if popup is closed"); + + if (lastElement) + lastElement.blur(); + element.focus(); + + // Some elements need time to focus and spellcheck before any tests are + // run on them. + function actuallyOpenContextMenuFor() { + lastElement = element; + var eventDetails = { type : "contextmenu", button : 2, shiftKey : shiftkey }; + synthesizeMouse(element, 2, 2, eventDetails, element.ownerDocument.defaultView); + } + + if (waitForSpellCheck) + onSpellCheck(element, actuallyOpenContextMenuFor); + else + actuallyOpenContextMenuFor(); +} + +function closeContextMenu() { + contextMenu.hidePopup(); +} + +function executeCopyCommand(command, expectedValue) +{ + // Just execute the command directly rather than simulating a context menu + // press to avoid having to deal with its asynchronous nature + SpecialPowers.wrap(subwindow).controllers.getControllerForCommand(command).doCommand(command); + + // The easiest way to check the clipboard is to paste the contents into a + // textbox + input.focus(); + input.value = ""; + SpecialPowers.wrap(input).controllers.getControllerForCommand("cmd_paste").doCommand("cmd_paste"); + is(input.value, expectedValue, "paste for command " + command); +} + +function invokeItemAction(generatedItemId) +{ + var item = contextMenu.getElementsByAttribute("generateditemid", + generatedItemId)[0]; + ok(item, "Got generated XUL menu item"); + item.doCommand(); + is(pagemenu.hasAttribute("hopeless"), false, "attribute got removed"); +} + +function getVisibleMenuItems(aMenu, aData) { + var items = []; + var accessKeys = {}; + for (var i = 0; i < aMenu.childNodes.length; i++) { + var item = aMenu.childNodes[i]; + if (item.hidden) + continue; + + var key = item.accessKey; + if (key) + key = key.toLowerCase(); + + var isGenerated = item.hasAttribute("generateditemid"); + + if (item.nodeName == "menuitem") { + var isSpellSuggestion = item.className == "spell-suggestion"; + if (isSpellSuggestion) { + is(item.id, "", "child menuitem #" + i + " is a spelling suggestion"); + } else if (isGenerated) { + is(item.id, "", "child menuitem #" + i + " is a generated item"); + } else { + ok(item.id, "child menuitem #" + i + " has an ID"); + } + var label = item.getAttribute("label"); + ok(label.length, "menuitem " + item.id + " has a label"); + if (isSpellSuggestion) { + is(key, "", "Spell suggestions shouldn't have an access key"); + items.push("*" + label); + } else if (isGenerated) { + items.push("+" + label); + } else if (item.id.indexOf("spell-check-dictionary-") != 0 && + item.id != "spell-no-suggestions") { + ok(key, "menuitem " + item.id + " has an access key"); + if (accessKeys[key]) + ok(false, "menuitem " + item.id + " has same accesskey as " + accessKeys[key]); + else + accessKeys[key] = item.id; + } + if (!isSpellSuggestion && !isGenerated) { + items.push(item.id); + } + if (isGenerated) { + var p = {}; + p.type = item.getAttribute("type"); + p.icon = item.getAttribute("image"); + p.checked = item.hasAttribute("checked"); + p.disabled = item.hasAttribute("disabled"); + items.push(p); + } else { + items.push(!item.disabled); + } + } else if (item.nodeName == "menuseparator") { + ok(true, "--- seperator id is " + item.id); + items.push("---"); + items.push(null); + } else if (item.nodeName == "menu") { + if (isGenerated) { + item.id = "generated-submenu-" + aData.generatedSubmenuId++; + } + ok(item.id, "child menu #" + i + " has an ID"); + if (!isGenerated) { + ok(key, "menu has an access key"); + if (accessKeys[key]) + ok(false, "menu " + item.id + " has same accesskey as " + accessKeys[key]); + else + accessKeys[key] = item.id; + } + items.push(item.id); + items.push(!item.disabled); + // Add a dummy item to that the indexes in checkMenu are the same + // for expectedItems and actualItems. + items.push([]); + items.push(null); + } else { + ok(false, "child #" + i + " of menu ID " + aMenu.id + + " has an unknown type (" + item.nodeName + ")"); + } + } + return items; +} + +function checkContextMenu(expectedItems) { + is(contextMenu.state, "open", "checking if popup is open"); + var data = { generatedSubmenuId: 1 }; + checkMenu(contextMenu, expectedItems, data); +} + +/* + * checkMenu - checks to see if the specified <menupopup> contains the + * expected items and state. + * expectedItems is a array of (1) item IDs and (2) a boolean specifying if + * the item is enabled or not (or null to ignore it). Submenus can be checked + * by providing a nested array entry after the expected <menu> ID. + * For example: ["blah", true, // item enabled + * "submenu", null, // submenu + * ["sub1", true, // submenu contents + * "sub2", false], null, // submenu contents + * "lol", false] // item disabled + * + */ +function checkMenu(menu, expectedItems, data) { + var actualItems = getVisibleMenuItems(menu, data); + //ok(false, "Items are: " + actualItems); + for (var i = 0; i < expectedItems.length; i+=2) { + var actualItem = actualItems[i]; + var actualEnabled = actualItems[i + 1]; + var expectedItem = expectedItems[i]; + var expectedEnabled = expectedItems[i + 1]; + if (expectedItem instanceof Array) { + ok(true, "Checking submenu..."); + var menuID = expectedItems[i - 2]; // The last item was the menu ID. + var submenu = menu.getElementsByAttribute("id", menuID)[0]; + ok(submenu && submenu.nodeName == "menu", "got expected submenu element"); + checkMenu(submenu.menupopup, expectedItem, data); + } else { + is(actualItem, expectedItem, + "checking item #" + i/2 + " (" + expectedItem + ") name"); + + if (typeof expectedEnabled == "object" && expectedEnabled != null || + typeof actualEnabled == "object" && actualEnabled != null) { + + ok(!(actualEnabled == null), "actualEnabled is not null"); + ok(!(expectedEnabled == null), "expectedEnabled is not null"); + is(typeof actualEnabled, typeof expectedEnabled, "checking types"); + + if (typeof actualEnabled != typeof expectedEnabled || + actualEnabled == null || expectedEnabled == null) + continue; + + is(actualEnabled.type, expectedEnabled.type, + "checking item #" + i/2 + " (" + expectedItem + ") type attr value"); + var icon = actualEnabled.icon; + if (icon) { + var tmp = ""; + var j = icon.length - 1; + while (j && icon[j] != "/") { + tmp = icon[j--] + tmp; + } + icon = tmp; + } + is(icon, expectedEnabled.icon, + "checking item #" + i/2 + " (" + expectedItem + ") icon attr value"); + is(actualEnabled.checked, expectedEnabled.checked, + "checking item #" + i/2 + " (" + expectedItem + ") has checked attr"); + is(actualEnabled.disabled, expectedEnabled.disabled, + "checking item #" + i/2 + " (" + expectedItem + ") has disabled attr"); + } else if (expectedEnabled != null) + is(actualEnabled, expectedEnabled, + "checking item #" + i/2 + " (" + expectedItem + ") enabled state"); + } + } + // Could find unexpected extra items at the end... + is(actualItems.length, expectedItems.length, "checking expected number of menu entries"); +} + +/* + * runTest + * + * Called by a popupshowing event handler. Each test checks for expected menu + * contents, closes the popup, and finally triggers the popup on a new element + * (thus kicking off another cycle). + * + */ +function runTest(testNum) { + ok(true, "Starting test #" + testNum); + + switch (testNum) { + case 1: + // Invoke context menu for next test. + openContextMenuFor(text); + break; + + case 2: + // Context menu for plain text + plainTextItems = ["context-back", false, + "context-forward", false, + "context-reload", true, + "context-stop", false, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "context-sendpage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "---", null, + "context-viewsource", true, + "context-viewinfo", true]; + checkContextMenu(plainTextItems); + closeContextMenu(); + openContextMenuFor(link); // Invoke context menu for next test. + break; + + case 3: + // Context menu for text link + checkContextMenu(["context-openlinkintab", true, + "context-openlink", true, + "context-openlinkinprivatewindow", true, + "---", null, + "context-bookmarklink", true, + "context-savelink", true, + "context-sendlink", true, + "context-copylink", true, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "---", null, + "context-metadata", true]); + closeContextMenu(); + openContextMenuFor(mailto); // Invoke context menu for next test. + break; + + case 4: + // Context menu for text mailto-link + checkContextMenu(["context-copyemail", true, + "context-copylink", true, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "---", null, + "context-metadata", true]); + closeContextMenu(); + openContextMenuFor(input); // Invoke context menu for next test. + break; + + case 5: + // Context menu for text input field + checkContextMenu(["context-undo", false, + "context-redo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", false, + "---", null, + "spell-check-enabled", true]); + closeContextMenu(); + input_sel.value = "test"; + input_sel.select(); + openContextMenuFor(input_sel); // Invoke context menu for next test. + break; + + case 6: + // Context menu for text input field with text + checkContextMenu(["context-undo", false, + "context-redo", false, + "---", null, + "context-cut", true, + "context-copy", true, + "context-paste", null, // ignore clipboard state + "context-delete", true, + "---", null, + "context-selectall", true, + "context-searchselect", true, + "---", null, + "spell-check-enabled", true]); + closeContextMenu(); + openContextMenuFor(img); // Invoke context menu for next test. + break; + + case 7: + // Context menu for an image + checkContextMenu(["context-reloadimage", true, + "context-viewimage", true, + "context-blockimage", true, + "context-copyimage", true, + "---", null, + "context-saveimage", true, + "context-sendimage", true].concat( + ("@mozilla.org/suite/shell-service;1" in Cc) ? + ["context-setDesktopBackground", true] : []).concat( + ["---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "context-sendpage", true, + "---", null, + "context-metadata", true])); + closeContextMenu(); + openContextMenuFor(canvas); // Invoke context menu for next test. + break; + + case 8: + // Context menu for a canvas + checkContextMenu(["context-viewimage", true, + "context-saveimage", true, + "context-bookmarkpage", true, + "context-sendpage", true, + "context-selectall", true]); + closeContextMenu(); + openContextMenuFor(video_ok); // Invoke context menu for next test. + break; + + case 9: + // Context menu for a video (with a VALID media source) + checkContextMenu(["context-media-play", true, + "context-media-mute", true, + "context-media-playbackrate", true, + ["context-media-playbackrate-050", true, + "context-media-playbackrate-100", true, + "context-media-playbackrate-150", true, + "context-media-playbackrate-200", true], null, + "context-media-hidecontrols", true, + "context-video-showstats", true, + "context-video-fullscreen", true, + "---", null, + "context-viewvideo", true, + "context-copyvideourl", true, + "---", null, + "context-savevideo", true, + "context-sendvideo", true, + "context-video-saveimage", true]); + closeContextMenu(); + openContextMenuFor(audio_in_video); // Invoke context menu for next test. + break; + + case 10: + // Context menu for a video (with an audio-only file) + checkContextMenu(["context-media-play", true, + "context-media-mute", true, + "context-media-playbackrate", true, + ["context-media-playbackrate-050", true, + "context-media-playbackrate-100", true, + "context-media-playbackrate-150", true, + "context-media-playbackrate-200", true], null, + "context-media-showcontrols", true, + "---", null, + "context-copyaudiourl", true, + "---", null, + "context-saveaudio", true, + "context-sendaudio", true]); + closeContextMenu(); + openContextMenuFor(video_bad); // Invoke context menu for next test. + break; + + case 11: + // Context menu for a video (with an INVALID media source) + checkContextMenu(["context-media-play", false, + "context-media-mute", false, + "context-media-playbackrate", false, + ["context-media-playbackrate-050", null, + "context-media-playbackrate-100", null, + "context-media-playbackrate-150", null, + "context-media-playbackrate-200", null], null, + "context-media-hidecontrols", false, + "context-video-showstats", false, + "context-video-fullscreen", false, + "---", null, + "context-viewvideo", true, + "context-copyvideourl", true, + "---", null, + "context-savevideo", true, + "context-sendvideo", true, + "context-video-saveimage", false]); + closeContextMenu(); + openContextMenuFor(video_bad2); // Invoke context menu for next test. + break; + + case 12: + // Context menu for a video (with an INVALID media source) + checkContextMenu(["context-media-play", false, + "context-media-mute", false, + "context-media-playbackrate", false, + ["context-media-playbackrate-050", null, + "context-media-playbackrate-100", null, + "context-media-playbackrate-150", null, + "context-media-playbackrate-200", null], null, + "context-media-hidecontrols", false, + "context-video-showstats", false, + "context-video-fullscreen", false, + "---", null, + "context-viewvideo", false, + "context-copyvideourl", false, + "---", null, + "context-savevideo", false, + "context-sendvideo", false, + "context-video-saveimage", false]); + closeContextMenu(); + openContextMenuFor(iframe); // Invoke context menu for next test. + break; + + case 13: + // Context menu for an iframe + checkContextMenu(["context-back", false, + "context-forward", false, + "context-reload", true, + "context-stop", false, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "context-sendpage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "---", null, + "context-viewsource", true, + "context-viewinfo", true, + "---", null, + "frame", null, + ["context-showonlythisframe", true, + "context-openframeintab", true, + "context-openframe", true, + "---", null, + "context-reloadframe", true, + "---", null, + "context-bookmarkframe", true, + "context-saveframe", true, + "context-sendframe", true, + "---", null, + "context-viewframesource", true, + "context-viewframeinfo", true], null]); + closeContextMenu(); + openContextMenuFor(video_in_iframe); // Invoke context menu for next test. + break; + + case 14: + // Context menu for a video in an iframe + checkContextMenu(["context-media-play", true, + "context-media-mute", true, + "context-media-playbackrate", true, + ["context-media-playbackrate-050", true, + "context-media-playbackrate-100", true, + "context-media-playbackrate-150", true, + "context-media-playbackrate-200", true], null, + "context-media-hidecontrols", true, + "context-video-showstats", true, + "context-video-fullscreen", true, + "---", null, + "context-viewvideo", true, + "context-copyvideourl", true, + "---", null, + "context-savevideo", true, + "context-sendvideo", true, + "context-video-saveimage", true, + "---", null, + "frame", null, + ["context-showonlythisframe", true, + "context-openframeintab", true, + "context-openframe", true, + "---", null, + "context-reloadframe", true, + "---", null, + "context-bookmarkframe", true, + "context-saveframe", true, + "context-sendframe", true, + "---", null, + "context-viewframesource", true, + "context-viewframeinfo", true], null]); + closeContextMenu(); + openContextMenuFor(image_in_iframe); // Invoke context menu for next test. + break; + + case 15: + // Context menu for an image in an iframe + checkContextMenu(["context-reloadimage", true, + "context-viewimage", true, + "context-blockimage", true, + "context-copyimage", true, + "---", null, + "context-saveimage", true, + "context-sendimage", true, + ].concat( + ("@mozilla.org/suite/shell-service;1" in Cc) ? + ["context-setDesktopBackground", true] : []) + .concat( + ["---", null, + "context-sendpage", true, + "---", null, + "context-metadata", true, + "---", null, + "frame", null, + ["context-showonlythisframe", true, + "context-openframeintab", true, + "context-openframe", true, + "---", null, + "context-reloadframe", true, + "---", null, + "context-bookmarkframe", true, + "context-saveframe", true, + "context-sendframe", true, + "---", null, + "context-viewframesource", true, + "context-viewframeinfo", true], null])); + closeContextMenu(); + openContextMenuFor(text); // Invoke context menu for next test. + break; + + case 16: + // Re-check context menu for plain text to make sure it hasn't changed + checkContextMenu(plainTextItems); + closeContextMenu(); + textarea_sel.select(); + openContextMenuFor(textarea_sel, false, true); + break; + + case 17: + // search for text with text area's selected value + checkContextMenu(["context-undo", false, + "context-redo", false, + "---", null, + "context-cut", true, + "context-copy", true, + "context-paste", null, // ignore clipboard state + "context-delete", true, + "---", null, + "context-selectall", true, + "context-searchselect", true, + "---", null, + "spell-check-enabled", true, + "spell-dictionaries", true, + ["spell-check-dictionary-en-US", true, + "---", null, + "spell-add-dictionaries", true], null]); + closeContextMenu(); + openContextMenuFor(textarea, false, true); // Invoke context menu for next test, but wait for the spellcheck. + break; + + case 18: + // Context menu for textarea + checkContextMenu(["*chubbiness", true, // spelling suggestion + "---", null, + "spell-add-to-dictionary", true, + "spell-ignore-word", true, + "---", null, + "context-undo", false, + "context-redo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", true, + "---", null, + "spell-check-enabled", true, + "spell-dictionaries", true, + ["spell-check-dictionary-en-US", true, + "---", null, + "spell-add-dictionaries", true], null]); + + contextMenu.ownerDocument.getElementById("spell-add-to-dictionary").doCommand(); // Add to dictionary + closeContextMenu(); + openContextMenuFor(textarea, false, true); // Invoke context menu for next test. + break; + + case 19: + // Context menu for textarea after a word has been added + // to the dictionary + checkContextMenu(["spell-undo-add-to-dictionary", true, + "---", null, + "context-undo", false, + "context-redo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", true, + "---", null, + "spell-check-enabled", true, + "spell-dictionaries", true, + ["spell-check-dictionary-en-US", true, + "---", null, + "spell-add-dictionaries", true], null, + ]); + + contextMenu.ownerDocument.getElementById("spell-undo-add-to-dictionary").doCommand(); // Undo add to dictionary + closeContextMenu(); + openContextMenuFor(contenteditable, false, true); + break; + + case 20: + // Context menu for contenteditable + checkContextMenu(["spell-no-suggestions", false, + "---", null, + "spell-add-to-dictionary", true, + "spell-ignore-word", true, + "---", null, + "context-undo", false, + "context-redo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", true, + "---", null, + "spell-check-enabled", true, + "spell-dictionaries", true, + ["spell-check-dictionary-en-US", true, + "---", null, + "spell-add-dictionaries", true], null]); + + closeContextMenu(); + openContextMenuFor(inputspell, false, true); // Invoke context menu for next test. + break; + + case 21: + // Context menu for spell-check input + checkContextMenu(["*prodigality", true, // spelling suggestion + "---", null, + "spell-add-to-dictionary", true, + "spell-ignore-word", true, + "---", null, + "context-undo", false, + "context-redo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", true, + "---", null, + "spell-check-enabled", true, + "spell-dictionaries", true, + ["spell-check-dictionary-en-US", true, + "---", null, + "spell-add-dictionaries", true], null]); + + closeContextMenu(); + openContextMenuFor(link); // Invoke context menu for next test. + break; + + case 22: + executeCopyCommand("cmd_copyLink", "http://mozilla.com/"); + closeContextMenu(); + openContextMenuFor(pagemenu); // Invoke context menu for next test. + break; + + case 23: + // Context menu for element with assigned content context menu + checkContextMenu(["+Plain item", {type: "", icon: "", checked: false, disabled: false}, + "+Disabled item", {type: "", icon: "", checked: false, disabled: true}, + "+Item w/ textContent", {type: "", icon: "", checked: false, disabled: false}, + "---", null, + "+Checkbox", {type: "checkbox", icon: "", checked: true, disabled: false}, + "---", null, + "+Radio1", {type: "checkbox", icon: "", checked: true, disabled: false}, + "+Radio2", {type: "checkbox", icon: "", checked: false, disabled: false}, + "+Radio3", {type: "checkbox", icon: "", checked: false, disabled: false}, + "---", null, + "+Item w/ icon", {type: "", icon: "favicon.ico", checked: false, disabled: false}, + "+Item w/ bad icon", {type: "", icon: "", checked: false, disabled: false}, + "---", null, + "generated-submenu-1", true, + ["+Radio1", {type: "checkbox", icon: "", checked: false, disabled: false}, + "+Radio2", {type: "checkbox", icon: "", checked: true, disabled: false}, + "+Radio3", {type: "checkbox", icon: "", checked: false, disabled: false}, + "---", null, + "+Checkbox", {type: "checkbox", icon: "", checked: false, disabled: false}], null, + "---", null, + "context-back", false, + "context-forward", false, + "context-reload", true, + "context-stop", false, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "context-sendpage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "---", null, + "context-viewsource", true, + "context-viewinfo", true]); + + invokeItemAction("0"); + closeContextMenu(); + openContextMenuFor(pagemenu, true); // Invoke context menu for next test. + break; + + case 24: + // Context menu for element with assigned content context menu + // The shift key should bypass content context menu processing + checkContextMenu(["context-back", false, + "context-forward", false, + "context-reload", true, + "context-stop", false, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "context-sendpage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "---", null, + "context-viewsource", true, + "context-viewinfo", true]); + closeContextMenu(); + + // Continue with SeaMonkey specific cases. + openContextMenuFor(img_link); // Invoke context menu for next test. + break; + + case 25: + // Context menu for an image with a link + checkContextMenu(["context-openlinkintab", true, + "context-openlink", true, + "context-openlinkinprivatewindow", true, + "---", null, + "context-bookmarklink", true, + "context-savelink", true, + "context-sendlink", true, + "context-copylink", true, + "---", null, + "context-reloadimage", true, + "context-viewimage", true, + "context-blockimage", true, + "context-copyimage", true, + "---", null, + "context-saveimage", true, + "context-sendimage", true].concat( + ("@mozilla.org/suite/shell-service;1" in Cc) ? + ["context-setDesktopBackground", true] : []).concat( + ["---", null, + "context-bookmarkpage", true, + "---", null, + "context-metadata", true])); + closeContextMenu(); + openContextMenuFor(img_mailto); // Invoke context menu for next test. + break; + + case 26: + // Context menu for an image with a mailto: link + checkContextMenu(["context-copyemail", true, + "context-copylink", true, + "---", null, + "context-reloadimage", true, + "context-viewimage", true, + "context-blockimage", true, + "context-copyimage", true, + "---", null, + "context-saveimage", true, + "context-sendimage", true].concat( + ("@mozilla.org/suite/shell-service;1" in Cc) ? + ["context-setDesktopBackground", true] : []).concat( + ["---", null, + "context-bookmarkpage", true, + "---", null, + "context-metadata", true])); + closeContextMenu(); + + subwindow.close(); + SimpleTest.finish(); + return; + + /* + * Other things that would be nice to test: + * - selected text + * - spelling / misspelled word (in text input?) + * - check state of disabled items + * - test execution of menu items (maybe as a separate test?) + */ + + default: + ok(false, "Unexpected invocation of test #" + testNum); + subwindow.close(); + SimpleTest.finish(); + return; + } + +} + + +var testNum = 1; +var subwindow, chromeWin, contextMenu, lastElement; +var text, link, mailto, input, img, canvas, video_ok, video_bad, video_bad2, + iframe, video_in_iframe, image_in_iframe, textarea, contenteditable, + inputspell, pagemenu, audio_in_video; + +// SeaMonkey specific variables. +var img_link, img_mailto; + +function startTest() { + chromeWin = SpecialPowers.wrap(subwindow) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + contextMenu = chromeWin.document.getElementById("contentAreaContextMenu"); + ok(contextMenu, "Got context menu XUL"); + + if (chromeWin.document.getElementById("context-stop").getAttribute("disabled") != "true") { + isnot(false, "Document still loading"); + SimpleTest.executeSoon(startTest); + return; + } + + lastElement = null; + + text = subwindow.document.getElementById("test-text"); + link = subwindow.document.getElementById("test-link"); + mailto = subwindow.document.getElementById("test-mailto"); + input = subwindow.document.getElementById("test-input"); + input_sel = subwindow.document.getElementById("test-input-select"); + img = subwindow.document.getElementById("test-image"); + canvas = subwindow.document.getElementById("test-canvas"); + video_ok = subwindow.document.getElementById("test-video-ok"); + audio_in_video = subwindow.document.getElementById("test-audio-in-video"); + video_bad = subwindow.document.getElementById("test-video-bad"); + video_bad2 = subwindow.document.getElementById("test-video-bad2"); + iframe = subwindow.document.getElementById("test-iframe"); + video_in_iframe = subwindow.document.getElementById("test-video-in-iframe").contentDocument.getElementsByTagName("video")[0]; + // Ensure that contextmenu has 'context-media-play' item when check runs. + video_in_iframe.pause(); + image_in_iframe = subwindow.document.getElementById("test-image-in-iframe").contentDocument.getElementsByTagName("img")[0]; + textarea = subwindow.document.getElementById("test-textarea"); + textarea_sel = subwindow.document.getElementById("test-textarea-sel"); + contenteditable = subwindow.document.getElementById("test-contenteditable"); + contenteditable.focus(); // content editable needs to be focused to enable spellcheck + inputspell = subwindow.document.getElementById("test-input-spellcheck"); + pagemenu = subwindow.document.getElementById("test-pagemenu"); + + // SeaMonkey specific elements. + img_link = subwindow.document.getElementById("test-image-link"); + img_mailto = subwindow.document.getElementById("test-image-mailto"); + + contextMenu.addEventListener("popupshown", function() { runTest(++testNum); }); + runTest(1); +} + +// We open this in a separate window, because the Mochitests run inside a frame. +// The frame causes an extra menu item, and prevents running the test +// standalone (ie, clicking the test name in the Mochitest window) to see +// success/failure messages. +var painted = false, loaded = false; + +function waitForEvents(event) +{ + if (event.type == "MozAfterPaint") + painted = true; + else if (event.type == "load") + loaded = true; + if (painted && loaded) { + subwindow.removeEventListener("MozAfterPaint", waitForEvents); + subwindow.onload = null; + startTest(); + } +} + +var subwindow = window.open("./subtst_contextmenu.html", "contextmenu-subtext", "width=600,height=700"); +subwindow.addEventListener("MozAfterPaint", waitForEvents); +subwindow.onload = waitForEvents; + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/comm/suite/browser/test/mochitest/test_feed_discovery.html b/comm/suite/browser/test/mochitest/test_feed_discovery.html new file mode 100644 index 0000000000..2f2a0a459e --- /dev/null +++ b/comm/suite/browser/test/mochitest/test_feed_discovery.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=240393 +--> +<head> + <title>Test for feed discovery</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377611">Mozilla Bug 377611</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody"> + +/** Tests for bug 240393 (from bug 377611 on Firefox side) **/ + +var rv = { tests: null }; +var testCheckInterval = null; + +function startTest() { + var url = window.location.href.replace(/test_feed_discovery\.html/, + 'feed_discovery.html'); + SpecialPowers.openDialog(window, [url, '', 'dialog=no,width=10,height=10', rv]); + testCheckInterval = window.setInterval(tryIfTestIsFinished, 500); +} + +function tryIfTestIsFinished() { + if (rv.tests) { + window.clearInterval(testCheckInterval); + checkTest(); + } +} + +function checkTest() { + for (var i = 0; i < rv.tests.length; ++ i) { + var test = rv.tests[i]; + if (test.todo) + todo(test.check, test.message); + else + ok(test.check, test.message); + } + SimpleTest.finish(); +} + +window.onload = startTest; + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/comm/suite/browser/test/mochitest/test_registerHandler.html b/comm/suite/browser/test/mochitest/test_registerHandler.html new file mode 100644 index 0000000000..20952ca9ee --- /dev/null +++ b/comm/suite/browser/test/mochitest/test_registerHandler.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=402788 +--> +<head> + <title>Test for Bug 402788</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=402788">Mozilla Bug 402788</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody"> + +/** Test for Bug 402788 **/ + + // return false if an exception has been catched, true otherwise + function testRegisterHandler(aIsProtocol, aTxt, aUri, aTitle) + { + try { + if (aIsProtocol) + navigator.registerProtocolHandler(aTxt, aUri, aTitle); + else + navigator.registerContentHandler(aTxt, aUri, aTitle); + } + catch(e) { + return false; + } + + return true; + } + + ok(navigator.registerProtocolHandler, "navigator.registerProtocolHandler should be defined"); + ok(navigator.registerContentHandler, "navigator.registerContentHandler should be defined"); + + // testing a generic case + is(true, testRegisterHandler(true, "foo", "http://mochi.test:8888/%s", "Foo handler"), "registering a foo protocol handler should work"); + is(true, testRegisterHandler(false, "application/rss+xml", "http://mochi.test:8888/%s", "Foo handler"), "registering a foo content handler should work"); + + // testing with wrong uris + is(false, testRegisterHandler(true, "foo", "http://mochi.test:8888/", "Foo handler"), "a protocol handler uri should contain %s"); + is(false, testRegisterHandler(false, "application/rss+xml", "http://mochi.test:8888/", "Foo handler"), "a content handler uri should contain %s"); + + // the spec says we should not throw here, but it probably needs to be changed + is(false, testRegisterHandler(true, "foo", "foo/%s", "Foo handler"), "a protocol handler uri should be valid"); + is(false, testRegisterHandler(false, "application/rss+xml", "foo/%s", "Foo handler"), "a content handler uri should be valid"); + + // we should only accept to register when the handler has the same host as the current page (bug 402287) + is(false, testRegisterHandler(true, "foo", "http://remotehost:8888/%s", "Foo handler"), "registering a foo protocol handler with a different host should not work"); + is(false, testRegisterHandler(false, "application/rss+xml", "http://remotehost:8888/%s", "Foo handler"), "registering a foo content handler with a different host should not work"); + + // restriction to http(s) for the uri of the handler (bug 401343) + // https should work (http already tested in the generic case) + is(true, testRegisterHandler(true, "foo", "https://mochi.test:8888/%s", "Foo handler"), "registering a foo protocol handler with https scheme should work"); + is(true, testRegisterHandler(false, "application/rss+xml", "https://mochi.test:8888/%s", "Foo handler"), "registering a foo content handler with https scheme should work"); + // ftp should not work + is(false, testRegisterHandler(true, "foo", "ftp://mochi.test:8888/%s", "Foo handler"), "registering a foo protocol handler with ftp scheme should not work"); + is(false, testRegisterHandler(false, "application/rss+xml", "ftp://mochi.test:8888/%s", "Foo handler"), "registering a foo content handler with ftp scheme should not work"); + // chrome should not work + is(false, testRegisterHandler(true, "foo", "chrome://mochi.test:8888/%s", "Foo handler"), "registering a foo protocol handler with chrome scheme should not work"); + is(false, testRegisterHandler(false, "application/rss+xml", "chrome://mochi.test:8888/%s", "Foo handler"), "registering a foo content handler with chrome scheme should not work"); + // foo should not work + is(false, testRegisterHandler(true, "foo", "foo://mochi.test:8888/%s", "Foo handler"), "registering a foo protocol handler with foo scheme should not work"); + is(false, testRegisterHandler(false, "application/rss+xml", "foo://mochi.test:8888/%s", "Foo handler"), "registering a foo content handler with foo scheme should not work"); + + // for security reasons, protocol handlers should never be registered for some schemes (chrome, vbscript, ...) (bug 402788) + is(false, testRegisterHandler(true, "chrome", "http://mochi.test:8888/%s", "chrome handler"), "registering a chrome protocol handler should not work"); + is(false, testRegisterHandler(true, "vbscript", "http://mochi.test:8888/%s", "vbscript handler"), "registering a vbscript protocol handler should not work"); + is(false, testRegisterHandler(true, "javascript", "http://mochi.test:8888/%s", "javascript handler"), "registering a javascript protocol handler should not work"); + is(false, testRegisterHandler(true, "moz-icon", "http://mochi.test:8888/%s", "moz-icon handler"), "registering a moz-icon protocol handler should not work"); + + // for security reasons, content handlers should never be registered for some types (html, ...) + is(true, testRegisterHandler(false, "application/rss+xml", "http://mochi.test:8888/%s", "Foo handler"), "registering rss content handlers should work"); + is(true, testRegisterHandler(false, "application/atom+xml", "http://mochi.test:8888/%s", "Foo handler"), "registering atom content handlers should work"); + todo(false, testRegisterHandler(false, "text/html", "http://mochi.test:8888/%s", "Foo handler"), "registering html content handlers should not work"); // bug 403798 + +</script> +</pre> +</body> +</html> diff --git a/comm/suite/browser/test/mochitest/valid-feed.xml b/comm/suite/browser/test/mochitest/valid-feed.xml new file mode 100644 index 0000000000..0e700b6d8d --- /dev/null +++ b/comm/suite/browser/test/mochitest/valid-feed.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom"> + + <title>Example Feed</title> + <link href="http://example.org/"/> + <updated>2010-08-22T18:30:02Z</updated> + + <author> + <name>John Doe</name> + </author> + <id>urn:uuid:e2df8375-99be-4848-b05e-b9d407555267</id> + + <entry> + + <title>Item</title> + <link href="http://example.org/first"/> + <id>urn:uuid:9e0f4bed-33d3-4a9d-97ab-ecaa31b3f14a</id> + <updated>2010-08-22T18:30:02Z</updated> + + <summary>Some text.</summary> + </entry> + +</feed> diff --git a/comm/suite/browser/test/mochitest/valid-unsniffable-feed.xml b/comm/suite/browser/test/mochitest/valid-unsniffable-feed.xml new file mode 100644 index 0000000000..e753157395 --- /dev/null +++ b/comm/suite/browser/test/mochitest/valid-unsniffable-feed.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 512 bytes! +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + --> +<feed xmlns="http://www.w3.org/2005/Atom"> + + <title>Example Feed</title> + <link href="http://example.org/"/> + <updated>2010-08-22T18:30:02Z</updated> + + <author> + <name>John Doe</name> + </author> + <id>urn:uuid:e2df8375-99be-4848-b05e-b9d407555267</id> + + <entry> + + <title>Item</title> + <link href="http://example.org/first"/> + <id>urn:uuid:9e0f4bed-33d3-4a9d-97ab-ecaa31b3f14a</id> + <updated>2010-08-22T18:30:02Z</updated> + + <summary>Some text.</summary> + </entry> + +</feed> diff --git a/comm/suite/browser/test/mochitest/video.ogg b/comm/suite/browser/test/mochitest/video.ogg Binary files differnew file mode 100644 index 0000000000..ac7ece3519 --- /dev/null +++ b/comm/suite/browser/test/mochitest/video.ogg diff --git a/comm/suite/browser/urlbarBindings.xml b/comm/suite/browser/urlbarBindings.xml new file mode 100644 index 0000000000..2026724e5a --- /dev/null +++ b/comm/suite/browser/urlbarBindings.xml @@ -0,0 +1,694 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<!DOCTYPE bindings [ + <!ENTITY % textcontextDTD SYSTEM "chrome://communicator/locale/utilityOverlay.dtd"> + %textcontextDTD; + <!ENTITY % navigatorDTD SYSTEM "chrome://navigator/locale/navigator.dtd"> + %navigatorDTD; +]> + +<bindings id="urlbarBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="urlbar" extends="chrome://global/content/autocomplete.xml#autocomplete"> + <content sizetopopup="pref"> + <xul:hbox class="autocomplete-textbox-container" flex="1"> + <xul:hbox class="urlbar-security-level" flex="1" align="center" xbl:inherits="level"> + <children includes="image|deck|stack|box"> + <xul:image class="autocomplete-icon" allowevents="true"/> + </children> + + <xul:hbox class="textbox-input-box paste-and-go" flex="1" tooltip="_child" xbl:inherits="context"> + <children/> + <html:input anonid="input" class="autocomplete-textbox textbox-input" + allowevents="true" + xbl:inherits="value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint,userAction"/> + <xul:tooltip anonid="tooltip" + onpopupshowing="document.getBindingParent(this)._showTooltip(event);"/> + </xul:hbox> + <children includes="hbox"/> + </xul:hbox> + </xul:hbox> + + <xul:dropmarker class="autocomplete-history-dropmarker" allowevents="true" + xbl:inherits="open" anonid="historydropmarker"/> + + <xul:popupset> + <xul:panel type="autocomplete" anonid="popup" + ignorekeys="true" noautofocus="true" level="top" + xbl:inherits="for=id,nomatch"/> + </xul:popupset> + + <children includes="menupopup"/> + </content> + + <implementation implements="nsIObserver, nsIDOMEventListener"> + <constructor><![CDATA[ + this._prefs = Services.prefs.getBranch("browser.urlbar."); + this._prefs.addObserver("", this); + + this.updatePref("showPopup"); + this.updatePref("autoFill"); + this.updatePref("showSearch"); + this.updatePref("formatting.enabled"); + this.inputField.controllers.insertControllerAt(0, this._editItemsController); + this.inputField.addEventListener("overflow", this); + this.inputField.addEventListener("underflow", this); + ]]></constructor> + + <destructor><![CDATA[ + // Somehow, it's possible for the XBL destructor to fire without the + // constructor ever having fired. Fix: + if (!this._prefs) { + return; + } + this._prefs.removeObserver("", this); + this._prefs = null; + this.inputField.removeEventListener("underflow", this); + this.inputField.removeEventListener("overflow", this); + this.inputField.controllers.removeController(this._editItemsController); + ]]></destructor> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body><![CDATA[ + if (aTopic == "nsPref:changed") + this.updatePref(aData); + ]]></body> + </method> + + <method name="updatePref"> + <parameter name="aPref"/> + <body><![CDATA[ + if (aPref == "showPopup") { + this.showPopup = this._prefs.getBoolPref(aPref); + } else if (aPref == "autoFill") { + this.autoFill = this._prefs.getBoolPref(aPref); + } else if (aPref == "showSearch") { + this.minResultsForPopup = this._prefs.getBoolPref(aPref) ? 0 : 1; + } else if (aPref == "formatting.enabled") { + this._formattingEnabled = this._prefs.getBoolPref(aPref); + this._formatValue(this._formattingEnabled && !this.focused); + } + ]]></body> + </method> + + <field name="_overflowing">false</field> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "overflow": + this._overflowing = true; + break; + case "underflow": + this._overflowing = false; + break; + } + ]]></body> + </method> + + <method name="_showTooltip"> + <parameter name="aEvent"/> + <body><![CDATA[ + var tooltip = aEvent.target; + if (this._overflowing) + tooltip.label = this.value; + else if (this.value) + tooltip.label = this.placeholder; + else + aEvent.preventDefault(); + ]]></body> + </method> + + <field name="_formattingEnabled">true</field> + + <method name="_formatValue"> + <parameter name="formattingEnabled"/> + <body><![CDATA[ + if (!this.editor) + return; + + var controller = this.editor.selectionController; + var selection = controller.getSelection(controller.SELECTION_URLSECONDARY); + selection.removeAllRanges(); + if (!formattingEnabled) + return; + + var textNode = this.editor.rootElement.firstChild; + var value = textNode.textContent; + + var protocol = value.match(/^[a-z\d.+\-]+:(?=[^\d])/); + if (protocol && !/^https?:|ftp:/.test(protocol[0])) + return; + var matchedURL = value.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/); + if (!matchedURL) + return; + + var [, preDomain, domain] = matchedURL; + var subDomain = ""; + // getBaseDomainFromHost doesn't recognize IPv6 literals in brackets as IPs (bug 667159) + if (domain[0] != "[") { + try { + var baseDomain = Services.eTLD.getBaseDomainFromHost(domain); + if (!domain.endsWith(baseDomain)) { + // getBaseDomainFromHost converts its resultant to ACE. + let IDNService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + baseDomain = IDNService.convertACEtoUTF8(baseDomain); + } + if (baseDomain != domain) { + subDomain = domain.slice(0, -baseDomain.length); + } + } catch (e) {} + } + + var startLength = preDomain.length + subDomain.length; + if (startLength) { + var startRange = document.createRange(); + startRange.setStart(textNode, 0); + startRange.setEnd(textNode, startLength); + selection.addRange(startRange); + } + + var endLength = preDomain.length + domain.length; + if (endLength < value.length) { + var endRange = document.createRange(); + endRange.setStart(textNode, endLength); + endRange.setEnd(textNode, value.length); + selection.addRange(endRange); + } + ]]></body> + </method> + + <method name="autoFillInput"> + <parameter name="aSessionName"/> + <parameter name="aResults"/> + <parameter name="aUseFirstMatchIfNoDefault"/> + <body><![CDATA[ + if (this.mInputElt.selectionEnd < this.currentSearchString.length || + this.mDefaultMatchFilled) + return; + + if (!this.mFinishAfterSearch && this.autoFill && + this.mLastKeyCode != KeyEvent.DOM_VK_BACK_SPACE && + this.mLastKeyCode != KeyEvent.DOM_VK_DELETE) { + var indexToUse = aResults.defaultItemIndex; + if (aUseFirstMatchIfNoDefault && indexToUse == -1) + indexToUse = 0; + + if (indexToUse != -1) { + var result = this.getSessionValueAt(aSessionName, indexToUse); + var entry = this.value; + var suffix = ""; + if (/^ftp:\/\/ftp\b/.test(result) && + result.lastIndexOf("ftp://" + entry, 0) == 0) + suffix = result.slice(entry.length + 6); + else if (!/^http:\/\/ftp\b/.test(result) && + result.lastIndexOf("http://" + entry, 0) == 0) + suffix = result.slice(entry.length + 7); + else if (result.lastIndexOf(entry, 0) == 0) + suffix = result.slice(entry.length); + + if (suffix) { + this.setTextValue(this.value + suffix); + this.mInputElt.setSelectionRange(entry.length, this.value.length); + this.mDefaultMatchFilled = true; + } + this.mNeedToComplete = true; + } + } + ]]></body> + </method> + + <method name="_getSelectedValueForClipboard"> + <body> + <![CDATA[ + var inputVal = this.inputField.value; + var val = inputVal.substring(this.selectionStart, this.selectionEnd); + + /* If the entire value is selected and it's a valid non-javascript, + non-data URI, encode it. */ + if (val == inputVal && + gProxyButton.getAttribute("pageproxystate") == "valid") { + var uri; + try { + uri = makeURI(val); + } catch (e) {} + + if (uri && !uri.schemeIs("javascript") && !uri.schemeIs("data")) { + val = uri.spec; + + // Parentheses are known to confuse third-party applications (bug 458565). + val = val.replace(/[()]/g, c => escape(c)); + } + } + + return val; + ]]> + </body> + </method> + + <field name="_editItemsController"><![CDATA[ + ({ + supportsCommand: function(aCommand) { + switch (aCommand) { + case "cmd_copy": + case "cmd_cut": + case "cmd_pasteAndGo": + return true; + } + return false; + }, + isCommandEnabled: function(aCommand) { + var hasSelection = this.selectionStart < this.selectionEnd; + switch (aCommand) { + case "cmd_copy": + return hasSelection; + case "cmd_cut": + return !this.readOnly && hasSelection; + case "cmd_pasteAndGo": + return document.commandDispatcher + .getControllerForCommand("cmd_paste") + .isCommandEnabled("cmd_paste"); + } + return false; + }.bind(this), + doCommand: function(aCommand) { + switch (aCommand) { + case "cmd_copy": + case "cmd_cut": + var val = this._getSelectedValueForClipboard(); + var controller = this._editItemsController; + if (!val || !controller.isCommandEnabled(aCommand)) + return; + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(val); + + if (aCommand == "cmd_cut") + goDoCommand("cmd_delete"); + break; + + case "cmd_pasteAndGo": + this.select(); + goDoCommand("cmd_paste"); + this._fireEvent("textentered", "pasting"); + break; + } + }.bind(this), + onEvent: function(aEventName) {} + }) + ]]></field> + </implementation> + + <handlers> + <handler event="keypress" + key="&locationBar.accesskey;" + modifiers="access" + action="this.select();"/> + + <handler event="ValueChange"><![CDATA[ + if (this._formattingEnabled && !this.focused) + this._formatValue(true); + ]]></handler> + + <handler event="blur"><![CDATA[ + if (this._formattingEnabled) + this._formatValue(true); + ]]></handler> + + <handler event="focus"><![CDATA[ + this._formatValue(false); + ]]></handler> + </handlers> + </binding> + + <binding id="autocomplete-result-popup" extends="chrome://global/content/autocomplete.xml#autocomplete-result-popup"> + <content> + <xul:tree anonid="tree" class="autocomplete-tree plain" flex="1"> + <xul:treecols anonid="treecols"> + <xul:treecol class="autocomplete-treecol" id="treecolAutoCompleteValue" flex="2"/> + <xul:treecol class="autocomplete-treecol" id="treecolAutoCompleteComment" flex="1" hidden="true"/> + </xul:treecols> + <xul:treechildren anonid="treebody" class="autocomplete-treebody" flex="1"/> + </xul:tree> + <xul:box role="search-box" class="autocomplete-search-box"/> + </content> + + <implementation implements="nsIObserver, nsIBrowserSearchInitObserver"> + <constructor><![CDATA[ + // listen for changes to default search engine + Services.prefs.addObserver("browser.search", this); + Services.prefs.addObserver("browser.urlbar", this); + Services.obs.addObserver(this, "browser-search-engine-modified"); + this._initialized = true; + Services.search.init(this); + ]]></constructor> + + <destructor><![CDATA[ + Services.prefs.removeObserver("browser.search", this); + Services.prefs.removeObserver("browser.urlbar", this); + if (this._initialized) { + this._initialized = false; + Services.obs.removeObserver(this, "browser-search-engine-modified"); + } + ]]></destructor> + + <property name="showSearch" onget="return this.mShowSearch;"> + <setter><![CDATA[ + this.mShowSearch = val; + if (val) { + this.updateEngines(); + this.mSearchBox.removeAttribute("hidden"); + } else { + this.clearEngines(); + this.mSearchBox.setAttribute("hidden", "true"); + } + ]]></setter> + </property> + + <property name="defaultSearchEngine" + onget="return this.textbox.getAttribute('defaultSearchEngine') == 'true';" + onset="this.textbox.setAttribute('defaultSearchEngine', val); return val;"/> + + <field name="mSearchBox"> + document.getAnonymousElementByAttribute(this, "role", "search-box"); + </field> + + <field name="mInputListener"><![CDATA[ + (function(aEvent) { + // "this" is the textbox, not the popup + if (this.mSearchInputTO) + window.clearTimeout(this.mSearchInputTO); + this.mSearchInputTO = window.setTimeout(this.popup.mInputTimeout, this.timeout, this); + }); + ]]></field> + + <field name="mInputTimeout"><![CDATA[ + (function(me) { + me.popup.mSearchBox.searchValue = me.value; + me.mSearchInputTO = 0; + }); + ]]></field> + + <field name="_initialized">false</field> + <field name="mEnginesReady">false</field> + + <property name="overrideValue" readonly="true"> + <getter><![CDATA[ + if (this.mSearchBox.selectedIndex != -1) { + return this.mSearchBox.overrideValue; + } + return null; + ]]></getter> + </property> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body><![CDATA[ + switch (aTopic) { + case "browser-search-engine-modified": + if (aData == "engine-current") { + this.updateEngines(); + } + break; + case "nsPref:changed": + if (/^browser\.search\./.test(aData)) + Services.search.init(this); + else if (aData == "browser.urlbar.showSearch") + this.updateShowSearch(); + break; + default: + } + ]]></body> + </method> + + <method name="updateShowSearch"> + <body><![CDATA[ + this.showSearch = Services.prefs.getBoolPref("browser.urlbar.showSearch"); + ]]></body> + </method> + + <method name="onInitComplete"> + <parameter name="aStatus"/> + <body><![CDATA[ + if (!this._initialized) + return; + if (Components.isSuccessCode(aStatus)) { + // Refresh the engines. + this.updateEngines(); + } else { + Cu.reportError("Cannot initialize search service, bailing out: " + aStatus); + } + ]]></body> + </method> + + <method name="addEngine"> + <parameter name="aName"/> + <parameter name="aIcon"/> + <parameter name="aSearchBarUrl"/> + <body><![CDATA[ + var box = document.createElement("box"); + box.setAttribute("class", "autocomplete-search-engine"); + box.setAttribute("name", aName); + if (aIcon) + box.setAttribute("icon", aIcon.spec); + box.setAttribute("searchBarUrl", aSearchBarUrl); + box.setAttribute("engineIndex", this.childNodes.length); + this.mSearchBox.appendChild(box); + ]]></body> + </method> + + <method name="clearEngines"> + <body><![CDATA[ + while (this.mSearchBox.hasChildNodes()) + this.mSearchBox.lastChild.remove(); + ]]></body> + </method> + + <method name="updateEngines"> + <body><![CDATA[ + var defaultEngine = Services.search.defaultEngine; + if (defaultEngine) { + this.clearEngines(); + this.addEngine(defaultEngine.name, defaultEngine.iconURI, + defaultEngine.searchForm); + } + + this.mEnginesReady = true; + ]]></body> + </method> + + <method name="clearSelection"> + <body> + this.view.selection.clearSelection(); + this.mSearchBox.selectedIndex = -1; + </body> + </method> + + <method name="selectBy"> + <parameter name="aReverse"/> + <parameter name="aPage"/> + <body><![CDATA[ + var sel; + if (this.selectedIndex == -1 && aReverse || + this.mSearchBox.selectedIndex != -1) { + sel = this.mSearchBox.selectBy(aReverse, aPage); + if (sel != -1 || !aReverse) + return -1; + } + + sel = this.getNextIndex(aReverse, aPage, this.selectedIndex, this.view.rowCount - 1); + this.selectedIndex = sel; + if (sel == -1 && !aReverse) + this.mSearchBox.selectBy(aReverse, aPage); + else if (this.mSearchBox.selectedIndex != -1) + this.mSearchBox.selectedIndex = -1; + return sel; + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="popupshowing"><![CDATA[ + // sync up with prefs about showing search in the URL bar if + // the URL bar hasn't finished initializing the default engine info + // -or- + // the search sidebar tab was displayed already triggering a change + // notification to the browser.search pref branch listener here which + // calls updateEngines but doesn't cause the ``Search for ...'' + // display string to be updated + if ( (!this.mEnginesReady && this.defaultSearchEngine) || + !("mShowSearch" in this) ) + this.updateShowSearch(); + + if (this.mShowSearch) { + this.textbox.mSearchInputTO = 0; + this.textbox.addEventListener("input", this.mInputListener); + if ("searchValue" in this.mSearchBox) + this.mSearchBox.searchValue = this.textbox.currentSearchString; + else + this.mSearchBox.setAttribute("searchvalue", this.textbox.currentSearchString); + } + ]]></handler> + + <handler event="popuphiding"><![CDATA[ + if (this.mShowSearch) + this.textbox.removeEventListener("input", this.mInputListener); + ]]></handler> + </handlers> + </binding> + + <binding id="autocomplete-search-box"> + <content orient="vertical"/> + + <implementation> + <constructor><![CDATA[ + this.navigatorBundle = Services.strings + .createBundle("chrome://navigator/locale/navigator.properties"); + + var text = this.getAttribute("searchvalue"); + if (text) + this.searchValue = text; + ]]></constructor> + + <field name="mSelectedIndex"> + -1 + </field> + + <property name="activeChild" + onget="return this.childNodes[this.mSelectedIndex]"/> + + <property name="selectedIndex"> + <getter>return this.mSelectedIndex;</getter> + + <setter><![CDATA[ + if (this.mSelectedIndex != -1) + this.activeChild.removeAttribute("menuactive"); + + this.mSelectedIndex = val; + + if (val != -1) + this.activeChild.setAttribute("menuactive", "true"); + return val; + ]]></setter> + + </property> + + <property name="searchValue"> + <getter><![CDATA[ + return this.mSearchValue; + ]]></getter> + <setter><![CDATA[ + this.mSearchValue = val; + + const kids = this.childNodes; + for (var i = 0; i < kids.length; ++i) { + kids[i].setAttribute("label", + this.navigatorBundle.formatStringFromName( + "searchFor", [kids[i].getAttribute("name"), val], 2)); + } + ]]></setter> + </property> + + <method name="selectBy"> + <parameter name="aReverse"/> + <parameter name="aPage"/> + <body><![CDATA[ + return this.selectedIndex = this.parentNode.getNextIndex(aReverse, aPage, this.selectedIndex, this.childNodes.length - 1); + ]]></body> + </method> + + <property name="overrideValue" readonly="true"> + <getter><![CDATA[ + var item = this.activeChild; + if (item) { + // XXXsearch: using default engine for now, this ignores the following values: + // item.getAttribute("searchEngine") and item.getAttribute("searchBarUrl") + var engine = Services.search.defaultEngine; + + var submission = engine.getSubmission(this.mSearchValue); // HTML response + + // getSubmission can return null if the engine doesn't have a URL + // with a text/html response type. This is unlikely (since + // SearchService._addEngineToStore() should fail for such an engine), + // but let's be on the safe side. + if (!submission) + return null; + + // XXXsearch: the submission object may have postData but .overrideValue can't deal with that + // see http://mxr.mozilla.org/comm-central/source/mozilla/netwerk/base/public/nsIBrowserSearchService.idl#47 + // we need to figure out what to do with that case here + + return submission.uri.spec; + } + return null; + ]]></getter> + </property> + + </implementation> + + <handlers> + <handler event="mouseup"> + this.parentNode.textbox.onResultClick(); + </handler> + </handlers> + + </binding> + + <binding id="autocomplete-search-engine"> + <content> + <xul:image class="autocomplete-search-engine-img" xbl:inherits="src=icon"/> + <xul:label class="autocomplete-search-engine-text" xbl:inherits="value=label" crop="right" flex="1"/> + </content> + + <handlers> + <handler event="mousemove"> + this.parentNode.selectedIndex = Number(this.getAttribute("engineIndex")); + </handler> + + <handler event="mouseout"> + this.parentNode.selectedIndex = -1; + </handler> + </handlers> + </binding> + + <binding id="input-box-paste" extends="chrome://global/content/bindings/textbox.xml#input-box"> + <content context="_child"> + <children/> + <xul:menupopup anonid="input-box-contextmenu" + class="textbox-contextmenu" + onpopupshowing="var input = + this.parentNode.getElementsByAttribute('anonid', 'input')[0]; + if (document.commandDispatcher.focusedElement != input) + input.focus(); + this.parentNode._doPopupItemEnabling(this);" + oncommand="var cmd = event.originalTarget.getAttribute('cmd'); if(cmd) { this.parentNode.doCommand(cmd); event.stopPropagation(); }"> + <xul:menuitem label="&undoCmd.label;" accesskey="&undoCmd.accesskey;" cmd="cmd_undo"/> + <xul:menuseparator/> + <xul:menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" cmd="cmd_cut"/> + <xul:menuitem label="©Cmd.label;" accesskey="©Cmd.accesskey;" cmd="cmd_copy"/> + <xul:menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;" cmd="cmd_paste"/> + <xul:menuitem label="&pasteGoCmd.label;" accesskey="&pasteGoCmd.accesskey;" cmd="cmd_pasteAndGo"/> + <xul:menuitem label="&deleteCmd.label;" accesskey="&deleteCmd.accesskey;" cmd="cmd_delete"/> + <xul:menuseparator/> + <xul:menuitem label="&selectAllCmd.label;" accesskey="&selectAllCmd.accesskey;" cmd="cmd_selectAll"/> + </xul:menupopup> + </content> + </binding> + +</bindings> diff --git a/comm/suite/browser/webDeveloperOverlay.js b/comm/suite/browser/webDeveloperOverlay.js new file mode 100644 index 0000000000..41277681f6 --- /dev/null +++ b/comm/suite/browser/webDeveloperOverlay.js @@ -0,0 +1,197 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +ChromeUtils.defineModuleGetter(this, "BrowserToolboxProcess", + "resource://devtools/client/framework/ToolboxProcess.jsm"); + +XPCOMUtils.defineLazyGetter(this, "DeveloperToolbar", function() { + var tmp = {}; + ChromeUtils.import("resource://devtools/shared/Loader.jsm", tmp); + var DeveloperToolbar = tmp.require("devtools/client/shared/developer-toolbar").DeveloperToolbar; + return new DeveloperToolbar(window); +}); + +var ResponsiveUI = { + toggle() { + this.ResponsiveUIManager.toggle(window, getBrowser().selectedTab); + } +}; + +XPCOMUtils.defineLazyGetter(ResponsiveUI, "ResponsiveUIManager", function() { + var tmp = {}; + ChromeUtils.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", tmp); + return tmp.ResponsiveUIManager; +}); + +var Scratchpad = { + openScratchpad() { + return this.ScratchpadManager.openScratchpad(); + } +}; + +XPCOMUtils.defineLazyGetter(Scratchpad, "ScratchpadManager", function() { + var tmp = {}; + ChromeUtils.import("resource://devtools/client/scratchpad/scratchpad-manager.jsm", tmp); + return tmp.ScratchpadManager; +}); + +ChromeUtils.defineModuleGetter(this, "gDevTools", + "resource://devtools/client/framework/gDevTools.jsm"); + +ChromeUtils.defineModuleGetter(this, "gDevToolsBrowser", + "resource://devtools/client/framework/gDevTools.jsm"); + +function openEyedropper() { + var eyedropper = new this.Eyedropper(this, { context: "menu", + copyOnSelect: true }); + eyedropper.open(); +} + +Object.defineProperty(this, "Eyedropper", { + get() { + var tmp = {}; + ChromeUtils.import("resource://devtools/shared/Loader.jsm", tmp); + return tmp.require("devtools/client/eyedropper/eyedropper").Eyedropper; + }, + configurable: true, + enumerable: true +}); + +Object.defineProperty(this, "HUDService", { + get() { + var tmp = {}; + ChromeUtils.import("resource://devtools/shared/Loader.jsm", tmp); + return tmp.require("devtools/client/webconsole/hudservice").HUDService; + }, + configurable: true, + enumerable: true +}); + +var gWebDeveloper = { + validateThisPage: function() { + var service = GetLocalizedStringPref("browser.validate.html.service"); + var uri = getBrowser().currentURI; + var checkURL = service + encodeURIComponent(uri.spec); + var opentab = Services.prefs.getBoolPref("browser.tabs.opentabfor.middleclick"); + openUILinkIn(checkURL, opentab ? "tabfocused" : "window", + { referrerURI: uri, relatedToCurrent: true }); + }, + + enableDebugger: function(aItem) { + var shouldEnable = aItem.getAttribute("checked") == "true"; + Services.prefs.setBoolPref("devtools.debugger.remote-enabled", shouldEnable); + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "load": + window.removeEventListener("load", gWebDeveloper); + window.addEventListener("unload", gWebDeveloper); + var popup = document.getElementById("toolsPopup"); + popup.addEventListener("popupshowing", gWebDeveloper); + // Don't use gDevToolsBrowser.updateCommandAvailability() at the moment + // because some tools aren't working. + if (!gDevToolsBrowser._old_updateCommandAvailability) { + gDevToolsBrowser._old_updateCommandAvailability = gDevToolsBrowser.updateCommandAvailability; + gDevToolsBrowser.updateCommandAvailability = this.updateCommandAvailability; + } + // Add Devtools menuitems, observers, and listeners + gDevToolsBrowser.registerBrowserWindow(window); + Services.prefs.addObserver(this.devtoolsThemePref, this); + this.updateDevtoolsThemeAttribute(); + break; + + case "unload": + window.removeEventListener("unload", gWebDeveloper); + gDevToolsBrowser.forgetBrowserWindow(window); + Services.prefs.removeObserver(this.devtoolsThemePref, this); + + var desc = Object.getOwnPropertyDescriptor(window, "DeveloperToolbar"); + if (desc && !desc.get) + DeveloperToolbar.destroy(); + break; + + case "popupshowing": + this.initMenuItems(); + this.updateCommandAvailability(window); + break; + } + }, + + initMenuItems: function() { + var menuitem = document.getElementById("validatePage"); + var uri = getBrowser().currentURI; + if (uri && (uri.schemeIs("http") || uri.schemeIs("https"))) + menuitem.removeAttribute("disabled"); + else + menuitem.setAttribute("disabled", true); + + var enabled = Services.prefs + .getBoolPref("devtools.debugger.remote-enabled"); + document.getElementById("devtoolsDebugger") + .setAttribute("checked", enabled); + }, + + devtoolsThemePref: "devtools.theme", + + observe: function (subject, topic, data) { + if (topic == "nsPref:changed" && data == this.devtoolsThemePref) { + this.updateDevtoolsThemeAttribute(); + } + }, + + updateDevtoolsThemeAttribute: function() { + // Set an attribute on root element to make it possible + // to change colors based on the selected devtools theme. + var devtoolsTheme = Services.prefs.getCharPref(this.devtoolsThemePref); + // Bug 1096469 - Make devedition theme work with custom devtools themes. + if (devtoolsTheme != "dark") + devtoolsTheme = "light"; + + document.documentElement.setAttribute("devtoolstheme", devtoolsTheme); + // document.getElementById("developer-toolbar").setAttribute("devtoolstheme", devtoolsTheme); + }, + + updateCommandAvailability: function(win) { + var doc = win.document; + + function toggleCmd(id, isEnabled) { + var cmd = doc.getElementById(id); + if (isEnabled) { + cmd.removeAttribute("disabled"); + cmd.removeAttribute("hidden"); + } else { + cmd.setAttribute("disabled", "true"); + cmd.setAttribute("hidden", "true"); + } + }; + + // Enable developer toolbar? + var devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled"); + toggleCmd("Tools:DevToolbar", devToolbarEnabled); + var focusEl = doc.getElementById("Tools:DevToolbarFocus"); + if (devToolbarEnabled) + focusEl.removeAttribute("disabled"); + else + focusEl.setAttribute("disabled", "true"); + + if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) + win.DeveloperToolbar.show(false).catch(console.error); + + // Enable Browser Toolbox? + var chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled"); + var devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled"); + var remoteEnabled = chromeEnabled && devtoolsRemoteEnabled; + toggleCmd("Tools:BrowserToolbox", remoteEnabled); + // Currently "gMultiProcessBrowser" is always falsey. + //toggleCmd("Tools:BrowserContentToolbox", remoteEnabled && win.gMultiProcessBrowser); + toggleCmd("Tools:BrowserContentToolbox", false); + + // Enable DevTools connection screen, if the preference allows this. + toggleCmd("Tools:DevToolsConnect", devtoolsRemoteEnabled); + }, +} + +window.addEventListener("load", gWebDeveloper); diff --git a/comm/suite/browser/webDeveloperOverlay.xul b/comm/suite/browser/webDeveloperOverlay.xul new file mode 100644 index 0000000000..09780ee6f0 --- /dev/null +++ b/comm/suite/browser/webDeveloperOverlay.xul @@ -0,0 +1,156 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://devtools/skin/devtools-browser.css" type="text/css"?> +<?xml-stylesheet href="chrome://navigator/skin/webDeveloper.css" type="text/css"?> + +<!DOCTYPE overlay SYSTEM "chrome://navigator/locale/webDeveloper.dtd"> + +<overlay id="webDeveloperOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://navigator/content/webDeveloperOverlay.js"/> + + <menupopup id="toolsPopup"> + <menuitem id="validatePage" + label="&validatePage.label;" + accesskey="&validatePage.accesskey;" + oncommand="gWebDeveloper.validateThisPage(); event.stopPropagation();"/> + <menuseparator id="devToolsStartSeparator"/> + <menuitem id="menu_devToolbox" + observes="devtoolsMenuBroadcaster_DevToolbox" + label="&devToolboxMenuItem.label;" + accesskey="&devToolboxMenuItem.accesskey;"/> + <menuseparator id="menu_devtools_separator"/> + <menuitem id="menu_devToolbar" + observes="devtoolsMenuBroadcaster_DevToolbar" + type="checkbox" autocheck="false" + label="&devToolbarMenu.label;" + accesskey="&devToolbarMenu.accesskey;"/> + <menuitem id="menu_browserToolbox" + observes="devtoolsMenuBroadcaster_BrowserToolbox" + label="&browserToolboxMenu.label;" + accesskey="&browserToolboxMenu.accesskey;"/> + <menuitem id="menu_browserContentToolbox" + observes="devtoolsMenuBroadcaster_BrowserContentToolbox" + label="&browserContentToolboxMenu.label;" + accesskey="&browserContentToolboxMenu.accesskey;" /> + <menuitem id="menu_browserConsole" + observes="devtoolsMenuBroadcaster_BrowserConsole" + label="&browserConsoleCmd.label;" + accesskey="&browserConsoleCmd.accesskey;"/> + <menuitem id="menu_responsiveUI" + observes="devtoolsMenuBroadcaster_ResponsiveUI" + type="checkbox" autocheck="false" + label="&responsiveDesignTool.label;" + accesskey="&responsiveDesignTool.accesskey;"/> + <menuitem id="menu_eyedropper" + observes="devtoolsMenuBroadcaster_Eyedropper" + type="checkbox" autocheck="false" + label="&eyedropper.label;" + accesskey="&eyedropper.accesskey;"/> + <menuitem id="menu_scratchpad" + observes="devtoolsMenuBroadcaster_Scratchpad" + label="&scratchpad.label;" + accesskey="&scratchpad.accesskey;"/> + <menuitem id="devtoolsDebugger" + type="checkbox" + label="&allowRemoteDebugging.label;" + accesskey="&allowRemoteDebugging.accesskey;" + oncommand="gWebDeveloper.enableDebugger(this);"/> + <menuitem id="menu_devtools_connect" + observes="devtoolsMenuBroadcaster_connect" + label="&devtoolsConnect.label;" + accesskey="&devtoolsConnect.accesskey;"/> + <menuseparator id="devToolsEndSeparator"/> + <menuitem id="getMoreDevtools" + observes="devtoolsMenuBroadcaster_GetMoreTools" + label="&getMoreDevtoolsCmd.label;" + accesskey="&getMoreDevtoolsCmd.accesskey;"/> + </menupopup> + + <commandset id="mainCommandSet"> + <command id="Tools:DevToolbox" oncommand="gDevToolsBrowser.toggleToolboxCommand(gBrowser);"/> + <command id="Tools:DevToolbar" oncommand="DeveloperToolbar.toggle();" disabled="true" hidden="true"/> + <command id="Tools:DevToolbarFocus" oncommand="DeveloperToolbar.focusToggle();" disabled="true"/> + <command id="Tools:BrowserToolbox" oncommand="BrowserToolboxProcess.init();" disabled="true" hidden="true"/> + <command id="Tools:BrowserContentToolbox" oncommand="gDevToolsBrowser.openContentProcessToolbox();" disabled="true" hidden="true"/> + <command id="Tools:BrowserConsole" oncommand="HUDService.openBrowserConsoleOrFocus();"/> + <command id="Tools:Scratchpad" oncommand="Scratchpad.openScratchpad();"/> + <command id="Tools:ResponsiveUI" oncommand="ResponsiveUI.toggle();"/> + <command id="Tools:Eyedropper" oncommand="openEyedropper();"/> + <command id="Tools:DevToolsConnect" oncommand="gDevToolsBrowser.openConnectScreen(gBrowser)" disabled="true" hidden="true"/> + </commandset> + + <keyset id="mainKeyset"> + <key id="key_devToolboxMenuItemF12" keycode="&devToolsCmd.keycode;" keytext="&devToolsCmd.keytext;" + command="Tools:DevToolbox"/> + <key id="key_browserConsole" key="&browserConsoleCmd.commandkey;" modifiers="accel,shift" + command="Tools:BrowserConsole"/> + <key id="key_browserToolbox" key="&browserToolboxCmd.commandkey;" modifiers="accel,alt,shift" + command="Tools:BrowserToolbox"/> + <key id="key_devToolbar" keycode="&devToolbar.keycode;" modifiers="shift" keytext="&devToolbar.keytext;" + command="Tools:DevToolbarFocus"/> + <key id="key_devToolboxMenuItem" keytext="&devToolboxMenuItem.keytext;" key="&devToolboxMenuItem.keytext;" modifiers="accel,shift" + command="Tools:DevToolbox"/> + <key id="key_scratchpad" keycode="&scratchpad.keycode;" modifiers="shift" keytext="&scratchpad.keytext;" + command="Tools:Scratchpad"/> + </keyset> + + <broadcasterset id="mainBroadcasterSet"> + <!-- DevTools broadcasters --> + <broadcaster id="devtoolsMenuBroadcaster_DevToolbox" + type="checkbox" autocheck="false" + command="Tools:DevToolbox" + key="key_devToolboxMenuItem"/> + <broadcaster id="devtoolsMenuBroadcaster_DevToolbar" + command="Tools:DevToolbar" + key="key_devToolbar"/> + <broadcaster id="devtoolsMenuBroadcaster_BrowserToolbox" + key="key_browserToolbox" + command="Tools:BrowserToolbox"/> + <broadcaster id="devtoolsMenuBroadcaster_BrowserContentToolbox" + command="Tools:BrowserContentToolbox"/> + <broadcaster id="devtoolsMenuBroadcaster_BrowserConsole" + key="key_browserConsole" + command="Tools:BrowserConsole"/> + <broadcaster id="devtoolsMenuBroadcaster_Scratchpad" + command="Tools:Scratchpad" + key="key_scratchpad"/> + <broadcaster id="devtoolsMenuBroadcaster_ResponsiveUI" + command="Tools:ResponsiveUI"/> + <broadcaster id="devtoolsMenuBroadcaster_Eyedropper" + command="Tools:Eyedropper"/> + <broadcaster id="devtoolsMenuBroadcaster_PageSource" + key="key_viewSource" + command="View:PageSource"/> + <broadcaster id="devtoolsMenuBroadcaster_GetMoreTools" + oncommand="openUILinkIn('https://addons.mozilla.org/firefox/collections/mozilla/webdeveloper/', 'tab');"/> + <broadcaster id="devtoolsMenuBroadcaster_connect" + command="Tools:DevToolsConnect"/> + </broadcasterset> + + <window id="main-window"> + <toolbar id="developer-toolbar" xpfe="false" hidden="true" + insertbefore="status-bar"> + <observes element="main-window" attribute="devtoolstheme"/> + <stack class="gclitoolbar-stack-node" flex="1"> + <textbox class="gclitoolbar-input-node" rows="1"/> + <hbox class="gclitoolbar-complete-node"/> + </stack> + <toolbarbutton id="developer-toolbar-toolbox-button" + class="developer-toolbar-button" + label="&devToolboxMenuItem.label;" + observes="devtoolsMenuBroadcaster_DevToolbox" + tooltiptext="&devToolbarToolsButton.tooltip;" + _defaultTooltipText="&devToolbarToolsButton.tooltip;"/> + + <toolbarbutton id="developer-toolbar-closebutton" + class="close-icon" + oncommand="DeveloperToolbar.hide();" + tooltiptext="&devToolbarCloseButton.tooltiptext;"/> + </toolbar> + </window> +</overlay> |