diff options
author | Thomas Groman <tgroman@nuegia.net> | 2020-04-20 20:49:37 -0700 |
---|---|---|
committer | Thomas Groman <tgroman@nuegia.net> | 2020-04-20 20:49:37 -0700 |
commit | f9cab004186edb425a9b88ad649726605080a17c (patch) | |
tree | e2dae51d3144e83d097a12e7a1499e3ea93f90be /webbrowser/base/content/browser.js | |
parent | f428692de8b59ab89a66502c079e1823dfda8aeb (diff) | |
download | webbrowser-f9cab004186edb425a9b88ad649726605080a17c.tar webbrowser-f9cab004186edb425a9b88ad649726605080a17c.tar.gz webbrowser-f9cab004186edb425a9b88ad649726605080a17c.tar.lz webbrowser-f9cab004186edb425a9b88ad649726605080a17c.tar.xz webbrowser-f9cab004186edb425a9b88ad649726605080a17c.zip |
move browser to webbrowser/
Diffstat (limited to 'webbrowser/base/content/browser.js')
-rw-r--r-- | webbrowser/base/content/browser.js | 7440 |
1 files changed, 7440 insertions, 0 deletions
diff --git a/webbrowser/base/content/browser.js b/webbrowser/base/content/browser.js new file mode 100644 index 0000000..7615bc9 --- /dev/null +++ b/webbrowser/base/content/browser.js @@ -0,0 +1,7440 @@ +# -*- 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 Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/RecentWindow.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", + "resource:///modules/CharsetMenu.jsm"); + +const nsIWebNavigation = Ci.nsIWebNavigation; +const gToolbarInfoSeparators = ["|", "-"]; + +var gLastBrowserCharset = null; +var gPrevCharset = null; +var gProxyFavIcon = null; +var gLastValidURLStr = ""; +var gInPrintPreviewMode = false; +var gContextMenu = null; // nsContextMenu instance +var gMultiProcessBrowser = false; + +#ifndef XP_MACOSX +var gEditUIVisible = true; +#endif + +[ + ["gBrowser", "content"], + ["gNavToolbox", "navigator-toolbox"], + ["gURLBar", "urlbar"], + ["gNavigatorBundle", "bundle_browser"] +].forEach(function (elementGlobal) { + var [name, id] = elementGlobal; + window.__defineGetter__(name, function () { + var element = document.getElementById(id); + if (!element) + return null; + delete window[name]; + return window[name] = element; + }); + window.__defineSetter__(name, function (val) { + delete window[name]; + return window[name] = val; + }); +}); + +// Smart getter for the findbar. If you don't wish to force the creation of +// the findbar, check gFindBarInitialized first. +var gFindBarInitialized = false; +XPCOMUtils.defineLazyGetter(window, "gFindBar", function() { + let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let findbar = document.createElementNS(XULNS, "findbar"); + findbar.id = "FindToolbar"; + + let browserBottomBox = document.getElementById("browser-bottombox"); + browserBottomBox.insertBefore(findbar, browserBottomBox.firstChild); + + // Force a style flush to ensure that our binding is attached. + findbar.clientTop; + findbar.browser = gBrowser.mCurrentBrowser; + window.gFindBarInitialized = true; + return findbar; +}); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gPrefService", function() { + return Services.prefs; +}); + +this.__defineGetter__("AddonManager", function() { + let tmp = {}; + Cu.import("resource://gre/modules/AddonManager.jsm", tmp); + return this.AddonManager = tmp.AddonManager; +}); +this.__defineSetter__("AddonManager", function (val) { + delete this.AddonManager; + return this.AddonManager = val; +}); + +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils", + "resource:///modules/AboutHomeUtils.jsm"); + +#ifdef MOZ_SERVICES_SYNC +XPCOMUtils.defineLazyModuleGetter(this, "Weave", + "resource://services-sync/main.js"); +#endif + +XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () { + let tmp = {}; + Cu.import("resource:///modules/PopupNotifications.jsm", tmp); + try { + return new tmp.PopupNotifications(gBrowser, + document.getElementById("notification-popup"), + document.getElementById("notification-popup-box")); + } catch (ex) { + Cu.reportError(ex); + return null; + } +}); + +XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs", + "resource://gre/modules/PageThumbs.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "gBrowserNewTabPreloader", + "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FormValidationHandler", + "resource:///modules/FormValidationHandler.jsm"); + +var gInitialPages = [ + "about:blank", + "about:newtab", + "about:home", + "about:privatebrowsing", + "about:sessionrestore", + "about:logopage" +]; + +#include browser-addons.js +#include browser-feeds.js +#include browser-fullScreen.js +#include browser-fullZoom.js +#include browser-places.js +#include browser-plugins.js +#include browser-tabPreviews.js +#include browser-thumbnails.js +#include browser-uacompat.js + +#ifdef MOZ_WEBRTC +#include browser-webrtcUI.js +#endif + +#include browser-gestureSupport.js + +#ifdef MOZ_SERVICES_SYNC +#include browser-syncui.js +#endif + +XPCOMUtils.defineLazyGetter(this, "Win7Features", function () { +#ifdef XP_WIN + const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1"; + if (WINTASKBAR_CONTRACTID in Cc && + Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available) { + let AeroPeek = Cu.import("resource:///modules/WindowsPreviewPerTab.jsm", {}).AeroPeek; + return { + onOpenWindow: function () { + AeroPeek.onOpenWindow(window); + }, + onCloseWindow: function () { + AeroPeek.onCloseWindow(window); + } + }; + } +#endif + return null; +}); + +XPCOMUtils.defineLazyGetter(this, "PageMenu", function() { + let tmp = {}; + Cu.import("resource:///modules/PageMenu.jsm", tmp); + return new tmp.PageMenu(); +}); + +/** +* We can avoid adding multiple load event listeners and save some time by adding +* one listener that calls all real handlers. +*/ +function pageShowEventHandlers(persisted) { + charsetLoadListener(); + XULBrowserWindow.asyncUpdateUI(); + + // The PluginClickToPlay events are not fired when navigating using the + // BF cache. |persisted| is true when the page is loaded from the + // BF cache, so this code reshows the notification if necessary. + if (persisted) + gPluginHandler.reshowClickToPlayNotification(); +} + +function UpdateBackForwardCommands(aWebNavigation) { + var backBroadcaster = document.getElementById("Browser:Back"); + var forwardBroadcaster = document.getElementById("Browser:Forward"); + + // 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"); + if (backDisabled == aWebNavigation.canGoBack) { + if (backDisabled) + backBroadcaster.removeAttribute("disabled"); + else + backBroadcaster.setAttribute("disabled", true); + } + + if (forwardDisabled == aWebNavigation.canGoForward) { + if (forwardDisabled) + forwardBroadcaster.removeAttribute("disabled"); + else + forwardBroadcaster.setAttribute("disabled", true); + } +} + +/** + * Click-and-Hold implementation for the Back and Forward buttons + * XXXmano: should this live in toolbarbutton.xml? + */ +function SetClickAndHoldHandlers() { + var timer; + + function openMenu(aButton) { + cancelHold(aButton); + aButton.firstChild.hidden = false; + aButton.open = true; + } + + function mousedownHandler(aEvent) { + if (aEvent.button != 0 || + aEvent.currentTarget.open || + aEvent.currentTarget.disabled) + return; + + // Prevent the menupopup from opening immediately + aEvent.currentTarget.firstChild.hidden = true; + + aEvent.currentTarget.addEventListener("mouseout", mouseoutHandler, false); + aEvent.currentTarget.addEventListener("mouseup", mouseupHandler, false); + timer = setTimeout(openMenu, 500, aEvent.currentTarget); + } + + function mouseoutHandler(aEvent) { + let buttonRect = aEvent.currentTarget.getBoundingClientRect(); + if (aEvent.clientX >= buttonRect.left && + aEvent.clientX <= buttonRect.right && + aEvent.clientY >= buttonRect.bottom) + openMenu(aEvent.currentTarget); + else + cancelHold(aEvent.currentTarget); + } + + function mouseupHandler(aEvent) { + cancelHold(aEvent.currentTarget); + } + + function cancelHold(aButton) { + clearTimeout(timer); + aButton.removeEventListener("mouseout", mouseoutHandler, false); + aButton.removeEventListener("mouseup", mouseupHandler, false); + } + + function clickHandler(aEvent) { + if (aEvent.button == 0 && + aEvent.target == aEvent.currentTarget && + !aEvent.currentTarget.open && + !aEvent.currentTarget.disabled) { + let cmdEvent = document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent("command", true, true, window, 0, + aEvent.ctrlKey, aEvent.altKey, aEvent.shiftKey, + aEvent.metaKey, null); + aEvent.currentTarget.dispatchEvent(cmdEvent); + } + } + + function _addClickAndHoldListenersOnElement(aElm) { + aElm.addEventListener("mousedown", mousedownHandler, true); + aElm.addEventListener("click", clickHandler, true); + } + + // Bug 414797: Clone unified-back-forward-button's context menu into both the + // back and the forward buttons. + var unifiedButton = document.getElementById("unified-back-forward-button"); + if (unifiedButton && !unifiedButton._clickHandlersAttached) { + unifiedButton._clickHandlersAttached = true; + + let popup = document.getElementById("backForwardMenu").cloneNode(true); + popup.removeAttribute("id"); + // Prevent the context attribute on unified-back-forward-button from being + // inherited. + popup.setAttribute("context", ""); + + let backButton = document.getElementById("back-button"); + backButton.setAttribute("type", "menu"); + backButton.appendChild(popup); + _addClickAndHoldListenersOnElement(backButton); + + let forwardButton = document.getElementById("forward-button"); + popup = popup.cloneNode(true); + forwardButton.setAttribute("type", "menu"); + forwardButton.appendChild(popup); + _addClickAndHoldListenersOnElement(forwardButton); + } +} + +const gSessionHistoryObserver = { + observe: function(subject, topic, data) + { + if (topic != "browser:purge-session-history") + return; + + var backCommand = document.getElementById("Browser:Back"); + backCommand.setAttribute("disabled", "true"); + var fwdCommand = document.getElementById("Browser:Forward"); + fwdCommand.setAttribute("disabled", "true"); + + // Hide session restore button on about:home + window.messageManager.broadcastAsyncMessage("Browser:HideSessionRestoreButton"); + + if (gURLBar) { + // Clear undo history of the URL bar + gURLBar.editor.transactionManager.clear() + } + } +}; + +var gFindBarSettings = { + messageName: "Findbar:Keypress", + prefName: "accessibility.typeaheadfind", + findAsYouType: null, + + init: function() { + window.messageManager.addMessageListener(this.messageName, this); + + gPrefService.addObserver(this.prefName, this, false); + this.writeFindAsYouType(); + }, + + uninit: function() { + window.messageManager.removeMessageListener(this.messageName, this); + + try { + gPrefService.removeObserver(this.prefName, this); + } catch (ex) { + Cu.reportError(ex); + } + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic != "nsPref:changed") { + return; + } + + this.writeFindAsYouType(); + }, + + writeFindAsYouType: function() { + this.findAsYouType = gPrefService.getBoolPref(this.prefName); + }, + + receiveMessage: function(aMessage) { + switch (aMessage.name) { + case this.messageName: + // If the find bar for chrome window's context is not yet alive, + // only initialize it if there's a possibility FindAsYouType + // will be used. + // There's no point in doing it for most random keypresses. + if (!gFindBarInitialized && aMessage.data.shouldFastFind) { + let shouldFastFind = this.findAsYouType; + if (!shouldFastFind) { + // Please keep in sync with toolkit/content/widgets/findbar.xml + const FAYT_LINKS_KEY = "'"; + const FAYT_TEXT_KEY = "/"; + let charCode = aMessage.data.fakeEvent.charCode; + let key = charCode ? String.fromCharCode(charCode) : null; + shouldFastFind = key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY; + } + if (shouldFastFind) { + // Make sure we return the result. + return gFindBar.receiveMessage(aMessage); + } + } + break; + } + } +}; + +var gURLBarSettings = { + prefSuggest: "browser.urlbar.suggest.", + /* + For searching in the source code: + browser.urlbar.suggest.bookmark + browser.urlbar.suggest.history + browser.urlbar.suggest.openpage + */ + prefSuggests: [ + "bookmark", + "history", + "openpage" + ], + prefKeyword: "keyword.enabled", + + observe: function(aSubject, aTopic, aData) { + if (aTopic != "nsPref:changed") + return; + + this.writePlaceholder(); + }, + + writePlaceholder: function() { + if (!gURLBar) { + return; + } + + let attribute = "placeholder"; + let prefs = this.prefSuggests.map(pref => { + return this.prefSuggest + pref; + }); + prefs.push(this.prefKeyword); + let placeholderDefault = prefs.some(pref => { + return gPrefService.getBoolPref(pref); + }); + + if (placeholderDefault) { + gURLBar.setAttribute( + attribute, gNavigatorBundle.getString("urlbar.placeholder")); + } else { + gURLBar.setAttribute( + attribute, gNavigatorBundle.getString("urlbar.placeholderURLOnly")); + } + } +}; + +/** + * Given a starting docshell and a URI to look up, find the docshell the URI + * is loaded in. + * @param aDocument + * A document to find instead of using just a URI - this is more specific. + * @param aDocShell + * The doc shell to start at + * @param aSoughtURI + * The URI that we're looking for + * @returns The doc shell that the sought URI is loaded in. Can be in + * subframes. + */ +function findChildShell(aDocument, aDocShell, aSoughtURI) { + aDocShell.QueryInterface(Components.interfaces.nsIWebNavigation); + aDocShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor); + var doc = aDocShell.getInterface(Components.interfaces.nsIDOMDocument); + if ((aDocument && doc == aDocument) || + (aSoughtURI && aSoughtURI.spec == aDocShell.currentURI.spec)) + return aDocShell; + + var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem); + for (var i = 0; i < node.childCount; ++i) { + var docShell = node.getChildAt(i); + docShell = findChildShell(aDocument, docShell, aSoughtURI); + if (docShell) + return docShell; + } + return null; +} + +var gPopupBlockerObserver = { + _reportButton: null, + + onReportButtonClick: function (aEvent) + { + if (aEvent.button != 0 || aEvent.target != this._reportButton) + return; + + document.getElementById("blockedPopupOptions") + .openPopup(this._reportButton, "after_end", 0, 2, false, false, aEvent); + }, + + handleEvent: function (aEvent) + { + if (aEvent.originalTarget != gBrowser.selectedBrowser) + return; + + if (!this._reportButton && gURLBar) + this._reportButton = document.getElementById("page-report-button"); + + if (!gBrowser.selectedBrowser.blockedPopups || + !gBrowser.selectedBrowser.blockedPopups.length) { + // Hide the icon in the location bar (if the location bar exists) + if (gURLBar) + this._reportButton.hidden = true; + return; + } + + if (gURLBar) + this._reportButton.hidden = false; + + // Only show the notification again if we've not already shown it. Since + // notifications are per-browser, we don't need to worry about re-adding + // it. + if (!gBrowser.selectedBrowser.blockedPopups.reported) { + if (gPrefService.getBoolPref("privacy.popups.showBrowserMessage")) { + var brandBundle = document.getElementById("bundle_brand"); + var brandShortName = brandBundle.getString("brandShortName"); + var popupCount = gBrowser.selectedBrowser.blockedPopups.length; + var popupButtonText = gNavigatorBundle.getString("popupWarningButton"); + var popupButtonAccesskey = gNavigatorBundle.getString("popupWarningButton.accesskey"); + var messageBase = gNavigatorBundle.getString("popupWarning.message"); + var message = PluralForm.get(popupCount, messageBase) + .replace("#1", brandShortName) + .replace("#2", popupCount); + + var notificationBox = gBrowser.getNotificationBox(); + var notification = notificationBox.getNotificationWithValue("popup-blocked"); + if (notification) { + notification.label = message; + } + else { + var buttons = [{ + label: popupButtonText, + accessKey: popupButtonAccesskey, + popup: "blockedPopupOptions", + callback: null + }]; + + const priority = notificationBox.PRIORITY_WARNING_MEDIUM; + notificationBox.appendNotification(message, "popup-blocked", + "chrome://browser/skin/Info.png", + priority, buttons); + } + } + + // Record the fact that we've reported this blocked popup, so we don't + // show it again. + gBrowser.selectedBrowser.blockedPopups.reported = true; + } + }, + + toggleAllowPopupsForSite: function (aEvent) + { + var pm = Services.perms; + var shouldBlock = aEvent.target.getAttribute("block") == "true"; + var perm = shouldBlock ? pm.DENY_ACTION : pm.ALLOW_ACTION; + pm.add(gBrowser.currentURI, "popup", perm); + + gBrowser.getNotificationBox().removeCurrentNotification(); + }, + + fillPopupList: function (aEvent) + { + // XXXben - rather than using |currentURI| here, which breaks down on multi-framed sites + // we should really walk the blockedPopups and create a list of "allow for <host>" + // menuitems for the common subset of hosts present in the report, this will + // make us frame-safe. + // + // XXXjst - Note that when this is fixed to work with multi-framed sites, + // also back out the fix for bug 343772 where + // nsGlobalWindow::CheckOpenAllow() was changed to also + // check if the top window's location is whitelisted. + let browser = gBrowser.selectedBrowser; + var uri = browser.currentURI; + var blockedPopupAllowSite = document.getElementById("blockedPopupAllowSite"); + try { + blockedPopupAllowSite.removeAttribute("hidden"); + + var pm = Services.perms; + if (pm.testPermission(uri, "popup") == pm.ALLOW_ACTION) { + // Offer an item to block popups for this site, if a whitelist entry exists + // already for it. + let blockString = gNavigatorBundle.getFormattedString("popupBlock", [uri.host || uri.spec]); + blockedPopupAllowSite.setAttribute("label", blockString); + blockedPopupAllowSite.setAttribute("block", "true"); + } + else { + // Offer an item to allow popups for this site + let allowString = gNavigatorBundle.getFormattedString("popupAllow", [uri.host || uri.spec]); + blockedPopupAllowSite.setAttribute("label", allowString); + blockedPopupAllowSite.removeAttribute("block"); + } + } + catch (e) { + blockedPopupAllowSite.setAttribute("hidden", "true"); + } + + if (PrivateBrowsingUtils.isWindowPrivate(window)) + blockedPopupAllowSite.setAttribute("disabled", "true"); + else + blockedPopupAllowSite.removeAttribute("disabled"); + + let blockedPopupDontShowMessage = document.getElementById("blockedPopupDontShowMessage"); + let showMessage = gPrefService.getBoolPref("privacy.popups.showBrowserMessage"); + blockedPopupDontShowMessage.setAttribute("checked", !showMessage); + if (aEvent.target.anchorNode.id == "page-report-button") { + aEvent.target.anchorNode.setAttribute("open", "true"); + blockedPopupDontShowMessage.setAttribute("label", gNavigatorBundle.getString("popupWarningDontShowFromLocationbar")); + } else { + blockedPopupDontShowMessage.setAttribute("label", gNavigatorBundle.getString("popupWarningDontShowFromMessage")); + } + + let blockedPopupsSeparator = + document.getElementById("blockedPopupsSeparator"); + blockedPopupsSeparator.setAttribute("hidden", true); + + gBrowser.selectedBrowser.retrieveListOfBlockedPopups().then(blockedPopups => { + let foundUsablePopupURI = false; + if (blockedPopups) { + for (let i = 0; i < blockedPopups.length; i++) { + let blockedPopup = blockedPopups[i]; + + // popupWindowURI will be null if the file picker popup is blocked. + // xxxdz this should make the option say "Show file picker" and do it (Bug 590306) + if (!blockedPopup.popupWindowURIspec) + continue; + + var popupURIspec = blockedPopup.popupWindowURIspec; + + // Sometimes the popup URI that we get back from the blockedPopup + // isn't useful (for instance, netscape.com's popup URI ends up + // being "http://www.netscape.com", which isn't really the URI of + // the popup they're trying to show). This isn't going to be + // useful to the user, so we won't create a menu item for it. + if (popupURIspec == "" || popupURIspec == "about:blank" || + popupURIspec == "<self>" || + popupURIspec == uri.spec) + continue; + + // Because of the short-circuit above, we may end up in a situation + // in which we don't have any usable popup addresses to show in + // the menu, and therefore we shouldn't show the separator. However, + // since we got past the short-circuit, we must've found at least + // one usable popup URI and thus we'll turn on the separator later. + foundUsablePopupURI = true; + + var menuitem = document.createElement("menuitem"); + var label = gNavigatorBundle.getFormattedString("popupShowPopupPrefix", + [popupURIspec]); + menuitem.setAttribute("label", label); + menuitem.setAttribute("oncommand", "gPopupBlockerObserver.showBlockedPopup(event);"); + menuitem.setAttribute("popupReportIndex", i); + menuitem.popupReportBrowser = browser; + aEvent.target.appendChild(menuitem); + } + } + + // Show the separator if we added any + // showable popup addresses to the menu. + if (foundUsablePopupURI) + blockedPopupsSeparator.removeAttribute("hidden"); + }, null); + }, + + onPopupHiding: function (aEvent) { + if (aEvent.target.anchorNode.id == "page-report-button") + aEvent.target.anchorNode.removeAttribute("open"); + + let item = aEvent.target.lastChild; + while (item && item.getAttribute("observes") != "blockedPopupsSeparator") { + let next = item.previousSibling; + item.parentNode.removeChild(item); + item = next; + } + }, + + showBlockedPopup: function (aEvent) + { + var target = aEvent.target; + var popupReportIndex = target.getAttribute("popupReportIndex"); + let browser = target.popupReportBrowser; + browser.unblockPopup(popupReportIndex); + }, + + editPopupSettings: function () + { + var host = ""; + try { + host = gBrowser.currentURI.host; + } + catch (e) { } + + var bundlePreferences = document.getElementById("bundle_preferences"); + var params = { blockVisible : false, + sessionVisible : false, + allowVisible : true, + prefilledHost : host, + permissionType : "popup", + windowTitle : bundlePreferences.getString("popuppermissionstitle"), + introText : bundlePreferences.getString("popuppermissionstext") }; + var existingWindow = Services.wm.getMostRecentWindow("Browser:Permissions"); + if (existingWindow) { + existingWindow.initWithParams(params); + existingWindow.focus(); + } + else + window.openDialog("chrome://browser/content/preferences/permissions.xul", + "_blank", "resizable,dialog=no,centerscreen", params); + }, + + dontShowMessage: function () + { + var showMessage = gPrefService.getBoolPref("privacy.popups.showBrowserMessage"); + gPrefService.setBoolPref("privacy.popups.showBrowserMessage", !showMessage); + gBrowser.getNotificationBox().removeCurrentNotification(); + } +}; + +const gXSSObserver = { + + observe: function (aSubject, aTopic, aData) + { + + // Don't do anything if the notification is disabled. + if (!gPrefService.getBoolPref("security.xssfilter.displayWarning")) + return; + + // Parse incoming XSS array + aSubject.QueryInterface(Ci.nsIArray); + var policy = aSubject.queryElementAt(0, Ci.nsISupportsString).data; + var content = aSubject.queryElementAt(1, Ci.nsISupportsString).data; + var domain = aSubject.queryElementAt(2, Ci.nsISupportsString).data; + var url = aSubject.queryElementAt(3, Ci.nsISupportsCString).data; + var blockMode = aSubject.queryElementAt(4, Ci.nsISupportsPRBool).data; + + // If it is a block mode event, do not display the infobar + if (blockMode) + return; + + var nb = gBrowser.getNotificationBox(); + const priority = nb.PRIORITY_WARNING_MEDIUM; + + var buttons = [{ + label: 'View Unsafe Content', + accessKey: 'V', + popup: null, + callback: function () { + alert(content); + } + }]; + + if (domain !== "") + buttons.push({ + label: 'Add Domain Exception', + accessKey: 'A', + popup: null, + callback: function () { + let whitelist = gPrefService.getCharPref("security.xssfilter.whitelist"); + if (whitelist != "") { + whitelist = whitelist + "," + domain; + } else { + whitelist = domain; + } + // Write the updated whitelist. Since this is observed by the XSS filter, + // it will automatically sync to the back-end and update immediately. + gPrefService.setCharPref("security.xssfilter.whitelist", whitelist); + // After setting this, we automatically reload the page. + BrowserReloadSkipCache(); + } + }); + + nb.appendNotification("The XSS Filter has detected a potential XSS attack. Type: " + + policy, 'popup-blocked', 'chrome://browser/skin/Info.png', + priority, buttons); + } +}; + +var gBrowserInit = { + delayedStartupFinished: false, + + onLoad: function() { + gMultiProcessBrowser = gPrefService.getBoolPref("browser.tabs.remote"); + + var mustLoadSidebar = false; + + Cc["@mozilla.org/eventlistenerservice;1"] + .getService(Ci.nsIEventListenerService) + .addSystemEventListener(gBrowser, "click", contentAreaClick, true); + + gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false); + + // Note that the XBL binding is untrusted + gBrowser.addEventListener("PluginBindingAttached", gPluginHandler, true, true); + gBrowser.addEventListener("PluginCrashed", gPluginHandler, true); + gBrowser.addEventListener("PluginOutdated", gPluginHandler, true); + gBrowser.addEventListener("PluginInstantiated", gPluginHandler, true); + gBrowser.addEventListener("PluginRemoved", gPluginHandler, true); + + Services.obs.addObserver(gPluginHandler.pluginCrashed, "plugin-crashed", false); + + window.addEventListener("AppCommand", HandleAppCommandEvent, true); + + // These routines add message listeners. They must run before + // loading the frame script to ensure that we don't miss any + // message sent between when the frame script is loaded and when + // the listener is registered. +#ifdef MOZ_DEVTOOLS + DevToolsTheme.init(); +#endif + gFindBarSettings.init(); + + messageManager.loadFrameScript("chrome://browser/content/content.js", true); + messageManager.loadFrameScript("chrome://browser/content/content-sessionStore.js", true); + + // initialize observers and listeners + // and give C++ access to gBrowser + XULBrowserWindow.init(); + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow) + .XULBrowserWindow = window.XULBrowserWindow; + window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = + new nsBrowserAccess(); + + // set default character set if provided + // window.arguments[1]: character set (string) + if ("arguments" in window && window.arguments.length > 1 && window.arguments[1]) { + if (window.arguments[1].startsWith("charset=")) { + var arrayArgComponents = window.arguments[1].split("="); + if (arrayArgComponents) { + //we should "inherit" the charset menu setting in a new window + //TFE FIXME: this is now a wrappednative and can't be set this way. + //getMarkupDocumentViewer().defaultCharacterSet = arrayArgComponents[1]; + } + } + } + + // Manually hook up session and global history for the first browser + // so that we don't have to load global history before bringing up a + // window. + // Wire up session and global history before any possible + // progress notifications for back/forward button updating + gBrowser.webNavigation.sessionHistory = Cc["@mozilla.org/browser/shistory;1"]. + createInstance(Ci.nsISHistory); + Services.obs.addObserver(gBrowser.browsers[0], "browser:purge-session-history", false); + + // remove the disablehistory attribute so the browser cleans up, as + // though it had done this work itself + gBrowser.browsers[0].removeAttribute("disablehistory"); + + // enable global history + try { + if (!gMultiProcessBrowser) + gBrowser.docShell.useGlobalHistory = true; + } catch(ex) { + Cu.reportError("Places database may be locked: " + ex); + } + + // hook up UI through progress listener + gBrowser.addProgressListener(window.XULBrowserWindow); + gBrowser.addTabsProgressListener(window.TabsProgressListener); + + // setup our common DOMLinkAdded listener + gBrowser.addEventListener("DOMLinkAdded", DOMLinkHandler, false); + + // setup our MozApplicationManifest listener + gBrowser.addEventListener("MozApplicationManifest", + OfflineApps, false); + + // setup simple gestures support + gGestureSupport.init(true); + + // setup history swipe animation + gHistorySwipeAnimation.init(); + + if (window.opener && !window.opener.closed) { + let openerSidebarBox = window.opener.document.getElementById("sidebar-box"); + // If the opener had a sidebar, open the same sidebar in our window. + // The opener can be the hidden window too, if we're coming from the state + // where no windows are open, and the hidden window has no sidebar box. + if (openerSidebarBox && !openerSidebarBox.hidden) { + let sidebarCmd = openerSidebarBox.getAttribute("sidebarcommand"); + let sidebarCmdElem = document.getElementById(sidebarCmd); + + // dynamically generated sidebars will fail this check. + if (sidebarCmdElem) { + let sidebarBox = document.getElementById("sidebar-box"); + let sidebarTitle = document.getElementById("sidebar-title"); + + sidebarTitle.setAttribute( + "value", window.opener.document.getElementById("sidebar-title").getAttribute("value")); + sidebarBox.setAttribute("width", openerSidebarBox.boxObject.width); + + sidebarBox.setAttribute("sidebarcommand", sidebarCmd); + // Note: we're setting 'src' on sidebarBox, which is a <vbox>, not on + // the <browser id="sidebar">. This lets us delay the actual load until + // delayedStartup(). + sidebarBox.setAttribute( + "src", window.opener.document.getElementById("sidebar").getAttribute("src")); + mustLoadSidebar = true; + + sidebarBox.hidden = false; + document.getElementById("sidebar-splitter").hidden = false; + sidebarCmdElem.setAttribute("checked", "true"); + } + } + } + else { + let box = document.getElementById("sidebar-box"); + if (box.hasAttribute("sidebarcommand")) { + let commandID = box.getAttribute("sidebarcommand"); + if (commandID) { + let command = document.getElementById(commandID); + if (command) { + mustLoadSidebar = true; + box.hidden = false; + document.getElementById("sidebar-splitter").hidden = false; + command.setAttribute("checked", "true"); + } + else { + // Remove the |sidebarcommand| attribute, because the element it + // refers to no longer exists, so we should assume this sidebar + // panel has been uninstalled. (249883) + box.removeAttribute("sidebarcommand"); + } + } + } + } + + // Certain kinds of automigration rely on this notification to complete their + // tasks BEFORE the browser window is shown. + Services.obs.notifyObservers(null, "browser-window-before-show", ""); + + // Set a sane starting width/height for all resolutions on new profiles. + if (!document.documentElement.hasAttribute("width")) { + let defaultWidth; + let defaultHeight; + + // Very small: maximize the window + // Portrait : use about full width and 3/4 height, to view entire pages + // at once (without being obnoxiously tall) + // Widescreen: use about half width, to suggest side-by-side page view + // Otherwise : use 3/4 height and width + if (screen.availHeight <= 600) { + document.documentElement.setAttribute("sizemode", "maximized"); + defaultWidth = 610; + defaultHeight = 450; + } + else { + if (screen.availWidth <= screen.availHeight) { + defaultWidth = screen.availWidth * .9; + defaultHeight = screen.availHeight * .75; + } + else if (screen.availWidth >= 2048) { + defaultWidth = (screen.availWidth / 2) - 20; + defaultHeight = screen.availHeight - 10; + } + else { + defaultWidth = screen.availWidth * .75; + defaultHeight = screen.availHeight * .75; + } + +#ifdef MOZ_WIDGET_GTK2 + // 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) + defaultHeight -= 28; +#endif + } + document.documentElement.setAttribute("width", defaultWidth); + document.documentElement.setAttribute("height", defaultHeight); + } + + if (!gShowPageResizers) + document.getElementById("status-bar").setAttribute("hideresizer", "true"); + + if (!window.toolbar.visible) { + // adjust browser UI for popups + if (gURLBar) { + gURLBar.setAttribute("readonly", "true"); + gURLBar.setAttribute("enablehistory", "false"); + } + goSetCommandEnabled("cmd_newNavigatorTab", false); + } + +#ifdef MENUBAR_CAN_AUTOHIDE + updateAppButtonDisplay(); +#endif + + // Misc. inits. + CombinedStopReload.init(); + allTabs.readPref(); + TabsOnTop.init(); + AudioIndicator.init(); + gPrivateBrowsingUI.init(); + TabsInTitlebar.init(); + retrieveToolbarIconsizesFromTheme(); + ToolbarIconColor.init(); + UserAgentCompatibility.init(); + +#ifdef XP_WIN + if (window.matchMedia("(-moz-os-version: windows-win8)").matches && + window.matchMedia("(-moz-windows-default-theme)").matches) { + let windows8WindowFrameColor = Cu.import("resource:///modules/Windows8WindowFrameColor.jsm", {}).Windows8WindowFrameColor; + + var windowFrameColor; + windowFrameColor = windows8WindowFrameColor.get_win8(); + + // Formula from Microsoft's UWP guideline. + let backgroundLuminance = (windowFrameColor[0] * 2 + + windowFrameColor[1] * 5 + + windowFrameColor[2]) / 8; + if (backgroundLuminance <= 128) { + document.documentElement.setAttribute("darkwindowframe", "true"); + } + } +#endif + + // Wait until chrome is painted before executing code not critical to making the window visible + this._boundDelayedStartup = this._delayedStartup.bind(this, mustLoadSidebar); + window.addEventListener("MozAfterPaint", this._boundDelayedStartup); + + this._loadHandled = true; + }, + + _cancelDelayedStartup: function () { + window.removeEventListener("MozAfterPaint", this._boundDelayedStartup); + this._boundDelayedStartup = null; + }, + + _delayedStartup: function(mustLoadSidebar) { + let tmp = {}; + + this._cancelDelayedStartup(); + + let uriToLoad = this._getUriToLoad(); + var isLoadingBlank = isBlankPageURL(uriToLoad); + + // This pageshow listener needs to be registered before we may call + // swapBrowsersAndCloseOther() to receive pageshow events fired by that. + gBrowser.addEventListener("pageshow", function(event) { + // Filter out events that are not about the document load we are interested in + if (content && event.target == content.document) + setTimeout(pageShowEventHandlers, 0, event.persisted); + }, true); + + if (uriToLoad && uriToLoad != "about:blank") { + if (uriToLoad instanceof Ci.nsISupportsArray) { + let count = uriToLoad.Count(); + let specs = []; + for (let i = 0; i < count; i++) { + let urisstring = uriToLoad.GetElementAt(i).QueryInterface(Ci.nsISupportsString); + specs.push(urisstring.data); + } + + // This function throws for certain malformed URIs, so use exception handling + // so that we don't disrupt startup + try { + gBrowser.loadTabs(specs, false, true); + } catch (e) {} + } + else if (uriToLoad instanceof XULElement) { + // swap the given tab with the default about:blank tab and then close + // the original tab in the other window. + + // Stop the about:blank load + gBrowser.stop(); + // make sure it has a docshell + gBrowser.docShell; + + gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, uriToLoad); + } + // window.arguments[2]: referrer (nsIURI | string) + // [3]: postData (nsIInputStream) + // [4]: allowThirdPartyFixup (bool) + // [5]: referrerPolicy (int) + // [6]: originPrincipal (nsIPrincipal) + // [7]: triggeringPrincipal (nsIPrincipal) + else if (window.arguments.length >= 3) { + 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_DEFAULT); + loadURI(uriToLoad, referrerURI, window.arguments[3] || null, + window.arguments[4] || false, referrerPolicy, + // pass the origin principal (if any) and force its use to create + // an initial about:blank viewer if present: + window.arguments[6], !!window.arguments[6], window.arguments[7]); + window.focus(); + } + // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3. + // Such callers expect that window.arguments[0] is handled as a single URI. + else + loadOneOrMoreURIs(uriToLoad); + } + + Services.obs.addObserver(gSessionHistoryObserver, "browser:purge-session-history", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-started", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-origin-blocked", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-failed", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false); + Services.obs.addObserver(gXSSObserver, "xss-on-violate-policy", false); + + gPrefService.addObserver(gURLBarSettings.prefSuggest, gURLBarSettings, false); + gPrefService.addObserver(gURLBarSettings.prefKeyword, gURLBarSettings, false); + + gURLBarSettings.writePlaceholder(); + + BrowserOffline.init(); + OfflineApps.init(); + IndexedDBPromptHelper.init(); + AddonManager.addAddonListener(AddonsMgrListener); +#ifdef MOZ_WEBRTC + WebrtcIndicator.init(); +#endif + + // Ensure login manager is up and running. + Services.logins; + + if (mustLoadSidebar) { + let sidebar = document.getElementById("sidebar"); + let sidebarBox = document.getElementById("sidebar-box"); + sidebar.setAttribute("src", sidebarBox.getAttribute("src")); + } + + UpdateUrlbarSearchSplitterState(); + + if (!isLoadingBlank || !focusAndSelectUrlBar()) + gBrowser.selectedBrowser.focus(); + + gNavToolbox.customizeDone = BrowserToolboxCustomizeDone; + gNavToolbox.customizeChange = BrowserToolboxCustomizeChange; + + // Set up Sanitize Item + this._initializeSanitizer(); + + // Enable/Disable auto-hide tabbar + gBrowser.tabContainer.updateVisibility(); + + gPrefService.addObserver(gHomeButton.prefDomain, gHomeButton, false); + + var homeButton = document.getElementById("home-button"); + gHomeButton.updateTooltip(homeButton); + gHomeButton.updatePersonalToolbarStyle(homeButton); + + // BiDi UI + gBidiUI = isBidiEnabled(); + if (gBidiUI) { + document.getElementById("documentDirection-separator").hidden = false; + document.getElementById("documentDirection-swap").hidden = false; + document.getElementById("textfieldDirection-separator").hidden = false; + document.getElementById("textfieldDirection-swap").hidden = false; + } + + // Setup click-and-hold gestures access to the session history + // menus if global click-and-hold isn't turned on + if (!Services.prefs.getBoolPref("ui.click_hold_context_menus", false)) + SetClickAndHoldHandlers(); + + // Initialize the full zoom setting. + // We do this before the session restore service gets initialized so we can + // apply full zoom settings to tabs restored by the session restore service. + FullZoom.init(); + + // Bug 666804 - NetworkPrioritizer support for e10s + if (!gMultiProcessBrowser) { + let NP = {}; + Cu.import("resource:///modules/NetworkPrioritizer.jsm", NP); + NP.trackBrowserWindow(window); + } + + // initialize the session-restore service (in case it's not already running) + let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + let ssPromise = ss.init(window); + + PlacesToolbarHelper.init(); + + ctrlTab.readPref(); + gPrefService.addObserver(ctrlTab.prefName, ctrlTab, false); + gPrefService.addObserver(allTabs.prefName, allTabs, false); + + // Initialize the download manager some time after the app starts so that + // auto-resume downloads begin (such as after crashing or quitting with + // active downloads) and speeds up the first-load of the download manager UI. + // If the user manually opens the download manager before the timeout, the + // downloads will start right away, and getting the service again won't hurt. + setTimeout(function() { + try { + Cu.import("resource:///modules/DownloadsCommon.jsm", {}) + .DownloadsCommon.initializeAllDataLinks(); + Cu.import("resource:///modules/DownloadsTaskbar.jsm", {}) + .DownloadsTaskbar.registerIndicator(window); + } catch(ex) { + Cu.reportError(ex); + } + }, 10000); + + // Load the Login Manager data from disk off the main thread, some time + // after startup. If the data is required before the timeout, for example + // because a restored page contains a password field, it will be loaded on + // the main thread, and this initialization request will be ignored. + setTimeout(function() { + try { + Services.logins; + } catch (ex) { + Cu.reportError(ex); + } + }, 3000); + + // The object handling the downloads indicator is also initialized here in the + // delayed startup function, but the actual indicator element is not loaded + // unless there are downloads to be displayed. + DownloadsButton.initializeIndicator(); + +#ifndef XP_MACOSX + updateEditUIVisibility(); + let placesContext = document.getElementById("placesContext"); + placesContext.addEventListener("popupshowing", updateEditUIVisibility, false); + placesContext.addEventListener("popuphiding", updateEditUIVisibility, false); +#endif + +#ifdef MOZ_PERSONAS + gBrowser.mPanelContainer.addEventListener("InstallBrowserTheme", LightWeightThemeWebInstaller, false, true); + gBrowser.mPanelContainer.addEventListener("PreviewBrowserTheme", LightWeightThemeWebInstaller, false, true); + gBrowser.mPanelContainer.addEventListener("ResetBrowserThemePreview", LightWeightThemeWebInstaller, false, true); +#endif + + // Bug 666808 - AeroPeek support for e10s + if (!gMultiProcessBrowser) { + if (Win7Features) + Win7Features.onOpenWindow(); + } + + // called when we go into full screen, even if initiated by a web page script + window.addEventListener("fullscreen", onFullScreen, true); + + // Called when we enter DOM full-screen mode. Note we can already be in browser + // full-screen mode when we enter DOM full-screen mode. + window.addEventListener("MozDOMFullscreen:NewOrigin", onMozEnteredDomFullscreen, true); + + if (window.fullScreen) + onFullScreen(); + if (document.mozFullScreen) + onMozEnteredDomFullscreen(); + +#ifdef MOZ_SERVICES_SYNC + // initialize the sync UI + gSyncUI.init(); +#endif + + gBrowserThumbnails.init(); + + setUrlAndSearchBarWidthForConditionalForwardButton(); + window.addEventListener("resize", function resizeHandler(event) { + if (event.target == window) + setUrlAndSearchBarWidthForConditionalForwardButton(); + }); + + // Enable Error Console? + let consoleEnabled = gPrefService.getBoolPref("devtools.errorconsole.enabled"); + if (consoleEnabled) { + let cmd = document.getElementById("Tools:ErrorConsole"); + cmd.removeAttribute("disabled"); + cmd.removeAttribute("hidden"); + } + +#ifdef MENUBAR_CAN_AUTOHIDE + // If the user (or the locale) hasn't enabled the top-level "Character + // Encoding" menu via the "browser.menu.showCharacterEncoding" preference, + // hide it. + if ("true" != gPrefService.getComplexValue("browser.menu.showCharacterEncoding", + Ci.nsIPrefLocalizedString).data) + document.getElementById("appmenu_charsetMenu").hidden = true; +#endif + + let appMenuButton = document.getElementById("appmenu-button"); + let appMenuPopup = document.getElementById("appmenu-popup"); + if (appMenuButton && appMenuPopup) { + let appMenuOpening = null; + appMenuButton.addEventListener("mousedown", function(event) { + if (event.button == 0) + appMenuOpening = new Date(); + }, false); + appMenuPopup.addEventListener("popupshown", function(event) { + if (event.target != appMenuPopup || !appMenuOpening) + return; + let duration = new Date() - appMenuOpening; + appMenuOpening = null; + }, false); + } + + window.addEventListener("mousemove", MousePosTracker, false); + window.addEventListener("dragover", MousePosTracker, false); + + // End startup crash tracking after a delay to catch crashes while restoring + // tabs and to postpone saving the pref to disk. + try { + const startupCrashEndDelay = 30 * 1000; + setTimeout(Services.startup.trackStartupCrashEnd, startupCrashEndDelay); + } catch (ex) { + Cu.reportError("Could not end startup crash tracking: " + ex); + } + + ssPromise.then(() =>{ + // Bail out if the window has been closed in the meantime. + if (window.closed) { + return; + } + if ("TabView" in window) { + TabView.init(); + } + // XXX: do we still need this?... + setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0); + }); + + this.delayedStartupFinished = true; + + Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); + }, + + // Returns the URI(s) to load at startup. + _getUriToLoad: function () { + // window.arguments[0]: URI to load (string), or an nsISupportsArray of + // nsISupportsStrings to load, or a xul:tab of + // a tabbrowser, which will be replaced by this + // window (for this case, all other arguments are + // ignored). + if (!window.arguments || !window.arguments[0]) + return null; + + let uri = window.arguments[0]; + let sessionStartup = Cc["@mozilla.org/browser/sessionstartup;1"] + .getService(Ci.nsISessionStartup); + let defaultArgs = Cc["@mozilla.org/browser/clh;1"] + .getService(Ci.nsIBrowserHandler) + .defaultArgs; + + // If the given URI matches defaultArgs (the default homepage) we want + // to block its load if we're going to restore a session anyway. + if (uri == defaultArgs && sessionStartup.willOverrideHomepage) + return null; + + return uri; + }, + + onUnload: function() { + // In certain scenarios it's possible for unload to be fired before onload, + // (e.g. if the window is being closed after browser.js loads but before the + // load completes). In that case, there's nothing to do here. + if (!this._loadHandled) + return; + + // First clean up services initialized in gBrowserInit.onLoad (or those whose + // uninit methods don't depend on the services having been initialized). + + allTabs.uninit(); + + CombinedStopReload.uninit(); + + gGestureSupport.init(false); + + gHistorySwipeAnimation.uninit(); + + FullScreen.cleanup(); + + Services.obs.removeObserver(gPluginHandler.pluginCrashed, "plugin-crashed"); + + try { + gBrowser.removeProgressListener(window.XULBrowserWindow); + gBrowser.removeTabsProgressListener(window.TabsProgressListener); + } catch (ex) { + } + + BookmarkingUI.uninit(); + + TabsOnTop.uninit(); + + AudioIndicator.uninit(); + + TabsInTitlebar.uninit(); + + ToolbarIconColor.uninit(); + +#ifdef MOZ_DEVTOOLS + DevToolsTheme.uninit(); +#endif + gFindBarSettings.uninit(); + + UserAgentCompatibility.uninit(); + + var enumerator = Services.wm.getEnumerator(null); + enumerator.getNext(); + if (!enumerator.hasMoreElements()) { + document.persist("sidebar-box", "sidebarcommand"); + document.persist("sidebar-box", "width"); + document.persist("sidebar-box", "src"); + document.persist("sidebar-title", "value"); + } + + // Now either cancel delayedStartup, or clean up the services initialized from + // it. + if (this._boundDelayedStartup) { + this._cancelDelayedStartup(); + } else { + if (Win7Features) + Win7Features.onCloseWindow(); + + gPrefService.removeObserver(ctrlTab.prefName, ctrlTab); + gPrefService.removeObserver(allTabs.prefName, allTabs); + ctrlTab.uninit(); + if ("TabView" in window) { + TabView.uninit(); + } + gBrowserThumbnails.uninit(); + FullZoom.destroy(); + + Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-started"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-origin-blocked"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete"); + Services.obs.removeObserver(gXSSObserver, "xss-on-violate-policy"); + + try { + gPrefService.removeObserver(gURLBarSettings.prefSuggest, gURLBarSettings); + gPrefService.removeObserver(gURLBarSettings.prefKeyword, gURLBarSettings); + } catch (ex) { + Cu.reportError(ex); + } + + try { + gPrefService.removeObserver(gHomeButton.prefDomain, gHomeButton); + } catch (ex) { + Cu.reportError(ex); + } + + BrowserOffline.uninit(); + OfflineApps.uninit(); + IndexedDBPromptHelper.uninit(); + AddonManager.removeAddonListener(AddonsMgrListener); + } + + // Final window teardown, do this last. + 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; + window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = null; + }, + +#ifdef XP_MACOSX + // nonBrowserWindowStartup(), nonBrowserWindowDelayedStartup(), and + // nonBrowserWindowShutdown() are used for non-browser windows in + // macBrowserOverlay + nonBrowserWindowStartup: function() { + // Disable inappropriate commands / submenus + var disabledItems = ['Browser:SavePage', + 'Browser:SendLink', 'cmd_pageSetup', 'cmd_print', 'cmd_find', 'cmd_findAgain', + 'viewToolbarsMenu', 'viewSidebarMenuMenu', 'Browser:Reload', + 'viewFullZoomMenu', 'pageStyleMenu', 'charsetMenu', 'View:PageSource', 'View:FullScreen', + 'viewHistorySidebar', 'Browser:AddBookmarkAs', 'Browser:BookmarkAllTabs', + 'View:PageInfo', 'Browser:ToggleAddonBar']; + var element; + + for (let disabledItem of disabledItems) { + element = document.getElementById(disabledItem); + if (element) + element.setAttribute("disabled", "true"); + } + + // If no windows are active (i.e. we're the hidden window), disable the close, minimize + // and zoom menu commands as well + if (window.location.href == "chrome://browser/content/hiddenWindow.xul") { + var hiddenWindowDisabledItems = ['cmd_close', 'minimizeWindow', 'zoomWindow']; + for (let hiddenWindowDisabledItem of hiddenWindowDisabledItems) { + element = document.getElementById(hiddenWindowDisabledItem); + if (element) + element.setAttribute("disabled", "true"); + } + + // also hide the window-list separator + element = document.getElementById("sep-window-list"); + element.setAttribute("hidden", "true"); + + // Setup the dock menu. + let dockMenuElement = document.getElementById("menu_mac_dockmenu"); + if (dockMenuElement != null) { + let nativeMenu = Cc["@mozilla.org/widget/standalonenativemenu;1"] + .createInstance(Ci.nsIStandaloneNativeMenu); + + try { + nativeMenu.init(dockMenuElement); + + let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"] + .getService(Ci.nsIMacDockSupport); + dockSupport.dockMenu = nativeMenu; + } + catch (e) { + } + } + } + + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + document.getElementById("macDockMenuNewWindow").hidden = true; + } + + this._delayedStartupTimeoutId = setTimeout(this.nonBrowserWindowDelayedStartup.bind(this), 0); + }, + + nonBrowserWindowDelayedStartup: function() { + this._delayedStartupTimeoutId = null; + + // initialise the offline listener + BrowserOffline.init(); + + // Set up Sanitize Item + this._initializeSanitizer(); + + // initialize the private browsing UI + gPrivateBrowsingUI.init(); + +#ifdef MOZ_SERVICES_SYNC + // initialize the sync UI + gSyncUI.init(); +#endif + }, + + nonBrowserWindowShutdown: function() { + // If nonBrowserWindowDelayedStartup hasn't run yet, we have no work to do - + // just cancel the pending timeout and return; + if (this._delayedStartupTimeoutId) { + clearTimeout(this._delayedStartupTimeoutId); + return; + } + + BrowserOffline.uninit(); + }, +#endif + + _initializeSanitizer: function() { + const kDidSanitizeDomain = "privacy.sanitize.didShutdownSanitize"; + if (gPrefService.prefHasUserValue(kDidSanitizeDomain)) { + gPrefService.clearUserPref(kDidSanitizeDomain); + // We need to persist this preference change, since we want to + // check it at next app start even if the browser exits abruptly + gPrefService.savePrefFile(null); + } + + /** + * Migrate Firefox 3.0 privacy.item prefs under one of these conditions: + * + * a) User has customized any privacy.item prefs + * b) privacy.sanitize.sanitizeOnShutdown is set + */ + if (!gPrefService.getBoolPref("privacy.sanitize.migrateFx3Prefs")) { + let itemBranch = gPrefService.getBranch("privacy.item."); + let itemArray = itemBranch.getChildList(""); + + // See if any privacy.item prefs are set + let doMigrate = itemArray.some(function (name) itemBranch.prefHasUserValue(name)); + // Or if sanitizeOnShutdown is set + if (!doMigrate) + doMigrate = gPrefService.getBoolPref("privacy.sanitize.sanitizeOnShutdown"); + + if (doMigrate) { + let cpdBranch = gPrefService.getBranch("privacy.cpd."); + let clearOnShutdownBranch = gPrefService.getBranch("privacy.clearOnShutdown."); + for (let name of itemArray) { + try { + // don't migrate password or offlineApps clearing in the CRH dialog since + // there's no UI for those anymore. They default to false. bug 497656 + if (name != "passwords" && name != "offlineApps") + cpdBranch.setBoolPref(name, itemBranch.getBoolPref(name)); + clearOnShutdownBranch.setBoolPref(name, itemBranch.getBoolPref(name)); + } + catch(e) { + Cu.reportError("Exception thrown during privacy pref migration: " + e); + } + } + } + + gPrefService.setBoolPref("privacy.sanitize.migrateFx3Prefs", true); + } + }, +} + + +/* Legacy global init functions */ +var BrowserStartup = gBrowserInit.onLoad.bind(gBrowserInit); +var BrowserShutdown = gBrowserInit.onUnload.bind(gBrowserInit); +#ifdef XP_MACOSX +var nonBrowserWindowStartup = gBrowserInit.nonBrowserWindowStartup.bind(gBrowserInit); +var nonBrowserWindowDelayedStartup = gBrowserInit.nonBrowserWindowDelayedStartup.bind(gBrowserInit); +var nonBrowserWindowShutdown = gBrowserInit.nonBrowserWindowShutdown.bind(gBrowserInit); +#endif + +function HandleAppCommandEvent(evt) { + switch (evt.command) { + case "Back": + BrowserBack(); + break; + case "Forward": + BrowserForward(); + break; + case "Reload": + BrowserReloadSkipCache(); + break; + case "Stop": + if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") + BrowserStop(); + break; + case "Search": + BrowserSearch.webSearch(); + break; + case "Bookmarks": + toggleSidebar('viewBookmarksSidebar'); + break; + case "Home": + BrowserHome(); + break; + case "New": + BrowserOpenTab(); + break; + case "Close": + BrowserCloseTabOrWindow(); + break; + case "Find": + gFindBar.onFindCommand(); + break; + case "Help": + openHelpLink('firefox-help'); + break; + case "Open": + BrowserOpenFileWindow(); + break; + case "Print": + PrintUtils.print(); + break; + case "Save": + saveDocument(window.content.document); + break; + case "SendMail": + MailIntegration.sendLinkForWindow(window.content); + break; + default: + return; + } + evt.stopPropagation(); + evt.preventDefault(); +} + +function gotoHistoryIndex(aEvent) { + let index = aEvent.target.getAttribute("index"); + if (!index) + return false; + + let where = whereToOpenLink(aEvent); + + if (where == "current") { + // Normal click. Go there in the current tab and update session history. + + try { + gBrowser.gotoIndex(index); + } + catch(ex) { + return false; + } + return true; + } + // Modified click. Go there in a new tab/window. + + duplicateTabIn(gBrowser.selectedTab, where, index - gBrowser.sessionHistory.index); + return true; +} + +function BrowserForward(aEvent) { + let where = whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goForward(); + } + catch(ex) { + } + } + else { + duplicateTabIn(gBrowser.selectedTab, where, 1); + } +} + +function BrowserBack(aEvent) { + let where = whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goBack(); + } + catch(ex) { + } + } + else { + duplicateTabIn(gBrowser.selectedTab, where, -1); + } +} + +function BrowserHandleBackspace() +{ + switch (gPrefService.getIntPref("browser.backspace_action")) { + case 0: + BrowserBack(); + break; + case 1: + goDoCommand("cmd_scrollPageUp"); + break; + } +} + +function BrowserHandleShiftBackspace() +{ + switch (gPrefService.getIntPref("browser.backspace_action")) { + case 0: + BrowserForward(); + break; + case 1: + goDoCommand("cmd_scrollPageDown"); + break; + } +} + +function BrowserStop() { + const stopFlags = nsIWebNavigation.STOP_ALL; + gBrowser.webNavigation.stop(stopFlags); +} + +function BrowserReloadOrDuplicate(aEvent) { + var backgroundTabModifier = aEvent.button == 1 || +#ifdef XP_MACOSX + aEvent.metaKey; +#else + aEvent.ctrlKey; +#endif + if (aEvent.shiftKey && !backgroundTabModifier) { + BrowserReloadSkipCache(); + return; + } + + let where = whereToOpenLink(aEvent, false, true); + if (where == "current") + BrowserReload(); + else + duplicateTabIn(gBrowser.selectedTab, where); +} + +function BrowserReload() { + const reloadFlags = nsIWebNavigation.LOAD_FLAGS_NONE; + BrowserReloadWithFlags(reloadFlags); +} + +function BrowserReloadSkipCache() { + // Bypass proxy and cache. + const reloadFlags = nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + BrowserReloadWithFlags(reloadFlags); +} + +var BrowserHome = BrowserGoHome; +function BrowserGoHome(aEvent) { + if (aEvent && "button" in aEvent && + aEvent.button == 2) // right-click: do nothing + return; + + var homePage = gHomeButton.getHomePage(); + var where = whereToOpenLink(aEvent, false, true); + var urls; + + // Home page should open in a new tab when current tab is an app tab + if (where == "current" && + gBrowser && + gBrowser.selectedTab.pinned) + where = "tab"; + + // openUILinkIn in utilityOverlay.js doesn't handle loading multiple pages + switch (where) { + case "current": + loadOneOrMoreURIs(homePage); + break; + case "tabshifted": + case "tab": + urls = homePage.split("|"); + var loadInBackground = Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground", false); + gBrowser.loadTabs(urls, loadInBackground); + break; + case "window": + OpenBrowserWindow(); + break; + } +} + +function loadOneOrMoreURIs(aURIString) +{ +#ifdef XP_MACOSX + // 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; + } +#endif + // This function throws for certain malformed URIs, so use exception handling + // so that we don't disrupt startup + try { + gBrowser.loadTabs(aURIString.split("|"), false, true); + } + catch (e) { + } +} + +function focusAndSelectUrlBar() { + if (gURLBar) { + if (window.fullScreen) + FullScreen.showNavToolbox(); + + gURLBar.select(); + if (document.activeElement == gURLBar.inputField) + return true; + } + return false; +} + +function openLocation() { + if (focusAndSelectUrlBar()) + return; + +#ifdef XP_MACOSX + if (window.location.href != getBrowserURL()) { + var win = getTopWin(); + if (win) { + // If there's an open browser window, it should handle this command + win.focus() + win.openLocation(); + } + else { + // If there are no open browser windows, open a new one + win = window.openDialog("chrome://browser/content/", "_blank", + "chrome,all,dialog=no", BROWSER_NEW_TAB_URL); + win.addEventListener("load", openLocationCallback, false); + } + return; + } +#endif + openDialog("chrome://browser/content/openLocation.xul", "_blank", + "chrome,modal,titlebar", window); +} + +function openLocationCallback() +{ + // make sure the DOM is ready + setTimeout(function() { this.openLocation(); }, 0); +} + +function BrowserOpenTab() +{ + openUILinkIn(BROWSER_NEW_TAB_URL, "tab"); +} + +/* Called from the openLocation dialog. This allows that dialog to instruct + its opener to open a new window and then step completely out of the way. + Anything less byzantine is causing horrible crashes, rather believably, + though oddly only on Linux. */ +function delayedOpenWindow(chrome, flags, href, postData) +{ + // The other way to use setTimeout, + // setTimeout(openDialog, 10, chrome, "_blank", flags, url), + // doesn't work here. The extra "magic" extra argument setTimeout adds to + // the callback function would confuse gBrowserInit.onLoad() by making + // window.arguments[1] be an integer instead of null. + setTimeout(function() { openDialog(chrome, "_blank", flags, href, null, null, postData); }, 10); +} + +/* Required because the tab needs time to set up its content viewers and get the load of + the URI kicked off before becoming the active content area. */ +function delayedOpenTab(aUrl, aReferrer, aCharset, aPostData, aAllowThirdPartyFixup) +{ + gBrowser.loadOneTab(aUrl, { + referrerURI: aReferrer, + charset: aCharset, + postData: aPostData, + inBackground: false, + allowThirdPartyFixup: aAllowThirdPartyFixup}); +} + +var gLastOpenDirectory = { + _lastDir: null, + get path() { + if (!this._lastDir || !this._lastDir.exists()) { + try { + this._lastDir = gPrefService.getComplexValue("browser.open.lastDir", + Ci.nsILocalFile); + 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 (!PrivateBrowsingUtils.isWindowPrivate(window)) + gPrefService.setComplexValue("browser.open.lastDir", Ci.nsILocalFile, + this._lastDir); + }, + reset: function() { + this._lastDir = null; + } +}; + +function BrowserOpenFileWindow() +{ + // 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 { + if (fp.file) { + gLastOpenDirectory.path = + fp.file.parent.QueryInterface(Ci.nsILocalFile); + } + } 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.displayDirectory = gLastOpenDirectory.path; + fp.open(fpCallback); + } catch (ex) { + } +} + +function BrowserCloseTabOrWindow() { +#ifdef XP_MACOSX + // If we're not a browser window, just close the window + if (window.location.href != getBrowserURL()) { + closeWindow(true); + return; + } +#endif + + // If the current tab is the last one, this will close the window. + gBrowser.removeCurrentTab({animate: true}); +} + +function BrowserTryToCloseWindow() +{ + if (WindowIsClosing()) + window.close(); // WindowIsClosing does all the necessary checks +} + +function loadURI(uri, referrer, postData, allowThirdPartyFixup, referrerPolicy, + originPrincipal, forceAboutBlankViewerInCurrent, + triggeringPrincipal) { + if (postData === undefined) + postData = null; + + var flags = nsIWebNavigation.LOAD_FLAGS_NONE; + if (allowThirdPartyFixup) { + flags |= nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + flags |= nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + + try { + gBrowser.loadURIWithFlags(uri, { + flags: flags, + referrerURI: referrer, + referrerPolicy: referrerPolicy, + postData: postData, + originPrincipal: originPrincipal, + triggeringPrincipal: triggeringPrincipal, + forceAboutBlankViewerInCurrent: forceAboutBlankViewerInCurrent, + }); + } catch (e) {} +} + +/** + * Given a urlbar value, discerns between URIs, keywords and aliases. + * + * @param url + * The urlbar value. + * @param callback (optional, deprecated) + * The callback function invoked when done. This parameter is + * deprecated, please use the Promise that is returned. + * + * @return Promise<{ postData, url, mayInheritPrincipal }> + */ +function getShortcutOrURIAndPostData(url, callback = null) { + if (callback) { + Deprecated.warning("Please use the Promise returned by " + + "getShortcutOrURIAndPostData() instead of passing a " + + "callback", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1100294"); + } + + return Task.spawn(function* () { + let mayInheritPrincipal = false; + let postData = null; + let shortcutURL = null; + let keyword = url; + let param = ""; + + let offset = url.indexOf(" "); + if (offset > 0) { + keyword = url.substr(0, offset); + param = url.substr(offset + 1); + } + + let engine = Services.search.getEngineByAlias(keyword); + if (engine) { + let submission = engine.getSubmission(param, null, "keyword"); + postData = submission.postData; + return { postData: submission.postData, url: submission.uri.spec, + mayInheritPrincipal }; + } + + let entry = yield PlacesUtils.keywords.fetch(keyword); + if (entry) { + shortcutURL = entry.url.href; + postData = entry.postData; + } + + if (!shortcutURL) { + return { postData, url, mayInheritPrincipal }; + } + + let escapedPostData = ""; + if (postData) + escapedPostData = unescape(postData); + + if (/%s/i.test(shortcutURL) || /%s/i.test(escapedPostData)) { + let charset = ""; + const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/; + let matches = shortcutURL.match(re); + + if (matches) { + [, shortcutURL, charset] = matches; + } else { + let uri; + try { + // makeURI() throws if URI is invalid. + uri = makeURI(shortcutURL); + } catch (ex) {} + + if (uri) { + // Try to get the saved character-set. + // Will return an empty string if character-set is not found. + charset = yield PlacesUtils.getCharsetForURI(uri); + } + } + + // encodeURIComponent produces UTF-8, and cannot be used for other charsets. + // escape() works in those cases, but it doesn't uri-encode +, @, and /. + // Therefore we need to manually replace these ASCII characters by their + // encodeURIComponent result, to match the behavior of nsEscape() with + // url_XPAlphas + let encodedParam = ""; + if (charset && charset != "UTF-8") + encodedParam = escape(convertFromUnicode(charset, param)). + replace(/[+@\/]+/g, encodeURIComponent); + else // Default charset is UTF-8 + encodedParam = encodeURIComponent(param); + + shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param); + + if (/%s/i.test(escapedPostData)) // POST keyword + postData = getPostDataStream(escapedPostData, param, encodedParam, + "application/x-www-form-urlencoded"); + + // This URL came from a bookmark, so it's safe to let it inherit the current + // document's principal. + mayInheritPrincipal = true; + + return { postData, url: shortcutURL, mayInheritPrincipal }; + } + + if (param) { + // This keyword doesn't take a parameter, but one was provided. Just return + // the original URL. + postData = null; + + return { postData, url, mayInheritPrincipal }; + } + + // This URL came from a bookmark, so it's safe to let it inherit the current + // document's principal. + mayInheritPrincipal = true; + + return { postData, url: shortcutURL, mayInheritPrincipal }; + }).then(data => { + if (callback) { + callback(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.addContentLength = true; + mimeStream.setData(dataStream); + return mimeStream.QueryInterface(Ci.nsIInputStream); +} + +function getLoadContext() { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); +} + +function readFromClipboard() +{ + var url; + + try { + // Create transferable that will transfer the text. + var trans = Components.classes["@mozilla.org/widget/transferable;1"] + .createInstance(Components.interfaces.nsITransferable); + trans.init(getLoadContext()); + + trans.addDataFlavor("text/unicode"); + + // If available, use selection clipboard, otherwise global one + if (Services.clipboard.supportsSelectionClipboard()) + Services.clipboard.getData(trans, Services.clipboard.kSelectionClipboard); + else + Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); + + var data = {}; + var dataLen = {}; + trans.getTransferData("text/unicode", data, dataLen); + + if (data) { + data = data.value.QueryInterface(Components.interfaces.nsISupportsString); + url = data.data.substring(0, dataLen.value / 2); + } + } catch (ex) { + } + + return url; +} + +function BrowserViewSourceOfDocument(aArgsOrDocument) +{ + let args; + + if (aArgsOrDocument instanceof Document) { + let doc = aArgsOrDocument; + + let requestor = doc.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; + args = { browser, outerWindowID, URL }; + } else { + args = aArgsOrDocument; + } + + let viewInternal = () => { + top.gViewSourceUtils.viewSource(args); + } + + // Check if external view source is enabled. If so, try it. If it fails, + // fallback to internal view source. + if (Services.prefs.getBoolPref("view_source.editor.external")) { + top.gViewSourceUtils + .openInExternalEditor(args, null, null, null, result => { + if (!result) { + viewInternal(); + } + }); + } else { + // Display using internal view source + viewInternal(); + } +} + +// doc - document to use for source, or null for this window's document +// initialTab - name 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 +function BrowserPageInfo(doc, initialTab, imageElement) { + var args = {doc: doc, initialTab: initialTab, imageElement: imageElement}; + var windows = Services.wm.getEnumerator("Browser:page-info"); + + var documentURL = doc ? doc.location : window.content.document.location; + + // Check for windows matching the url + while (windows.hasMoreElements()) { + var currentWindow = windows.getNext(); + if (currentWindow.document.documentElement.getAttribute("relatedUrl") == documentURL) { + currentWindow.focus(); + currentWindow.resetPageInfo(args); + return currentWindow; + } + } + + // We didn't find a matching window, so open a new one. + return openDialog("chrome://browser/content/pageinfo/pageInfo.xul", "", + "chrome,toolbar,dialog=no,resizable", args); +} + +function URLBarSetURI(aURI) { + var value = gBrowser.userTypedValue; + var valid = false; + + if (value == null) { + let uri = aURI || gBrowser.currentURI; + // Strip off "wyciwyg://" and passwords for the location bar + try { + uri = Services.uriFixup.createExposableURI(uri); + } catch (e) {} + + // Replace initial page URIs with an empty string + // only if there's no opener (bug 370555). + // Bug 863515 - Make content.opener checks work in electrolysis. + if (gInitialPages.indexOf(uri.spec) != -1) + value = !gMultiProcessBrowser && content.opener ? uri.spec : ""; + else + value = losslessDecodeURI(uri); + + valid = !isBlankPageURL(uri.spec); + } + + let isDifferentValidValue = valid && value != gURLBar.value; + gURLBar.value = value; + gURLBar.valueIsTyped = !valid; + if (isDifferentValidValue) { + gURLBar.selectionStart = gURLBar.selectionEnd = 0; + } + + SetPageProxyState(valid ? "valid" : "invalid"); +} + +function losslessDecodeURI(aURI) { + let scheme = aURI.scheme; + let decodeASCIIOnly = !(/(https|http|file|ftp)/i.test(scheme)); + + var value = aURI.spec; + + // 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. Re-encode whitespace so that it doesn't get eaten away + // by the location bar (bug 410726). + .replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)|[\r\n\t]/ig, + encodeURIComponent); + } catch (e) {} + } + } + + // Encode invisible characters (C0/C1 control characters, U+007F [DEL], + // U+00A0 [no-break space], line and paragraph separator, + // object replacement character) (bug 452979, bug 909264) + value = value.replace(/[\u0000-\u001f\u007f-\u00a0\u2028\u2029\ufffc]/g, + encodeURIComponent); + + // Encode default ignorable characters (bug 546013) + // except ZWNJ (U+200C) and ZWJ (U+200D) (bug 582186). + // This includes all bidirectional formatting characters. + // (RFC 3987 sections 3.2 and 4.1 paragraph 6) + value = value.replace(/[\u00ad\u034f\u115f-\u1160\u17b4-\u17b5\u180b-\u180d\u200b\u200e-\u200f\u202a-\u202e\u2060-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufff8]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]/g, + encodeURIComponent); + return value; +} + +function UpdateUrlbarSearchSplitterState() +{ + var splitter = document.getElementById("urlbar-search-splitter"); + var urlbar = document.getElementById("urlbar-container"); + var searchbar = document.getElementById("search-container"); + var stop = document.getElementById("stop-button"); + + var ibefore = null; + if (urlbar && searchbar) { + if (urlbar.nextSibling == searchbar || + urlbar.getAttribute("combined") && + stop && stop.nextSibling == searchbar) + ibefore = searchbar; + else if (searchbar.nextSibling == urlbar) + ibefore = urlbar; + } + + if (ibefore) { + if (!splitter) { + splitter = document.createElement("splitter"); + splitter.id = "urlbar-search-splitter"; + splitter.setAttribute("resizebefore", "flex"); + splitter.setAttribute("resizeafter", "flex"); + splitter.setAttribute("skipintoolbarset", "true"); + splitter.className = "chromeclass-toolbar-additional"; + } + urlbar.parentNode.insertBefore(splitter, ibefore); + } else if (splitter) + splitter.parentNode.removeChild(splitter); +} + +function setUrlAndSearchBarWidthForConditionalForwardButton() { + // Workaround for bug 694084: Showing/hiding the conditional forward button resizes + // the search bar when the url/search bar splitter hasn't been used. + var urlbarContainer = document.getElementById("urlbar-container"); + var searchbarContainer = document.getElementById("search-container"); + if (!urlbarContainer || + !searchbarContainer || + urlbarContainer.hasAttribute("width") || + searchbarContainer.hasAttribute("width") || + urlbarContainer.parentNode != searchbarContainer.parentNode) + return; + urlbarContainer.style.width = searchbarContainer.style.width = ""; + var urlbarWidth = urlbarContainer.clientWidth; + var searchbarWidth = searchbarContainer.clientWidth; + urlbarContainer.style.width = urlbarWidth + "px"; + searchbarContainer.style.width = searchbarWidth + "px"; +} + +function UpdatePageProxyState() +{ + if (gURLBar && gURLBar.value != gLastValidURLStr) + SetPageProxyState("invalid"); +} + +function SetPageProxyState(aState) +{ + BookmarkingUI.onPageProxyStateChanged(aState); + + if (!gURLBar) + return; + + if (!gProxyFavIcon) + gProxyFavIcon = document.getElementById("page-proxy-favicon"); + + gURLBar.setAttribute("pageproxystate", aState); + gProxyFavIcon.setAttribute("pageproxystate", aState); + + // the page proxy state is set to valid via OnLocationChange, which + // gets called when we switch tabs. + if (aState == "valid") { + gLastValidURLStr = gURLBar.value; + gURLBar.addEventListener("input", UpdatePageProxyState, false); + PageProxySetIcon(gBrowser.getIcon()); + } else if (aState == "invalid") { + gURLBar.removeEventListener("input", UpdatePageProxyState, false); + PageProxyClearIcon(); + } +} + +function PageProxySetIcon (aURL) +{ + if (!gProxyFavIcon) + return; + + if (gBrowser.selectedBrowser.contentDocument instanceof ImageDocument) { + // PageProxyClearIcon(); + gProxyFavIcon.setAttribute("src", "chrome://browser/skin/imagedocument.png"); + return; + } + + if (!aURL) + PageProxyClearIcon(); + else if (gProxyFavIcon.getAttribute("src") != aURL) + gProxyFavIcon.setAttribute("src", aURL); +} + +function PageProxyClearIcon () +{ + gProxyFavIcon.removeAttribute("src"); +} + + +function PageProxyClickHandler(aEvent) +{ + if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste")) + middleMousePaste(aEvent); +} + +/** + * Handle load of some pages (about:*) so that we can make modifications + * to the DOM for unprivileged pages. + */ +function BrowserOnAboutPageLoad(doc) { + + /* === about:home === */ + + if (doc.documentURI.toLowerCase() == "about:home") { + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + let wrapper = {}; + Cu.import("resource:///modules/sessionstore/SessionStore.jsm", wrapper); + let ss = wrapper.SessionStore; + ss.promiseInitialized.then(function() { + if (ss.canRestoreLastSession) { + doc.getElementById("launcher").setAttribute("session", "true"); + } + }).then(null, function onError(x) { + Cu.reportError("Error in SessionStore init while processing 'about:home': " + x); + }); + } + + // Inject search engine and snippets URL. + let docElt = doc.documentElement; + if (AboutHomeUtils.showKnowYourRights) { + docElt.setAttribute("showKnowYourRights", "true"); + // Set pref to indicate we've shown the notification. + let currentVersion = Services.prefs.getIntPref("browser.rights.version"); + Services.prefs.setBoolPref("browser.rights." + currentVersion + ".shown", true); + } + + function updateSearchEngine() { + let engine = AboutHomeUtils.defaultSearchEngine; + docElt.setAttribute("searchEngineName", engine.name); + docElt.setAttribute("searchEnginePostData", engine.postDataString || ""); + docElt.setAttribute("searchEngineURL", engine.searchURL); + } + Services.search.init(updateSearchEngine); + + // Listen for the event that's triggered when the user changes search engine. + // At this point we simply reload about:home to reflect the change. + Services.obs.addObserver(updateSearchEngine, "browser-search-engine-modified", false); + + // Remove the observer when the page is reloaded or closed. + doc.defaultView.addEventListener("pagehide", function removeObserver() { + doc.defaultView.removeEventListener("pagehide", removeObserver); + Services.obs.removeObserver(updateSearchEngine, "browser-search-engine-modified"); + }, false); + } + + /* === about:newtab === */ + + if (doc.documentURI.toLowerCase() == "about:newtab") { + + let docElt = doc.documentElement; + + function updateSearchEngine() { + let engine = AboutHomeUtils.defaultSearchEngine; + docElt.setAttribute("searchEngineName", engine.name); + docElt.setAttribute("searchEnginePostData", engine.postDataString || ""); + docElt.setAttribute("searchEngineURL", engine.searchURL); + } + Services.search.init(updateSearchEngine); + + // Listen for the event that's triggered when the user changes search engine. + // At this point we simply reload about:newtab to reflect the change. + Services.obs.addObserver(updateSearchEngine, "browser-search-engine-modified", false); + + // Remove the observer when the page is reloaded or closed. + doc.defaultView.addEventListener("pagehide", function removeObserver() { + doc.defaultView.removeEventListener("pagehide", removeObserver); + Services.obs.removeObserver(updateSearchEngine, "browser-search-engine-modified"); + }, false); + } + +} + +/** + * Handle command events bubbling up from error page content + */ +var BrowserOnClick = { + handleEvent: function BrowserOnClick_handleEvent(aEvent) { + if (!aEvent.isTrusted || // Don't trust synthetic events + aEvent.button == 2 || aEvent.target.localName != "button") { + return; + } + + let originalTarget = aEvent.originalTarget; + let ownerDoc = originalTarget.ownerDocument; + + // If the event came from an ssl error page, it is probably either the "Add + // Exception…" or "Get me out of here!" button + if (ownerDoc.documentURI.startsWith("about:certerror")) { + this.onAboutCertError(originalTarget, ownerDoc); + } + else if (ownerDoc.documentURI.startsWith("about:neterror")) { + this.onAboutNetError(originalTarget, ownerDoc); + } + else if (ownerDoc.documentURI.toLowerCase() == "about:home") { + this.onAboutHome(originalTarget, ownerDoc); + } + }, + + onAboutCertError: function BrowserOnClick_onAboutCertError(aTargetElm, aOwnerDoc) { + let elmId = aTargetElm.getAttribute("id"); + let isTopFrame = (aOwnerDoc.defaultView.parent === aOwnerDoc.defaultView); + + switch (elmId) { + case "exceptionDialogButton": + let params = { exceptionAdded : false }; + + try { + switch (Services.prefs.getIntPref("browser.ssl_override_behavior")) { + case 2 : // Pre-fetch & pre-populate + params.prefetchCert = true; + case 1 : // Pre-populate + params.location = aOwnerDoc.location.href; + } + } catch (e) { + Components.utils.reportError("Couldn't get ssl_override pref: " + e); + } + + window.openDialog('chrome://pippki/content/exceptionDialog.xul', + '','chrome,centerscreen,modal', params); + + // If the user added the exception cert, attempt to reload the page + if (params.exceptionAdded) { + aOwnerDoc.location.reload(); + } + break; + + case "getMeOutOfHereButton": + getMeOutOfHere(); + break; + + case "technicalContent": + break; + + case "expertContent": + break; + + } + }, + + onAboutNetError: function BrowserOnClick_onAboutNetError(aTargetElm, aOwnerDoc) { + let elmId = aTargetElm.getAttribute("id"); + if (elmId != "errorTryAgain" || !/e=netOffline/.test(aOwnerDoc.documentURI)) + return; + Services.io.offline = false; + }, + + onAboutHome: function BrowserOnClick_onAboutHome(aTargetElm, aOwnerDoc) { + let elmId = aTargetElm.getAttribute("id"); + + switch (elmId) { + case "restorePreviousSession": + let ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + if (ss.canRestoreLastSession) { + ss.restoreLastSession(); + } + aOwnerDoc.getElementById("launcher").removeAttribute("session"); + break; + + case "downloads": + BrowserDownloadsUI(); + break; + + case "bookmarks": + PlacesCommandHook.showPlacesOrganizer("AllBookmarks"); + break; + + case "history": + PlacesCommandHook.showPlacesOrganizer("History"); + break; + + case "addons": + BrowserOpenAddonsMgr(); + break; + + case "sync": + openPreferences("paneSync"); + break; + + case "settings": + openPreferences(); + break; + } + }, +}; + +/** + * Re-direct the browser to a known-safe page. This function is + * used when, for example, the user browses to a known malware page + * and is presented with about:blocked. The "Get me out of here!" + * button should take the user to the default start page so that even + * when their own homepage is infected, we can get them somewhere safe. + */ +function getMeOutOfHere() { + try { + let toBlank = Services.prefs.getBoolPref("browser.escape_to_blank"); + if (toBlank) { + content.location = "about:logopage"; + return; + } + } catch(e) { + Components.utils.reportError("Couldn't get escape pref: " + e); + } + // Get the start page from the *default* pref branch, not the user's + var prefs = Services.prefs.getDefaultBranch(null); + var url = BROWSER_NEW_TAB_URL; + try { + url = prefs.getComplexValue("browser.startup.homepage", + Ci.nsIPrefLocalizedString).data; + // If url is a pipe-delimited set of pages, just take the first one. + if (url.includes("|")) + url = url.split("|")[0]; + } catch(e) { + Components.utils.reportError("Couldn't get homepage pref: " + e); + } + content.location = url; +} + +function BrowserFullScreen() +{ + window.fullScreen = !window.fullScreen; +} + +function onFullScreen() { + FullScreen.toggle(); +} + +function onMozEnteredDomFullscreen(event) { + FullScreen.enterDomFullscreen(event); +} + +function getWebNavigation() +{ + return gBrowser.webNavigation; +} + +function BrowserReloadWithFlags(reloadFlags) { + + // Reset DOS mitigation for auth prompts when user initiates a reload. + let browser = gBrowser.selectedBrowser; + delete browser.authPromptCounter; + + /* 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. + */ + + var webNav = gBrowser.webNavigation; + try { + var sh = webNav.sessionHistory; + if (sh) + webNav = sh.QueryInterface(nsIWebNavigation); + } catch (e) { + } + + try { + webNav.reload(reloadFlags); + } catch (e) { + } +} + +var PrintPreviewListener = { + _printPreviewTab: null, + _tabBeforePrintPreview: null, + + getPrintPreviewBrowser: function () { + if (!this._printPreviewTab) { + this._tabBeforePrintPreview = gBrowser.selectedTab; + this._printPreviewTab = gBrowser.loadOneTab("about:blank", + { inBackground: false }); + gBrowser.selectedTab = this._printPreviewTab; + } + return gBrowser.getBrowserForTab(this._printPreviewTab); + }, + getSourceBrowser: function () { + return this._tabBeforePrintPreview ? + this._tabBeforePrintPreview.linkedBrowser : gBrowser.selectedBrowser; + }, + getNavToolbox: function () { + return gNavToolbox; + }, + onEnter: function () { + // We might have accidentally switched tabs since the user invoked print + // preview + if (gBrowser.selectedTab != this._printPreviewTab) { + gBrowser.selectedTab = this._printPreviewTab; + } + gInPrintPreviewMode = true; + this._toggleAffectedChrome(); + }, + onExit: function () { + gBrowser.selectedTab = this._tabBeforePrintPreview; + this._tabBeforePrintPreview = null; + gInPrintPreviewMode = false; + this._toggleAffectedChrome(); + gBrowser.removeTab(this._printPreviewTab); + this._printPreviewTab = null; + }, + _toggleAffectedChrome: function () { + gNavToolbox.collapsed = gInPrintPreviewMode; + + if (gInPrintPreviewMode) + this._hideChrome(); + else + this._showChrome(); + + if (this._chromeState.sidebarOpen) + toggleSidebar(this._sidebarCommand); + +#ifdef MENUBAR_CAN_AUTOHIDE + updateAppButtonDisplay(); +#endif + }, + _hideChrome: function () { + this._chromeState = {}; + + var sidebar = document.getElementById("sidebar-box"); + this._chromeState.sidebarOpen = !sidebar.hidden; + this._sidebarCommand = sidebar.getAttribute("sidebarcommand"); + + var notificationBox = gBrowser.getNotificationBox(); + this._chromeState.notificationsOpen = !notificationBox.notificationsHidden; + notificationBox.notificationsHidden = true; + + document.getElementById("sidebar").setAttribute("src", "about:blank"); + var addonBar = document.getElementById("addon-bar"); + this._chromeState.addonBarOpen = !addonBar.collapsed; + addonBar.collapsed = true; + gBrowser.updateWindowResizers(); + + this._chromeState.findOpen = gFindBarInitialized && !gFindBar.hidden; + if (gFindBarInitialized) + gFindBar.close(); + + var globalNotificationBox = document.getElementById("global-notificationbox"); + this._chromeState.globalNotificationsOpen = !globalNotificationBox.notificationsHidden; + globalNotificationBox.notificationsHidden = true; + + this._chromeState.syncNotificationsOpen = false; + var syncNotifications = document.getElementById("sync-notifications"); + if (syncNotifications) { + this._chromeState.syncNotificationsOpen = !syncNotifications.notificationsHidden; + syncNotifications.notificationsHidden = true; + } + }, + _showChrome: function () { + if (this._chromeState.notificationsOpen) + gBrowser.getNotificationBox().notificationsHidden = false; + + if (this._chromeState.addonBarOpen) { + document.getElementById("addon-bar").collapsed = false; + gBrowser.updateWindowResizers(); + } + + if (this._chromeState.findOpen) + gFindBar.open(); + + if (this._chromeState.globalNotificationsOpen) + document.getElementById("global-notificationbox").notificationsHidden = false; + + if (this._chromeState.syncNotificationsOpen) + document.getElementById("sync-notifications").notificationsHidden = false; + } +} + +function getMarkupDocumentViewer() +{ + return gBrowser.markupDocumentViewer; +} + +// This function is obsolete. Newer code should use <tooltip page="true"/> instead. +function FillInHTMLTooltip(tipElement) +{ + document.getElementById("aHTMLTooltip").fillInPageTooltip(tipElement); +} + +var browserDragAndDrop = { + canDropLink: function (aEvent) Services.droppedLinkHandler.canDropLink(aEvent, true), + + dragOver: function (aEvent) + { + if (this.canDropLink(aEvent)) { + aEvent.preventDefault(); + } + }, + + dropLinks: function (aEvent, aDisallowInherit) { + return Services.droppedLinkHandler.dropLinks(aEvent, aDisallowInherit); + } +}; + +var homeButtonObserver = { + onDrop: function (aEvent) + { + // disallow setting home pages that inherit the principal + let links = browserDragAndDrop.dropLinks(aEvent, true); + if (links.length) { + setTimeout(openHomeDialog, 0, links.map(link => link.url).join("|")); + } + }, + + onDragOver: function (aEvent) + { + browserDragAndDrop.dragOver(aEvent); + aEvent.dropEffect = "link"; + }, + onDragExit: function (aEvent) + { + } +} + +function openHomeDialog(aURL) +{ + var promptTitle = gNavigatorBundle.getString("droponhometitle"); + var promptMsg; + if (aURL.includes("|")) { + promptMsg = gNavigatorBundle.getString("droponhomemsgMultiple"); + } else { + promptMsg = gNavigatorBundle.getString("droponhomemsg"); + } + + var pressedVal = Services.prompt.confirmEx(window, promptTitle, promptMsg, + Services.prompt.STD_YES_NO_BUTTONS, + null, null, null, null, {value:0}); + + if (pressedVal == 0) { + try { + var homepageStr = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString); + homepageStr.data = aURL; + gPrefService.setComplexValue("browser.startup.homepage", + Components.interfaces.nsISupportsString, homepageStr); + } catch (ex) { + dump("Failed to set the home page.\n"+ex+"\n"); + } + } +} + +var bookmarksButtonObserver = { + onDrop: function (aEvent) + { + let name = { }; + let url = browserDragAndDrop.drop(aEvent, name); + try { + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: makeURI(url) + , title: name + , hiddenRows: [ "description" + , "location" + , "loadInSidebar" + , "keyword" ] + }, window); + } catch(ex) { } + }, + + onDragOver: function (aEvent) + { + browserDragAndDrop.dragOver(aEvent); + aEvent.dropEffect = "link"; + }, + + onDragExit: function (aEvent) + { + } +} + +var newTabButtonObserver = { + onDragOver: function (aEvent) + { + browserDragAndDrop.dragOver(aEvent); + }, + + onDragExit: function (aEvent) + { + }, + + onDrop: function (aEvent) + { + let links = browserDragAndDrop.dropLinks(aEvent); + Task.spawn(function*() { + for (let link of links) { + let data = yield getShortcutOrURIAndPostData(link.url); + if (data.url) { + // allow third-party services to fixup this URL + openNewTabWith(data.url, null, data.postData, aEvent, true); + } + } + }); + } +} + +var newWindowButtonObserver = { + onDragOver: function (aEvent) + { + browserDragAndDrop.dragOver(aEvent); + }, + onDragExit: function (aEvent) + { + }, + onDrop: function (aEvent) + { + let links = browserDragAndDrop.dropLinks(aEvent); + Task.spawn(function*() { + for (let link of links) { + let data = yield getShortcutOrURIAndPostData(link.url); + if (data.url) { + // allow third-party services to fixup this URL + openNewWindowWith(data.url, null, data.postData, true); + } + } + }); + } +} + +const DOMLinkHandler = { + handleEvent: function (event) { + switch (event.type) { + case "DOMLinkAdded": + this.onLinkAdded(event); + break; + } + }, + getLinkIconURI: function(aLink) { + let targetDoc = aLink.ownerDocument; + var uri = makeURI(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. + var isAllowedPage = [ + /^about:neterror\?/, + /^about:blocked\?/, + /^about:certerror\?/, + /^about:home$/, + ].some(function (re) re.test(targetDoc.documentURI)); + + if (!isAllowedPage || !uri.schemeIs("chrome")) { + var ssm = Services.scriptSecurityManager; + try { + ssm.checkLoadURIWithPrincipal(targetDoc.nodePrincipal, uri, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + } catch(e) { + return null; + } + } + + 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. + } + + // Security says okay, now ask content policy + if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_IMAGE, + uri, targetDoc.documentURIObject, + aLink, aLink.type, null) + != Ci.nsIContentPolicy.ACCEPT) + return null; + + try { + uri.userPass = ""; + } catch(e) { + // some URIs are immutable + } + return uri; + }, + onLinkAdded: function (event) { + var link = event.originalTarget; + var rel = link.rel && link.rel.toLowerCase(); + if (!link || !link.ownerDocument || !rel || !link.href) + return; + + var feedAdded = false; + var iconAdded = false; + var searchAdded = false; + var rels = {}; + for (let relString of rel.split(/\s+/)) + rels[relString] = true; + + for (let relVal in rels) { + switch (relVal) { + case "feed": + case "alternate": + if (!feedAdded) { + if (!rels.feed && rels.alternate && rels.stylesheet) + break; + + if (isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) { + FeedHandler.addFeed(link, link.ownerDocument); + feedAdded = true; + } + } + break; + case "icon": + if (!iconAdded) { + if (!gPrefService.getBoolPref("browser.chrome.site_icons")) + break; + + var uri = this.getLinkIconURI(link); + if (!uri) + break; + + if (gBrowser.isFailedIcon(uri)) + break; + + var browserIndex = gBrowser.getBrowserIndexForDocument(link.ownerDocument); + // no browser? no favicon. + if (browserIndex == -1) + break; + + let tab = gBrowser.tabs[browserIndex]; + gBrowser.setIcon(tab, uri.spec, link.ownerDocument.nodePrincipal); + iconAdded = true; + } + break; + case "search": + if (!searchAdded) { + var type = link.type && link.type.toLowerCase(); + type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); + + if (type == "application/opensearchdescription+xml" && link.title && + /^(?:https?|ftp):/i.test(link.href) && + !PrivateBrowsingUtils.isWindowPrivate(window)) { + var engine = { title: link.title, href: link.href }; + Services.search.init(function () { + BrowserSearch.addEngine(engine, link.ownerDocument); + }); + searchAdded = true; + } + } + break; + } + } + } +} + +const BrowserSearch = { + addEngine: function(engine, targetDoc) { + if (!this.searchBar) + return; + + var browser = gBrowser.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(function (e) e.title == engine.title)) + return; + } + + // Append the URI and an appropriate title to the browser data. + // Use documentURIObject in the check for shouldLoadFavIcon so that we + // do the right thing with about:-style error pages. Bug 453442 + var iconURL = null; + if (gBrowser.shouldLoadFavIcon(targetDoc.documentURIObject)) + iconURL = targetDoc.documentURIObject.prePath + "/favicon.ico"; + + 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; + }, + + /** + * Gives focus to the search bar, if it is present on the toolbar, 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() { +#ifdef XP_MACOSX + if (window.location.href != getBrowserURL()) { + var win = getTopWin(); + if (win) { + // If there's an open browser window, it should handle this command + win.focus(); + win.BrowserSearch.webSearch(); + } else { + // If there are no open browser windows, open a new one + var observer = function observer(subject, topic, data) { + if (subject == win) { + BrowserSearch.webSearch(); + Services.obs.removeObserver(observer, "browser-delayed-startup-finished"); + } + } + win = window.openDialog(getBrowserURL(), "_blank", + "chrome,all,dialog=no", "about:blank"); + Services.obs.addObserver(observer, "browser-delayed-startup-finished", false); + } + return; + } +#endif + var searchBar = this.searchBar; + if (searchBar && window.fullScreen) + FullScreen.showNavToolbox(); + if (searchBar) + searchBar.select(); + if (!searchBar || document.activeElement != searchBar.textbox.inputField) + openUILinkIn(Services.search.defaultEngine.searchForm, "current"); + }, + + /** + * 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 searchText + * The search terms to use for the search. + * + * @param useNewTab + * Boolean indicating whether or not the search should load in a new + * tab. + * + * @param purpose [optional] + * A string meant to indicate the context of the search request. This + * allows the search service to provide a different nsISearchSubmission + * depending on e.g. where the search is triggered in the UI. + * + * @return string Name of the search engine used to perform a search or null + * if a search was not performed. + */ + loadSearch: function BrowserSearch_search(searchText, useNewTab, purpose) { + var engine; + + // If the search bar is visible, use the current engine, otherwise, fall + // back to the default engine. + if (isElementVisible(this.searchBar)) + engine = Services.search.currentEngine; + else + engine = Services.search.defaultEngine; + + var submission = engine.getSubmission(searchText, null, purpose); // 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; + } + + let inBackground = Services.prefs.getBoolPref("browser.search.context.loadInBackground"); + openLinkIn(submission.uri.spec, + useNewTab ? "tab" : "current", + { postData: submission.postData, + inBackground: inBackground, + relatedToCurrent: true }); + + return engine.name; + }, + + /** + * Perform a search initiated from the context menu. + * + * This should only be called from the context menu. See + * BrowserSearch.loadSearch for the preferred API. + */ + loadSearchFromContext: function (terms) { + let engine = BrowserSearch.loadSearch(terms, true, "contextmenu"); + }, + + /** + * Returns the search bar element if it is present in the toolbar, null otherwise. + */ + get searchBar() { + return document.getElementById("searchbar"); + }, + + loadAddEngines: function BrowserSearch_loadAddEngines() { + var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow"); + var where = newWindowPref == 3 ? "tab" : "window"; + var searchEnginesURL = formatURL("browser.search.searchEnginesURL", true); + openUILinkIn(searchEnginesURL, where); + }, +}; + +XPCOMUtils.defineConstant(this, "BrowserSearch", BrowserSearch); + +function FillHistoryMenu(aParent) { + // Lazily add the hover listeners on first showing and never remove them + if (!aParent.hasStatusListener) { + // Show history item's uri in the status bar when hovering, and clear on exit + aParent.addEventListener("DOMMenuItemActive", function(aEvent) { + // Only the current page should have the checked attribute, so skip it + if (!aEvent.target.hasAttribute("checked")) + XULBrowserWindow.setOverLink(aEvent.target.getAttribute("uri")); + }, false); + aParent.addEventListener("DOMMenuItemInactive", function() { + XULBrowserWindow.setOverLink(""); + }, false); + + aParent.hasStatusListener = true; + } + + // Remove old entries if any + var children = aParent.childNodes; + for (var i = children.length - 1; i >= 0; --i) { + if (children[i].hasAttribute("index")) + aParent.removeChild(children[i]); + } + + var webNav = gBrowser.webNavigation; + var sessionHistory = webNav.sessionHistory; + + var count = sessionHistory.count; + if (count <= 1) // don't display the popup for a single item + return false; + + const MAX_HISTORY_MENU_ITEMS = 15; + var index = sessionHistory.index; + var half_length = Math.floor(MAX_HISTORY_MENU_ITEMS / 2); + var start = Math.max(index - half_length, 0); + var end = Math.min(start == 0 ? MAX_HISTORY_MENU_ITEMS : index + half_length + 1, count); + if (end == count) + start = Math.max(count - MAX_HISTORY_MENU_ITEMS, 0); + + var tooltipBack = gNavigatorBundle.getString("tabHistory.goBack"); + var tooltipCurrent = gNavigatorBundle.getString("tabHistory.current"); + var tooltipForward = gNavigatorBundle.getString("tabHistory.goForward"); + + for (var j = end - 1; j >= start; j--) { + let item = document.createElement("menuitem"); + let entry = sessionHistory.getEntryAtIndex(j, false); + let uri = entry.URI.spec; + + item.setAttribute("uri", uri); + item.setAttribute("label", entry.title || uri); + item.setAttribute("index", j); + + if (j != index) { + PlacesUtils.favicons.getFaviconURLForPage(entry.URI, function (aURI) { + if (aURI) { + let iconURL = PlacesUtils.favicons.getFaviconLinkForIcon(aURI).spec; + item.style.listStyleImage = "url(" + iconURL + ")"; + } + }); + } + + if (j < index) { + item.className = "unified-nav-back menuitem-iconic menuitem-with-favicon"; + item.setAttribute("tooltiptext", tooltipBack); + } else if (j == index) { + item.setAttribute("type", "radio"); + item.setAttribute("checked", "true"); + item.className = "unified-nav-current"; + item.setAttribute("tooltiptext", tooltipCurrent); + } else { + item.className = "unified-nav-forward menuitem-iconic menuitem-with-favicon"; + item.setAttribute("tooltiptext", tooltipForward); + } + + aParent.appendChild(item); + } + return true; +} + +function addToUrlbarHistory(aUrlToAdd) { + if (!PrivateBrowsingUtils.isWindowPrivate(window) && + aUrlToAdd && + !aUrlToAdd.includes(" ") && + !/[\x00-\x1F]/.test(aUrlToAdd)) + PlacesUIUtils.markPageAsTyped(aUrlToAdd); +} + +function toJavaScriptConsole() +{ + toOpenWindowByType("global:console", "chrome://global/content/console.xul"); +} + +function BrowserDownloadsUI() +{ + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + openUILinkIn("about:downloads", "tab"); + } else { + PlacesCommandHook.showPlacesOrganizer("Downloads"); + } +} + +function toOpenWindowByType(inType, uri, features) +{ + var topWindow = Services.wm.getMostRecentWindow(inType); + + if (topWindow) + topWindow.focus(); + else if (features) + window.open(uri, "_blank", features); + else + window.open(uri, "_blank", "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar"); +} + +function OpenBrowserWindow(options) +{ + function newDocumentShown(doc, topic, data) { + if (topic == "document-shown" && + doc != document && + doc.defaultView == win) { + Services.obs.removeObserver(newDocumentShown, "document-shown"); + } + }; + Services.obs.addObserver(newDocumentShown, "document-shown", false); + + var charsetArg = new String(); + var handler = Components.classes["@mozilla.org/browser/clh;1"] + .getService(Components.interfaces.nsIBrowserHandler); + var defaultArgs = handler.defaultArgs; + var wintype = document.documentElement.getAttribute('windowtype'); + + var extraFeatures = ""; + if (options && options.private) { + extraFeatures = ",private"; + if (!PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Force the new window to load about:privatebrowsing instead of the default home page + defaultArgs = "about:privatebrowsing"; + } + } else { + extraFeatures = ",non-private"; + } + + // if and only if the current window is a browser window and it has a document with a character + // set, then extract the current charset menu setting from the current document and use it to + // initialize the new browser window... + var win; + if (window && (wintype == "navigator:browser") && window.content && window.content.document) + { + var DocCharset = window.content.document.characterSet; + charsetArg = "charset="+DocCharset; + + //we should "inherit" the charset menu setting in a new window + win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs, charsetArg); + } + else // forget about the charset information. + { + win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs); + } + + return win; +} + +var gCustomizeSheet = false; +function BrowserCustomizeToolbar() { + // Disable the toolbar context menu items + var menubar = document.getElementById("main-menubar"); + for (let childNode of menubar.childNodes) + childNode.setAttribute("disabled", true); + + var cmd = document.getElementById("cmd_CustomizeToolbars"); + cmd.setAttribute("disabled", "true"); + + var splitter = document.getElementById("urlbar-search-splitter"); + if (splitter) + splitter.parentNode.removeChild(splitter); + + CombinedStopReload.uninit(); + + PlacesToolbarHelper.customizeStart(); + BookmarkingUI.customizeStart(); + DownloadsButton.customizeStart(); + + TabsInTitlebar.allowedBy("customizing-toolbars", false); + + var customizeURL = "chrome://global/content/customizeToolbar.xul"; + gCustomizeSheet = Services.prefs.getBoolPref("toolbar.customization.usesheet", false); + + if (gCustomizeSheet) { + let sheetFrame = document.createElement("iframe"); + let panel = document.getElementById("customizeToolbarSheetPopup"); + sheetFrame.id = "customizeToolbarSheetIFrame"; + sheetFrame.toolbox = gNavToolbox; + sheetFrame.panel = panel; + sheetFrame.setAttribute("style", panel.getAttribute("sheetstyle")); + panel.appendChild(sheetFrame); + + // Open the panel, but make it invisible until the iframe has loaded so + // that the user doesn't see a white flash. + panel.style.visibility = "hidden"; + gNavToolbox.addEventListener("beforecustomization", function onBeforeCustomization() { + gNavToolbox.removeEventListener("beforecustomization", onBeforeCustomization, false); + panel.style.removeProperty("visibility"); + }, false); + + sheetFrame.setAttribute("src", customizeURL); + + panel.openPopup(gNavToolbox, "after_start", 0, 0); + } else { + window.openDialog(customizeURL, + "CustomizeToolbar", + "chrome,titlebar,toolbar,location,resizable,dependent", + gNavToolbox); + } +} + +function BrowserToolboxCustomizeDone(aToolboxChanged) { + if (gCustomizeSheet) { + document.getElementById("customizeToolbarSheetPopup").hidePopup(); + let iframe = document.getElementById("customizeToolbarSheetIFrame"); + iframe.parentNode.removeChild(iframe); + } + + // Update global UI elements that may have been added or removed + if (aToolboxChanged) { + gURLBar = document.getElementById("urlbar"); + + gProxyFavIcon = document.getElementById("page-proxy-favicon"); + gHomeButton.updateTooltip(); + gIdentityHandler._cacheElements(); + window.XULBrowserWindow.init(); + +#ifndef XP_MACOSX + updateEditUIVisibility(); +#endif + + // Hacky: update the PopupNotifications' object's reference to the iconBox, + // if it already exists, since it may have changed if the URL bar was + // added/removed. + if (!window.__lookupGetter__("PopupNotifications")) + PopupNotifications.iconBox = document.getElementById("notification-popup-box"); + } + + PlacesToolbarHelper.customizeDone(); + BookmarkingUI.customizeDone(); + DownloadsButton.customizeDone(); + + // The url bar splitter state is dependent on whether stop/reload + // and the location bar are combined, so we need this ordering + CombinedStopReload.init(); + UpdateUrlbarSearchSplitterState(); + setUrlAndSearchBarWidthForConditionalForwardButton(); + + // Update the urlbar + if (gURLBar) { + gURLBarSettings.writePlaceholder(); + URLBarSetURI(); + XULBrowserWindow.asyncUpdateUI(); + BookmarkingUI.updateStarState(); + } + + TabsInTitlebar.allowedBy("customizing-toolbars", true); + + // Re-enable parts of the UI we disabled during the dialog + var menubar = document.getElementById("main-menubar"); + for (let childNode of menubar.childNodes) + childNode.setAttribute("disabled", false); + var cmd = document.getElementById("cmd_CustomizeToolbars"); + cmd.removeAttribute("disabled"); + + // make sure to re-enable click-and-hold + if (!Services.prefs.getBoolPref("ui.click_hold_context_menus", false)) + SetClickAndHoldHandlers(); + + gBrowser.selectedBrowser.focus(); +} + +function BrowserToolboxCustomizeChange(aType) { + switch (aType) { + case "iconsize": + case "mode": + retrieveToolbarIconsizesFromTheme(); + break; + default: + gHomeButton.updatePersonalToolbarStyle(); + BookmarkingUI.customizeChange(); + allTabs.readPref(); + } +} + +/** + * Allows themes to override the "iconsize" attribute on toolbars. + */ +function retrieveToolbarIconsizesFromTheme() { + function retrieveToolbarIconsize(aToolbar) { + if (aToolbar.localName != "toolbar") + return; + + // The theme indicates that it wants to override the "iconsize" attribute + // by specifying a special value for the "counter-reset" property on the + // toolbar. A custom property cannot be used because getComputedStyle can + // only return the values of standard CSS properties. + let counterReset = getComputedStyle(aToolbar).counterReset; + if (counterReset == "smallicons 0") + aToolbar.setAttribute("iconsize", "small"); + else if (counterReset == "largeicons 0") + aToolbar.setAttribute("iconsize", "large"); + } + + Array.forEach(gNavToolbox.childNodes, retrieveToolbarIconsize); + gNavToolbox.externalToolbars.forEach(retrieveToolbarIconsize); +} + +/** + * Update the global flag that tracks whether or not any edit UI (the Edit menu, + * edit-related items in the context menu, and edit-related toolbar buttons + * is visible, then update the edit commands' enabled state accordingly. We use + * this flag to skip updating the edit commands on focus or selection changes + * when no UI is visible to improve performance (including pageload performance, + * since focus changes when you load a new page). + * + * If UI is visible, we use goUpdateGlobalEditMenuItems to set the commands' + * enabled state so the UI will reflect it appropriately. + * + * If the UI isn't visible, we enable all edit commands so keyboard shortcuts + * still work and just lazily disable them as needed when the user presses a + * shortcut. + * + * This doesn't work on Mac, since Mac menus flash when users press their + * keyboard shortcuts, so edit UI is essentially always visible on the Mac, + * and we need to always update the edit commands. Thus on Mac this function + * is a no op. + */ +function updateEditUIVisibility() +{ +#ifndef XP_MACOSX + let editMenuPopupState = document.getElementById("menu_EditPopup").state; + let contextMenuPopupState = document.getElementById("contentAreaContextMenu").state; + let placesContextMenuPopupState = document.getElementById("placesContext").state; +#ifdef MENUBAR_CAN_AUTOHIDE + let appMenuPopupState = document.getElementById("appmenu-popup").state; +#endif + + // The UI is visible if the Edit menu is opening or open, if the context menu + // is open, or if the toolbar has been customized to include the Cut, Copy, + // or Paste toolbar buttons. + gEditUIVisible = editMenuPopupState == "showing" || + editMenuPopupState == "open" || + contextMenuPopupState == "showing" || + contextMenuPopupState == "open" || + placesContextMenuPopupState == "showing" || + placesContextMenuPopupState == "open" || +#ifdef MENUBAR_CAN_AUTOHIDE + appMenuPopupState == "showing" || + appMenuPopupState == "open" || +#endif + document.getElementById("cut-button") || + document.getElementById("copy-button") || + document.getElementById("paste-button") ? true : false; + + // If UI is visible, update the edit commands' enabled state to reflect + // whether or not they are actually enabled for the current focus/selection. + if (gEditUIVisible) + goUpdateGlobalEditMenuItems(); + + // Otherwise, enable all commands, so that keyboard shortcuts still work, + // then lazily determine their actual enabled state when the user presses + // a keyboard shortcut. + else { + goSetCommandEnabled("cmd_undo", true); + goSetCommandEnabled("cmd_redo", true); + goSetCommandEnabled("cmd_cut", true); + goSetCommandEnabled("cmd_copy", true); + goSetCommandEnabled("cmd_paste", true); + goSetCommandEnabled("cmd_selectAll", true); + goSetCommandEnabled("cmd_delete", true); + goSetCommandEnabled("cmd_switchTextDirection", true); + } +#endif +} + +/** + * Makes the Character Encoding menu enabled or disabled as appropriate. + * To be called when the View menu or the app menu is opened. + */ +function updateCharacterEncodingMenuState() +{ + let charsetMenu = document.getElementById("charsetMenu"); + let appCharsetMenu = document.getElementById("appmenu_charsetMenu"); + let appDevCharsetMenu = + document.getElementById("appmenu_developer_charsetMenu"); + // gBrowser is null on Mac when the menubar shows in the context of + // non-browser windows. The above elements may be null depending on + // what parts of the menubar are present. E.g. no app menu on Mac. + if (gBrowser && + gBrowser.docShell && + gBrowser.docShell.mayEnableCharacterEncodingMenu) { + if (charsetMenu) { + charsetMenu.removeAttribute("disabled"); + } + if (appCharsetMenu) { + appCharsetMenu.removeAttribute("disabled"); + } + if (appDevCharsetMenu) { + appDevCharsetMenu.removeAttribute("disabled"); + } + } else { + if (charsetMenu) { + charsetMenu.setAttribute("disabled", "true"); + } + if (appCharsetMenu) { + appCharsetMenu.setAttribute("disabled", "true"); + } + if (appDevCharsetMenu) { + appDevCharsetMenu.setAttribute("disabled", "true"); + } + } +} + +var XULBrowserWindow = { + // Stored Status, Link and Loading values + status: "", + defaultStatus: "", + overLink: "", + startTime: 0, + statusText: "", + isBusy: false, +/* Pale Moon: Don't hide navigation controls and toolbars for "special" pages. SBaD, M! + inContentWhitelist: ["about:addons", "about:downloads", "about:permissions", + "about:sync-progress"],*/ + inContentWhitelist: [], + + QueryInterface: function (aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsIWebProgressListener2) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsIXULBrowserWindow) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + }, + + get stopCommand () { + delete this.stopCommand; + return this.stopCommand = document.getElementById("Browser:Stop"); + }, + get reloadCommand () { + delete this.reloadCommand; + return this.reloadCommand = document.getElementById("Browser:Reload"); + }, + get statusTextField () { + delete this.statusTextField; + return this.statusTextField = document.getElementById("statusbar-display"); + }, + get isImage () { + delete this.isImage; + return this.isImage = document.getElementById("isImage"); + }, + + init: function () { + this.throbberElement = document.getElementById("navigator-throbber"); + + // Bug 666809 - SecurityUI support for e10s + if (gMultiProcessBrowser) + return; + + // Initialize the security button's state and tooltip text. Remember to reset + // _hostChanged, otherwise onSecurityChange will short circuit. + var securityUI = gBrowser.securityUI; + this._hostChanged = true; + this.onSecurityChange(null, null, securityUI.state); + }, + + destroy: function () { + // XXXjag to avoid leaks :-/, see bug 60729 + delete this.throbberElement; + delete this.stopCommand; + delete this.reloadCommand; + delete this.statusTextField; + delete this.statusText; + }, + + setJSStatus: function () { + // unsupported + }, + + setDefaultStatus: function (status) { + this.defaultStatus = status; + this.updateStatusField(); + }, + + setOverLink: function (url, anchorElt) { + // Encode bidirectional formatting characters. + // (RFC 3987 sections 3.2 and 4.1 paragraph 6) + url = url.replace(/[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g, + encodeURIComponent); + + if (gURLBar && gURLBar._mayTrimURLs /* corresponds to browser.urlbar.trimURLs */) + url = trimURL(url); + + this.overLink = url; + LinkTargetDisplay.update(); + }, + + updateStatusField: function () { + var text, type, types = ["overLink"]; + if (this._busyUI) + types.push("status"); + types.push("defaultStatus"); + for (type of types) { + text = this[type]; + if (text) + break; + } + + // check the current value so we don't trigger an attribute change + // and cause needless (slow!) UI updates + if (this.statusText != text) { + let field = this.statusTextField; + field.setAttribute("previoustype", field.getAttribute("type")); + field.setAttribute("type", type); + field.label = text; + field.setAttribute("crop", type == "overLink" ? "center" : "end"); + this.statusText = text; + } + }, + + // Called before links are navigated to to allow us to retarget them if needed. + onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) { + let target = this._onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab); + return target; + }, + + _onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) { + // Don't modify non-default targets or targets that aren't in top-level app + // tab docshells (isAppTab will be false for app tab subframes). + if (originalTarget != "" || !isAppTab) + return originalTarget; + + // External links from within app tabs should always open in new tabs + // instead of replacing the app tab's page (Bug 575561) + let linkHost; + let docHost; + try { + linkHost = linkURI.host; + docHost = linkNode.ownerDocument.documentURIObject.host; + } catch(e) { + // nsIURI.host can throw for non-nsStandardURL nsIURIs. + // If we fail to get either host, just return originalTarget. + return originalTarget; + } + + if (docHost == linkHost) + return originalTarget; + + // Special case: ignore "www" prefix if it is part of host string + let [longHost, shortHost] = + linkHost.length > docHost.length ? [linkHost, docHost] : [docHost, linkHost]; + if (longHost == "www." + shortHost) + return originalTarget; + + return "_blank"; + }, + + onLinkIconAvailable: function (aIconURL) { + if (gProxyFavIcon && gBrowser.userTypedValue === null) { + PageProxySetIcon(aIconURL); // update the favicon in the URL bar + } + }, + + onProgressChange: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + // Do nothing. + }, + + onProgressChange64: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + return this.onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress); + }, + + // This function fires only for the currently selected tab. + onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) { + const nsIWebProgressListener = Ci.nsIWebProgressListener; + const nsIChannel = Ci.nsIChannel; + + if (aStateFlags & nsIWebProgressListener.STATE_START && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + + if (aRequest && aWebProgress.isTopLevel) { + // clear out feed data + gBrowser.selectedBrowser.feeds = null; + + // clear out search-engine data + gBrowser.selectedBrowser.engines = null; + } + + this.isBusy = true; + + if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { + this._busyUI = true; + + // Turn the throbber on. + if (this.throbberElement) + this.throbberElement.setAttribute("busy", "true"); + + // XXX: This needs to be based on window activity... + this.stopCommand.removeAttribute("disabled"); + CombinedStopReload.switchToStop(); + } + } + else if (aStateFlags & nsIWebProgressListener.STATE_STOP) { + // 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) { + let msg = ""; + let location; + // Get the URI either from a channel or a pseudo-object + if (aRequest instanceof nsIChannel || "URI" in aRequest) { + location = aRequest.URI; + + // For keyword URIs clear the user typed value since they will be changed into real URIs + if (location.scheme == "keyword" && aWebProgress.isTopLevel) + gBrowser.userTypedValue = null; + + if (location.spec != "about:blank") { + switch (aStatus) { + case Components.results.NS_ERROR_NET_TIMEOUT: + msg = gNavigatorBundle.getString("nv_timeout"); + break; + } + } + } + + this.status = ""; + this.setDefaultStatus(msg); + + // Disable menu entries for images, enable otherwise + if (!gMultiProcessBrowser && content.document && BrowserUtils.mimeTypeIsTextBased(content.document.contentType)) + this.isImage.removeAttribute('disabled'); + else + this.isImage.setAttribute('disabled', 'true'); + } + + this.isBusy = false; + + if (this._busyUI) { + this._busyUI = false; + + // Turn the throbber off. + if (this.throbberElement) + this.throbberElement.removeAttribute("busy"); + + this.stopCommand.setAttribute("disabled", "true"); + CombinedStopReload.switchToReload(aRequest instanceof Ci.nsIRequest); + } + } + }, + + onLocationChange: function (aWebProgress, aRequest, aLocationURI, aFlags) { + var location = aLocationURI ? aLocationURI.spec : ""; + this._hostChanged = true; + + // If displayed, hide the form validation popup. + FormValidationHandler.hidePopup(); + + let pageTooltip = document.getElementById("aHTMLTooltip"); + let tooltipNode = pageTooltip.triggerNode; + if (tooltipNode) { + // Optimise for the common case + if (aWebProgress.isTopLevel) { + pageTooltip.hidePopup(); + } + else { + for (let tooltipWindow = tooltipNode.ownerDocument.defaultView; + tooltipWindow != tooltipWindow.parent; + tooltipWindow = tooltipWindow.parent) { + if (tooltipWindow == aWebProgress.DOMWindow) { + pageTooltip.hidePopup(); + break; + } + } + } + } + + // Disable menu entries for images, enable otherwise + if (!gMultiProcessBrowser && content.document && BrowserUtils.mimeTypeIsTextBased(content.document.contentType)) + this.isImage.removeAttribute('disabled'); + else + this.isImage.setAttribute('disabled', 'true'); + + this.hideOverLinkImmediately = true; + this.setOverLink("", null); + this.hideOverLinkImmediately = false; + + // 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 = gBrowser.selectedBrowser; + if (aWebProgress.isTopLevel) { + if ((location == "about:blank" && (gMultiProcessBrowser || !content.opener)) || + location == "") { // Second condition is for new tabs, otherwise + // reload function is enabled until tab is refreshed. + this.reloadCommand.setAttribute("disabled", "true"); + } else { + this.reloadCommand.removeAttribute("disabled"); + } + + if (gURLBar) { + URLBarSetURI(aLocationURI); + + // Update starring UI + BookmarkingUI.updateStarState(); + } + + // Show or hide browser chrome based on the whitelist + if (this.hideChromeForLocation(location)) { + document.documentElement.setAttribute("disablechrome", "true"); + } else { + let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + if (ss.getTabValue(gBrowser.selectedTab, "appOrigin")) + document.documentElement.setAttribute("disablechrome", "true"); + else + document.documentElement.removeAttribute("disablechrome"); + } + + // Utility functions for disabling find + var shouldDisableFind = function shouldDisableFind(aDocument) { + let docElt = aDocument.documentElement; + return docElt && docElt.getAttribute("disablefastfind") == "true"; + } + + var disableFindCommands = function disableFindCommands(aDisable) { + let findCommands = [document.getElementById("cmd_find"), + document.getElementById("cmd_findAgain"), + document.getElementById("cmd_findPrevious")]; + for (let elt of findCommands) { + if (aDisable) + elt.setAttribute("disabled", "true"); + else + elt.removeAttribute("disabled"); + } + if (gFindBarInitialized) { + if (!gFindBar.hidden && aDisable) { + gFindBar.hidden = true; + this._findbarTemporarilyHidden = true; + } else if (this._findbarTemporarilyHidden && !aDisable) { + gFindBar.hidden = false; + this._findbarTemporarilyHidden = false; + } + } + }.bind(this); + + var onContentRSChange = function onContentRSChange(e) { + if (e.target.readyState != "interactive" && e.target.readyState != "complete") + return; + + e.target.removeEventListener("readystatechange", onContentRSChange); + disableFindCommands(shouldDisableFind(e.target)); + } + + // Disable find commands in documents that ask for them to be disabled. + if (!gMultiProcessBrowser && aLocationURI && + (aLocationURI.schemeIs("about") || aLocationURI.schemeIs("chrome"))) { + // Don't need to re-enable/disable find commands for same-document location changes + // (e.g. the replaceStates in about:addons) + if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) { + if (content.document.readyState == "interactive" || content.document.readyState == "complete") + disableFindCommands(shouldDisableFind(content.document)); + else { + content.document.addEventListener("readystatechange", onContentRSChange); + } + } + } else + disableFindCommands(false); + + if (gFindBarInitialized) { + if (gFindBar.findMode != gFindBar.FIND_NORMAL) { + // Close the Find toolbar if we're in old-style TAF mode + gFindBar.close(); + } + + // XXX + // See: https://github.com/MoonchildProductions/Pale-Moon/issues/364 + // An actual preference: findbar.highlightAll + /* + if (!(gPrefService.getBoolPref("accessibility.typeaheadfind.highlightallremember") || + gPrefService.getBoolPref("accessibility.typeaheadfind.highlightallbydefault"))) { + // fix bug 253793 - turn off highlight when page changes + gFindBar.getElement("highlight").checked = false; + } + */ + } + } + UpdateBackForwardCommands(gBrowser.webNavigation); + + gGestureSupport.restoreRotationState(); + + // See bug 358202, when tabs are switched during a drag operation, + // timers don't fire on windows (bug 203573) + if (aRequest) + setTimeout(function () { XULBrowserWindow.asyncUpdateUI(); }, 0); + else + this.asyncUpdateUI(); + }, + + asyncUpdateUI: function () { + FeedHandler.updateFeeds(); + }, + + hideChromeForLocation: function(aLocation) { + aLocation = aLocation.toLowerCase(); + return this.inContentWhitelist.some(function(aSpec) { + return aSpec == aLocation; + }); + }, + + onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) { + this.status = aMessage; + this.updateStatusField(); + }, + + // Properties used to cache security state used to update the UI + _state: null, + _hostChanged: false, // onLocationChange will flip this bit + + onSecurityChange: function (aWebProgress, aRequest, aState) { + // Don't need to do anything if the data we use to update the UI hasn't + // changed + if (this._state == aState && + !this._hostChanged) { +#ifdef DEBUG + try { + var contentHost = gBrowser.contentWindow.location.host; + if (this._host !== undefined && this._host != contentHost) { + Components.utils.reportError( + "ASSERTION: browser.js host is inconsistent. Content window has " + + "<" + contentHost + "> but cached host is <" + this._host + ">.\n" + ); + } + } catch (ex) {} +#endif + return; + } + this._state = aState; + +#ifdef DEBUG + try { + this._host = gBrowser.contentWindow.location.host; + } catch(ex) { + this._host = null; + } +#endif + + this._hostChanged = false; + + // aState is defined as a bitmask that may be extended in the future. + // We filter out any unknown bits before testing for known values. + const wpl = Components.interfaces.nsIWebProgressListener; + const wpl_security_bits = wpl.STATE_IS_SECURE | + wpl.STATE_IS_BROKEN | + wpl.STATE_IS_INSECURE; + var level; + + switch (this._state & wpl_security_bits) { + case wpl.STATE_IS_SECURE: + level = "high"; + break; + case wpl.STATE_IS_BROKEN: + level = "broken"; + break; + } + + if (level) { + // We don't style the Location Bar based on the the 'level' attribute + // anymore, but still set it for third-party themes. + if (gURLBar) + gURLBar.setAttribute("level", level); + } else { + if (gURLBar) + gURLBar.removeAttribute("level"); + } + + if (gMultiProcessBrowser) + return; + + // Don't pass in the actual location object, since it can cause us to + // hold on to the window object too long. Just pass in the fields we + // care about. (bug 424829) + var location = gBrowser.contentWindow.location; + var locationObj = {}; + try { + // about:blank can be used by webpages so pretend it is http + locationObj.protocol = location == "about:blank" ? "http:" : location.protocol; + locationObj.host = location.host; + locationObj.hostname = location.hostname; + locationObj.port = location.port; + } catch (ex) { + // Can sometimes throw if the URL being visited has no host/hostname, + // e.g. about:blank. The _state for these pages means we won't need these + // properties anyways, though. + } + gIdentityHandler.checkIdentity(this._state, locationObj); + }, + + // simulate all change notifications after switching tabs + onUpdateCurrentBrowser: function XWB_onUpdateCurrentBrowser(aStateFlags, aStatus, aMessage, aTotalProgress) { + if (FullZoom.updateBackgroundTabs) + FullZoom.onLocationChange(gBrowser.currentURI, true); + var nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; + var loadingDone = aStateFlags & nsIWebProgressListener.STATE_STOP; + // use a pseudo-object instead of a (potentially nonexistent) channel for getting + // a correct error message - and make sure that the UI is always either in + // loading (STATE_START) or done (STATE_STOP) mode + this.onStateChange( + gBrowser.webProgress, + { URI: gBrowser.currentURI }, + loadingDone ? nsIWebProgressListener.STATE_STOP : nsIWebProgressListener.STATE_START, + aStatus + ); + // status message and progress value are undefined if we're done with loading + if (loadingDone) + return; + this.onStatusChange(gBrowser.webProgress, null, 0, aMessage); + } +}; + +var LinkTargetDisplay = { + get DELAY_SHOW() { + delete this.DELAY_SHOW; + return this.DELAY_SHOW = Services.prefs.getIntPref("browser.overlink-delay"); + }, + + DELAY_HIDE: 250, + _timer: 0, + + get _isVisible () XULBrowserWindow.statusTextField.label != "", + + update: function () { + clearTimeout(this._timer); + window.removeEventListener("mousemove", this, true); + + if (!XULBrowserWindow.overLink) { + if (XULBrowserWindow.hideOverLinkImmediately) + this._hide(); + else + this._timer = setTimeout(this._hide.bind(this), this.DELAY_HIDE); + return; + } + + if (this._isVisible) { + XULBrowserWindow.updateStatusField(); + } else { + // Let the display appear when the mouse doesn't move within the delay + this._showDelayed(); + window.addEventListener("mousemove", this, true); + } + }, + + handleEvent: function (event) { + switch (event.type) { + case "mousemove": + // Restart the delay since the mouse was moved + clearTimeout(this._timer); + this._showDelayed(); + break; + } + }, + + _showDelayed: function () { + this._timer = setTimeout(function (self) { + XULBrowserWindow.updateStatusField(); + window.removeEventListener("mousemove", self, true); + }, this.DELAY_SHOW, this); + }, + + _hide: function () { + clearTimeout(this._timer); + + XULBrowserWindow.updateStatusField(); + } +}; + +var CombinedStopReload = { + init: function () { + if (this._initialized) + return; + + var urlbar = document.getElementById("urlbar-container"); + var reload = document.getElementById("reload-button"); + var stop = document.getElementById("stop-button"); + + if (urlbar) { + if (urlbar.parentNode.getAttribute("mode") != "icons" || + !reload || urlbar.nextSibling != reload || + !stop || reload.nextSibling != stop) + urlbar.removeAttribute("combined"); + else { + urlbar.setAttribute("combined", "true"); + reload = document.getElementById("urlbar-reload-button"); + stop = document.getElementById("urlbar-stop-button"); + } + } + if (!stop || !reload || reload.nextSibling != stop) + return; + + this._initialized = true; + if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") + reload.setAttribute("displaystop", "true"); + stop.addEventListener("click", this, false); + this.reload = reload; + this.stop = stop; + }, + + uninit: function () { + if (!this._initialized) + return; + + this._cancelTransition(); + this._initialized = false; + this.stop.removeEventListener("click", this, false); + this.reload = null; + this.stop = null; + }, + + handleEvent: function (event) { + // the only event we listen to is "click" on the stop button + if (event.button == 0 && + !this.stop.disabled) + this._stopClicked = true; + }, + + switchToStop: function () { + if (!this._initialized) + return; + + this._cancelTransition(); + this.reload.setAttribute("displaystop", "true"); + }, + + switchToReload: function (aDelay) { + if (!this._initialized) + return; + + this.reload.removeAttribute("displaystop"); + + if (!aDelay || this._stopClicked) { + this._stopClicked = false; + this._cancelTransition(); + this.reload.disabled = XULBrowserWindow.reloadCommand + .getAttribute("disabled") == "true"; + return; + } + + if (this._timer) + return; + + // Temporarily disable the reload button to prevent the user from + // accidentally reloading the page when intending to click the stop button + this.reload.disabled = true; + this._timer = setTimeout(function (self) { + self._timer = 0; + self.reload.disabled = XULBrowserWindow.reloadCommand + .getAttribute("disabled") == "true"; + }, 650, this); + }, + + _cancelTransition: function () { + if (this._timer) { + clearTimeout(this._timer); + this._timer = 0; + } + } +}; + +var TabsProgressListener = { + onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + + // Attach a listener to watch for "click" events bubbling up from error + // pages and other similar page. This lets us fix bugs like 401575 which + // require error page UI to do privileged things, without letting error + // pages have any privilege themselves. + // We can't look for this during onLocationChange since at that point the + // document URI is not yet the about:-uri of the error page. + + let doc = gMultiProcessBrowser ? null : aWebProgress.DOMWindow.document; + if (!gMultiProcessBrowser && + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + Components.isSuccessCode(aStatus) && + doc.documentURI.startsWith("about:") && + !doc.documentURI.toLowerCase().startsWith("about:blank") && + !doc.documentElement.hasAttribute("hasBrowserHandlers")) { + // STATE_STOP may be received twice for documents, thus store an + // attribute to ensure handling it just once. + doc.documentElement.setAttribute("hasBrowserHandlers", "true"); + aBrowser.addEventListener("click", BrowserOnClick, true); + aBrowser.addEventListener("pagehide", function onPageHide(event) { + if (event.target.defaultView.frameElement) + return; + aBrowser.removeEventListener("click", BrowserOnClick, true); + aBrowser.removeEventListener("pagehide", onPageHide, true); + if (event.target.documentElement) + event.target.documentElement.removeAttribute("hasBrowserHandlers"); + }, true); + + // We also want to make changes to page UI for unprivileged about pages. + BrowserOnAboutPageLoad(doc); + } + }, + + onLocationChange: function (aBrowser, aWebProgress, aRequest, aLocationURI, + aFlags) { + // Filter out location changes caused by anchor navigation + // or history.push/pop/replaceState. + if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) + return; + + // Only need to call locationChange if the PopupNotifications object + // for this window has already been initialized (i.e. its getter no + // longer exists) + if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get) + PopupNotifications.locationChange(aBrowser); + + gBrowser.getNotificationBox(aBrowser).removeTransientNotifications(); + + // Filter out location changes in sub documents. + if (aWebProgress.isTopLevel) { + // Initialize the click-to-play state. + aBrowser._clickToPlayPluginsActivated = new Map(); + aBrowser._clickToPlayAllPluginsActivated = false; + aBrowser._pluginScriptedState = gPluginHandler.PLUGIN_SCRIPTED_STATE_NONE; + + FullZoom.onLocationChange(aLocationURI, false, aBrowser); + } + }, + + onRefreshAttempted: function (aBrowser, aWebProgress, aURI, aDelay, aSameURI) { + if (gPrefService.getBoolPref("accessibility.blockautorefresh")) { + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let refreshButtonText = + gNavigatorBundle.getString("refreshBlocked.goButton"); + let refreshButtonAccesskey = + gNavigatorBundle.getString("refreshBlocked.goButton.accesskey"); + let message = + gNavigatorBundle.getFormattedString(aSameURI ? "refreshBlocked.refreshLabel" + : "refreshBlocked.redirectLabel", + [brandShortName]); + let docShell = aWebProgress.DOMWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + let notificationBox = gBrowser.getNotificationBox(aBrowser); + let notification = notificationBox.getNotificationWithValue("refresh-blocked"); + if (notification) { + notification.label = message; + notification.refreshURI = aURI; + notification.delay = aDelay; + notification.docShell = docShell; + } else { + let buttons = [{ + label: refreshButtonText, + accessKey: refreshButtonAccesskey, + callback: function (aNotification, aButton) { + var refreshURI = aNotification.docShell + .QueryInterface(Ci.nsIRefreshURI); + refreshURI.forceRefreshURI(aNotification.refreshURI, + aNotification.delay, true); + } + }]; + notification = + notificationBox.appendNotification(message, "refresh-blocked", + "chrome://browser/skin/Info.png", + notificationBox.PRIORITY_INFO_MEDIUM, + buttons); + notification.refreshURI = aURI; + notification.delay = aDelay; + notification.docShell = docShell; + } + return false; + } + return true; + } +} + +function nsBrowserAccess() { } + +nsBrowserAccess.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]), + + openURI: function (aURI, aOpener, aWhere, aContext) { + var newWindow = null; + var isExternal = !!(aContext & Ci.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 (isExternal && aURI && aURI.schemeIs("chrome")) { + dump("use -chrome command-line option to load external chrome urls\n"); + return null; + } + + if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) { + if (isExternal && + gPrefService.prefHasUserValue("browser.link.open_newwindow.override.external")) + aWhere = gPrefService.getIntPref("browser.link.open_newwindow.override.external"); + else + aWhere = gPrefService.getIntPref("browser.link.open_newwindow"); + } + + let referrer = aOpener ? makeURI(aOpener.location.href) : null; + let triggeringPrincipal = null; + let referrerPolicy = Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT; + if (aOpener && aOpener.document) { + referrerPolicy = aOpener.document.referrerPolicy; + triggeringPrincipal = aOpener.document.nodePrincipal; + } + + switch (aWhere) { + case Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW : + // FIXME: Bug 408379. So how come this doesn't send the + // referrer like the other loads do? + var url = aURI ? aURI.spec : "about:blank"; + // Pass all params to openDialog to ensure that "url" isn't passed through + // loadOneOrMoreURIs, which splits based on "|" + newWindow = openDialog(getBrowserURL(), "_blank", "all,dialog=no", url, null, null, null); + break; + case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB : + let win, needToFocusWin; + + // try the current window. if we're in a popup, fall back on the most recent browser window + if (window.toolbar.visible) + win = window; + else { + let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aOpener || window); + win = RecentWindow.getMostRecentBrowserWindow({private: isPrivate}); + needToFocusWin = true; + } + + if (!win) { + // we couldn't find a suitable window, a new one needs to be opened. + return null; + } + + if (isExternal && (!aURI || aURI.spec == "about:blank")) { + win.BrowserOpenTab(); // this also focuses the location bar + win.focus(); + newWindow = win.content; + break; + } + + let loadInBackground = gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground"); + let openerWindow = (aContext & Ci.nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener; + + let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank", { + triggeringPrincipal: triggeringPrincipal, + referrerURI: referrer, + referrerPolicy: referrerPolicy, + fromExternal: isExternal, + inBackground: loadInBackground, + opener: openerWindow }); + let browser = win.gBrowser.getBrowserForTab(tab); + + if (gPrefService.getBoolPref("browser.tabs.noWindowActivationOnExternal")) { + isExternal = false; // this is a hack, but it works + } + + newWindow = browser.contentWindow; + if (needToFocusWin || (!loadInBackground && isExternal)) + newWindow.focus(); + break; + default : // OPEN_CURRENTWINDOW or an illegal value + newWindow = content; + if (aURI) { + let loadflags = isExternal ? + Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL : + Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + gBrowser.loadURIWithFlags(aURI.spec, { + flags: loadflags, + triggeringPrincipal: triggeringPrincipal, + referrerURI: referrer, + referrerPolicy: referrerPolicy, + }); + } + if (!gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground")) + window.focus(); + } + return newWindow; + }, + + isTabContentWindow: function (aWindow) { + return gBrowser.browsers.some(function (browser) browser.contentWindow == aWindow); + } +} + +function onViewToolbarsPopupShowing(aEvent, aInsertPoint) { + var popup = aEvent.target; + if (popup != aEvent.currentTarget) + return; + + // Empty the menu + for (var i = popup.childNodes.length-1; i >= 0; --i) { + var deadItem = popup.childNodes[i]; + if (deadItem.hasAttribute("toolbarId")) + popup.removeChild(deadItem); + } + + var firstMenuItem = aInsertPoint || popup.firstChild; + + let toolbarNodes = Array.slice(gNavToolbox.childNodes); + toolbarNodes.push(document.getElementById("addon-bar")); + + for (let toolbar of toolbarNodes) { + let toolbarName = toolbar.getAttribute("toolbarname"); + if (toolbarName) { + let menuItem = document.createElement("menuitem"); + let hidingAttribute = toolbar.getAttribute("type") == "menubar" ? + "autohide" : "collapsed"; + menuItem.setAttribute("id", "toggle_" + toolbar.id); + menuItem.setAttribute("toolbarId", toolbar.id); + menuItem.setAttribute("type", "checkbox"); + menuItem.setAttribute("label", toolbarName); + menuItem.setAttribute("checked", toolbar.getAttribute(hidingAttribute) != "true"); + if (popup.id != "appmenu_customizeMenu") + menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey")); + if (popup.id != "toolbar-context-menu") + menuItem.setAttribute("key", toolbar.getAttribute("key")); + + popup.insertBefore(menuItem, firstMenuItem); + + menuItem.addEventListener("command", onViewToolbarCommand, false); + } + } +} + +function onViewToolbarCommand(aEvent) { + var toolbarId = aEvent.originalTarget.getAttribute("toolbarId"); + var toolbar = document.getElementById(toolbarId); + var isVisible = aEvent.originalTarget.getAttribute("checked") == "true"; + setToolbarVisibility(toolbar, isVisible); +} + +function setToolbarVisibility(toolbar, isVisible) { + var hidingAttribute = toolbar.getAttribute("type") == "menubar" ? + "autohide" : "collapsed"; + + toolbar.setAttribute(hidingAttribute, !isVisible); + document.persist(toolbar.id, hidingAttribute); + + // Customizable toolbars - persist the hiding attribute. + if (toolbar.hasAttribute("customindex")) { + var toolbox = toolbar.parentNode; + var name = toolbar.getAttribute("toolbarname"); + if (toolbox.toolbarset) { + try { + // Checking all attributes starting with "toolbar". + Array.prototype.slice.call(toolbox.toolbarset.attributes, 0) + .find(x => { + if (x.name.startsWith("toolbar")) { + var toolbarInfo = x.value; + var infoSplit = toolbarInfo.split(gToolbarInfoSeparators[0]); + if (infoSplit[0] == name) { + infoSplit[1] = [ + infoSplit[1].split(gToolbarInfoSeparators[1], 1), !isVisible + ].join(gToolbarInfoSeparators[1]); + toolbox.toolbarset.setAttribute( + x.name, infoSplit.join(gToolbarInfoSeparators[0])); + document.persist(toolbox.toolbarset.id, x.name); + } + } + }); + } catch (e) { + Components.utils.reportError( + "Customizable toolbars - persist the hiding attribute: " + e); + } + } + } + + PlacesToolbarHelper.init(); + BookmarkingUI.onToolbarVisibilityChange(); + gBrowser.updateWindowResizers(); + +#ifdef MENUBAR_CAN_AUTOHIDE + updateAppButtonDisplay(); +#endif + + if (isVisible) + ToolbarIconColor.inferFromText(); +} + +var AudioIndicator = { + init: function () { + Services.prefs.addObserver(this._prefName, this, false); + this.syncUI(); + }, + + uninit: function () { + Services.prefs.removeObserver(this._prefName, this); + }, + + toggle: function () { + this.enabled = !Services.prefs.getBoolPref(this._prefName); + }, + + syncUI: function () { + document.getElementById("context_toggleMuteTab").setAttribute("hidden", this.enabled); + document.getElementById("key_toggleMute").setAttribute("disabled", this.enabled); + }, + + get enabled () { + return !Services.prefs.getBoolPref(this._prefName); + }, + + set enabled (val) { + Services.prefs.setBoolPref(this._prefName, !!val); + return val; + }, + + observe: function (subject, topic, data) { + if (topic == "nsPref:changed") + this.syncUI(); + }, + + _prefName: "browser.tabs.showAudioPlayingIcon" +} + +var TabsOnTop = { + init: function TabsOnTop_init() { + Services.prefs.addObserver(this._prefName, this, false); +// Pale Moon: Stop Being a Derp, Mozilla (#3) + // Only show the toggle UI if the user disabled tabs on top. +// if (Services.prefs.getBoolPref(this._prefName)) { +// for (let item of document.querySelectorAll("menuitem[command=cmd_ToggleTabsOnTop]")) +// item.parentNode.removeChild(item); +// } + }, + + uninit: function TabsOnTop_uninit() { + Services.prefs.removeObserver(this._prefName, this); + }, + + toggle: function () { + this.enabled = !Services.prefs.getBoolPref(this._prefName); + }, + + syncUI: function () { + let userEnabled = Services.prefs.getBoolPref(this._prefName); + let enabled = userEnabled && gBrowser.tabContainer.visible; + + document.getElementById("cmd_ToggleTabsOnTop") + .setAttribute("checked", userEnabled); + + document.documentElement.setAttribute("tabsontop", enabled); + document.getElementById("navigator-toolbox").setAttribute("tabsontop", enabled); + document.getElementById("TabsToolbar").setAttribute("tabsontop", enabled); + document.getElementById("nav-bar").setAttribute("tabsontop", enabled); + gBrowser.tabContainer.setAttribute("tabsontop", enabled); + TabsInTitlebar.allowedBy("tabs-on-top", enabled); + }, + + get enabled () { + return gNavToolbox.getAttribute("tabsontop") == "true"; + }, + + set enabled (val) { + Services.prefs.setBoolPref(this._prefName, !!val); + return val; + }, + + observe: function (subject, topic, data) { + if (topic == "nsPref:changed") + this.syncUI(); + }, + + _prefName: "browser.tabs.onTop" +} + +var TabsInTitlebar = { + init: function () { +#ifdef CAN_DRAW_IN_TITLEBAR + this._readPref(); + Services.prefs.addObserver(this._prefName, this, false); + + // Don't trust the initial value of the sizemode attribute; wait for + // the resize event (handled in tabbrowser.xml). + this.allowedBy("sizemode", false); + + this._initialized = true; +#endif + }, + + allowedBy: function (condition, allow) { +#ifdef CAN_DRAW_IN_TITLEBAR + if (allow) { + if (condition in this._disallowed) { + delete this._disallowed[condition]; + this._update(); + } + } else { + if (!(condition in this._disallowed)) { + this._disallowed[condition] = null; + this._update(); + } + } +#endif + }, + + get enabled() { + return document.documentElement.getAttribute("tabsintitlebar") == "true"; + }, + +#ifdef CAN_DRAW_IN_TITLEBAR + observe: function (subject, topic, data) { + if (topic == "nsPref:changed") + this._readPref(); + }, + + _initialized: false, + _disallowed: {}, + _prefName: "browser.tabs.drawInTitlebar", + + _readPref: function () { + this.allowedBy("pref", + Services.prefs.getBoolPref(this._prefName)); + }, + + _update: function () { + function $(id) document.getElementById(id); + function rect(ele) ele.getBoundingClientRect(); + + if (!this._initialized || window.fullScreen) + return; + + let allowed = true; + for (let something in this._disallowed) { + allowed = false; + break; + } + + if (allowed == this.enabled) + return; + + let titlebar = $("titlebar"); + + if (allowed) { + let tabsToolbar = $("TabsToolbar"); + +#ifdef MENUBAR_CAN_AUTOHIDE + let appmenuButtonBox = $("appmenu-button-container"); + this._sizePlaceholder("appmenu-button", rect(appmenuButtonBox).width); +#endif + let captionButtonsBox = $("titlebar-buttonbox"); + this._sizePlaceholder("caption-buttons", rect(captionButtonsBox).width); + + let tabsToolbarRect = rect(tabsToolbar); + let titlebarTop = rect($("titlebar-content")).top; + titlebar.style.marginBottom = - Math.min(tabsToolbarRect.top - titlebarTop, + tabsToolbarRect.height) + "px"; + + document.documentElement.setAttribute("tabsintitlebar", "true"); + + if (!this._draghandle) { + let tmp = {}; + Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp); + this._draghandle = new tmp.WindowDraggingElement(tabsToolbar); + this._draghandle.mouseDownCheck = function () { + return !this._dragBindingAlive && TabsInTitlebar.enabled; + }; + } + } else { + document.documentElement.removeAttribute("tabsintitlebar"); + + titlebar.style.marginBottom = ""; + } + + ToolbarIconColor.inferFromText(); + }, + + _sizePlaceholder: function (type, width) { + Array.forEach(document.querySelectorAll(".titlebar-placeholder[type='"+ type +"']"), + function (node) { node.width = width; }); + }, +#endif + + uninit: function () { +#ifdef CAN_DRAW_IN_TITLEBAR + this._initialized = false; + Services.prefs.removeObserver(this._prefName, this); +#endif + } +}; + +#ifdef MENUBAR_CAN_AUTOHIDE +function updateAppButtonDisplay() { + var displayAppButton = + !gInPrintPreviewMode && + window.menubar.visible && + document.getElementById("toolbar-menubar").getAttribute("autohide") == "true"; + +#ifdef CAN_DRAW_IN_TITLEBAR + document.getElementById("titlebar").hidden = !displayAppButton; + + if (displayAppButton) + document.documentElement.setAttribute("chromemargin", "0,2,2,2"); + else + document.documentElement.removeAttribute("chromemargin"); + + TabsInTitlebar.allowedBy("drawing-in-titlebar", displayAppButton); +#else + document.getElementById("appmenu-toolbar-button").hidden = + !displayAppButton; +#endif +} +#endif + +#ifdef CAN_DRAW_IN_TITLEBAR +function onTitlebarMaxClick() { + if (window.windowState == window.STATE_MAXIMIZED) + window.restore(); + else + window.maximize(); +} +#endif + +function displaySecurityInfo() +{ + BrowserPageInfo(null, "securityTab"); +} + +/** + * Opens or closes the sidebar identified by commandID. + * + * @param commandID a string identifying the sidebar to toggle; see the + * note below. (Optional if a sidebar is already open.) + * @param forceOpen boolean indicating whether the sidebar should be + * opened regardless of its current state (optional). + * @note + * We expect to find a xul:broadcaster element with the specified ID. + * The following attributes on that element may be used and/or modified: + * - id (required) the string to match commandID. The convention + * is to use this naming scheme: 'view<sidebar-name>Sidebar'. + * - sidebarurl (required) specifies the URL to load in this sidebar. + * - sidebartitle or label (in that order) specify the title to + * display on the sidebar. + * - checked indicates whether the sidebar is currently displayed. + * Note that toggleSidebar updates this attribute when + * it changes the sidebar's visibility. + * - group this attribute must be set to "sidebar". + */ +function toggleSidebar(commandID, forceOpen) { + + var sidebarBox = document.getElementById("sidebar-box"); + if (!commandID) + commandID = sidebarBox.getAttribute("sidebarcommand"); + + var sidebarBroadcaster = document.getElementById(commandID); + var sidebar = document.getElementById("sidebar"); // xul:browser + var sidebarTitle = document.getElementById("sidebar-title"); + var sidebarSplitter = document.getElementById("sidebar-splitter"); + + if (sidebarBroadcaster.getAttribute("checked") == "true") { + if (!forceOpen) { + // Replace the document currently displayed in the sidebar with about:blank + // so that we can free memory by unloading the page. We need to explicitly + // create a new content viewer because the old one doesn't get destroyed + // until about:blank has loaded (which does not happen as long as the + // element is hidden). + sidebar.setAttribute("src", "about:blank"); + sidebar.docShell.createAboutBlankContentViewer(null); + + sidebarBroadcaster.removeAttribute("checked"); + sidebarBox.setAttribute("sidebarcommand", ""); + sidebarTitle.value = ""; + sidebarBox.hidden = true; + sidebarSplitter.hidden = true; + gBrowser.selectedBrowser.focus(); + } else { + fireSidebarFocusedEvent(); + } + return; + } + + // now we need to show the specified sidebar + + // ..but first update the 'checked' state of all sidebar broadcasters + var broadcasters = document.getElementsByAttribute("group", "sidebar"); + for (let broadcaster of broadcasters) { + // skip elements that observe sidebar broadcasters and random + // other elements + if (broadcaster.localName != "broadcaster") + continue; + + if (broadcaster != sidebarBroadcaster) + broadcaster.removeAttribute("checked"); + else + sidebarBroadcaster.setAttribute("checked", "true"); + } + + sidebarBox.hidden = false; + sidebarSplitter.hidden = false; + + var url = sidebarBroadcaster.getAttribute("sidebarurl"); + var title = sidebarBroadcaster.getAttribute("sidebartitle"); + if (!title) + title = sidebarBroadcaster.getAttribute("label"); + sidebar.setAttribute("src", url); // kick off async load + sidebarBox.setAttribute("sidebarcommand", sidebarBroadcaster.id); + sidebarTitle.value = title; + + // We set this attribute here in addition to setting it on the <browser> + // element itself, because the code in gBrowserInit.onUnload persists this + // attribute, not the "src" of the <browser id="sidebar">. The reason it + // does that is that we want to delay sidebar load a bit when a browser + // window opens. See delayedStartup(). + sidebarBox.setAttribute("src", url); + + if (sidebar.contentDocument.location.href != url) + sidebar.addEventListener("load", sidebarOnLoad, true); + else // older code handled this case, so we do it too + fireSidebarFocusedEvent(); +} + +function sidebarOnLoad(event) { + var sidebar = document.getElementById("sidebar"); + sidebar.removeEventListener("load", sidebarOnLoad, true); + // We're handling the 'load' event before it bubbles up to the usual + // (non-capturing) event handlers. Let it bubble up before firing the + // SidebarFocused event. + setTimeout(fireSidebarFocusedEvent, 0); +} + +/** + * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar + * a chance to adjust focus as needed. An additional event is needed, because + * we don't want to focus the sidebar when it's opened on startup or in a new + * window, only when the user opens the sidebar. + */ +function fireSidebarFocusedEvent() { + var sidebar = document.getElementById("sidebar"); + var event = document.createEvent("Events"); + event.initEvent("SidebarFocused", true, false); + sidebar.contentWindow.dispatchEvent(event); +} + +var gHomeButton = { + prefDomain: "browser.startup.homepage", + observe: function (aSubject, aTopic, aPrefName) + { + if (aTopic != "nsPref:changed" || aPrefName != this.prefDomain) + return; + + this.updateTooltip(); + }, + + updateTooltip: function (homeButton) + { + if (!homeButton) + homeButton = document.getElementById("home-button"); + if (homeButton) { + var homePage = this.getHomePage(); + homePage = homePage.replace(/\|/g,', '); + if (homePage.toLowerCase() == "about:home") + homeButton.setAttribute("tooltiptext", homeButton.getAttribute("aboutHomeOverrideTooltip")); + else + homeButton.setAttribute("tooltiptext", homePage); + } + }, + + getHomePage: function () + { + var url; + try { + url = gPrefService.getComplexValue(this.prefDomain, + Components.interfaces.nsIPrefLocalizedString).data; + } catch (e) { + } + + // use this if we can't find the pref + if (!url) { + var configBundle = Services.strings + .createBundle("chrome://branding/locale/browserconfig.properties"); + url = configBundle.GetStringFromName(this.prefDomain); + } + + return url; + }, + + updatePersonalToolbarStyle: function (homeButton) + { + if (!homeButton) + homeButton = document.getElementById("home-button"); + if (homeButton) + homeButton.className = homeButton.parentNode.id == "PersonalToolbar" + || homeButton.parentNode.parentNode.id == "PersonalToolbar" ? + homeButton.className.replace("toolbarbutton-1", "bookmark-item") : + homeButton.className.replace("bookmark-item", "toolbarbutton-1"); + } +}; + +/** + * Gets the selected text in the active browser. Leading and trailing + * whitespace is removed, and consecutive whitespace is replaced by a single + * space. A maximum of 150 characters will be returned, regardless of the value + * of aCharLen. + * + * @param aCharLen + * The maximum number of characters to return. + */ +function getBrowserSelection(aCharLen) { + // selections of more than 150 characters aren't useful + const kMaxSelectionLen = 150; + const charLen = Math.min(aCharLen || kMaxSelectionLen, kMaxSelectionLen); + let commandDispatcher = document.commandDispatcher; + + var focusedWindow = commandDispatcher.focusedWindow; + var selection = focusedWindow.getSelection().toString(); + // try getting a selected text in text input. + if (!selection) { + let element = commandDispatcher.focusedElement; + var isOnTextInput = function isOnTextInput(elem) { + // we avoid to return a value if a selection is in password field. + // ref. bug 565717 + return elem instanceof HTMLTextAreaElement || + (elem instanceof HTMLInputElement && elem.mozIsTextField(true)); + }; + + if (isOnTextInput(element)) { + selection = element.QueryInterface(Ci.nsIDOMNSEditableElement) + .editor.selection.toString(); + } + } + + if (selection) { + if (selection.length > charLen) { + // only use the first charLen important chars. see bug 221361 + var pattern = new RegExp("^(?:\\s*.){0," + charLen + "}"); + pattern.test(selection); + selection = RegExp.lastMatch; + } + + selection = selection.trim().replace(/\s+/g, " "); + + if (selection.length > charLen) + selection = selection.substr(0, charLen); + } + return selection; +} + +var gWebPanelURI; +function openWebPanel(aTitle, aURI) +{ + // Ensure that the web panels sidebar is open. + toggleSidebar('viewWebPanelsSidebar', true); + + // Set the title of the panel. + document.getElementById("sidebar-title").value = aTitle; + + // Tell the Web Panels sidebar to load the bookmark. + var sidebar = document.getElementById("sidebar"); + if (sidebar.docShell && sidebar.contentDocument && sidebar.contentDocument.getElementById('web-panels-browser')) { + sidebar.contentWindow.loadWebPanel(aURI); + if (gWebPanelURI) { + gWebPanelURI = ""; + sidebar.removeEventListener("load", asyncOpenWebPanel, true); + } + } + else { + // The panel is still being constructed. Attach an onload handler. + if (!gWebPanelURI) + sidebar.addEventListener("load", asyncOpenWebPanel, true); + gWebPanelURI = aURI; + } +} + +function asyncOpenWebPanel(event) +{ + var sidebar = document.getElementById("sidebar"); + if (gWebPanelURI && sidebar.contentDocument && sidebar.contentDocument.getElementById('web-panels-browser')) + sidebar.contentWindow.loadWebPanel(gWebPanelURI); + gWebPanelURI = ""; + sidebar.removeEventListener("load", asyncOpenWebPanel, true); +} + +/* + * - [ Dependencies ] --------------------------------------------------------- + * utilityOverlay.js: + * - gatherTextUnder + */ + +/** + * Extracts linkNode and href for the current click target. + * + * @param event + * The click event. + * @return [href, linkNode]. + * + * @note linkNode will be null if the click wasn't on an anchor + * element (or XLink). + */ +function hrefAndLinkNodeForClickEvent(event) +{ + function isHTMLLink(aNode) + { + // Be consistent with what nsContextMenu.js does. + return ((aNode instanceof HTMLAnchorElement && aNode.href) || + (aNode instanceof HTMLAreaElement && aNode.href) || + aNode instanceof HTMLLinkElement); + } + + let node = event.target; + while (node && !isHTMLLink(node)) { + node = node.parentNode; + } + + if (node) + return [node.href, node]; + + // If there is no linkNode, try simple XLink. + let href, baseURI; + node = event.target; + while (node && !href) { + if (node.nodeType == Node.ELEMENT_NODE && + (node.localName == "a" || + node.namespaceURI == "http://www.w3.org/1998/Math/MathML")) { + href = node.getAttributeNS("http://www.w3.org/1999/xlink", "href"); + if (href) { + baseURI = node.baseURI; + break; + } + } + node = node.parentNode; + } + + // In case of XLink, we don't return the node we got href from since + // callers expect <a>-like elements. + return [href ? makeURLAbsolute(baseURI, href) : null, null]; +} + +/** + * Called whenever the user clicks in the content area. + * + * @param event + * The click event. + * @param isPanelClick + * Whether the event comes from a web panel. + * @note default event is prevented if the click is handled. + */ +function contentAreaClick(event, isPanelClick) +{ + if (!event.isTrusted || event.defaultPrevented || event.button == 2) + return; + + let [href, linkNode] = hrefAndLinkNodeForClickEvent(event); + if (!href) { + // Not a link, handle middle mouse navigation. + if (event.button == 1 && + gPrefService.getBoolPref("middlemouse.contentLoadURL") && + !gPrefService.getBoolPref("general.autoScroll")) { + middleMousePaste(event); + event.preventDefault(); + } + return; + } + + // This code only applies if we have a linkNode (i.e. clicks on real anchor + // elements, as opposed to XLink). + if (linkNode && event.button == 0 && + !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) { + // A Web panel's links should target the main content area. Do this + // if no modifier keys are down and if there's no target or the target + // equals _main (the IE convention) or _content (the Mozilla convention). + let target = linkNode.target; + let mainTarget = !target || target == "_content" || target == "_main"; + if (isPanelClick && mainTarget) { + // javascript and data links should be executed in the current browser. + if (linkNode.getAttribute("onclick") || + href.startsWith("javascript:") || + href.startsWith("data:")) + return; + + try { + urlSecurityCheck(href, linkNode.ownerDocument.nodePrincipal); + } + catch(ex) { + // Prevent loading unsecure destinations. + event.preventDefault(); + return; + } + + loadURI(href, null, null, false); + event.preventDefault(); + return; + } + + if (linkNode.getAttribute("rel") == "sidebar") { + // This is the Opera convention for a special link that, when clicked, + // allows to add a sidebar panel. The link's title attribute contains + // the title that should be used for the sidebar panel. + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: makeURI(href) + , title: linkNode.getAttribute("title") + , loadBookmarkInSidebar: true + , hiddenRows: [ "description" + , "location" + , "keyword" ] + }, window); + event.preventDefault(); + return; + } + } + + handleLinkClick(event, href, linkNode); + + // Mark the page as a user followed link. This is done so that history can + // distinguish automatic embed visits from user activated ones. For example + // pages loaded in frames are embed visits and lost with the session, while + // visits across frames should be preserved. + try { + if (!PrivateBrowsingUtils.isWindowPrivate(window)) + PlacesUIUtils.markPageAsFollowedLink(href); + } catch (ex) { /* Skip invalid URIs. */ } +} + +/** + * Handles clicks on links. + * + * @return true if the click event was handled, false otherwise. + */ +function handleLinkClick(event, href, linkNode) { + if (event.button == 2) // right click + return false; + + var where = whereToOpenLink(event); + if (where == "current") + return false; + + var doc = event.target.ownerDocument; + + if (where == "save") { + saveURL(href, linkNode ? gatherTextUnder(linkNode) : "", null, true, + true, doc.documentURIObject, doc); + event.preventDefault(); + return true; + } + + urlSecurityCheck(href, doc.nodePrincipal); + openLinkIn(href, where, { referrerURI: doc.documentURIObject, + charset: doc.characterSet, + referrerPolicy: doc.referrerPolicy, + originPrincipal: doc.nodePrincipal, + triggeringPrincipal: doc.nodePrincipal }); + event.preventDefault(); + return true; +} + +function middleMousePaste(event) { + let clipboard = readFromClipboard(); + if (!clipboard) + return; + + // Strip embedded newlines and surrounding whitespace, to match the URL + // bar's behavior (stripsurroundingwhitespace) + clipboard = clipboard.replace(/\s*\n\s*/g, ""); + + // if it's not the current tab, we don't need to do anything because the + // browser doesn't exist. + let where = whereToOpenLink(event, true, false); + let lastLocationChange; + if (where == "current") { + lastLocationChange = gBrowser.selectedBrowser.lastLocationChange; + } + + getShortcutOrURIAndPostData(clipboard).then(data => { + try { + makeURI(data.url); + } catch (ex) { + // Not a valid URI. + return; + } + + try { + addToUrlbarHistory(data.url); + } catch (ex) { + // Things may go wrong when adding url to session history, + // but don't let that interfere with the loading of the url. + Cu.reportError(ex); + } + + if (where != "current" || + lastLocationChange == gBrowser.selectedBrowser.lastLocationChange) { + openUILink(data.url, event, + { ignoreButton: true, + disallowInheritPrincipal: !data.mayInheritPrincipal, + initiatingDoc: event ? event.target.ownerDocument : null }); + } + }); + + event.stopPropagation(); +} + +// handleDroppedLink has the following 2 overloads: +// handleDroppedLink(event, url, name) +// handleDroppedLink(event, links) +function handleDroppedLink(event, urlOrLinks, name) +{ + let links; + if (Array.isArray(urlOrLinks)) { + links = urlOrLinks; + } else { + links = [{ url: urlOrLinks, name, type: "" }]; + } + + let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange; + + let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + if (event.shiftKey) + inBackground = !inBackground; + + Task.spawn(function*() { + let urls = []; + let postDatas = []; + for (let link of links) { + let data = yield 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, + }); + } + }); + + // Keep the event from being handled by the dragDrop listeners + // built-in to goanna if they happen to be above us. + event.preventDefault(); +}; + +function MultiplexHandler(event) +{ try { + var node = event.target; + var name = node.getAttribute('name'); + + if (name == 'detectorGroup') { + BrowserCharsetReload(); + SelectDetector(event, false); + } else if (name == 'charsetGroup') { + var charset = node.getAttribute('id'); + charset = charset.substring(charset.indexOf('charset.') + 'charset.'.length); + BrowserSetForcedCharacterSet(charset); + } else if (name == 'charsetCustomize') { + //do nothing - please remove this else statement, once the charset prefs moves to the pref window + } else { + BrowserSetForcedCharacterSet(node.getAttribute('id')); + } + } catch(ex) { alert(ex); } +} + +function SelectDetector(event, doReload) +{ + var uri = event.target.getAttribute("id"); + var prefvalue = uri.substring(uri.indexOf('chardet.') + 'chardet.'.length); + if ("off" == prefvalue) { // "off" is special value to turn off the detectors + prefvalue = ""; + } + + try { + var str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + + str.data = prefvalue; + gPrefService.setComplexValue("intl.charset.detector", Ci.nsISupportsString, str); + if (doReload) + window.content.location.reload(); + } + catch (ex) { + dump("Failed to set the intl.charset.detector preference.\n"); + } +} + +function BrowserSetForcedCharacterSet(aCharset) +{ + gBrowser.docShell.charset = aCharset; + // Save the forced character-set + if (!PrivateBrowsingUtils.isWindowPrivate(window)) + PlacesUtils.setCharsetForURI(getWebNavigation().currentURI, aCharset); + BrowserCharsetReload(); +} + +function BrowserCharsetReload() +{ + BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); +} + +function charsetMenuGetElement(parent, id) { + return parent.getElementsByAttribute("id", id)[0]; +} + +function UpdateCurrentCharset(target) { + // extract the charset from DOM + var wnd = document.commandDispatcher.focusedWindow; + if ((window == wnd) || (wnd == null)) wnd = window.content; + + // Uncheck previous item + if (gPrevCharset) { + var pref_item = charsetMenuGetElement(target, "charset." + gPrevCharset); + if (pref_item) + pref_item.setAttribute('checked', 'false'); + } + + var menuitem = charsetMenuGetElement(target, "charset." + wnd.document.characterSet); + if (menuitem) { + menuitem.setAttribute('checked', 'true'); + } +} + +function UpdateCharsetDetector(target) { + var prefvalue; + + try { + prefvalue = gPrefService.getComplexValue("intl.charset.detector", Ci.nsIPrefLocalizedString).data; + } + catch (ex) {} + + if (!prefvalue) + prefvalue = "off"; + + var menuitem = charsetMenuGetElement(target, "chardet." + prefvalue); + if (menuitem) + menuitem.setAttribute("checked", "true"); +} + +function UpdateMenus(event) { + UpdateCurrentCharset(event.target); + UpdateCharsetDetector(event.target); +} + +function charsetLoadListener() { + var charset = window.content.document.characterSet; + + if (charset.length > 0 && (charset != gLastBrowserCharset)) { + gPrevCharset = gLastBrowserCharset; + gLastBrowserCharset = charset; + } +} + +var gPageStyleMenu = { + + _getAllStyleSheets: function (frameset) { + var styleSheetsArray = Array.slice(frameset.document.styleSheets); + for (let i = 0; i < frameset.frames.length; i++) { + let frameSheets = this._getAllStyleSheets(frameset.frames[i]); + styleSheetsArray = styleSheetsArray.concat(frameSheets); + } + return styleSheetsArray; + }, + + fillPopup: function (menuPopup) { + var noStyle = menuPopup.firstChild; + var persistentOnly = noStyle.nextSibling; + var sep = persistentOnly.nextSibling; + while (sep.nextSibling) + menuPopup.removeChild(sep.nextSibling); + + var styleSheets = this._getAllStyleSheets(window.content); + var currentStyleSheets = {}; + var styleDisabled = getMarkupDocumentViewer().authorStyleDisabled; + var haveAltSheets = false; + var altStyleSelected = false; + + for (let currentStyleSheet of styleSheets) { + if (!currentStyleSheet.title) + continue; + + // Skip any stylesheets whose media attribute doesn't match. + if (currentStyleSheet.media.length > 0) { + let mediaQueryList = currentStyleSheet.media.mediaText; + if (!window.content.matchMedia(mediaQueryList).matches) + continue; + } + + if (!currentStyleSheet.disabled) + altStyleSelected = true; + + haveAltSheets = true; + + let lastWithSameTitle = null; + if (currentStyleSheet.title in currentStyleSheets) + lastWithSameTitle = currentStyleSheets[currentStyleSheet.title]; + + if (!lastWithSameTitle) { + let menuItem = document.createElement("menuitem"); + menuItem.setAttribute("type", "radio"); + menuItem.setAttribute("label", currentStyleSheet.title); + menuItem.setAttribute("data", currentStyleSheet.title); + menuItem.setAttribute("checked", !currentStyleSheet.disabled && !styleDisabled); + menuItem.setAttribute("oncommand", "gPageStyleMenu.switchStyleSheet(this.getAttribute('data'));"); + menuPopup.appendChild(menuItem); + currentStyleSheets[currentStyleSheet.title] = menuItem; + } else if (currentStyleSheet.disabled) { + lastWithSameTitle.removeAttribute("checked"); + } + } + + noStyle.setAttribute("checked", styleDisabled); + persistentOnly.setAttribute("checked", !altStyleSelected && !styleDisabled); + persistentOnly.hidden = (window.content.document.preferredStyleSheetSet) ? haveAltSheets : false; + sep.hidden = (noStyle.hidden && persistentOnly.hidden) || !haveAltSheets; + }, + + _stylesheetInFrame: function (frame, title) { + return Array.some(frame.document.styleSheets, + function (stylesheet) stylesheet.title == title); + }, + + _stylesheetSwitchFrame: function (frame, title) { + var docStyleSheets = frame.document.styleSheets; + + for (let i = 0; i < docStyleSheets.length; ++i) { + let docStyleSheet = docStyleSheets[i]; + + if (docStyleSheet.title) + docStyleSheet.disabled = (docStyleSheet.title != title); + else if (docStyleSheet.disabled) + docStyleSheet.disabled = false; + } + }, + + _stylesheetSwitchAll: function (frameset, title) { + if (!title || this._stylesheetInFrame(frameset, title)) + this._stylesheetSwitchFrame(frameset, title); + + for (let i = 0; i < frameset.frames.length; i++) + this._stylesheetSwitchAll(frameset.frames[i], title); + }, + + switchStyleSheet: function (title, contentWindow) { + getMarkupDocumentViewer().authorStyleDisabled = false; + this._stylesheetSwitchAll(contentWindow || content, title); + }, + + disableStyle: function () { + getMarkupDocumentViewer().authorStyleDisabled = true; + }, +}; + +/* Legacy global page-style functions */ +var getAllStyleSheets = gPageStyleMenu._getAllStyleSheets.bind(gPageStyleMenu); +var stylesheetFillPopup = gPageStyleMenu.fillPopup.bind(gPageStyleMenu); +function stylesheetSwitchAll(contentWindow, title) { + gPageStyleMenu.switchStyleSheet(title, contentWindow); +} +function setStyleDisabled(disabled) { + if (disabled) + gPageStyleMenu.disableStyle(); +} + + +var BrowserOffline = { + _inited: false, + + ///////////////////////////////////////////////////////////////////////////// + // BrowserOffline Public Methods + init: function () + { + if (!this._uiElement) + this._uiElement = document.getElementById("workOfflineMenuitemState"); + + Services.obs.addObserver(this, "network:offline-status-changed", false); + + this._updateOfflineUI(Services.io.offline); + + this._inited = true; + }, + + uninit: function () + { + if (this._inited) { + Services.obs.removeObserver(this, "network:offline-status-changed"); + } + }, + + toggleOfflineStatus: function () + { + var ioService = Services.io; + + // Stop automatic management of the offline status + try { + ioService.manageOfflineStatus = false; + } catch (ex) { + } + + if (!ioService.offline && !this._canGoOffline()) { + this._updateOfflineUI(false); + return; + } + + ioService.offline = !ioService.offline; + }, + + ///////////////////////////////////////////////////////////////////////////// + // nsIObserver + observe: function (aSubject, aTopic, aState) + { + if (aTopic != "network:offline-status-changed") + return; + + this._updateOfflineUI(aState == "offline"); + }, + + ///////////////////////////////////////////////////////////////////////////// + // BrowserOffline Implementation Methods + _canGoOffline: function () + { + try { + var cancelGoOffline = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelGoOffline, "offline-requested", null); + + // Something aborted the quit process. + if (cancelGoOffline.data) + return false; + } + catch (ex) { + } + + return true; + }, + + _uiElement: null, + _updateOfflineUI: function (aOffline) + { + var offlineLocked = gPrefService.prefIsLocked("network.online"); + if (offlineLocked) + this._uiElement.setAttribute("disabled", "true"); + + this._uiElement.setAttribute("checked", aOffline); + } +}; + +var OfflineApps = { + ///////////////////////////////////////////////////////////////////////////// + // OfflineApps Public Methods + init: function () + { + Services.obs.addObserver(this, "offline-cache-update-completed", false); + }, + + uninit: function () + { + Services.obs.removeObserver(this, "offline-cache-update-completed"); + }, + + handleEvent: function(event) { + if (event.type == "MozApplicationManifest") { + this.offlineAppRequested(event.originalTarget.defaultView); + } + }, + + ///////////////////////////////////////////////////////////////////////////// + // OfflineApps Implementation Methods + + // XXX: _getBrowserWindowForContentWindow and _getBrowserForContentWindow + // were taken from browser/components/feeds/src/WebContentConverter. + _getBrowserWindowForContentWindow: function(aContentWindow) { + return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .wrappedJSObject; + }, + + _getBrowserForContentWindow: function(aBrowserWindow, aContentWindow) { + // This depends on pseudo APIs of browser.js and tabbrowser.xml + aContentWindow = aContentWindow.top; + var browsers = aBrowserWindow.gBrowser.browsers; + for (let browser of browsers) { + if (browser.contentWindow == aContentWindow) + return browser; + } + // handle other browser/iframe elements that may need popupnotifications + let browser = aContentWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + if (browser.getAttribute("popupnotificationanchor")) + return browser; + return null; + }, + + _getManifestURI: function(aWindow) { + if (!aWindow.document.documentElement) + return null; + + var attr = aWindow.document.documentElement.getAttribute("manifest"); + if (!attr) + return null; + + try { + var contentURI = makeURI(aWindow.location.href, null, null); + return makeURI(attr, aWindow.document.characterSet, contentURI); + } catch (e) { + return null; + } + }, + + // A cache update isn't tied to a specific window. Try to find + // the best browser in which to warn the user about space usage + _getBrowserForCacheUpdate: function(aCacheUpdate) { + // Prefer the current browser + var uri = this._getManifestURI(content); + if (uri && uri.equals(aCacheUpdate.manifestURI)) { + return gBrowser.selectedBrowser; + } + + var browsers = gBrowser.browsers; + for (let browser of browsers) { + uri = this._getManifestURI(browser.contentWindow); + if (uri && uri.equals(aCacheUpdate.manifestURI)) { + return browser; + } + } + + // is this from a non-tab browser/iframe? + browsers = document.querySelectorAll("iframe[popupnotificationanchor] | browser[popupnotificationanchor]"); + for (let browser of browsers) { + uri = this._getManifestURI(browser.contentWindow); + if (uri && uri.equals(aCacheUpdate.manifestURI)) { + return browser; + } + } + + return null; + }, + + _warnUsage: function(aBrowser, aURI) { + if (!aBrowser) + return; + + let mainAction = { + label: gNavigatorBundle.getString("offlineApps.manageUsage"), + accessKey: gNavigatorBundle.getString("offlineApps.manageUsageAccessKey"), + callback: OfflineApps.manage + }; + + let warnQuota = gPrefService.getIntPref("offline-apps.quota.warn"); + let message = gNavigatorBundle.getFormattedString("offlineApps.usage", + [ aURI.host, + warnQuota / 1024 ]); + + let anchorID = "indexedDB-notification-icon"; + PopupNotifications.show(aBrowser, "offline-app-usage", message, + anchorID, mainAction); + + // Now that we've warned once, prevent the warning from showing up + // again. + Services.perms.add(aURI, "offline-app", + Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN); + }, + + // XXX: duplicated in preferences/advanced.js + _getOfflineAppUsage: function (host, groups) + { + var cacheService = Cc["@mozilla.org/network/application-cache-service;1"]. + getService(Ci.nsIApplicationCacheService); + if (!groups) + groups = cacheService.getGroups(); + + var usage = 0; + for (let group of groups) { + var uri = Services.io.newURI(group, null, null); + if (uri.asciiHost == host) { + var cache = cacheService.getActiveCache(group); + usage += cache.usage; + } + } + + return usage; + }, + + _checkUsage: function(aURI) { + // if the user has already allowed excessive usage, don't bother checking + if (Services.perms.testExactPermission(aURI, "offline-app") != + Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN) { + var usage = this._getOfflineAppUsage(aURI.asciiHost); + var warnQuota = gPrefService.getIntPref("offline-apps.quota.warn"); + if (usage >= warnQuota * 1024) { + return true; + } + } + + return false; + }, + + offlineAppRequested: function(aContentWindow) { + if (!gPrefService.getBoolPref("browser.offline-apps.notify")) { + return; + } + + let browserWindow = this._getBrowserWindowForContentWindow(aContentWindow); + let browser = this._getBrowserForContentWindow(browserWindow, + aContentWindow); + + let currentURI = aContentWindow.document.documentURIObject; + + // don't bother showing UI if the user has already made a decision + if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION) + return; + + try { + if (gPrefService.getBoolPref("offline-apps.allow_by_default")) { + // all pages can use offline capabilities, no need to ask the user + return; + } + } catch(e) { + // this pref isn't set by default, ignore failures + } + + let host = currentURI.asciiHost; + let notificationID = "offline-app-requested-" + host; + let notification = PopupNotifications.getNotification(notificationID, browser); + + if (notification) { + notification.options.documents.push(aContentWindow.document); + } else { + let mainAction = { + label: gNavigatorBundle.getString("offlineApps.allow"), + accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"), + callback: function() { + for (let document of notification.options.documents) { + OfflineApps.allowSite(document); + } + } + }; + let secondaryActions = [{ + label: gNavigatorBundle.getString("offlineApps.never"), + accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"), + callback: function() { + for (let document of notification.options.documents) { + OfflineApps.disallowSite(document); + } + } + }]; + let message = gNavigatorBundle.getFormattedString("offlineApps.available", + [ host ]); + let anchorID = "indexedDB-notification-icon"; + let options= { + documents : [ aContentWindow.document ] + }; + notification = PopupNotifications.show(browser, notificationID, message, + anchorID, mainAction, + secondaryActions, options); + } + }, + + allowSite: function(aDocument) { + Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.ALLOW_ACTION); + + // When a site is enabled while loading, manifest resources will + // start fetching immediately. This one time we need to do it + // ourselves. + this._startFetching(aDocument); + }, + + disallowSite: function(aDocument) { + Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.DENY_ACTION); + }, + + manage: function() { + openAdvancedPreferences("networkTab"); + }, + + _startFetching: function(aDocument) { + if (!aDocument.documentElement) + return; + + var manifest = aDocument.documentElement.getAttribute("manifest"); + if (!manifest) + return; + + var manifestURI = makeURI(manifest, aDocument.characterSet, + aDocument.documentURIObject); + + var updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"]. + getService(Ci.nsIOfflineCacheUpdateService); + updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject, window); + }, + + ///////////////////////////////////////////////////////////////////////////// + // nsIObserver + observe: function (aSubject, aTopic, aState) + { + if (aTopic == "offline-cache-update-completed") { + var cacheUpdate = aSubject.QueryInterface(Ci.nsIOfflineCacheUpdate); + + var uri = cacheUpdate.manifestURI; + if (OfflineApps._checkUsage(uri)) { + var browser = this._getBrowserForCacheUpdate(cacheUpdate); + if (browser) { + OfflineApps._warnUsage(browser, cacheUpdate.manifestURI); + } + } + } + } +}; + +var IndexedDBPromptHelper = { + _permissionsPrompt: "indexedDB-permissions-prompt", + _permissionsResponse: "indexedDB-permissions-response", + + _quotaPrompt: "indexedDB-quota-prompt", + _quotaResponse: "indexedDB-quota-response", + _quotaCancel: "indexedDB-quota-cancel", + + _notificationIcon: "indexedDB-notification-icon", + + init: + function IndexedDBPromptHelper_init() { + Services.obs.addObserver(this, this._permissionsPrompt, false); + Services.obs.addObserver(this, this._quotaPrompt, false); + Services.obs.addObserver(this, this._quotaCancel, false); + }, + + uninit: + function IndexedDBPromptHelper_uninit() { + Services.obs.removeObserver(this, this._permissionsPrompt); + Services.obs.removeObserver(this, this._quotaPrompt); + Services.obs.removeObserver(this, this._quotaCancel); + }, + + observe: + function IndexedDBPromptHelper_observe(subject, topic, data) { + if (topic != this._permissionsPrompt && + topic != this._quotaPrompt && + topic != this._quotaCancel) { + throw new Error("Unexpected topic!"); + } + + var requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor); + + var contentWindow = requestor.getInterface(Ci.nsIDOMWindow); + var contentDocument = contentWindow.document; + var browserWindow = + OfflineApps._getBrowserWindowForContentWindow(contentWindow); + + if (browserWindow != window) { + // Must belong to some other window. + return; + } + + var browser = + OfflineApps._getBrowserForContentWindow(browserWindow, contentWindow); + + var host = contentDocument.documentURIObject.asciiHost; + + var message; + var responseTopic; + if (topic == this._permissionsPrompt) { + message = gNavigatorBundle.getFormattedString("offlineApps.available", + [ host ]); + responseTopic = this._permissionsResponse; + } + else if (topic == this._quotaPrompt) { + message = gNavigatorBundle.getFormattedString("indexedDB.usage", + [ host, data ]); + responseTopic = this._quotaResponse; + } + else if (topic == this._quotaCancel) { + responseTopic = this._quotaResponse; + } + + const hiddenTimeoutDuration = 30000; // 30 seconds + const firstTimeoutDuration = 300000; // 5 minutes + + var timeoutId; + + var observer = requestor.getInterface(Ci.nsIObserver); + + var mainAction = { + label: gNavigatorBundle.getString("offlineApps.allow"), + accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"), + callback: function() { + clearTimeout(timeoutId); + observer.observe(null, responseTopic, + Ci.nsIPermissionManager.ALLOW_ACTION); + } + }; + + var secondaryActions = [ + { + label: gNavigatorBundle.getString("offlineApps.never"), + accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"), + callback: function() { + clearTimeout(timeoutId); + observer.observe(null, responseTopic, + Ci.nsIPermissionManager.DENY_ACTION); + } + } + ]; + + // This will be set to the result of PopupNotifications.show() below, or to + // the result of PopupNotifications.getNotification() if this is a + // quotaCancel notification. + var notification; + + function timeoutNotification() { + // Remove the notification. + if (notification) { + notification.remove(); + } + + // Clear all of our timeout stuff. We may be called directly, not just + // when the timeout actually elapses. + clearTimeout(timeoutId); + + // And tell the page that the popup timed out. + observer.observe(null, responseTopic, + Ci.nsIPermissionManager.UNKNOWN_ACTION); + } + + var options = { + eventCallback: function(state) { + // Don't do anything if the timeout has not been set yet. + if (!timeoutId) { + return; + } + + // If the popup is being dismissed start the short timeout. + if (state == "dismissed") { + clearTimeout(timeoutId); + timeoutId = setTimeout(timeoutNotification, hiddenTimeoutDuration); + return; + } + + // If the popup is being re-shown then clear the timeout allowing + // unlimited waiting. + if (state == "shown") { + clearTimeout(timeoutId); + } + } + }; + + if (topic == this._quotaCancel) { + notification = PopupNotifications.getNotification(this._quotaPrompt, + browser); + timeoutNotification(); + return; + } + + notification = PopupNotifications.show(browser, topic, message, + this._notificationIcon, mainAction, + secondaryActions, options); + + // Set the timeoutId after the popup has been created, and use the long + // timeout value. If the user doesn't notice the popup after this amount of + // time then it is most likely not visible and we want to alert the page. + timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration); + } +}; + +function WindowIsClosing() +{ + let event = document.createEvent("Events"); + event.initEvent("WindowIsClosing", true, true); + if (!window.dispatchEvent(event)) + return false; + + if (!closeWindow(false, warnAboutClosingWindow)) + return false; + + for (let browser of gBrowser.browsers) { + let ds = browser.docShell; + if (ds.contentViewer && !ds.contentViewer.permitUnload()) + return false; + } + + return true; +} + +/** + * Checks if this is the last full *browser* window around. If it is, this will + * be communicated like quitting. Otherwise, we warn about closing multiple tabs. + * @returns true if closing can proceed, false if it got cancelled. + */ +function warnAboutClosingWindow() { + // Popups aren't considered full browser windows. + let isPBWindow = PrivateBrowsingUtils.isWindowPrivate(window); + if (!isPBWindow && !toolbar.visible) + return gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL); + + // Figure out if there's at least one other browser window around. + let e = Services.wm.getEnumerator("navigator:browser"); + let otherPBWindowExists = false; + let nonPopupPresent = false; + while (e.hasMoreElements()) { + let win = e.getNext(); + if (win != window) { + if (isPBWindow && PrivateBrowsingUtils.isWindowPrivate(win)) + otherPBWindowExists = true; + if (win.toolbar.visible) + nonPopupPresent = true; + // If the current window is not in private browsing mode we don't need to + // look for other pb windows, we can leave the loop when finding the + // first non-popup window. If however the current window is in private + // browsing mode then we need at least one other pb and one non-popup + // window to break out early. + if ((!isPBWindow || otherPBWindowExists) && nonPopupPresent) + break; + } + } + + if (isPBWindow && !otherPBWindowExists) { + let exitingCanceled = Cc["@mozilla.org/supports-PRBool;1"]. + createInstance(Ci.nsISupportsPRBool); + exitingCanceled.data = false; + Services.obs.notifyObservers(exitingCanceled, + "last-pb-context-exiting", + null); + if (exitingCanceled.data) + return false; + } + + if (nonPopupPresent) { + return isPBWindow || gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL); + } + + let os = Services.obs; + + let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"]. + createInstance(Ci.nsISupportsPRBool); + os.notifyObservers(closingCanceled, + "browser-lastwindow-close-requested", null); + if (closingCanceled.data) + return false; + + os.notifyObservers(null, "browser-lastwindow-close-granted", null); + +#ifdef XP_MACOSX + // OS X doesn't quit the application when the last window is closed, but keeps + // the session alive. Hence don't prompt users to save tabs, but warn about + // closing multiple tabs. + return isPBWindow || gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL); +#else + return true; +#endif +} + +var MailIntegration = { + sendLinkForWindow: function (aWindow) { + this.sendMessage(aWindow.location.href, + aWindow.document.title); + }, + + sendMessage: function (aBody, aSubject) { + // generate a mailto url based on the url and the url's title + var mailtoUrl = "mailto:"; + if (aBody) { + mailtoUrl += "?body=" + encodeURIComponent(aBody); + mailtoUrl += "&subject=" + encodeURIComponent(aSubject); + } + + var uri = makeURI(mailtoUrl); + + // now pass this uri to the operating system + this._launchExternalUrl(uri); + }, + + // a generic method which can be used to pass arbitrary urls to the operating + // system. + // aURL --> a nsIURI which represents the url to launch + _launchExternalUrl: function (aURL) { + var extProtocolSvc = + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + if (extProtocolSvc) + extProtocolSvc.loadUrl(aURL); + } +}; + +function BrowserOpenAddonsMgr(aView) { + if (aView) { + let emWindow; + let browserWindow; + + var receivePong = function receivePong(aSubject, aTopic, aData) { + let browserWin = aSubject.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + if (!emWindow || browserWin == window /* favor the current window */) { + emWindow = aSubject; + browserWindow = browserWin; + } + } + Services.obs.addObserver(receivePong, "EM-pong", false); + Services.obs.notifyObservers(null, "EM-ping", ""); + Services.obs.removeObserver(receivePong, "EM-pong"); + + if (emWindow) { + emWindow.loadView(aView); + browserWindow.gBrowser.selectedTab = + browserWindow.gBrowser._getTabForContentWindow(emWindow); + emWindow.focus(); + return; + } + } + + var newLoad = !switchToTabHavingURI("about:addons", true); + + if (aView) { + // This must be a new load, else the ping/pong would have + // found the window above. + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + Services.obs.removeObserver(observer, aTopic); + aSubject.loadView(aView); + }, "EM-loaded", false); + } +} + +function BrowserOpenPermissionsMgr() { + switchToTabHavingURI("about:permissions", true); +} + +function AddKeywordForSearchField() { + var node = document.popupNode; + + var charset = node.ownerDocument.characterSet; + + var docURI = makeURI(node.ownerDocument.URL, + charset); + + var formURI = makeURI(node.form.getAttribute("action"), + charset, + docURI); + + var spec = formURI.spec; + + var isURLEncoded = + (node.form.method.toUpperCase() == "POST" + && (node.form.enctype == "application/x-www-form-urlencoded" || + node.form.enctype == "")); + + var title = gNavigatorBundle.getFormattedString("addKeywordTitleAutoFill", + [node.ownerDocument.title]); + var description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument); + + var formData = []; + + function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) { + if (aIsFormUrlEncoded) + return escape(aName + "=" + aValue); + else + return escape(aName) + "=" + escape(aValue); + } + + for (let el of node.form.elements) { + if (!el.type) // happens with fieldsets + continue; + + if (el == node) { + formData.push((isURLEncoded) ? escapeNameValuePair(el.name, "%s", true) : + // Don't escape "%s", just append + escapeNameValuePair(el.name, "", false) + "%s"); + continue; + } + + let type = el.type.toLowerCase(); + + if (((el instanceof HTMLInputElement && el.mozIsTextField(true)) || + type == "hidden" || type == "textarea") || + ((type == "checkbox" || type == "radio") && el.checked)) { + formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded)); + } else if (el instanceof HTMLSelectElement && el.selectedIndex >= 0) { + for (var j=0; j < el.options.length; j++) { + if (el.options[j].selected) + formData.push(escapeNameValuePair(el.name, el.options[j].value, + isURLEncoded)); + } + } + } + + var postData; + + if (isURLEncoded) + postData = formData.join("&"); + else + spec += "?" + formData.join("&"); + + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: makeURI(spec) + , title: title + , description: description + , keyword: "" + , postData: postData + , charSet: charset + , hiddenRows: [ "location" + , "description" + , "tags" + , "loadInSidebar" ] + }, window); +} + +function SwitchDocumentDirection(aWindow) { + // document.dir can also be "auto", in which case it won't change + if (aWindow.document.dir == "ltr" || aWindow.document.dir == "") { + aWindow.document.dir = "rtl"; + } else if (aWindow.document.dir == "rtl") { + aWindow.document.dir = "ltr"; + } + for (var run = 0; run < aWindow.frames.length; run++) + SwitchDocumentDirection(aWindow.frames[run]); +} + +function convertFromUnicode(charset, str) +{ + try { + var unicodeConverter = Components + .classes["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); + unicodeConverter.charset = charset; + str = unicodeConverter.ConvertFromUnicode(str); + return str + unicodeConverter.Finish(); + } catch(ex) { + return null; + } +} + +/** + * Re-open a closed tab. + * @param aIndex + * The index of the tab (via nsSessionStore.getClosedTabData) + * @returns a reference to the reopened tab. + */ +function undoCloseTab(aIndex) { + // wallpaper patch to prevent an unnecessary blank tab (bug 343895) + var blankTabToRemove = null; + if (gBrowser.tabs.length == 1 && + !gPrefService.getBoolPref("browser.tabs.autoHide") && + isTabEmpty(gBrowser.selectedTab)) + blankTabToRemove = gBrowser.selectedTab; + + var tab = null; + var ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + if (ss.getClosedTabCount(window) > (aIndex || 0)) { + tab = ss.undoCloseTab(window, aIndex || 0); + + if (blankTabToRemove) + gBrowser.removeTab(blankTabToRemove); + } + + return tab; +} + +/** + * Re-open a closed window. + * @param aIndex + * The index of the window (via nsSessionStore.getClosedWindowData) + * @returns a reference to the reopened window. + */ +function undoCloseWindow(aIndex) { + let ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + let window = null; + if (ss.getClosedWindowCount() > (aIndex || 0)) + window = ss.undoCloseWindow(aIndex || 0); + + return window; +} + +/* + * Determines if a tab is "empty", usually used in the context of determining + * if it's ok to close the tab. + */ +function isTabEmpty(aTab) { + if (aTab.hasAttribute("busy")) + return false; + + let browser = aTab.linkedBrowser; + if (!isBlankPageURL(browser.currentURI.spec)) + return false; + + // Bug 863515 - Make content.opener checks work in electrolysis. + if (!gMultiProcessBrowser && browser.contentWindow.opener) + return false; + + if (browser.sessionHistory && browser.sessionHistory.count >= 2) + return false; + + return true; +} + +#ifdef MOZ_SERVICES_SYNC +function BrowserOpenSyncTabs() { + switchToTabHavingURI("about:sync-tabs", true); +} +#endif + +/** + * Format a URL + * eg: + * echo formatURL("https://addons.mozilla.org/%LOCALE%/%APP%/%VERSION%/"); + * > https://addons.mozilla.org/en-US/firefox/3.0a1/ + * + * Currently supported built-ins are LOCALE, APP, and any value from nsIXULAppInfo, uppercased. + */ +function formatURL(aFormat, aIsPref) { + var formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter); + return aIsPref ? formatter.formatURLPref(aFormat) : formatter.formatURL(aFormat); +} + +/** + * Utility object to handle manipulations of the identity indicators in the UI + */ +var gIdentityHandler = { + // Mode strings used to control CSS display + IDENTITY_MODE_IDENTIFIED : "verifiedIdentity", // High-quality identity information + IDENTITY_MODE_DOMAIN_VERIFIED : "verifiedDomain", // Minimal SSL CA-signed domain verification + IDENTITY_MODE_UNKNOWN : "unknownIdentity", // No trusted identity information + IDENTITY_MODE_MIXED_CONTENT : "unknownIdentity mixedContent", // SSL with unauthenticated content + IDENTITY_MODE_MIXED_ACTIVE_CONTENT : "unknownIdentity mixedContent mixedActiveContent", // SSL with unauthenticated content + IDENTITY_MODE_CHROMEUI : "chromeUI", // Part of the product's UI + + // Cache the most recent SSLStatus and Location seen in checkIdentity + _lastStatus : null, + _lastLocation : null, + _mode : "unknownIdentity", + + // smart getters + get _encryptionLabel () { + delete this._encryptionLabel; + this._encryptionLabel = {}; + this._encryptionLabel[this.IDENTITY_MODE_DOMAIN_VERIFIED] = + gNavigatorBundle.getString("identity.encrypted"); + this._encryptionLabel[this.IDENTITY_MODE_IDENTIFIED] = + gNavigatorBundle.getString("identity.encrypted"); + this._encryptionLabel[this.IDENTITY_MODE_UNKNOWN] = + gNavigatorBundle.getString("identity.unencrypted"); + this._encryptionLabel[this.IDENTITY_MODE_MIXED_CONTENT] = + gNavigatorBundle.getString("identity.mixed_content"); + this._encryptionLabel[this.IDENTITY_MODE_MIXED_ACTIVE_CONTENT] = + gNavigatorBundle.getString("identity.mixed_content"); + return this._encryptionLabel; + }, + get _identityPopup () { + delete this._identityPopup; + return this._identityPopup = document.getElementById("identity-popup"); + }, + get _identityBox () { + delete this._identityBox; + return this._identityBox = document.getElementById("identity-box"); + }, + get _identityPopupContentBox () { + delete this._identityPopupContentBox; + return this._identityPopupContentBox = + document.getElementById("identity-popup-content-box"); + }, + get _identityPopupContentHost () { + delete this._identityPopupContentHost; + return this._identityPopupContentHost = + document.getElementById("identity-popup-content-host"); + }, + get _identityPopupContentOwner () { + delete this._identityPopupContentOwner; + return this._identityPopupContentOwner = + document.getElementById("identity-popup-content-owner"); + }, + get _identityPopupContentSupp () { + delete this._identityPopupContentSupp; + return this._identityPopupContentSupp = + document.getElementById("identity-popup-content-supplemental"); + }, + get _identityPopupContentVerif () { + delete this._identityPopupContentVerif; + return this._identityPopupContentVerif = + document.getElementById("identity-popup-content-verifier"); + }, + get _identityPopupEncLabel () { + delete this._identityPopupEncLabel; + return this._identityPopupEncLabel = + document.getElementById("identity-popup-encryption-label"); + }, + get _identityIconLabel () { + delete this._identityIconLabel; + return this._identityIconLabel = document.getElementById("identity-icon-label"); + }, + get _overrideService () { + delete this._overrideService; + return this._overrideService = Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService); + }, + get _identityIconCountryLabel () { + delete this._identityIconCountryLabel; + return this._identityIconCountryLabel = document.getElementById("identity-icon-country-label"); + }, + get _identityIcon () { + delete this._identityIcon; + return this._identityIcon = document.getElementById("page-proxy-favicon"); + }, + + /** + * Rebuild cache of the elements that may or may not exist depending + * on whether there's a location bar. + */ + _cacheElements : function() { + delete this._identityBox; + delete this._identityIconLabel; + delete this._identityIconCountryLabel; + delete this._identityIcon; + this._identityBox = document.getElementById("identity-box"); + this._identityIconLabel = document.getElementById("identity-icon-label"); + this._identityIconCountryLabel = document.getElementById("identity-icon-country-label"); + this._identityIcon = document.getElementById("page-proxy-favicon"); + }, + + /** + * Handler for mouseclicks on the "More Information" button in the + * "identity-popup" panel. + */ + handleMoreInfoClick : function(event) { + displaySecurityInfo(); + event.stopPropagation(); + }, + + /** + * Helper to parse out the important parts of _lastStatus (of the SSL cert in + * particular) for use in constructing identity UI strings + */ + getIdentityData : function() { + var result = {}; + var status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus); + var cert = status.serverCert; + + // Human readable name of Subject + result.subjectOrg = cert.organization; + + // SubjectName fields, broken up for individual access + if (cert.subjectName) { + result.subjectNameFields = {}; + cert.subjectName.split(",").forEach(function(v) { + var field = v.split("="); + this[field[0]] = field[1]; + }, result.subjectNameFields); + + // Call out city, state, and country specifically + result.city = result.subjectNameFields.L; + result.state = result.subjectNameFields.ST; + result.country = result.subjectNameFields.C; + } + + // Human readable name of Certificate Authority + result.caOrg = cert.issuerOrganization || cert.issuerCommonName; + result.cert = cert; + + return result; + }, + + /** + * Determine the identity of the page being displayed by examining its SSL cert + * (if available) and, if necessary, update the UI to reflect this. Intended to + * be called by onSecurityChange + * + * @param PRUint32 state + * @param JS Object location that mirrors an nsLocation (i.e. has .host and + * .hostname and .port) + */ + checkIdentity : function(state, location) { + var currentStatus = gBrowser.securityUI + .QueryInterface(Components.interfaces.nsISSLStatusProvider) + .SSLStatus; + this._lastStatus = currentStatus; + this._lastLocation = location; + + let nsIWebProgressListener = Ci.nsIWebProgressListener; + if (location.protocol == "chrome:" || location.protocol == "about:") { + this.setMode(this.IDENTITY_MODE_CHROMEUI); + } else if (state & nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) { + this.setMode(this.IDENTITY_MODE_IDENTIFIED); + } else if (state & nsIWebProgressListener.STATE_IS_SECURE) { + this.setMode(this.IDENTITY_MODE_DOMAIN_VERIFIED); + } else if (state & nsIWebProgressListener.STATE_IS_BROKEN) { + if ((state & nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) && + gPrefService.getBoolPref("security.mixed_content.block_active_content")) { + this.setMode(this.IDENTITY_MODE_MIXED_ACTIVE_CONTENT); + } else { + this.setMode(this.IDENTITY_MODE_MIXED_CONTENT); + } + } else { + this.setMode(this.IDENTITY_MODE_UNKNOWN); + } + + // Ensure the doorhanger is shown when mixed active content is blocked. + if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) + this.showMixedContentDoorhanger(); + }, + + /** + * Display the Mixed Content Blocker doohanger, providing an option + * to the user to override mixed content blocking + */ + showMixedContentDoorhanger : function() { + // If we've already got an active notification, bail out to avoid showing it repeatedly. + if (PopupNotifications.getNotification("mixed-content-blocked", gBrowser.selectedBrowser)) + return; + + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let messageString = gNavigatorBundle.getFormattedString("mixedContentBlocked.message", [brandShortName]); + let action = { + label: gNavigatorBundle.getString("mixedContentBlocked.keepBlockingButton.label"), + accessKey: gNavigatorBundle.getString("mixedContentBlocked.keepBlockingButton.accesskey"), + callback: function() { /* NOP */ } + }; + let secondaryActions = [ + { + label: gNavigatorBundle.getString("mixedContentBlocked.unblock.label"), + accessKey: gNavigatorBundle.getString("mixedContentBlocked.unblock.accesskey"), + callback: function() { + // Reload the page with the content unblocked + BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT); + } + } + ]; + let options = { + dismissed: true, + learnMoreURL: Services.urlFormatter.formatURLPref("browser.mixedcontent.warning.infoURL"), + }; + PopupNotifications.show(gBrowser.selectedBrowser, "mixed-content-blocked", + messageString, "mixed-content-blocked-notification-icon", + action, secondaryActions, options); + }, + + /** + * Return the eTLD+1 version of the current hostname + */ + getEffectiveHost : function() { + try { + let baseDomain = + Services.eTLD.getBaseDomainFromHost(this._lastLocation.hostname); + return this._IDNService.convertToDisplayIDN(baseDomain, {}); + } catch (e) { + // If something goes wrong (e.g. hostname is an IP address) just fail back + // to the full domain. + return this._lastLocation.hostname; + } + }, + + /** + * Update the UI to reflect the specified mode, which should be one of the + * IDENTITY_MODE_* constants. + */ + setMode : function(newMode) { + if (!this._identityBox) { + // No identity box means the identity box is not visible, in which + // case there's nothing to do. + return; + } + + this._identityBox.className = newMode; + this.setIdentityMessages(newMode); + + // Update the popup too, if it's open + if (this._identityPopup.state == "open") + this.setPopupMessages(newMode); + + this._mode = newMode; + }, + + /** + * Set up the messages for the primary identity UI based on the specified mode, + * and the details of the SSL cert, where applicable + * + * @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants. + */ + setIdentityMessages : function(newMode) { + let icon_label = ""; + let tooltip = ""; + let icon_country_label = ""; + let icon_labels_dir = "ltr"; + + if (!this._IDNService) + this._IDNService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + let punyID = gPrefService.getIntPref("browser.identity.display_punycode", 1); + + switch (newMode) { + case this.IDENTITY_MODE_DOMAIN_VERIFIED: { + let iData = this.getIdentityData(); + + let label_display = ""; + + //Pale Moon: honor browser.identity.ssl_domain_display! + switch (gPrefService.getIntPref("browser.identity.ssl_domain_display")) { + case 2 : // Show full domain + label_display = this._lastLocation.hostname; + break; + case 1 : // Show eTLD. + label_display = this.getEffectiveHost(); + } + + if (punyID >= 1) { + // Display punycode version in identity panel + icon_label = this._IDNService.convertUTF8toACE(label_display); + } else { + icon_label = label_display; + } + + // Verifier is either the CA Org, for a normal cert, or a special string + // for certs that are trusted because of a security exception. + tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier", + [iData.caOrg]); + + // Check whether this site is a security exception. XPConnect does the right + // thing here in terms of converting _lastLocation.port from string to int, but + // the overrideService doesn't like undefined ports, so make sure we have + // something in the default case (bug 432241). + // .hostname can return an empty string in some exceptional cases - + // hasMatchingOverride does not handle that, so avoid calling it. + // Updating the tooltip value in those cases isn't critical. + // FIXME: Fixing bug 646690 would probably makes this check unnecessary + if (this._lastLocation.hostname && + this._overrideService.hasMatchingOverride(this._lastLocation.hostname, + (this._lastLocation.port || 443), + iData.cert, {}, {})) + tooltip = gNavigatorBundle.getString("identity.identified.verified_by_you"); + break; } + case this.IDENTITY_MODE_IDENTIFIED: { + // If it's identified, then we can populate the dialog with credentials + let iData = this.getIdentityData(); + tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier", + [iData.caOrg]); + icon_label = iData.subjectOrg; + if (iData.country) + icon_country_label = "(" + iData.country + ")"; + + // If the organization name starts with an RTL character, then + // swap the positions of the organization and country code labels. + // The Unicode ranges reflect the definition of the UCS2_CHAR_IS_BIDI + // macro in intl/unicharutil/util/nsBidiUtils.h. When bug 218823 gets + // fixed, this test should be replaced by one adhering to the + // Unicode Bidirectional Algorithm proper (at the paragraph level). + icon_labels_dir = /^[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/.test(icon_label) ? + "rtl" : "ltr"; + break; } + case this.IDENTITY_MODE_CHROMEUI: + break; + default: + tooltip = gNavigatorBundle.getString("identity.unknown.tooltip"); + if (punyID == 2) { + // Check for IDN and display if so... + let rawHost = this._IDNService.convertUTF8toACE(this._lastLocation.hostname); + if (this._IDNService.isACE(rawHost)) { + icon_label = rawHost; + } + } + } + + // Push the appropriate strings out to the UI + this._identityBox.tooltipText = tooltip; + this._identityIconLabel.value = icon_label; + this._identityIconCountryLabel.value = icon_country_label; + // Set cropping and direction + this._identityIconLabel.crop = icon_country_label ? "end" : "center"; + this._identityIconLabel.parentNode.style.direction = icon_labels_dir; + // Hide completely if the organization label is empty + this._identityIconLabel.parentNode.collapsed = icon_label ? false : true; + }, + + /** + * Set up the title and content messages for the identity message popup, + * based on the specified mode, and the details of the SSL cert, where + * applicable + * + * @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants. + */ + setPopupMessages : function(newMode) { + + this._identityPopup.className = newMode; + this._identityPopupContentBox.className = newMode; + + // Set the static strings up front + this._identityPopupEncLabel.textContent = this._encryptionLabel[newMode]; + + // Initialize the optional strings to empty values + let supplemental = ""; + let verifier = ""; + let host = ""; + let owner = ""; + + switch (newMode) { + case this.IDENTITY_MODE_DOMAIN_VERIFIED: + host = this.getEffectiveHost(); + owner = gNavigatorBundle.getString("identity.ownerUnknown2"); + verifier = this._identityBox.tooltipText; + break; + case this.IDENTITY_MODE_IDENTIFIED: { + // If it's identified, then we can populate the dialog with credentials + let iData = this.getIdentityData(); + host = this.getEffectiveHost(); + owner = iData.subjectOrg; + verifier = this._identityBox.tooltipText; + + // Build an appropriate supplemental block out of whatever location data we have + if (iData.city) + supplemental += iData.city + "\n"; + if (iData.state && iData.country) + supplemental += gNavigatorBundle.getFormattedString("identity.identified.state_and_country", + [iData.state, iData.country]); + else if (iData.state) // State only + supplemental += iData.state; + else if (iData.country) // Country only + supplemental += iData.country; + break; } + } + + // Push the appropriate strings out to the UI + this._identityPopupContentHost.textContent = host; + this._identityPopupContentOwner.textContent = owner; + this._identityPopupContentSupp.textContent = supplemental; + this._identityPopupContentVerif.textContent = verifier; + }, + + hideIdentityPopup : function() { + this._identityPopup.hidePopup(); + }, + + /** + * Click handler for the identity-box element in primary chrome. + */ + handleIdentityButtonEvent : function(event) { + event.stopPropagation(); + + if ((event.type == "click" && event.button != 0) || + (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE && + event.keyCode != KeyEvent.DOM_VK_RETURN)) { + return; // Left click, space or enter only + } + + // Don't allow left click, space or enter if the location + // is chrome UI or the location has been modified. + if (this._mode == this.IDENTITY_MODE_CHROMEUI || + gURLBar.getAttribute("pageproxystate") != "valid") { + return; + } + + // Make sure that the display:none style we set in xul is removed now that + // the popup is actually needed + this._identityPopup.hidden = false; + + // Update the popup strings + this.setPopupMessages(this._identityBox.className); + + // Add the "open" attribute to the identity box for styling + this._identityBox.setAttribute("open", "true"); + var self = this; + this._identityPopup.addEventListener("popuphidden", function onPopupHidden(e) { + e.currentTarget.removeEventListener("popuphidden", onPopupHidden, false); + self._identityBox.removeAttribute("open"); + }, false); + + // Now open the popup, anchored off the primary chrome element + this._identityPopup.openPopup(this._identityIcon, "bottomcenter topleft"); + }, + + onPopupShown : function(event) { + document.getElementById('identity-popup-more-info-button').focus(); + }, + + onDragStart: function (event) { + if (gURLBar.getAttribute("pageproxystate") != "valid") + return; + + var value = content.location.href; + var urlString = value + "\n" + content.document.title; + var htmlString = "<a href=\"" + value + "\">" + value + "</a>"; + + var dt = event.dataTransfer; + dt.setData("text/x-moz-url", urlString); + dt.setData("text/uri-list", value); + dt.setData("text/plain", value); + dt.setData("text/html", htmlString); + dt.setDragImage(gProxyFavIcon, 16, 16); + } +}; + +function getNotificationBox(aWindow) { + var foundBrowser = gBrowser.getBrowserForDocument(aWindow.document); + if (foundBrowser) + return gBrowser.getNotificationBox(foundBrowser) + return null; +}; + +function getTabModalPromptBox(aWindow) { + var foundBrowser = gBrowser.getBrowserForDocument(aWindow.document); + if (foundBrowser) + return gBrowser.getTabModalPromptBox(foundBrowser); + return null; +}; + +/* DEPRECATED */ +function getBrowser() gBrowser; +function getNavToolbox() gNavToolbox; + +var gPrivateBrowsingUI = { + init: function PBUI_init() { + // Do nothing for normal windows + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + return; + } + + // Disable the Clear Recent History... menu item when in PB mode + // temporary fix until bug 463607 is fixed + document.getElementById("Tools:Sanitize").setAttribute("disabled", "true"); + + if (window.location.href == getBrowserURL()) { +#ifdef XP_MACOSX + if (!PrivateBrowsingUtils.permanentPrivateBrowsing) { + document.documentElement.setAttribute("drawintitlebar", true); + } +#endif + + // Adjust the window's title + let docElement = document.documentElement; + if (!PrivateBrowsingUtils.permanentPrivateBrowsing) { + docElement.setAttribute("title", + docElement.getAttribute("title_privatebrowsing")); + docElement.setAttribute("titlemodifier", + docElement.getAttribute("titlemodifier_privatebrowsing")); + } + docElement.setAttribute("privatebrowsingmode", + PrivateBrowsingUtils.permanentPrivateBrowsing ? "permanent" : "temporary"); + gBrowser.updateTitlebar(); + + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Adjust the New Window menu entries + [ + { normal: "menu_newNavigator", private: "menu_newPrivateWindow" }, + { normal: "appmenu_newNavigator", private: "appmenu_newPrivateWindow" }, + ].forEach(function(menu) { + let newWindow = document.getElementById(menu.normal); + let newPrivateWindow = document.getElementById(menu.private); + if (newWindow && newPrivateWindow) { + newPrivateWindow.hidden = true; + newWindow.label = newPrivateWindow.label; + newWindow.accessKey = newPrivateWindow.accessKey; + newWindow.command = newPrivateWindow.command; + } + }); + } + } + + if (gURLBar && + !PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Disable switch to tab autocompletion for private windows + // (not for "Always use private browsing" mode) + gURLBar.setAttribute("autocompletesearchparam", ""); + } + } +}; + + +/** + * Switch to a tab that has a given URI, and focusses its browser window. + * If a matching tab is in this window, it will be switched to. Otherwise, other + * windows will be searched. + * + * @param aURI + * URI to search for + * @param aOpenNew + * True to open a new tab and switch to it, if no existing tab is found. + * If no suitable window is found, a new one will be opened. + * @return True if an existing tab was found, false otherwise + */ +function switchToTabHavingURI(aURI, aOpenNew) { + // This will switch to the tab in aWindow having aURI, if present. + function switchIfURIInWindow(aWindow) { + // Only switch to the tab if neither the source and desination window are + // private and they are not in permanent private borwsing mode + if ((PrivateBrowsingUtils.isWindowPrivate(window) || + PrivateBrowsingUtils.isWindowPrivate(aWindow)) && + !PrivateBrowsingUtils.permanentPrivateBrowsing) { + return false; + } + + let browsers = aWindow.gBrowser.browsers; + for (let i = 0; i < browsers.length; i++) { + let browser = browsers[i]; + if (browser.currentURI.equals(aURI)) { + // Focus the matching window & tab + aWindow.focus(); + aWindow.gBrowser.tabContainer.selectedIndex = i; + return true; + } + } + return false; + } + + // This can be passed either nsIURI or a string. + if (!(aURI instanceof Ci.nsIURI)) + aURI = Services.io.newURI(aURI, null, null); + + let isBrowserWindow = !!window.gBrowser; + + // Prioritise this window. + if (isBrowserWindow && switchIfURIInWindow(window)) + return true; + + let winEnum = Services.wm.getEnumerator("navigator:browser"); + while (winEnum.hasMoreElements()) { + let browserWin = winEnum.getNext(); + // Skip closed (but not yet destroyed) windows, + // and the current window (which was checked earlier). + if (browserWin.closed || browserWin == window) + continue; + if (switchIfURIInWindow(browserWin)) + return true; + } + + // No opened tab has that url. + if (aOpenNew) { + if (isBrowserWindow && isTabEmpty(gBrowser.selectedTab)) + gBrowser.selectedBrowser.loadURI(aURI.spec); + else + openUILinkIn(aURI.spec, "tab"); + } + + return false; +} + +function restoreLastSession() { + let ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + ss.restoreLastSession(); +} + +var TabContextMenu = { + contextTab: null, + _updateToggleMuteMenuItem(aTab, aConditionFn) { + ["muted", "soundplaying"].forEach(attr => { + if (!aConditionFn || aConditionFn(attr)) { + if (aTab.hasAttribute(attr)) { + aTab.toggleMuteMenuItem.setAttribute(attr, "true"); + } else { + aTab.toggleMuteMenuItem.removeAttribute(attr); + } + } + }); + }, + updateContextMenu: function updateContextMenu(aPopupMenu) { + this.contextTab = aPopupMenu.triggerNode.localName == "tab" ? + aPopupMenu.triggerNode : gBrowser.selectedTab; + let disabled = gBrowser.tabs.length == 1; + + // Enable the "Close Tab" menuitem when the window doesn't close with the last tab. + document.getElementById("context_closeTab").disabled = + disabled && gBrowser.tabContainer._closeWindowWithLastTab; + + var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple"); + for (let menuItem of menuItems) + menuItem.disabled = disabled; + + disabled = gBrowser.visibleTabs.length == 1; + menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple-visible"); + for (let menuItem of menuItems) + menuItem.disabled = disabled; + + // Session store + document.getElementById("context_undoCloseTab").disabled = + Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore). + getClosedTabCount(window) == 0; + + // Only one of pin/unpin should be visible + document.getElementById("context_pinTab").hidden = this.contextTab.pinned; + document.getElementById("context_unpinTab").hidden = !this.contextTab.pinned; + + // Disable "Close Tabs to the Right" if there are no tabs + // following it and hide it when the user rightclicked on a pinned + // tab. + document.getElementById("context_closeTabsToTheEnd").disabled = + gBrowser.getTabsToTheEndFrom(this.contextTab).length == 0; + document.getElementById("context_closeTabsToTheEnd").hidden = this.contextTab.pinned; + + // Disable "Close other Tabs" if there is only one unpinned tab and + // hide it when the user rightclicked on a pinned tab. + let unpinnedTabs = gBrowser.visibleTabs.length - gBrowser._numPinnedTabs; + document.getElementById("context_closeOtherTabs").disabled = unpinnedTabs <= 1; + document.getElementById("context_closeOtherTabs").hidden = this.contextTab.pinned; + + // Hide "Bookmark All Tabs" for a pinned tab. Update its state if visible. + let bookmarkAllTabs = document.getElementById("context_bookmarkAllTabs"); + bookmarkAllTabs.hidden = this.contextTab.pinned; + if (!bookmarkAllTabs.hidden) + PlacesCommandHook.updateBookmarkAllTabsCommand(); + + // Adjust the state of the toggle mute menu item. + let toggleMute = document.getElementById("context_toggleMuteTab"); + if (this.contextTab.hasAttribute("muted")) { + toggleMute.label = gNavigatorBundle.getString("unmuteTab.label"); + toggleMute.accessKey = gNavigatorBundle.getString("unmuteTab.accesskey"); + } else { + toggleMute.label = gNavigatorBundle.getString("muteTab.label"); + toggleMute.accessKey = gNavigatorBundle.getString("muteTab.accesskey"); + } + + this.contextTab.toggleMuteMenuItem = toggleMute; + this._updateToggleMuteMenuItem(this.contextTab); + + this.contextTab.addEventListener("TabAttrModified", this, false); + aPopupMenu.addEventListener("popuphiding", this, false); + }, + handleEvent(aEvent) { + switch (aEvent.type) { + case "popuphiding": + gBrowser.removeEventListener("TabAttrModified", this); + aEvent.target.removeEventListener("popuphiding", this); + break; + case "TabAttrModified": + let tab = aEvent.target; + this._updateToggleMuteMenuItem(tab, + attr => aEvent.detail.changed.indexOf(attr) >= 0); + break; + } + } +}; + +#ifdef MOZ_DEVTOOLS +// Note: Do not delete! It is used for: base/content/nsContextMenu.js +XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", + "resource://devtools/client/framework/gDevTools.jsm"); +#endif + +// Prompt user to restart the browser in safe mode or normally +function restart(safeMode) +{ + let promptTitleString = null; + let promptMessageString = null; + let restartTextString = null; + if (safeMode) { + promptTitleString = "safeModeRestartPromptTitle"; + promptMessageString = "safeModeRestartPromptMessage"; + restartTextString = "safeModeRestartButton"; + } else { + promptTitleString = "restartPromptTitle"; + promptMessageString = "restartPromptMessage"; + restartTextString = "restartButton"; + } + + let flags = Ci.nsIAppStartup.eAttemptQuit; + + // Prompt the user to confirm + let promptTitle = gNavigatorBundle.getString(promptTitleString); + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let promptMessage = + gNavigatorBundle.getFormattedString(promptMessageString, [brandShortName]); + let restartText = gNavigatorBundle.getString(restartTextString); + let buttonFlags = (Services.prompt.BUTTON_POS_0 * + Services.prompt.BUTTON_TITLE_IS_STRING) + + (Services.prompt.BUTTON_POS_1 * + Services.prompt.BUTTON_TITLE_CANCEL) + + Services.prompt.BUTTON_POS_0_DEFAULT; + + let rv = Services.prompt.confirmEx(window, promptTitle, promptMessage, + buttonFlags, restartText, null, null, + null, {}); + + if (rv == 0) { + // Notify all windows that an application quit has been requested. + let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + + // Something aborted the quit process. + if (cancelQuit.data) { + return; + } + + if (safeMode) { + Services.startup.restartInSafeMode(flags); + } else { + Services.startup.quit(flags | Ci.nsIAppStartup.eRestart); + } + } +} + +/* 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 + * "window" new window + * + * delta is the offset to the history entry that you want to load. + */ +function duplicateTabIn(aTab, where, delta) { + let newTab = Cc['@mozilla.org/browser/sessionstore;1'] + .getService(Ci.nsISessionStore) + .duplicateTab(window, aTab, delta); + + switch (where) { + case "window": + gBrowser.hideTab(newTab); + gBrowser.replaceTabWithWindow(newTab); + break; + case "tabshifted": + // A background tab has been opened, nothing else to do here. + break; + case "tab": + gBrowser.selectedTab = newTab; + break; + } +} + +function toggleAddonBar() { + let addonBar = document.getElementById("addon-bar"); + setToolbarVisibility(addonBar, addonBar.collapsed); +} + +XPCOMUtils.defineLazyGetter(window, "gShowPageResizers", function () { +#ifdef XP_WIN + // Only show resizers on Windows 2000 and XP + return parseFloat(Services.sysinfo.getProperty("version")) < 6; +#else + return false; +#endif +}); + +var MousePosTracker = { + _listeners: [], + _x: 0, + _y: 0, + get _windowUtils() { + delete this._windowUtils; + return this._windowUtils = window.getInterface(Ci.nsIDOMWindowUtils); + }, + + addListener: function (listener) { + if (this._listeners.indexOf(listener) >= 0) + return; + + listener._hover = false; + this._listeners.push(listener); + + this._callListener(listener); + }, + + removeListener: function (listener) { + var index = this._listeners.indexOf(listener); + if (index < 0) + return; + + this._listeners.splice(index, 1); + }, + + handleEvent: function (event) { + var fullZoom = this._windowUtils.fullZoom; + this._x = event.screenX / fullZoom - window.mozInnerScreenX; + this._y = event.screenY / fullZoom - window.mozInnerScreenY; + + this._listeners.forEach(function (listener) { + try { + this._callListener(listener); + } catch (e) { + Cu.reportError(e); + } + }, this); + }, + + _callListener: function (listener) { + let rect = listener.getMouseTargetRect(); + let hover = this._x >= rect.left && + this._x <= rect.right && + this._y >= rect.top && + this._y <= rect.bottom; + + if (hover == listener._hover) + return; + + listener._hover = hover; + + if (hover) { + if (listener.onMouseEnter) + listener.onMouseEnter(); + } else { + if (listener.onMouseLeave) + listener.onMouseLeave(); + } + } +}; + +var BrowserChromeTest = { + _cb: null, + _ready: false, + markAsReady: function () { + this._ready = true; + if (this._cb) { + this._cb(); + this._cb = null; + } + }, + runWhenReady: function (cb) { + if (this._ready) + cb(); + else + this._cb = cb; + } +}; + +var ToolbarIconColor = { + init: function () { + this._initialized = true; + + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); + Services.obs.addObserver(this, "lightweight-theme-styling-update", false); + gPrefService.addObserver("ui.colorChanged", this, false); + + // If the window isn't active now, we assume that it has never been active + // before and will soon become active such that inferFromText will be + // called from the initial activate event. + if (Services.focus.activeWindow == window) + this.inferFromText(); + }, + + uninit: function () { + this._initialized = false; + + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); + Services.obs.removeObserver(this, "lightweight-theme-styling-update"); + gPrefService.removeObserver("ui.colorChanged", this); + }, + + handleEvent: function (event) { + switch (event.type) { + case "activate": + case "deactivate": + this.inferFromText(); + break; + } + }, + + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "lightweight-theme-styling-update": + // inferFromText needs to run after LightweightThemeConsumer.jsm's + // lightweight-theme-styling-update observer. + setTimeout(() => { this.inferFromText(); }, 0); + break; + case "nsPref:changed": + // system color change + var colorChangedPref = false; + try { + colorChangedPref = gPrefService.getBoolPref("ui.colorChanged"); + } catch(e) { } + // if pref indicates change, call inferFromText() on a small delay + if (colorChangedPref == true) + setTimeout(() => { this.inferFromText(); }, 300); + break; + default: + console.error("ToolbarIconColor: Uncaught topic " + aTopic); + } + }, + + inferFromText: function () { + if (!this._initialized) + return; + + function parseRGB(aColorString) { + let rgb = aColorString.match(/^rgba?\((\d+), (\d+), (\d+)/); + rgb.shift(); + return rgb.map(x => parseInt(x)); + } + + let toolbarSelector = "toolbar:not([collapsed=true])"; +#ifdef XP_MACOSX + toolbarSelector += ":not([type=menubar])"; +#endif + + // The getComputedStyle calls and setting the brighttext are separated in + // two loops to avoid flushing layout and making it dirty repeatedly. + + let luminances = new Map; + for (let toolbar of document.querySelectorAll(toolbarSelector)) { + let [r, g, b] = parseRGB(getComputedStyle(toolbar).color); + let luminance = (2 * r + 5 * g + b) / 8; + luminances.set(toolbar, luminance); + } + + for (let [toolbar, luminance] of luminances) { + if (luminance <= 128) + toolbar.removeAttribute("brighttext"); + else + toolbar.setAttribute("brighttext", "true"); + } + + // Clear pref if set, since we're done applying the color changes. + gPrefService.clearUserPref("ui.colorChanged"); + } +} |