diff options
author | wolfbeast <mcwerewolf@gmail.com> | 2018-07-18 08:24:24 +0200 |
---|---|---|
committer | wolfbeast <mcwerewolf@gmail.com> | 2018-07-18 08:24:24 +0200 |
commit | fc61780b35af913801d72086456f493f63197da6 (patch) | |
tree | f85891288a7bd988da9f0f15ae64e5c63f00d493 /application/basilisk/base/content/browser.js | |
parent | 69f7f9e5f1475891ce11cc4f431692f965b0cd30 (diff) | |
parent | 50d3e596bbe89c95615f96eb71f6bc5be737a1db (diff) | |
download | UXP-fc61780b35af913801d72086456f493f63197da6.tar UXP-fc61780b35af913801d72086456f493f63197da6.tar.gz UXP-fc61780b35af913801d72086456f493f63197da6.tar.lz UXP-fc61780b35af913801d72086456f493f63197da6.tar.xz UXP-fc61780b35af913801d72086456f493f63197da6.zip |
Merge commit '50d3e596bbe89c95615f96eb71f6bc5be737a1db' into Basilisk-releasev2018.07.18
# Conflicts:
# browser/app/profile/firefox.js
# browser/components/preferences/jar.mn
Diffstat (limited to 'application/basilisk/base/content/browser.js')
-rw-r--r-- | application/basilisk/base/content/browser.js | 8139 |
1 files changed, 8139 insertions, 0 deletions
diff --git a/application/basilisk/base/content/browser.js b/application/basilisk/base/content/browser.js new file mode 100644 index 000000000..9ec7715fa --- /dev/null +++ b/application/basilisk/base/content/browser.js @@ -0,0 +1,8139 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cc = Components.classes; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/ContextualIdentityService.jsm"); +Cu.import("resource://gre/modules/NotificationDB.jsm"); + +// lazy module getters +[ + ["AboutHome", "resource:///modules/AboutHome.jsm"], + ["AddonWatcher", "resource://gre/modules/AddonWatcher.jsm"], + ["AppConstants", "resource://gre/modules/AppConstants.jsm"], + ["BrowserUsageTelemetry", "resource:///modules/BrowserUsageTelemetry.jsm"], + ["BrowserUtils", "resource://gre/modules/BrowserUtils.jsm"], + ["CastingApps", "resource:///modules/CastingApps.jsm"], + ["CharsetMenu", "resource://gre/modules/CharsetMenu.jsm"], + ["Color", "resource://gre/modules/Color.jsm"], + ["ContentSearch", "resource:///modules/ContentSearch.jsm"], + ["Deprecated", "resource://gre/modules/Deprecated.jsm"], + ["E10SUtils", "resource:///modules/E10SUtils.jsm"], + ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"], + ["GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm"], + ["LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"], + ["Log", "resource://gre/modules/Log.jsm"], + ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"], + ["NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"], + ["PageThumbs", "resource://gre/modules/PageThumbs.jsm"], + ["PluralForm", "resource://gre/modules/PluralForm.jsm"], + ["Preferences", "resource://gre/modules/Preferences.jsm"], + ["PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"], + ["ProcessHangMonitor", "resource:///modules/ProcessHangMonitor.jsm"], + ["PromiseUtils", "resource://gre/modules/PromiseUtils.jsm"], + ["ReaderMode", "resource://gre/modules/ReaderMode.jsm"], + ["ReaderParent", "resource:///modules/ReaderParent.jsm"], + ["RecentWindow", "resource:///modules/RecentWindow.jsm"], + ["SessionStore", "resource:///modules/sessionstore/SessionStore.jsm"], + ["ShortcutUtils", "resource://gre/modules/ShortcutUtils.jsm"], + ["SimpleServiceDiscovery", "resource://gre/modules/SimpleServiceDiscovery.jsm"], + ["SitePermissions", "resource:///modules/SitePermissions.jsm"], + ["TabCrashHandler", "resource:///modules/ContentCrashHandlers.jsm"], + ["Task", "resource://gre/modules/Task.jsm"], + ["TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm"], + ["Translation", "resource:///modules/translation/Translation.jsm"], + ["UpdateUtils", "resource://gre/modules/UpdateUtils.jsm"], + ["Weave", "resource://services-sync/main.js"], + ["fxAccounts", "resource://gre/modules/FxAccounts.jsm"], +#ifdef MOZ_DEVTOOLS + // Note: Do not delete! It is used for: base/content/nsContextMenu.js + ["gDevTools", "resource://devtools/client/framework/gDevTools.jsm"], +#endif + ["webrtcUI", "resource:///modules/webrtcUI.jsm", ] +].forEach(([name, resource]) => XPCOMUtils.defineLazyModuleGetter(this, name, resource)); + +#ifdef MOZ_SAFE_BROWSING + XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", + "resource://gre/modules/SafeBrowsing.jsm"); +#endif + +// lazy service getters +[ + ["Favicons", "@mozilla.org/browser/favicon-service;1", "mozIAsyncFavicons"], + ["WindowsUIUtils", "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"], + ["gAboutNewTabService", "@mozilla.org/browser/aboutnewtab-service;1", "nsIAboutNewTabService"], + ["gDNSService", "@mozilla.org/network/dns-service;1", "nsIDNSService"], +].forEach(([name, cc, ci]) => XPCOMUtils.defineLazyServiceGetter(this, name, cc, ci)); + +XPCOMUtils.defineLazyServiceGetter(this, "gSerializationHelper", + "@mozilla.org/network/serialization-helper;1", + "nsISerializationHelper"); + +XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function() { + let tmp = {}; + Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", tmp); + return tmp.BrowserToolboxProcess; +}); + +XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() { + return Services.strings.createBundle('chrome://browser/locale/browser.properties'); +}); + +XPCOMUtils.defineLazyGetter(this, "gCustomizeMode", function() { + let scope = {}; + Cu.import("resource:///modules/CustomizeMode.jsm", scope); + return new scope.CustomizeMode(window); +}); + +XPCOMUtils.defineLazyGetter(this, "gPrefService", function() { + return Services.prefs; +}); + +XPCOMUtils.defineLazyGetter(this, "InlineSpellCheckerUI", function() { + let tmp = {}; + Cu.import("resource://gre/modules/InlineSpellChecker.jsm", tmp); + return new tmp.InlineSpellChecker(); +}); + +XPCOMUtils.defineLazyGetter(this, "PageMenuParent", function() { + let tmp = {}; + Cu.import("resource://gre/modules/PageMenu.jsm", tmp); + return new tmp.PageMenuParent(); +}); + +XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () { + let tmp = {}; + Cu.import("resource://gre/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.defineLazyGetter(this, "Win7Features", function () { + if (AppConstants.platform != "win") + return null; + + 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); + } + }; + } + return null; +}); + +const nsIWebNavigation = Ci.nsIWebNavigation; + +var gLastBrowserCharset = null; +var gLastValidURLStr = ""; +var gInPrintPreviewMode = false; +var gContextMenu = null; // nsContextMenu instance +var gMultiProcessBrowser = + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext) + .useRemoteTabs; +var gAppInfo = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULAppInfo) + .QueryInterface(Ci.nsIXULRuntime); + +if (AppConstants.platform != "macosx") { + var gEditUIVisible = true; +} + +/* globals gBrowser, gNavToolbox, gURLBar, gNavigatorBundle*/ +[ + ["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. + +this.__defineGetter__("gFindBar", function() { + return window.gBrowser.getFindBar(); +}); + +this.__defineGetter__("gFindBarInitialized", function() { + return window.gBrowser.isFindBarInitialized(); +}); + +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; +}); + + +var gInitialPages = [ + "about:blank", + "about:newtab", + "about:home", + "about:privatebrowsing", + "about:welcomeback", + "about:sessionrestore", + "about:logopage" +]; + +function* browserWindows() { + let windows = Services.wm.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) + yield windows.getNext(); +} + +/** +* We can avoid adding multiple load event listeners and save some time by adding +* one listener that calls all real handlers. +*/ +function pageShowEventHandlers(persisted) { + XULBrowserWindow.asyncUpdateUI(); +} + +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() { + // Bug 414797: Clone the back/forward buttons' context menu into both buttons. + let popup = document.getElementById("backForwardMenu").cloneNode(true); + popup.removeAttribute("id"); + // Prevent the back/forward buttons' context attributes from being inherited. + popup.setAttribute("context", ""); + + let backButton = document.getElementById("back-button"); + backButton.setAttribute("type", "menu"); + backButton.appendChild(popup); + gClickAndHoldListenersOnElement.add(backButton); + + let forwardButton = document.getElementById("forward-button"); + popup = popup.cloneNode(true); + forwardButton.setAttribute("type", "menu"); + forwardButton.appendChild(popup); + gClickAndHoldListenersOnElement.add(forwardButton); +} + + +const gClickAndHoldListenersOnElement = { + _timers: new Map(), + + _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", this, false); + aEvent.currentTarget.addEventListener("mouseup", this, false); + this._timers.set(aEvent.currentTarget, setTimeout((b) => this._openMenu(b), 500, aEvent.currentTarget)); + }, + + _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); + + // This is here to cancel the XUL default event + // dom.click() triggers a command even if there is a click handler + // however this can now be prevented with preventDefault(). + aEvent.preventDefault(); + } + }, + + _openMenu(aButton) { + this._cancelHold(aButton); + aButton.firstChild.hidden = false; + aButton.open = true; + }, + + _mouseoutHandler(aEvent) { + let buttonRect = aEvent.currentTarget.getBoundingClientRect(); + if (aEvent.clientX >= buttonRect.left && + aEvent.clientX <= buttonRect.right && + aEvent.clientY >= buttonRect.bottom) + this._openMenu(aEvent.currentTarget); + else + this._cancelHold(aEvent.currentTarget); + }, + + _mouseupHandler(aEvent) { + this._cancelHold(aEvent.currentTarget); + }, + + _cancelHold(aButton) { + clearTimeout(this._timers.get(aButton)); + aButton.removeEventListener("mouseout", this, false); + aButton.removeEventListener("mouseup", this, false); + }, + + handleEvent(e) { + switch (e.type) { + case "mouseout": + this._mouseoutHandler(e); + break; + case "mousedown": + this._mousedownHandler(e); + break; + case "click": + this._clickHandler(e); + break; + case "mouseup": + this._mouseupHandler(e); + break; + } + }, + + remove(aButton) { + aButton.removeEventListener("mousedown", this, true); + aButton.removeEventListener("click", this, true); + }, + + add(aElm) { + this._timers.delete(aElm); + + aElm.addEventListener("mousedown", this, true); + aElm.addEventListener("click", this, true); + } +}; + +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"); + + // Clear undo history of the URL bar + gURLBar.editor.transactionManager.clear() + } +}; + +/** + * 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, + + onReportButtonMousedown: function (aEvent) + { + // If this method is called on the same event tick as the popup gets + // hidden, do nothing to avoid re-opening the popup. + if (aEvent.button != 0 || aEvent.target != this._reportButton || this.isPopupHidingTick) + 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) + 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) + this._reportButton.hidden = true; + + // Hide the notification box (if it's visible). + let notificationBox = gBrowser.getNotificationBox(); + let notification = notificationBox.getNotificationWithValue("popup-blocked"); + if (notification) { + notificationBox.removeNotification(notification, false); + } + return; + } + + 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 stringKey = "popupWarningButton"; + + var popupButtonText = gNavigatorBundle.getString(stringKey); + var popupButtonAccesskey = gNavigatorBundle.getString(stringKey + ".accesskey"); + + var messageBase = gNavigatorBundle.getString("popupWarning.message"); + var message = PluralForm.get(popupCount, messageBase) + .replace("#1", brandShortName) + .replace("#2", popupCount); + + let notificationBox = gBrowser.getNotificationBox(); + let 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); + + if (!shouldBlock) + this.showAllBlockedPopups(gBrowser.selectedBrowser); + + 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"); + + this.isPopupHidingTick = true; + setTimeout(() => this.isPopupHidingTick = false, 0); + + 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); + }, + + showAllBlockedPopups: function (aBrowser) + { + aBrowser.retrieveListOfBlockedPopups().then(popups => { + for (let i = 0; i < popups.length; i++) { + if (popups[i].popupWindowURIspec) + aBrowser.unblockPopup(i); + } + }, null); + }, + + 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(); + } +}; + +function gKeywordURIFixup({ target: browser, data: fixupInfo }) { + let deserializeURI = (spec) => spec ? makeURI(spec) : null; + + // We get called irrespective of whether we did a keyword search, or + // whether the original input would be vaguely interpretable as a URL, + // so figure that out first. + let alternativeURI = deserializeURI(fixupInfo.fixedURI); + if (!fixupInfo.keywordProviderName || !alternativeURI || !alternativeURI.host) { + return; + } + + // At this point we're still only just about to load this URI. + // When the async DNS lookup comes back, we may be in any of these states: + // 1) still on the previous URI, waiting for the preferredURI (keyword + // search) to respond; + // 2) at the keyword search URI (preferredURI) + // 3) at some other page because the user stopped navigation. + // We keep track of the currentURI to detect case (1) in the DNS lookup + // callback. + let previousURI = browser.currentURI; + let preferredURI = deserializeURI(fixupInfo.preferredURI); + + // now swap for a weak ref so we don't hang on to browser needlessly + // even if the DNS query takes forever + let weakBrowser = Cu.getWeakReference(browser); + browser = null; + + // Additionally, we need the host of the parsed url + let hostName = alternativeURI.host; + // and the ascii-only host for the pref: + let asciiHost = alternativeURI.asciiHost; + // Normalize out a single trailing dot - NB: not using endsWith/lastIndexOf + // because we need to be sure this last dot is the *only* dot, too. + // More generally, this is used for the pref and should stay in sync with + // the code in nsDefaultURIFixup::KeywordURIFixup . + if (asciiHost.indexOf('.') == asciiHost.length - 1) { + asciiHost = asciiHost.slice(0, -1); + } + + let isIPv4Address = host => { + let parts = host.split("."); + if (parts.length != 4) { + return false; + } + return parts.every(part => { + let n = parseInt(part, 10); + return n >= 0 && n <= 255; + }); + }; + // Avoid showing fixup information if we're suggesting an IP. Note that + // decimal representations of IPs are normalized to a 'regular' + // dot-separated IP address by network code, but that only happens for + // numbers that don't overflow. Longer numbers do not get normalized, + // but still work to access IP addresses. So for instance, + // 1097347366913 (ff7f000001) gets resolved by using the final bytes, + // making it the same as 7f000001, which is 127.0.0.1 aka localhost. + // While 2130706433 would get normalized by network, 1097347366913 + // does not, and we have to deal with both cases here: + if (isIPv4Address(asciiHost) || /^(?:\d+|0x[a-f0-9]+)$/i.test(asciiHost)) + return; + + let onLookupComplete = (request, record, status) => { + let browser = weakBrowser.get(); + if (!Components.isSuccessCode(status) || !browser) + return; + + let currentURI = browser.currentURI; + // If we're in case (3) (see above), don't show an info bar. + if (!currentURI.equals(previousURI) && + !currentURI.equals(preferredURI)) { + return; + } + + // show infobar offering to visit the host + let notificationBox = gBrowser.getNotificationBox(browser); + if (notificationBox.getNotificationWithValue("keyword-uri-fixup")) + return; + + let message = gNavigatorBundle.getFormattedString( + "keywordURIFixup.message", [hostName]); + let yesMessage = gNavigatorBundle.getFormattedString( + "keywordURIFixup.goTo", [hostName]) + + let buttons = [ + { + label: yesMessage, + accessKey: gNavigatorBundle.getString("keywordURIFixup.goTo.accesskey"), + callback: function() { + // Do not set this preference while in private browsing. + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + let pref = "browser.fixup.domainwhitelist." + asciiHost; + Services.prefs.setBoolPref(pref, true); + } + openUILinkIn(alternativeURI.spec, "current"); + } + }, + { + label: gNavigatorBundle.getString("keywordURIFixup.dismiss"), + accessKey: gNavigatorBundle.getString("keywordURIFixup.dismiss.accesskey"), + callback: function() { + let notification = notificationBox.getNotificationWithValue("keyword-uri-fixup"); + notificationBox.removeNotification(notification, true); + } + } + ]; + let notification = + notificationBox.appendNotification(message, "keyword-uri-fixup", null, + notificationBox.PRIORITY_INFO_HIGH, + buttons); + notification.persistence = 1; + }; + + try { + gDNSService.asyncResolve(hostName, 0, onLookupComplete, Services.tm.mainThread); + } catch (ex) { + // Do nothing if the URL is invalid (we don't want to show a notification in that case). + if (ex.result != Cr.NS_ERROR_UNKNOWN_HOST) { + // ... otherwise, report: + Cu.reportError(ex); + } + } +} + +// A shared function used by both remote and non-remote browser XBL bindings to +// load a URI or redirect it to the correct process. +function _loadURIWithFlags(browser, uri, params) { + if (!uri) { + uri = "about:blank"; + } + let triggeringPrincipal = params.triggeringPrincipal || null; + let flags = params.flags || 0; + let referrer = params.referrerURI; + let referrerPolicy = ('referrerPolicy' in params ? params.referrerPolicy : + Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT); + let postData = params.postData; + + let wasRemote = browser.isRemoteBrowser; + + let process = browser.isRemoteBrowser ? Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT + : Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + let mustChangeProcess = gMultiProcessBrowser && + !E10SUtils.canLoadURIInProcess(uri, process); + if ((!wasRemote && !mustChangeProcess) || + (wasRemote && mustChangeProcess)) { + browser.inLoadURI = true; + } + try { + if (!mustChangeProcess) { + if (params.userContextId) { + browser.webNavigation.setOriginAttributesBeforeLoading({ userContextId: params.userContextId }); + } + + browser.webNavigation.loadURIWithOptions(uri, flags, + referrer, referrerPolicy, + postData, null, null, triggeringPrincipal); + } else { + // Check if the current browser is allowed to unload. + let {permitUnload, timedOut} = browser.permitUnload(); + if (!timedOut && !permitUnload) { + return; + } + + if (postData) { + postData = NetUtil.readInputStreamToString(postData, postData.available()); + } + + let loadParams = { + uri: uri, + triggeringPrincipal: triggeringPrincipal + ? gSerializationHelper.serializeToString(triggeringPrincipal) + : null, + flags: flags, + referrer: referrer ? referrer.spec : null, + referrerPolicy: referrerPolicy, + postData: postData + } + + if (params.userContextId) { + loadParams.userContextId = params.userContextId; + } + + LoadInOtherProcess(browser, loadParams); + } + } catch (e) { + // If anything goes wrong when switching remoteness, just switch remoteness + // manually and load the URI. + // We might lose history that way but at least the browser loaded a page. + // This might be necessary if SessionStore wasn't initialized yet i.e. + // when the homepage is a non-remote page. + if (mustChangeProcess) { + Cu.reportError(e); + gBrowser.updateBrowserRemotenessByURL(browser, uri); + + if (params.userContextId) { + browser.webNavigation.setOriginAttributesBeforeLoading({ userContextId: params.userContextId }); + } + + browser.webNavigation.loadURIWithOptions(uri, flags, referrer, referrerPolicy, + postData, null, null, triggeringPrincipal); + } else { + throw e; + } + } finally { + if ((!wasRemote && !mustChangeProcess) || + (wasRemote && mustChangeProcess)) { + browser.inLoadURI = false; + } + } +} + +// Starts a new load in the browser first switching the browser to the correct +// process +function LoadInOtherProcess(browser, loadOptions, historyIndex = -1) { + let tab = gBrowser.getTabForBrowser(browser); + SessionStore.navigateAndRestore(tab, loadOptions, historyIndex); +} + +// Called when a docshell has attempted to load a page in an incorrect process. +// This function is responsible for loading the page in the correct process. +function RedirectLoad({ target: browser, data }) { + // We should only start the redirection if the browser window has finished + // starting up. Otherwise, we should wait until the startup is done. + if (gBrowserInit.delayedStartupFinished) { + LoadInOtherProcess(browser, data.loadOptions, data.historyIndex); + } else { + let delayedStartupFinished = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && + subject == window) { + Services.obs.removeObserver(delayedStartupFinished, topic); + LoadInOtherProcess(browser, data.loadOptions, data.historyIndex); + } + }; + Services.obs.addObserver(delayedStartupFinished, + "browser-delayed-startup-finished", + false); + } +} + +addEventListener("DOMContentLoaded", function onDCL() { + removeEventListener("DOMContentLoaded", onDCL); + + // There are some windows, like macBrowserOverlay.xul, that + // load browser.js, but never load tabbrowser.xml. We can ignore + // those cases. + if (!gBrowser || !gBrowser.updateBrowserRemoteness) { + return; + } + + 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(); + + let initBrowser = + document.getAnonymousElementByAttribute(gBrowser, "anonid", "initialBrowser"); + + // The window's first argument is a tab if and only if we are swapping tabs. + // We must set the browser's usercontextid before updateBrowserRemoteness(), + // so that the newly created remote tab child has the correct usercontextid. + if (window.arguments) { + let tabToOpen = window.arguments[0]; + if (tabToOpen instanceof XULElement && tabToOpen.hasAttribute("usercontextid")) { + initBrowser.setAttribute("usercontextid", tabToOpen.getAttribute("usercontextid")); + } + } + + gBrowser.updateBrowserRemoteness(initBrowser, gMultiProcessBrowser); +}); + +var gBrowserInit = { + delayedStartupFinished: false, + + onLoad: function() { + gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false); + + Services.obs.addObserver(gPluginHandler.NPAPIPluginCrashed, "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. + DOMLinkHandler.init(); + gPageStyleMenu.init(); + LanguageDetectionListener.init(); + BrowserOnClick.init(); + FeedHandler.init(); + DevEdition.init(); + AboutPrivateBrowsingListener.init(); + TrackingProtection.init(); + RefreshBlocker.init(); + CaptivePortalWatcher.init(); + + let mm = window.getGroupMessageManager("browsers"); + mm.loadFrameScript("chrome://browser/content/tab-content.js", true); + mm.loadFrameScript("chrome://browser/content/content.js", true); + mm.loadFrameScript("chrome://global/content/manifestMessages.js", true); + + // initialize observers and listeners + // and give C++ access to gBrowser + XULBrowserWindow.init(); + + window.messageManager.addMessageListener("Browser:LoadURI", RedirectLoad); + + if (!gMultiProcessBrowser) { + // There is a Content:Click message manually sent from content. + Cc["@mozilla.org/eventlistenerservice;1"] + .getService(Ci.nsIEventListenerService) + .addSystemEventListener(gBrowser, "click", contentAreaClick, true); + } + + // hook up UI through progress listener + gBrowser.addProgressListener(window.XULBrowserWindow); + gBrowser.addTabsProgressListener(window.TabsProgressListener); + + // setup simple gestures support + gGestureSupport.init(true); + + // setup history swipe animation + gHistorySwipeAnimation.init(); + + SidebarUI.init(); + + // Certain kinds of automigration rely on this notification to complete + // their tasks BEFORE the browser window is shown. SessionStore uses it to + // restore tabs into windows AFTER important parts like gMultiProcessBrowser + // have been initialized. + Services.obs.notifyObservers(window, "browser-window-before-show", ""); + + // Set a sane starting width/height for all resolutions on new profiles. + if (!document.documentElement.hasAttribute("width")) { + const TARGET_WIDTH = 1280; + const TARGET_HEIGHT = 1040; + let width = Math.min(screen.availWidth * .9, TARGET_WIDTH); + let height = Math.min(screen.availHeight * .9, TARGET_HEIGHT); + + document.documentElement.setAttribute("width", width); + document.documentElement.setAttribute("height", height); + + if (width < TARGET_WIDTH && height < TARGET_HEIGHT) { + document.documentElement.setAttribute("sizemode", "maximized"); + } + } + + if (!window.toolbar.visible) { + // adjust browser UI for popups + gURLBar.setAttribute("readonly", "true"); + gURLBar.setAttribute("enablehistory", "false"); + } + + // Misc. inits. + TabletModeUpdater.init(); + CombinedStopReload.init(); + gPrivateBrowsingUI.init(); + + if (window.matchMedia("(-moz-os-version: windows-win8)").matches && + window.matchMedia("(-moz-windows-default-theme)").matches) { + let windowFrameColor = new Color(...Cu.import("resource:///modules/Windows8WindowFrameColor.jsm", {}) + .Windows8WindowFrameColor.get()); + // Check if window frame color is dark. + if ((windowFrameColor.r * 2 + + windowFrameColor.g * 5 + + windowFrameColor.b) <= 128 * 8) { + document.documentElement.setAttribute("darkwindowframe", "true"); + } + } + + ToolbarIconColor.init(); + + // Wait until chrome is painted before executing code not critical to making the window visible + this._boundDelayedStartup = this._delayedStartup.bind(this); + window.addEventListener("MozAfterPaint", this._boundDelayedStartup); + + this._loadHandled = true; + }, + + _cancelDelayedStartup: function () { + window.removeEventListener("MozAfterPaint", this._boundDelayedStartup); + this._boundDelayedStartup = null; + }, + + _delayedStartup: function() { + let tmp = {}; + Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp); + let TelemetryTimestamps = tmp.TelemetryTimestamps; + TelemetryTimestamps.add("delayedStartupStarted"); + + this._cancelDelayedStartup(); + + // We need to set the OfflineApps message listeners up before we + // load homepages, which might need them. + OfflineApps.init(); + + // This pageshow listener needs to be registered before we may call + // swapBrowsersAndCloseOther() to receive pageshow events fired by that. + let mm = window.messageManager; + mm.addMessageListener("PageVisibility:Show", function(message) { + if (message.target == gBrowser.selectedBrowser) { + setTimeout(pageShowEventHandlers, 0, message.data.persisted); + } + }); + + gBrowser.addEventListener("AboutTabCrashedLoad", function(event) { + let ownerDoc = event.originalTarget; + + if (!ownerDoc.documentURI.startsWith("about:tabcrashed")) { + return; + } + + let browser = gBrowser.getBrowserForDocument(event.target); + // Reset the zoom for the tabcrashed page. + ZoomManager.setZoomForBrowser(browser, 1); + }, false, true); + + gBrowser.addEventListener("InsecureLoginFormsStateChange", function() { + gIdentityHandler.refreshForInsecureLoginForms(); + }); + + let uriToLoad = this._getUriToLoad(); + if (uriToLoad && uriToLoad != "about:blank") { + if (uriToLoad instanceof Ci.nsIArray) { + let count = uriToLoad.length; + let specs = []; + for (let i = 0; i < count; i++) { + let urisstring = uriToLoad.queryElementAt(i, 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. + let tabToOpen = uriToLoad; + + // If this tab was passed as a window argument, clear the + // reference to it from the arguments array. + if (window.arguments[0] == tabToOpen) { + window.arguments[0] = null; + } + + // Stop the about:blank load + gBrowser.stop(); + // make sure it has a docshell + gBrowser.docShell; + + // We must set usercontextid before updateBrowserRemoteness() + // so that the newly created remote tab child has correct usercontextid + if (tabToOpen.hasAttribute("usercontextid")) { + let usercontextid = tabToOpen.getAttribute("usercontextid"); + gBrowser.selectedBrowser.setAttribute("usercontextid", usercontextid); + } + + // If the browser that we're swapping in was remote, then we'd better + // be able to support remote browsers, and then make our selectedTab + // remote. + try { + if (tabToOpen.linkedBrowser.isRemoteBrowser) { + if (!gMultiProcessBrowser) { + throw new Error("Cannot drag a remote browser into a window " + + "without the remote tabs load context."); + } + gBrowser.updateBrowserRemoteness(gBrowser.selectedBrowser, true); + } else if (gBrowser.selectedBrowser.isRemoteBrowser) { + // If the browser is remote, then it's implied that + // gMultiProcessBrowser is true. We need to flip the remoteness + // of this tab to false in order for the tab drag to work. + gBrowser.updateBrowserRemoteness(gBrowser.selectedBrowser, false); + } + gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, tabToOpen); + } catch (e) { + Cu.reportError(e); + } + } + // window.arguments[2]: referrer (nsIURI | string) + // [3]: postData (nsIInputStream) + // [4]: allowThirdPartyFixup (bool) + // [5]: referrerPolicy (int) + // [6]: userContextId (int) + // [7]: originPrincipal (nsIPrincipal) + // [8]: 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); + let userContextId = (window.arguments[6] != undefined ? + window.arguments[6] : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID); + loadURI(uriToLoad, referrerURI, window.arguments[3] || null, + window.arguments[4] || false, referrerPolicy, userContextId, + // pass the origin principal (if any) and force its use to create + // an initial about:blank viewer if present: + window.arguments[7], !!window.arguments[7], window.arguments[8]); + 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); + } + } + +#ifdef MOZ_SAFE_BROWSING + // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008. + setTimeout(function() { SafeBrowsing.init(); }, 2000); +#endif + + Services.obs.addObserver(gIdentityHandler, "perm-changed", false); + 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-confirmation", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false); + window.messageManager.addMessageListener("Browser:URIFixup", gKeywordURIFixup); + + BrowserOffline.init(); + IndexedDBPromptHelper.init(); + + // 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(); + PanelUI.init(); + LightweightThemeListener.init(); + + Services.telemetry.getHistogramById("E10S_WINDOW").add(gMultiProcessBrowser); + + SidebarUI.startDelayedLoad(); + + UpdateUrlbarSearchSplitterState(); + + if (!(isBlankPageURL(uriToLoad) || uriToLoad == "about:privatebrowsing") || + !focusAndSelectUrlBar()) { + if (gBrowser.selectedBrowser.isRemoteBrowser) { + // If the initial browser is remote, in order to optimize for first paint, + // we'll defer switching focus to that browser until it has painted. + let focusedElement = document.commandDispatcher.focusedElement; + let mm = window.messageManager; + mm.addMessageListener("Browser:FirstPaint", function onFirstPaint() { + mm.removeMessageListener("Browser:FirstPaint", onFirstPaint); + // If focus didn't move while we were waiting for first paint, we're okay + // to move to the browser. + if (document.commandDispatcher.focusedElement == focusedElement) { + gBrowser.selectedBrowser.focus(); + } + }); + } else { + // If the initial browser is not remote, we can focus the browser + // immediately with no paint performance impact. + gBrowser.selectedBrowser.focus(); + } + } + + // Enable/Disable auto-hide tabbar + gBrowser.tabContainer.updateVisibility(); + + BookmarkingUI.init(); + AutoShowBookmarksToolbar.init(); + + gPrefService.addObserver(gHomeButton.prefDomain, gHomeButton, false); + + var homeButton = document.getElementById("home-button"); + gHomeButton.updateTooltip(homeButton); + + let safeMode = document.getElementById("helpSafeMode"); + if (Services.appinfo.inSafeMode) { + safeMode.label = safeMode.getAttribute("stoplabel"); + safeMode.accesskey = safeMode.getAttribute("stopaccesskey"); + } + + // 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 (!getBoolPref("ui.click_hold_context_menus", false)) + SetClickAndHoldHandlers(); + + let NP = {}; + Cu.import("resource:///modules/NetworkPrioritizer.jsm", NP); + NP.trackBrowserWindow(window); + + PlacesToolbarHelper.init(); + + ctrlTab.readPref(); + gPrefService.addObserver(ctrlTab.prefName, ctrlTab, 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 initializing 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(); + + if (AppConstants.platform != "macosx") { + updateEditUIVisibility(); + let placesContext = document.getElementById("placesContext"); + placesContext.addEventListener("popupshowing", updateEditUIVisibility, false); + placesContext.addEventListener("popuphiding", updateEditUIVisibility, false); + } + + LightWeightThemeWebInstaller.init(); + + if (Win7Features) + Win7Features.onOpenWindow(); + + PointerlockFsWarning.init(); + FullScreen.init(); + PointerLock.init(); + + // initialize the sync UI + gSyncUI.init(); + gFxAccounts.init(); + + if (AppConstants.MOZ_DATA_REPORTING) + gDataNotificationInfoBar.init(); + + gBrowserThumbnails.init(); + + gMenuButtonBadgeManager.init(); + + gMenuButtonUpdateBadge.init(); + + window.addEventListener("mousemove", MousePosTracker, false); + window.addEventListener("dragover", MousePosTracker, false); + + gNavToolbox.addEventListener("customizationstarting", CustomizationHandler); + gNavToolbox.addEventListener("customizationchange", CustomizationHandler); + gNavToolbox.addEventListener("customizationending", CustomizationHandler); + + // 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); + } + + // Delay this a minute because there's no rush + setTimeout(() => { + this.gmpInstallManager = new GMPInstallManager(); + // We don't really care about the results, if someone is interested they + // can check the log. + this.gmpInstallManager.simpleCheckAndInstall().then(null, () => {}); + }, 1000 * 60); + + // Report via telemetry whether we're able to play MP4/H.264/AAC video. + // We suspect that some Windows users have a broken or have not installed + // Windows Media Foundation, and we'd like to know how many. We'd also like + // to know how good our coverage is on other platforms. + // Note: we delay by 90 seconds reporting this, as calling canPlayType() + // on Windows will cause DLLs to load, i.e. cause disk I/O. + setTimeout(() => { + let v = document.createElementNS("http://www.w3.org/1999/xhtml", "video"); + let aacWorks = v.canPlayType("audio/mp4") != ""; + Services.telemetry.getHistogramById("VIDEO_CAN_CREATE_AAC_DECODER").add(aacWorks); + let h264Works = v.canPlayType("video/mp4") != ""; + Services.telemetry.getHistogramById("VIDEO_CAN_CREATE_H264_DECODER").add(h264Works); + }, 90 * 1000); + + SessionStore.promiseInitialized.then(() => { + // Bail out if the window has been closed in the meantime. + if (window.closed) { + return; + } + + // Enable the Restore Last Session command if needed + RestoreLastSessionObserver.init(); + + // Start monitoring slow add-ons + AddonWatcher.init(); + + // Telemetry for master-password - we do this after 5 seconds as it + // can cause IO if NSS/PSM has not already initialized. + setTimeout(() => { + if (window.closed) { + return; + } + let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"] + .getService(Ci.nsIPKCS11ModuleDB); + let slot = secmodDB.findSlotByName(""); + let mpEnabled = slot && + slot.status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED && + slot.status != Ci.nsIPKCS11Slot.SLOT_READY; + if (mpEnabled) { + Services.telemetry.getHistogramById("MASTER_PASSWORD_ENABLED").add(mpEnabled); + } + }, 5000); + + PanicButtonNotifier.init(); + }); + + gBrowser.tabContainer.addEventListener("TabSelect", function() { + for (let panel of document.querySelectorAll("panel[tabspecific='true']")) { + if (panel.state == "open") { + panel.hidePopup(); + } + } + }); + + this.delayedStartupFinished = true; + + Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); + TelemetryTimestamps.add("delayedStartupFinished"); + }, + + // Returns the URI(s) to load at startup. + _getUriToLoad: function () { + // window.arguments[0]: URI to load (string), or an nsIArray 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). + + CombinedStopReload.uninit(); + + gGestureSupport.init(false); + + gHistorySwipeAnimation.uninit(); + + FullScreen.uninit(); + + gFxAccounts.uninit(); + + Services.obs.removeObserver(gPluginHandler.NPAPIPluginCrashed, "plugin-crashed"); + + try { + gBrowser.removeProgressListener(window.XULBrowserWindow); + gBrowser.removeTabsProgressListener(window.TabsProgressListener); + } catch (ex) { + } + + PlacesToolbarHelper.uninit(); + + BookmarkingUI.uninit(); + + TabsInTitlebar.uninit(); + + ToolbarIconColor.uninit(); + + TabletModeUpdater.uninit(); + + gTabletModePageCounter.finish(); + + BrowserOnClick.uninit(); + + FeedHandler.uninit(); + + DevEdition.uninit(); + + TrackingProtection.uninit(); + + RefreshBlocker.uninit(); + + CaptivePortalWatcher.uninit(); + + gMenuButtonUpdateBadge.uninit(); + + gMenuButtonBadgeManager.uninit(); + + SidebarUI.uninit(); + + // 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); + ctrlTab.uninit(); + gBrowserThumbnails.uninit(); + FullZoom.destroy(); + + Services.obs.removeObserver(gIdentityHandler, "perm-changed"); + 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-confirmation"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete"); + window.messageManager.removeMessageListener("Browser:URIFixup", gKeywordURIFixup); + window.messageManager.removeMessageListener("Browser:LoadURI", RedirectLoad); + + try { + gPrefService.removeObserver(gHomeButton.prefDomain, gHomeButton); + } catch (ex) { + Cu.reportError(ex); + } + + if (this.gmpInstallManager) { + this.gmpInstallManager.uninit(); + } + + BrowserOffline.uninit(); + IndexedDBPromptHelper.uninit(); + LightweightThemeListener.uninit(); + PanelUI.uninit(); + AutoShowBookmarksToolbar.uninit(); + } + + // Final window teardown, do this last. + 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; + }, +}; + +if (AppConstants.platform == "macosx") { + // nonBrowserWindowStartup(), nonBrowserWindowDelayedStartup(), and + // nonBrowserWindowShutdown() are used for non-browser windows in + // macBrowserOverlay + gBrowserInit.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']; + 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); + }; + + gBrowserInit.nonBrowserWindowDelayedStartup = function() { + this._delayedStartupTimeoutId = null; + + // initialise the offline listener + BrowserOffline.init(); + + // initialize the private browsing UI + gPrivateBrowsingUI.init(); + + // initialize the sync UI + gSyncUI.init(); + }; + + gBrowserInit.nonBrowserWindowShutdown = function() { + let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"] + .getService(Ci.nsIMacDockSupport); + dockSupport.dockMenu = null; + + // 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(); + }; +} + + +/* Legacy global init functions */ +var BrowserStartup = gBrowserInit.onLoad.bind(gBrowserInit); +var BrowserShutdown = gBrowserInit.onUnload.bind(gBrowserInit); + +if (AppConstants.platform == "macosx") { + var nonBrowserWindowStartup = gBrowserInit.nonBrowserWindowStartup.bind(gBrowserInit); + var nonBrowserWindowDelayedStartup = gBrowserInit.nonBrowserWindowDelayedStartup.bind(gBrowserInit); + var nonBrowserWindowShutdown = gBrowserInit.nonBrowserWindowShutdown.bind(gBrowserInit); +} + +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": + SidebarUI.toggle("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.printWindow(gBrowser.selectedBrowser.outerWindowID, + gBrowser.selectedBrowser); + break; + case "Save": + saveBrowser(gBrowser.selectedBrowser); + break; + case "SendMail": + MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser); + 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. + + let historyindex = aEvent.target.getAttribute("historyindex"); + duplicateTabIn(gBrowser.selectedTab, where, Number(historyindex)); + 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) { + let metaKeyPressed = AppConstants.platform == "macosx" + ? aEvent.metaKey + : aEvent.ctrlKey; + var backgroundTabModifier = aEvent.button == 1 || metaKeyPressed; + + if (aEvent.shiftKey && !backgroundTabModifier) { + BrowserReloadSkipCache(); + return; + } + + let where = whereToOpenLink(aEvent, false, true); + if (where == "current") + BrowserReload(); + else + duplicateTabIn(gBrowser.selectedTab, where); +} + +function BrowserReload() { + if (gBrowser.currentURI.schemeIs("view-source")) { + // Bug 1167797: For view source, we always skip the cache + return BrowserReloadSkipCache(); + } + 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 = getBoolPref("browser.tabs.loadBookmarksInBackground", false); + gBrowser.loadTabs(urls, loadInBackground); + break; + case "window": + OpenBrowserWindow(); + break; + } +} + +function loadOneOrMoreURIs(aURIString) +{ + // we're not a browser window, pass the URI string to a new browser window + if (window.location.href != getBrowserURL()) + { + window.openDialog(getBrowserURL(), "_blank", "all,dialog=no", aURIString); + return; + } + + // This function throws for certain malformed URIs, so use exception handling + // so that we don't disrupt startup + try { + gBrowser.loadTabs(aURIString.split("|"), false, true); + } + catch (e) { + } +} + +function focusAndSelectUrlBar() { + // In customize mode, the url bar is disabled. If a new tab is opened or the + // user switches to a different tab, this function gets called before we've + // finished leaving customize mode, and the url bar will still be disabled. + // We can't focus it when it's disabled, so we need to re-run ourselves when + // we've finished leaving customize mode. + if (CustomizationHandler.isExitingCustomizeMode) { + gNavToolbox.addEventListener("aftercustomization", function afterCustomize() { + gNavToolbox.removeEventListener("aftercustomization", afterCustomize); + focusAndSelectUrlBar(); + }); + + return true; + } + + if (gURLBar) { + if (window.fullScreen) + FullScreen.showNavToolbox(); + + gURLBar.select(); + if (document.activeElement == gURLBar.inputField) + return true; + } + return false; +} + +function openLocation() { + if (focusAndSelectUrlBar()) + return; + + 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 + window.openDialog("chrome://browser/content/", "_blank", + "chrome,all,dialog=no", BROWSER_NEW_TAB_URL); + } + } +} + +function BrowserOpenTab(event) { + let where = "tab"; + let relatedToCurrent = false; + + if (event) { + where = whereToOpenLink(event, false, true); + + switch (where) { + case "tab": + case "tabshifted": + // When accel-click or middle-click are used, open the new tab as + // related to the current tab. + relatedToCurrent = true; + break; + case "current": + where = "tab"; + break; + } + } + + openUILinkIn(BROWSER_NEW_TAB_URL, where, { relatedToCurrent }); +} + +/* 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() { + // If we're not a browser window, just close the window + if (window.location.href != getBrowserURL()) { + closeWindow(true); + return; + } + + // 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, + userContextId, originPrincipal, forceAboutBlankViewerInCurrent, + triggeringPrincipal) { + try { + openLinkIn(uri, "current", + { referrerURI: referrer, + referrerPolicy: referrerPolicy, + postData: postData, + allowThirdPartyFixup: allowThirdPartyFixup, + userContextId: userContextId, + originPrincipal, + triggeringPrincipal, + forceAboutBlankViewerInCurrent, + }); + } catch (e) {} +} + +/** + * Given a string, will generate a more appropriate urlbar value if a Places + * keyword or a search alias is found at the beginning of it. + * + * @param url + * A string that may begin with a keyword or an alias. + * + * @return {Promise} + * @resolves { url, postData, mayInheritPrincipal }. If it's not possible + * to discern a keyword or an alias, url will be the input string. + */ +function getShortcutOrURIAndPostData(url, 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; + // Split on the first whitespace. + let [keyword, param = ""] = url.trim().split(/\s(.+)/, 2); + + if (!keyword) { + return { url, postData, mayInheritPrincipal }; + } + + let engine = Services.search.getEngineByAlias(keyword); + if (engine) { + let submission = engine.getSubmission(param, null, "keyword"); + return { url: submission.uri.spec, + postData: submission.postData, + mayInheritPrincipal }; + } + + // A corrupt Places database could make this throw, breaking navigation + // from the location bar. + let entry = null; + try { + entry = yield PlacesUtils.keywords.fetch(keyword); + } catch (ex) { + Cu.reportError(`Unable to fetch Places keyword "${keyword}": ${ex}`); + } + if (!entry || !entry.url) { + // This is not a Places keyword. + return { url, postData, mayInheritPrincipal }; + } + + try { + [url, postData] = + yield BrowserUtils.parseUrlAndPostData(entry.url.href, + entry.postData, + param); + if (postData) { + postData = getPostDataStream(postData); + } + + // Since this URL came from a bookmark, it's safe to let it inherit the + // current document's principal. + mayInheritPrincipal = true; + } catch (ex) { + // It was not possible to bind the param, just use the original url value. + } + + return { url, postData, mayInheritPrincipal }; + }).then(data => { + if (callback) { + callback(data); + } + return data; + }); +} + +function getPostDataStream(aPostDataString, + aType = "application/x-www-form-urlencoded") { + let dataStream = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + dataStream.data = aPostDataString; + + let 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; +} + +/** + * Open the View Source dialog. + * + * @param aArgsOrDocument + * Either an object or a Document. Passing a Document is deprecated, + * and is not supported with e10s. This function will throw if + * aArgsOrDocument is a CPOW. + * + * If aArgsOrDocument is an object, that object can take the + * following properties: + * + * URL (required): + * A string URL for the page we'd like to view the source of. + * browser (optional): + * The browser containing the document that we would like to view the + * source of. This is required if outerWindowID is passed. + * outerWindowID (optional): + * The outerWindowID of the content window containing the document that + * we want to view the source of. You only need to provide this if you + * want to attempt to retrieve the document source from the network + * cache. + * lineNumber (optional): + * The line number to focus on once the source is loaded. + */ +function BrowserViewSourceOfDocument(aArgsOrDocument) { + let args; + + if (aArgsOrDocument instanceof Document) { + let doc = aArgsOrDocument; + // Deprecated API - callers should pass args object instead. + if (Cu.isCrossProcessWrapper(doc)) { + throw new Error("BrowserViewSourceOfDocument cannot accept a CPOW " + + "as a document."); + } + + 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 = () => { + let inTab = Services.prefs.getBoolPref("view_source.tab"); + if (inTab) { + let tabBrowser = gBrowser; + let forceNotRemote = false; + if (!tabBrowser) { + if (!args.browser) { + throw new Error("BrowserViewSourceOfDocument should be passed the " + + "subject browser if called from a window without " + + "gBrowser defined."); + } + forceNotRemote = !args.browser.isRemoteBrowser; + } else { + // Some internal URLs (such as specific chrome: and about: URLs that are + // not yet remote ready) cannot be loaded in a remote browser. View + // source in tab expects the new view source browser's remoteness to match + // that of the original URL, so disable remoteness if necessary for this + // URL. + let contentProcess = Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT + forceNotRemote = + gMultiProcessBrowser && + !E10SUtils.canLoadURIInProcess(args.URL, contentProcess) + } + + // In the case of popups, we need to find a non-popup browser window. + if (!tabBrowser || !window.toolbar.visible) { + // This returns only non-popup browser windows by default. + let browserWindow = RecentWindow.getMostRecentBrowserWindow(); + tabBrowser = browserWindow.gBrowser; + } + + // `viewSourceInBrowser` will load the source content from the page + // descriptor for the tab (when possible) or fallback to the network if + // that fails. Either way, the view source module will manage the tab's + // location, so use "about:blank" here to avoid unnecessary redundant + // requests. + let tab = tabBrowser.loadOneTab("about:blank", { + relatedToCurrent: true, + inBackground: false, + forceNotRemote, + relatedBrowser: args.browser + }); + args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab); + top.gViewSourceUtils.viewSourceInBrowser(args); + } else { + 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(); + } +} + +/** + * Opens the View Source dialog for the source loaded in the root + * top-level document of the browser. This is really just a + * convenience wrapper around BrowserViewSourceOfDocument. + * + * @param browser + * The browser that we want to load the source of. + */ +function BrowserViewSource(browser) { + BrowserViewSourceOfDocument({ + browser: browser, + outerWindowID: browser.outerWindowID, + URL: browser.currentURI.spec, + }); +} + +// documentURL - URL of the document to view, 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 +// frameOuterWindowID - the id of the frame that the context menu opened in; can be null/omitted +// browser - the browser containing the document we're interested in inspecting; can be null/omitted +function BrowserPageInfo(documentURL, initialTab, imageElement, frameOuterWindowID, browser) { + if (documentURL instanceof HTMLDocument) { + Deprecated.warning("Please pass the location URL instead of the document " + + "to BrowserPageInfo() as the first argument.", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1238180"); + documentURL = documentURL.location; + } + + let args = { initialTab, imageElement, frameOuterWindowID, browser }; + var windows = Services.wm.getEnumerator("Browser:page-info"); + + documentURL = documentURL || window.gBrowser.selectedBrowser.currentURI.spec; + + // Check for windows matching the url + while (windows.hasMoreElements()) { + var currentWindow = windows.getNext(); + if (currentWindow.closed) { + continue; + } + 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 + // 1. only if there's no opener (bug 370555). + // 2. if remote newtab is enabled and it's the default remote newtab page + let defaultRemoteURL = gAboutNewTabService.remoteEnabled && + uri.spec === gAboutNewTabService.newTabURL; + if ((gInitialPages.includes(uri.spec) || defaultRemoteURL) && + checkEmptyPageOrigin(gBrowser.selectedBrowser, uri)) { + value = ""; + } else { + // We should deal with losslessDecodeURI throwing for exotic URIs + try { + value = losslessDecodeURI(uri); + } catch (ex) { + value = "about:blank"; + } + } + + 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; + if (scheme == "moz-action") + throw new Error("losslessDecodeURI should never get a moz-action URI"); + + var value = aURI.spec; + + let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme); + // Try to decode as UTF-8 if there's no encoding sequence that we would break. + if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) { + if (decodeASCIIOnly) { + // This only decodes ascii characters (hex) 20-7e, except 25 (%). + // This avoids both cases stipulated below (%-related issues, and \r, \n + // and \t, which would be %0d, %0a and %09, respectively) as well as any + // non-US-ascii characters. + value = value.replace(/%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, decodeURI); + } else { + try { + value = decodeURI(value) + // 1. decodeURI decodes %25 to %, which creates unintended + // encoding sequences. Re-encode it, unless it's part of + // a sequence that survived decodeURI, i.e. one for: + // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#' + // (RFC 3987 section 3.2) + // 2. Re-encode select whitespace so that it doesn't get eaten + // away by the location bar (bug 410726). Re-encode all + // adjacent whitespace, to prevent spoofing attempts where + // invisible characters would push part of the URL to + // overflow the location bar (bug 1395508). + .replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)|[\r\n\t]|\s(?=\s)|\s$/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\u061c\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"); + + if (document.documentElement.getAttribute("customizing") == "true") { + if (splitter) { + splitter.remove(); + } + return; + } + + // If the splitter is already in the right place, we don't need to do anything: + if (splitter && + ((splitter.nextSibling == searchbar && splitter.previousSibling == urlbar) || + (splitter.nextSibling == urlbar && splitter.previousSibling == searchbar))) { + return; + } + + var ibefore = null; + if (urlbar && searchbar) { + if (urlbar.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.setAttribute("overflows", "false"); + splitter.className = "chromeclass-toolbar-additional"; + } + urlbar.parentNode.insertBefore(splitter, ibefore); + } else if (splitter) + splitter.parentNode.removeChild(splitter); +} + +function UpdatePageProxyState() +{ + if (gURLBar && gURLBar.value != gLastValidURLStr) + SetPageProxyState("invalid"); +} + +function SetPageProxyState(aState) +{ + if (!gURLBar) + return; + + gURLBar.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); + } else if (aState == "invalid") { + gURLBar.removeEventListener("input", UpdatePageProxyState, false); + } +} + +function PageProxyClickHandler(aEvent) +{ + if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste")) + middleMousePaste(aEvent); +} + +var gMenuButtonBadgeManager = { + BADGEID_APPUPDATE: "update", + BADGEID_DOWNLOAD: "download", + BADGEID_FXA: "fxa", + + fxaBadge: null, + downloadBadge: null, + appUpdateBadge: null, + + init: function () { + PanelUI.panel.addEventListener("popupshowing", this, true); + }, + + uninit: function () { + PanelUI.panel.removeEventListener("popupshowing", this, true); + }, + + handleEvent: function (e) { + if (e.type === "popupshowing") { + this.clearBadges(); + } + }, + + _showBadge: function () { + let badgeToShow = this.downloadBadge || this.appUpdateBadge || this.fxaBadge; + + if (badgeToShow) { + PanelUI.menuButton.setAttribute("badge-status", badgeToShow); + } else { + PanelUI.menuButton.removeAttribute("badge-status"); + } + }, + + _changeBadge: function (badgeId, badgeStatus = null) { + if (badgeId == this.BADGEID_APPUPDATE) { + this.appUpdateBadge = badgeStatus; + } else if (badgeId == this.BADGEID_DOWNLOAD) { + this.downloadBadge = badgeStatus; + } else if (badgeId == this.BADGEID_FXA) { + this.fxaBadge = badgeStatus; + } else { + Cu.reportError("The badge ID '" + badgeId + "' is unknown!"); + } + this._showBadge(); + }, + + addBadge: function (badgeId, badgeStatus) { + if (!badgeStatus) { + Cu.reportError("badgeStatus must be defined"); + return; + } + this._changeBadge(badgeId, badgeStatus); + }, + + removeBadge: function (badgeId) { + this._changeBadge(badgeId); + }, + + clearBadges: function () { + this.appUpdateBadge = null; + this.downloadBadge = null; + this.fxaBadge = null; + this._showBadge(); + } +}; + +// Setup the hamburger button badges for updates, if enabled. +var gMenuButtonUpdateBadge = { + enabled: false, + badgeWaitTime: 0, + timer: null, + cancelObserverRegistered: false, + + init: function () { + try { + this.enabled = Services.prefs.getBoolPref("app.update.badge"); + } catch (e) {} + if (this.enabled) { + try { + this.badgeWaitTime = Services.prefs.getIntPref("app.update.badgeWaitTime"); + } catch (e) { + this.badgeWaitTime = 345600; // 4 days + } + Services.obs.addObserver(this, "update-staged", false); + Services.obs.addObserver(this, "update-downloaded", false); + } + }, + + uninit: function () { + if (this.timer) + this.timer.cancel(); + if (this.enabled) { + Services.obs.removeObserver(this, "update-staged"); + Services.obs.removeObserver(this, "update-downloaded"); + this.enabled = false; + } + if (this.cancelObserverRegistered) { + Services.obs.removeObserver(this, "update-canceled"); + this.cancelObserverRegistered = false; + } + }, + + onMenuPanelCommand: function(event) { + if (event.originalTarget.getAttribute("update-status") === "succeeded") { + // restart the app + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + + if (!cancelQuit.data) { + Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart); + } + } else { + // open the page for manual update + let url = Services.urlFormatter.formatURLPref("app.update.url.manual"); + openUILinkIn(url, "tab"); + } + }, + + observe: function (subject, topic, status) { + if (topic == "update-canceled") { + this.reset(); + return; + } + if (status == "failed") { + // Background update has failed, let's show the UI responsible for + // prompting the user to update manually. + this.uninit(); + this.displayBadge(false); + return; + } + + // Give the user badgeWaitTime seconds to react before prompting. + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.timer.initWithCallback(this, this.badgeWaitTime * 1000, + this.timer.TYPE_ONE_SHOT); + // The timer callback will call uninit() when it completes. + }, + + notify: function () { + // If the update is successfully applied, or if the updater has fallen back + // to non-staged updates, add a badge to the hamburger menu to indicate an + // update will be applied once the browser restarts. + this.uninit(); + this.displayBadge(true); + }, + + displayBadge: function (succeeded) { + let status = succeeded ? "succeeded" : "failed"; + let badgeStatus = "update-" + status; + gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, badgeStatus); + + let stringId; + let updateButtonText; + if (succeeded) { + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + stringId = "appmenu.restartNeeded.description"; + updateButtonText = gNavigatorBundle.getFormattedString(stringId, + [brandShortName]); + Services.obs.addObserver(this, "update-canceled", false); + this.cancelObserverRegistered = true; + } else { + stringId = "appmenu.updateFailed.description"; + updateButtonText = gNavigatorBundle.getString(stringId); + } + + let updateButton = document.getElementById("PanelUI-update-status"); + updateButton.setAttribute("label", updateButtonText); + updateButton.setAttribute("update-status", status); + updateButton.hidden = false; + }, + + reset: function () { + gMenuButtonBadgeManager.removeBadge( + gMenuButtonBadgeManager.BADGEID_APPUPDATE); + let updateButton = document.getElementById("PanelUI-update-status"); + updateButton.hidden = true; + this.uninit(); + this.init(); + } +}; + +// Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json +const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED = 2; +const TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED = 3; +const TLS_ERROR_REPORT_TELEMETRY_MANUAL_SEND = 4; +const TLS_ERROR_REPORT_TELEMETRY_AUTO_SEND = 5; + +const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."]; + +const PREF_SSL_IMPACT = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => { + return prefs.concat(Services.prefs.getChildList(root)); +}, []); + +/** + * Handle command events bubbling up from error page content + * or from about:newtab or from remote error pages that invoke + * us via async messaging. + */ +var BrowserOnClick = { + init: function () { + let mm = window.messageManager; + mm.addMessageListener("Browser:CertExceptionError", this); + mm.addMessageListener("Browser:OpenCaptivePortalPage", this); + mm.addMessageListener("Browser:SiteBlockedError", this); + mm.addMessageListener("Browser:EnableOnlineMode", this); + mm.addMessageListener("Browser:ResetSSLPreferences", this); + mm.addMessageListener("Browser:SSLErrorReportTelemetry", this); + mm.addMessageListener("Browser:OverrideWeakCrypto", this); + mm.addMessageListener("Browser:SSLErrorGoBack", this); + + Services.obs.addObserver(this, "captive-portal-login-abort", false); + Services.obs.addObserver(this, "captive-portal-login-success", false); + }, + + uninit: function () { + let mm = window.messageManager; + mm.removeMessageListener("Browser:CertExceptionError", this); + mm.removeMessageListener("Browser:SiteBlockedError", this); + mm.removeMessageListener("Browser:EnableOnlineMode", this); + mm.removeMessageListener("Browser:ResetSSLPreferences", this); + mm.removeMessageListener("Browser:SSLErrorReportTelemetry", this); + mm.removeMessageListener("Browser:OverrideWeakCrypto", this); + mm.removeMessageListener("Browser:SSLErrorGoBack", this); + + Services.obs.removeObserver(this, "captive-portal-login-abort"); + Services.obs.removeObserver(this, "captive-portal-login-success"); + }, + + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "captive-portal-login-abort": + case "captive-portal-login-success": + // Broadcast when a captive portal is freed so that error pages + // can refresh themselves. + window.messageManager.broadcastAsyncMessage("Browser:CaptivePortalFreed"); + break; + } + }, + + receiveMessage: function (msg) { + switch (msg.name) { + case "Browser:CertExceptionError": + this.onCertError(msg.target, msg.data.elementId, + msg.data.isTopFrame, msg.data.location, + msg.data.securityInfoAsString); + break; + case "Browser:OpenCaptivePortalPage": + CaptivePortalWatcher.ensureCaptivePortalTab(); + break; + case "Browser:SiteBlockedError": + this.onAboutBlocked(msg.data.elementId, msg.data.reason, + msg.data.isTopFrame, msg.data.location); + break; + case "Browser:EnableOnlineMode": + if (Services.io.offline) { + // Reset network state and refresh the page. + Services.io.offline = false; + msg.target.reload(); + } + break; + case "Browser:ResetSSLPreferences": + for (let prefName of PREF_SSL_IMPACT) { + Services.prefs.clearUserPref(prefName); + } + msg.target.reload(); + break; + case "Browser:SetSSLErrorReportAuto": + Services.prefs.setBoolPref("security.ssl.errorReporting.automatic", msg.json.automatic); + let bin = TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED; + if (msg.json.automatic) { + bin = TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED; + } + Services.telemetry.getHistogramById("TLS_ERROR_REPORT_UI").add(bin); + break; + case "Browser:SSLErrorReportTelemetry": + let reportStatus = msg.data.reportStatus; + Services.telemetry.getHistogramById("TLS_ERROR_REPORT_UI") + .add(reportStatus); + break; + case "Browser:OverrideWeakCrypto": + let weakCryptoOverride = Cc["@mozilla.org/security/weakcryptooverride;1"] + .getService(Ci.nsIWeakCryptoOverride); + weakCryptoOverride.addWeakCryptoOverride( + msg.data.uri.host, + PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)); + break; + case "Browser:SSLErrorGoBack": + goBackFromErrorPage(); + break; + } + }, + + onSSLErrorReport: function(browser, uri, securityInfo) { + Cu.reportError("User requested certificate error report sending, but certificate error reporting is disabled"); + }, + + onCertError: function (browser, elementId, isTopFrame, location, securityInfoAsString) { + let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI"); + let securityInfo; + + switch (elementId) { + case "exceptionDialogButton": + if (isTopFrame) { + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_CLICK_ADD_EXCEPTION); + } + + securityInfo = getSecurityInfo(securityInfoAsString); + let sslStatus = securityInfo.QueryInterface(Ci.nsISSLStatusProvider) + .SSLStatus; + let params = { exceptionAdded : false, + sslStatus : sslStatus }; + + try { + switch (Services.prefs.getIntPref("browser.ssl_override_behavior")) { + case 2 : // Pre-fetch & pre-populate + params.prefetchCert = true; + case 1 : // Pre-populate + params.location = location; + } + } 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) { + browser.reload(); + } + break; + + case "returnButton": + if (isTopFrame) { + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_GET_ME_OUT_OF_HERE); + } + goBackFromErrorPage(); + break; + + case "advancedButton": + if (isTopFrame) { + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_UNDERSTAND_RISKS); + } + + securityInfo = getSecurityInfo(securityInfoAsString); + let errorInfo = getDetailedCertErrorInfo(location, + securityInfo); + browser.messageManager.sendAsyncMessage( "CertErrorDetails", { + code: securityInfo.errorCode, + info: errorInfo + }); + break; + + case "copyToClipboard": + const gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper); + securityInfo = getSecurityInfo(securityInfoAsString); + let detailedInfo = getDetailedCertErrorInfo(location, + securityInfo); + gClipboardHelper.copyString(detailedInfo); + break; + + } + }, + + onAboutBlocked: function (elementId, reason, isTopFrame, location) { + // Depending on what page we are displaying here (malware/phishing/unwanted) + // use the right strings and links for each. + let bucketName = ""; + let sendTelemetry = false; + if (reason === 'malware') { + sendTelemetry = true; + bucketName = "WARNING_MALWARE_PAGE_"; + } else if (reason === 'phishing') { + sendTelemetry = true; + bucketName = "WARNING_PHISHING_PAGE_"; + } else if (reason === 'unwanted') { + sendTelemetry = true; + bucketName = "WARNING_UNWANTED_PAGE_"; + } + let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI"); + let nsISecTel = Ci.nsISecurityUITelemetry; + bucketName += isTopFrame ? "TOP_" : "FRAME_"; + switch (elementId) { + case "getMeOutButton": + if (sendTelemetry) { + secHistogram.add(nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]); + } + getMeOutOfHere(); + break; + + case "reportButton": + // This is the "Why is this site blocked" button. We redirect + // to the generic page describing phishing/malware protection. + + // We log even if malware/phishing/unwanted info URL couldn't be found: + // the measurement is for how many users clicked the WHY BLOCKED button + if (sendTelemetry) { + secHistogram.add(nsISecTel[bucketName + "WHY_BLOCKED"]); + } + openHelpLink("phishing-malware", false, "current"); + break; + + case "ignoreWarningButton": + if (gPrefService.getBoolPref("browser.safebrowsing.allowOverride")) { + if (sendTelemetry) { + secHistogram.add(nsISecTel[bucketName + "IGNORE_WARNING"]); + } + this.ignoreWarningButton(reason); + } + break; + } + }, + + ignoreWarningButton: function (reason) { + // Allow users to override and continue through to the site, + // but add a notify bar as a reminder, so that they don't lose + // track after, e.g., tab switching. + gBrowser.loadURIWithFlags(gBrowser.currentURI.spec, + nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, + null, null, null); + + Services.perms.add(gBrowser.currentURI, "safe-browsing", + Ci.nsIPermissionManager.ALLOW_ACTION, + Ci.nsIPermissionManager.EXPIRE_SESSION); + + let buttons = [{ + label: gNavigatorBundle.getString("safebrowsing.getMeOutOfHereButton.label"), + accessKey: gNavigatorBundle.getString("safebrowsing.getMeOutOfHereButton.accessKey"), + callback: function() { getMeOutOfHere(); } + }]; + + let title; + if (reason === 'malware') { + title = gNavigatorBundle.getString("safebrowsing.reportedAttackSite"); + buttons[1] = { + label: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.label"), + accessKey: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.accessKey"), + callback: function() { + openUILinkIn(gSafeBrowsing.getReportURL('MalwareMistake'), 'tab'); + } + }; + } else if (reason === 'phishing') { + title = gNavigatorBundle.getString("safebrowsing.deceptiveSite"); + buttons[1] = { + label: gNavigatorBundle.getString("safebrowsing.notADeceptiveSiteButton.label"), + accessKey: gNavigatorBundle.getString("safebrowsing.notADeceptiveSiteButton.accessKey"), + callback: function() { + openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab'); + } + }; + } else if (reason === 'unwanted') { + title = gNavigatorBundle.getString("safebrowsing.reportedUnwantedSite"); + // There is no button for reporting errors since Google doesn't currently + // provide a URL endpoint for these reports. + } + + let notificationBox = gBrowser.getNotificationBox(); + let value = "blocked-badware-page"; + + let previousNotification = notificationBox.getNotificationWithValue(value); + if (previousNotification) { + notificationBox.removeNotification(previousNotification); + } + + let notification = notificationBox.appendNotification( + title, + value, + "chrome://global/skin/icons/blacklist_favicon.png", + notificationBox.PRIORITY_CRITICAL_HIGH, + buttons + ); + // Persist the notification until the user removes so it + // doesn't get removed on redirects. + notification.persistence = -1; + }, +}; + +/** + * 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() { + gBrowser.loadURI(getDefaultHomePage()); +} + +/** + * Re-direct the browser to the previous page or a known-safe page if no + * previous page is found in history. This function is used when the user + * browses to a secure page with certificate issues and is presented with + * about:certerror. The "Go Back" button should take the user to the previous + * or a default start page so that even when their own homepage is on a server + * that has certificate errors, we can get them somewhere safe. + */ +function goBackFromErrorPage() { + const ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + let state = JSON.parse(ss.getTabState(gBrowser.selectedTab)); + if (state.index == 1) { + // If the unsafe page is the first or the only one in history, go to the + // start page. + gBrowser.loadURI(getDefaultHomePage()); + } else { + BrowserBack(); + } +} + +/** + * Return the default start page for the cases when the user's own homepage is + * infected, so we can get them somewhere safe. + */ +function getDefaultHomePage() { + // 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); + } + return url; +} + +function BrowserFullScreen() +{ + window.fullScreen = !window.fullScreen; +} + +function mirrorShow(popup) { + let services = []; + if (Services.prefs.getBoolPref("browser.casting.enabled")) { + services = CastingApps.getServicesForMirroring(); + } + popup.ownerDocument.getElementById("menu_mirrorTabCmd").hidden = !services.length; +} + +function mirrorMenuItemClicked(event) { + gBrowser.selectedBrowser.messageManager.sendAsyncMessage("SecondScreen:tab-mirror", + {service: event.originalTarget._service}); +} + +function populateMirrorTabMenu(popup) { + popup.innerHTML = null; + if (!Services.prefs.getBoolPref("browser.casting.enabled")) { + return; + } + let doc = popup.ownerDocument; + let services = CastingApps.getServicesForMirroring(); + services.forEach(service => { + let item = doc.createElement("menuitem"); + item.setAttribute("label", service.friendlyName); + item._service = service; + item.addEventListener("command", mirrorMenuItemClicked); + popup.appendChild(item); + }); +} + +function getWebNavigation() +{ + return gBrowser.webNavigation; +} + +function BrowserReloadWithFlags(reloadFlags) { + let url = gBrowser.currentURI.spec; + if (gBrowser.updateBrowserRemotenessByURL(gBrowser.selectedBrowser, url)) { + // If the remoteness has changed, the new browser doesn't have any + // information of what was loaded before, so we need to load the previous + // URL again. + gBrowser.loadURIWithFlags(url, reloadFlags); + return; + } + + let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + gBrowser.selectedBrowser + .messageManager + .sendAsyncMessage("Browser:Reload", + { flags: reloadFlags, + handlingUserInput: windowUtils.isHandlingUserInput }); +} + +function getSecurityInfo(securityInfoAsString) { + if (!securityInfoAsString) + return null; + + const serhelper = Cc["@mozilla.org/network/serialization-helper;1"] + .getService(Ci.nsISerializationHelper); + let securityInfo = serhelper.deserializeObject(securityInfoAsString); + securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); + + return securityInfo; +} + +/** + * Returns a string with detailed information about the certificate validation + * failure from the specified URI that can be used to send a report. + */ +function getDetailedCertErrorInfo(location, securityInfo) { + if (!securityInfo) + return ""; + + let certErrorDetails = location; + let code = securityInfo.errorCode; + let errors = Cc["@mozilla.org/nss_errors_service;1"] + .getService(Ci.nsINSSErrorsService); + + certErrorDetails += "\r\n\r\n" + errors.getErrorMessage(errors.getXPCOMFromNSSError(code)); + + const sss = Cc["@mozilla.org/ssservice;1"] + .getService(Ci.nsISiteSecurityService); + // SiteSecurityService uses different storage if the channel is + // private. Thus we must give isSecureHost correct flags or we + // might get incorrect results. + let flags = PrivateBrowsingUtils.isWindowPrivate(window) ? + Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0; + + let uri = Services.io.newURI(location, null, null); + + let hasHSTS = sss.isSecureHost(sss.HEADER_HSTS, uri.host, flags); + let hasHPKP = sss.isSecureHost(sss.HEADER_HPKP, uri.host, flags); + certErrorDetails += "\r\n\r\n" + + gNavigatorBundle.getFormattedString("certErrorDetailsHSTS.label", + [hasHSTS]); + certErrorDetails += "\r\n" + + gNavigatorBundle.getFormattedString("certErrorDetailsKeyPinning.label", + [hasHPKP]); + + let certChain = ""; + if (securityInfo.failedCertChain) { + let certs = securityInfo.failedCertChain.getEnumerator(); + while (certs.hasMoreElements()) { + let cert = certs.getNext(); + cert.QueryInterface(Ci.nsIX509Cert); + certChain += getPEMString(cert); + } + } + + certErrorDetails += "\r\n\r\n" + + gNavigatorBundle.getString("certErrorDetailsCertChain.label") + + "\r\n\r\n" + certChain; + + return certErrorDetails; +} + +// TODO: can we pull getDERString and getPEMString in from pippki.js instead of +// duplicating them here? +function getDERString(cert) +{ + var length = {}; + var derArray = cert.getRawDER(length); + var derString = ''; + for (var i = 0; i < derArray.length; i++) { + derString += String.fromCharCode(derArray[i]); + } + return derString; +} + +function getPEMString(cert) +{ + var derb64 = btoa(getDERString(cert)); + // Wrap the Base64 string into lines of 64 characters, + // with CRLF line breaks (as specified in RFC 1421). + var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n"); + return "-----BEGIN CERTIFICATE-----\r\n" + + wrapped + + "\r\n-----END CERTIFICATE-----\r\n"; +} + +var PrintPreviewListener = { + _printPreviewTab: null, + _tabBeforePrintPreview: null, + _simplifyPageTab: null, + + getPrintPreviewBrowser: function () { + if (!this._printPreviewTab) { + let browser = gBrowser.selectedTab.linkedBrowser; + let forceNotRemote = gMultiProcessBrowser && !browser.isRemoteBrowser; + this._tabBeforePrintPreview = gBrowser.selectedTab; + this._printPreviewTab = gBrowser.loadOneTab("about:blank", + { inBackground: false, + forceNotRemote, + relatedBrowser: browser }); + gBrowser.selectedTab = this._printPreviewTab; + } + return gBrowser.getBrowserForTab(this._printPreviewTab); + }, + createSimplifiedBrowser: function () { + this._simplifyPageTab = gBrowser.loadOneTab("about:blank", + { inBackground: true }); + return this.getSimplifiedSourceBrowser(); + }, + getSourceBrowser: function () { + return this._tabBeforePrintPreview ? + this._tabBeforePrintPreview.linkedBrowser : gBrowser.selectedBrowser; + }, + getSimplifiedSourceBrowser: function () { + return this._simplifyPageTab ? + gBrowser.getBrowserForTab(this._simplifyPageTab) : null; + }, + 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(); + if (this._simplifyPageTab) { + gBrowser.removeTab(this._simplifyPageTab); + this._simplifyPageTab = null; + } + gBrowser.removeTab(this._printPreviewTab); + gBrowser.deactivatePrintPreviewBrowsers(); + this._printPreviewTab = null; + }, + _toggleAffectedChrome: function () { + gNavToolbox.collapsed = gInPrintPreviewMode; + + if (gInPrintPreviewMode) + this._hideChrome(); + else + this._showChrome(); + + TabsInTitlebar.allowedBy("print-preview", !gInPrintPreviewMode); + }, + _hideChrome: function () { + this._chromeState = {}; + + this._chromeState.sidebarOpen = SidebarUI.isOpen; + this._sidebarCommand = SidebarUI.currentID; + SidebarUI.hide(); + + var notificationBox = gBrowser.getNotificationBox(); + this._chromeState.notificationsOpen = !notificationBox.notificationsHidden; + notificationBox.notificationsHidden = true; + + 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.findOpen) + gFindBar.open(); + + if (this._chromeState.globalNotificationsOpen) + document.getElementById("global-notificationbox").notificationsHidden = false; + + if (this._chromeState.syncNotificationsOpen) + document.getElementById("sync-notifications").notificationsHidden = false; + + if (this._chromeState.sidebarOpen) + SidebarUI.show(this._sidebarCommand); + }, + + activateBrowser(browser) { + gBrowser.activateBrowserForPrintPreview(browser); + }, +} + +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: 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) + { + if (gPrefService.prefIsLocked("browser.startup.homepage")) { + return; + } + 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 newTabButtonObserver = { + onDragOver(aEvent) { + browserDragAndDrop.dragOver(aEvent); + }, + onDragExit(aEvent) {}, + onDrop: Task.async(function* (aEvent) { + let links = browserDragAndDrop.dropLinks(aEvent); + for (let link of links) { + if (link.url) { + let data = yield getShortcutOrURIAndPostData(link.url); + // Allow third-party services to fixup this URL. + openNewTabWith(data.url, null, data.postData, aEvent, true); + } + } + }) +} + +var newWindowButtonObserver = { + onDragOver(aEvent) { + browserDragAndDrop.dragOver(aEvent); + }, + onDragExit(aEvent) {}, + onDrop: Task.async(function* (aEvent) { + let links = browserDragAndDrop.dropLinks(aEvent); + for (let link of links) { + if (link.url) { + let data = yield getShortcutOrURIAndPostData(link.url); + // Allow third-party services to fixup this URL. + openNewWindowWith(data.url, null, data.postData, true); + } + } + }) +} + +const DOMLinkHandler = { + init: function() { + let mm = window.messageManager; + mm.addMessageListener("Link:AddFeed", this); + mm.addMessageListener("Link:SetIcon", this); + mm.addMessageListener("Link:AddSearch", this); + }, + + receiveMessage: function (aMsg) { + switch (aMsg.name) { + case "Link:AddFeed": + let link = {type: aMsg.data.type, href: aMsg.data.href, title: aMsg.data.title}; + FeedHandler.addFeed(link, aMsg.target); + break; + + case "Link:SetIcon": + this.setIcon(aMsg.target, aMsg.data.url, aMsg.data.loadingPrincipal); + break; + + case "Link:AddSearch": + this.addSearch(aMsg.target, aMsg.data.engine, aMsg.data.url); + break; + } + }, + + setIcon: function(aBrowser, aURL, aLoadingPrincipal) { + if (gBrowser.isFailedIcon(aURL)) + return false; + + let tab = gBrowser.getTabForBrowser(aBrowser); + if (!tab) + return false; + + gBrowser.setIcon(tab, aURL, aLoadingPrincipal); + return true; + }, + + addSearch: function(aBrowser, aEngine, aURL) { + let tab = gBrowser.getTabForBrowser(aBrowser); + if (!tab) + return; + + BrowserSearch.addEngine(aBrowser, aEngine, makeURI(aURL)); + }, +} + +const BrowserSearch = { + addEngine: function(browser, engine, uri) { + // Check to see whether we've already added an engine with this title + if (browser.engines) { + if (browser.engines.some(e => e.title == engine.title)) + return; + } + + 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, + get icon() { return browser.mIconURL; } + }); + + if (hidden) + browser.hiddenEngines = engines; + else { + browser.engines = engines; + if (browser == gBrowser.selectedBrowser) + this.updateOpenSearchBadge(); + } + }, + + /** + * Update the browser UI to show whether or not additional engines are + * available when a page is loaded or the user switches tabs to a page that + * has search engines. + */ + updateOpenSearchBadge: function() { + var searchBar = this.searchBar; + if (!searchBar) + return; + + var engines = gBrowser.selectedBrowser.engines; + if (engines && engines.length > 0) + searchBar.setAttribute("addengines", "true"); + else + searchBar.removeAttribute("addengines"); + }, + + /** + * 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() { + 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; + } + + let focusUrlBarIfSearchFieldIsNotActive = function(aSearchBar) { + if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) { + focusAndSelectUrlBar(); + } + }; + + let searchBar = this.searchBar; + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + let focusSearchBar = () => { + searchBar = this.searchBar; + searchBar.select(); + focusUrlBarIfSearchFieldIsNotActive(searchBar); + }; + if (placement && placement.area == CustomizableUI.AREA_PANEL) { + // The panel is not constructed until the first time it is shown. + PanelUI.show().then(focusSearchBar); + return; + } + if (placement && placement.area == CustomizableUI.AREA_NAVBAR && searchBar && + searchBar.parentNode.getAttribute("overflowedItem") == "true") { + let navBar = document.getElementById(CustomizableUI.AREA_NAVBAR); + navBar.overflowable.show().then(() => { + focusSearchBar(); + }); + return; + } + if (searchBar) { + if (window.fullScreen) + FullScreen.showNavToolbox(); + searchBar.select(); + } + focusUrlBarIfSearchFieldIsNotActive(searchBar); + }, + + /** + * 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 engine The search engine used to perform a search, or null if no + * search was performed. + */ + _loadSearch: function (searchText, useNewTab, purpose) { + let 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; + + let 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; + }, + + /** + * Just like _loadSearch, but preserving an old API. + * + * @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) { + let engine = BrowserSearch._loadSearch(searchText, useNewTab, purpose); + if (!engine) { + return null; + } + 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"); + if (engine) { + BrowserSearch.recordSearchInTelemetry(engine, "contextmenu"); + } + }, + + pasteAndSearch: function (event) { + BrowserSearch.searchBar.select(); + goDoCommand("cmd_paste"); + BrowserSearch.searchBar.handleSearchCommand(event); + }, + + /** + * Returns the search bar element if it is present in the toolbar, null otherwise. + */ + get searchBar() { + return document.getElementById("searchbar"); + }, + + get searchEnginesURL() { + return formatURL("browser.search.searchEnginesURL", true); + }, + + loadAddEngines: function BrowserSearch_loadAddEngines() { + var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow"); + var where = newWindowPref == 3 ? "tab" : "window"; + openUILinkIn(this.searchEnginesURL, where); + }, + + /** + * Helper to record a search with Telemetry. + * + * Telemetry records only search counts and nothing pertaining to the search itself. + * + * @param engine + * (nsISearchEngine) The engine handling the search. + * @param source + * (string) Where the search originated from. See BrowserUsageTelemetry for + * allowed values. + * @param details [optional] + * An optional parameter passed to |BrowserUsageTelemetry.recordSearch|. + * See its documentation for allowed options. + * Additionally, if the search was a suggested search, |details.selection| + * indicates where the item was in the suggestion list and how the user + * selected it: {selection: {index: The selected index, kind: "key" or "mouse"}} + */ + recordSearchInTelemetry: function (engine, source, details={}) { + try { + BrowserUsageTelemetry.recordSearch(engine, source, details); + } catch (ex) { + Cu.reportError(ex); + } + }, + + /** + * Helper to record a one-off search with Telemetry. + * + * Telemetry records only search counts and nothing pertaining to the search itself. + * + * @param engine + * (nsISearchEngine) The engine handling the search. + * @param source + * (string) Where the search originated from. See BrowserUsageTelemetry for + * allowed values. + * @param type + * (string) Indicates how the user selected the search item. + * @param where + * (string) Where was the search link opened (e.g. new tab, current tab, ..). + */ + recordOneoffSearchInTelemetry: function (engine, source, type, where) { + try { + const details = {type, isOneOff: true}; + BrowserUsageTelemetry.recordSearch(engine, source, details); + } catch (ex) { + Cu.reportError(ex); + } + } +}; + +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 + let children = aParent.childNodes; + for (var i = children.length - 1; i >= 0; --i) { + if (children[i].hasAttribute("index")) + aParent.removeChild(children[i]); + } + + const MAX_HISTORY_MENU_ITEMS = 15; + + const tooltipBack = gNavigatorBundle.getString("tabHistory.goBack"); + const tooltipCurrent = gNavigatorBundle.getString("tabHistory.current"); + const tooltipForward = gNavigatorBundle.getString("tabHistory.goForward"); + + function updateSessionHistory(sessionHistory, initial) + { + let count = sessionHistory.entries.length; + + if (!initial) { + if (count <= 1) { + // if there is only one entry now, close the popup. + aParent.hidePopup(); + return; + } else if (aParent.id != "backForwardMenu" && !aParent.parentNode.open) { + // if the popup wasn't open before, but now needs to be, reopen the menu. + // It should trigger FillHistoryMenu again. This might happen with the + // delay from click-and-hold menus but skip this for the context menu + // (backForwardMenu) rather than figuring out how the menu should be + // positioned and opened as it is an extreme edgecase. + aParent.parentNode.open = true; + return; + } + } + + let index = sessionHistory.index; + let half_length = Math.floor(MAX_HISTORY_MENU_ITEMS / 2); + let start = Math.max(index - half_length, 0); + let 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); + } + + let existingIndex = 0; + + for (let j = end - 1; j >= start; j--) { + let entry = sessionHistory.entries[j]; + let uri = entry.url; + + let item = existingIndex < children.length ? + children[existingIndex] : document.createElement("menuitem"); + + let entryURI = BrowserUtils.makeURI(entry.url, entry.charset, null); + item.setAttribute("uri", uri); + item.setAttribute("label", entry.title || uri); + item.setAttribute("index", j); + + // Cache this so that gotoHistoryIndex doesn't need the original index + item.setAttribute("historyindex", j - index); + + if (j != index) { + PlacesUtils.favicons.getFaviconURLForPage(entryURI, 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); + } + + if (!item.parentNode) { + aParent.appendChild(item); + } + + existingIndex++; + } + + if (!initial) { + let existingLength = children.length; + while (existingIndex < existingLength) { + aParent.removeChild(aParent.lastChild); + existingIndex++; + } + } + } + + let sessionHistory = SessionStore.getSessionHistory(gBrowser.selectedTab, updateSessionHistory); + if (!sessionHistory) + return false; + + // don't display the popup for a single item + if (sessionHistory.entries.length <= 1) + return false; + + updateSessionHistory(sessionHistory, true); + 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) +{ + var telemetryObj = {}; + TelemetryStopwatch.start("FX_NEW_WINDOW_MS", telemetryObj); + + function newDocumentShown(doc, topic, data) { + if (topic == "document-shown" && + doc != document && + doc.defaultView == win) { + Services.obs.removeObserver(newDocumentShown, "document-shown"); + Services.obs.removeObserver(windowClosed, "domwindowclosed"); + TelemetryStopwatch.finish("FX_NEW_WINDOW_MS", telemetryObj); + } + } + + function windowClosed(subject) { + if (subject == win) { + Services.obs.removeObserver(newDocumentShown, "document-shown"); + Services.obs.removeObserver(windowClosed, "domwindowclosed"); + } + } + + // Make sure to remove the 'document-shown' observer in case the window + // is being closed right after it was opened to avoid leaking. + Services.obs.addObserver(newDocumentShown, "document-shown", false); + Services.obs.addObserver(windowClosed, "domwindowclosed", 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 (options && options.remote) { + extraFeatures += ",remote"; + } else if (options && options.remote === false) { + extraFeatures += ",non-remote"; + } + + // 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; +} + +// Only here for backwards compat, we should remove this soon +function BrowserCustomizeToolbar() { + gCustomizeMode.enter(); +} + +/** + * 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() +{ + if (AppConstants.platform == "macosx") + return; + + let editMenuPopupState = document.getElementById("menu_EditPopup").state; + let contextMenuPopupState = document.getElementById("contentAreaContextMenu").state; + let placesContextMenuPopupState = document.getElementById("placesContext").state; + + // 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" || + document.getElementById("edit-controls") ? 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); + } +} + +/** + * Opens a new tab with the userContextId specified as an attribute of + * sourceEvent. This attribute is propagated to the top level originAttributes + * living on the tab's docShell. + * + * @param event + * A click event on a userContext File Menu option + */ +function openNewUserContextTab(event) +{ + openUILinkIn(BROWSER_NEW_TAB_URL, "tab", { + userContextId: parseInt(event.target.getAttribute('data-usercontextid')), + }); +} + +/** + * Updates File Menu User Context UI visibility depending on + * privacy.userContext.enabled pref state. + */ +function updateUserContextUIVisibility() +{ + let menu = document.getElementById("menu_newUserContext"); + menu.hidden = !Services.prefs.getBoolPref("privacy.userContext.enabled"); + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + menu.setAttribute("disabled", "true"); + } +} + +/** + * Updates the User Context UI indicators if the browser is in a non-default context + */ +function updateUserContextUIIndicator() +{ + let hbox = document.getElementById("userContext-icons"); + + let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid"); + if (!userContextId) { + hbox.setAttribute("data-identity-color", ""); + hbox.hidden = true; + return; + } + + let identity = ContextualIdentityService.getIdentityFromId(userContextId); + if (!identity) { + hbox.setAttribute("data-identity-color", ""); + hbox.hidden = true; + return; + } + + hbox.setAttribute("data-identity-color", identity.color); + + let label = document.getElementById("userContext-label"); + label.setAttribute("value", ContextualIdentityService.getUserContextLabel(userContextId)); + + let indicator = document.getElementById("userContext-indicator"); + indicator.setAttribute("data-identity-icon", identity.icon); + + hbox.hidden = false; +} + +/** + * 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"); + // 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.selectedBrowser.mayEnableCharacterEncodingMenu) { + if (charsetMenu) { + charsetMenu.removeAttribute("disabled"); + } + } else if (charsetMenu) { + charsetMenu.setAttribute("disabled", "true"); + } +} + +var XULBrowserWindow = { + // Stored Status, Link and Loading values + status: "", + defaultStatus: "", + overLink: "", + startTime: 0, + statusText: "", + isBusy: false, + // Left here for add-on compatibility, see bug 752434 + 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 () { + return gBrowser.getStatusPanel(); + }, + get isImage () { + delete this.isImage; + return this.isImage = document.getElementById("isImage"); + }, + get canViewSource () { + delete this.canViewSource; + return this.canViewSource = document.getElementById("canViewSource"); + }, + + init: function () { + // Initialize the security button's state and tooltip text. + var securityUI = gBrowser.securityUI; + this.onSecurityChange(null, null, securityUI.state, true); + }, + + setJSStatus: function () { + // unsupported + }, + + forceInitialBrowserRemote: function() { + let initBrowser = + document.getAnonymousElementByAttribute(gBrowser, "anonid", "initialBrowser"); + return initBrowser.frameLoader.tabParent; + }, + + forceInitialBrowserNonRemote: function(aOpener) { + let initBrowser = + document.getAnonymousElementByAttribute(gBrowser, "anonid", "initialBrowser"); + gBrowser.updateBrowserRemoteness(initBrowser, false, aOpener); + }, + + 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(); + }, + + showTooltip: function (x, y, tooltip, direction) { + if (Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService). + getCurrentSession()) { + return; + } + + // The x,y coordinates are relative to the <browser> element using + // the chrome zoom level. + let elt = document.getElementById("remoteBrowserTooltip"); + elt.label = tooltip; + elt.style.direction = direction; + + let anchor = gBrowser.selectedBrowser; + elt.openPopupAtScreen(anchor.boxObject.screenX + x, anchor.boxObject.screenY + y, false, null); + }, + + hideTooltip: function () { + let elt = document.getElementById("remoteBrowserTooltip"); + elt.hidePopup(); + }, + + getTabCount: function () { + return gBrowser.tabs.length; + }, + + 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) { + return BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab); + }, + + // Check whether this URI should load in the current process + shouldLoadURI: function(aDocShell, aURI, aReferrer) { + if (!gMultiProcessBrowser) + return true; + + let browser = aDocShell.QueryInterface(Ci.nsIDocShellTreeItem) + .sameTypeRootTreeItem + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + + // Ignore loads that aren't in the main tabbrowser + if (browser.localName != "browser" || !browser.getTabBrowser || browser.getTabBrowser() != gBrowser) + return true; + + if (!E10SUtils.shouldLoadURI(aDocShell, aURI, aReferrer)) { + E10SUtils.redirectLoad(aDocShell, aURI, aReferrer); + return false; + } + + return true; + }, + + 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; + + let browser = gBrowser.selectedBrowser; + + if (aStateFlags & nsIWebProgressListener.STATE_START && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + + if (aRequest && aWebProgress.isTopLevel) { + // clear out feed data + browser.feeds = null; + + // clear out search-engine data + browser.engines = null; + } + + this.isBusy = true; + + if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { + this._busyUI = 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; + let canViewSource = true; + // 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; + + canViewSource = !Services.prefs.getBoolPref("view_source.tab") || + location.scheme != "view-source"; + + 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 (browser.documentContentType && BrowserUtils.mimeTypeIsTextBased(browser.documentContentType)) { + this.isImage.removeAttribute('disabled'); + } else { + canViewSource = false; + this.isImage.setAttribute('disabled', 'true'); + } + + if (canViewSource) { + this.canViewSource.removeAttribute('disabled'); + } else { + this.canViewSource.setAttribute('disabled', 'true'); + } + } + + this.isBusy = false; + + if (this._busyUI) { + this._busyUI = false; + + this.stopCommand.setAttribute("disabled", "true"); + CombinedStopReload.switchToReload(aRequest instanceof Ci.nsIRequest); + } + } + }, + + onLocationChange: function (aWebProgress, aRequest, aLocationURI, aFlags) { + var location = aLocationURI ? aLocationURI.spec : ""; + + // 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.ownerGlobal; + tooltipWindow != tooltipWindow.parent; + tooltipWindow = tooltipWindow.parent) { + if (tooltipWindow == aWebProgress.DOMWindow) { + pageTooltip.hidePopup(); + break; + } + } + } + } + + let browser = gBrowser.selectedBrowser; + + // Disable menu entries for images, enable otherwise + if (browser.documentContentType && BrowserUtils.mimeTypeIsTextBased(browser.documentContentType)) + 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 + + if (aWebProgress.isTopLevel) { + if ((location == "about:blank" && checkEmptyPageOrigin()) || + 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"); + } + + URLBarSetURI(aLocationURI); + + BookmarkingUI.onLocationChange(); + + gIdentityHandler.onLocationChange(); + + + gTabletModePageCounter.inc(); + + // 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"); + } + } + + 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); + + // Try not to instantiate gCustomizeMode as much as possible, + // so don't use CustomizeMode.jsm to check for URI or customizing. + if (location == "about:blank" && + gBrowser.selectedTab.hasAttribute("customizemode")) { + gCustomizeMode.enter(); + } else if (CustomizationHandler.isEnteringCustomizeMode || + CustomizationHandler.isCustomizing()) { + gCustomizeMode.exit(); + } + } + UpdateBackForwardCommands(gBrowser.webNavigation); + ReaderParent.updateReaderButton(gBrowser.selectedBrowser); + + 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(); + BrowserSearch.updateOpenSearchBadge(); + }, + + // Left here for add-on compatibility, see bug 752434 + hideChromeForLocation: function() {}, + + onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) { + this.status = aMessage; + this.updateStatusField(); + }, + + // Properties used to cache security state used to update the UI + _state: null, + _lastLocation: null, + + // This is called in multiple ways: + // 1. Due to the nsIWebProgressListener.onSecurityChange notification. + // 2. Called by tabbrowser.xml when updating the current browser. + // 3. Called directly during this object's initializations. + // aRequest will be null always in case 2 and 3, and sometimes in case 1 (for + // instance, there won't be a request when STATE_BLOCKED_TRACKING_CONTENT is observed). + onSecurityChange: function (aWebProgress, aRequest, aState, aIsSimulated) { + // Don't need to do anything if the data we use to update the UI hasn't + // changed + let uri = gBrowser.currentURI; + let spec = uri.spec; + if (this._state == aState && + this._lastLocation == spec) + return; + this._state = aState; + this._lastLocation = spec; + + if (typeof(aIsSimulated) != "boolean" && typeof(aIsSimulated) != "undefined") { + throw "onSecurityChange: aIsSimulated receieved an unexpected type"; + } + + // Make sure the "https" part of the URL is striked out or not, + // depending on the current mixed active content blocking state. + gURLBar.formatValue(); + + try { + uri = Services.uriFixup.createExposableURI(uri); + } catch (e) {} + gIdentityHandler.updateIdentity(this._state, uri); + TrackingProtection.onSecurityChange(this._state, aIsSimulated); + }, + + // 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 () { + return 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; + + let reload = document.getElementById("urlbar-reload-button"); + let 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 = { + // Keep track of which browsers we've started load timers for, since + // we won't see STATE_START events for pre-rendered tabs. + _startedLoadTimer: new WeakSet(), + + onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + // Collect telemetry data about tab load times. + if (aWebProgress.isTopLevel && (!aRequest.originalURI || aRequest.originalURI.spec.scheme != "about")) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { + this._startedLoadTimer.add(aBrowser); + TelemetryStopwatch.start("FX_PAGE_LOAD_MS", aBrowser); + Services.telemetry.getHistogramById("FX_TOTAL_TOP_VISITS").add(true); + } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + this._startedLoadTimer.has(aBrowser)) { + this._startedLoadTimer.delete(aBrowser); + TelemetryStopwatch.finish("FX_PAGE_LOAD_MS", aBrowser); + } + } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStatus == Cr.NS_BINDING_ABORTED && + this._startedLoadTimer.has(aBrowser)) { + this._startedLoadTimer.delete(aBrowser); + TelemetryStopwatch.cancel("FX_PAGE_LOAD_MS", aBrowser); + } + } + + // We used to listen for clicks in the browser here, but when that + // became unnecessary, removing the code below caused focus issues. + // This code should be removed. Tracked in bug 1337794. + let isRemoteBrowser = aBrowser.isRemoteBrowser; + // We check isRemoteBrowser here to avoid requesting the doc CPOW + let doc = isRemoteBrowser ? null : aWebProgress.DOMWindow.document; + + if (!isRemoteBrowser && + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + Components.isSuccessCode(aStatus) && + doc.documentURI.startsWith("about:") && + !doc.documentURI.toLowerCase().startsWith("about:blank") && + !doc.documentURI.toLowerCase().startsWith("about:home") && + !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("pagehide", function onPageHide(event) { + if (event.target.defaultView.frameElement) + return; + aBrowser.removeEventListener("pagehide", onPageHide, true); + if (event.target.documentElement) + event.target.documentElement.removeAttribute("hasBrowserHandlers"); + }, true); + } + }, + + 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) { + // Reader mode actually cares about these: + let mm = gBrowser.selectedBrowser.messageManager; + mm.sendAsyncMessage("Reader:PushState", {isArticle: gBrowser.selectedBrowser.isArticle}); + return; + } + + // Filter out location changes in sub documents. + if (!aWebProgress.isTopLevel) + 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); + + let tab = gBrowser.getTabForBrowser(aBrowser); + if (tab && tab._sharingState) { + gBrowser.setBrowserSharing(aBrowser, {}); + webrtcUI.forgetStreamsFromBrowser(aBrowser); + } + + gBrowser.getNotificationBox(aBrowser).removeTransientNotifications(); + + FullZoom.onLocationChange(aLocationURI, false, aBrowser); + }, +} + +function nsBrowserAccess() { } + +nsBrowserAccess.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]), + + _openURIInNewTab: function(aURI, aReferrer, aReferrerPolicy, aIsPrivate, + aIsExternal, aForceNotRemote=false, + aUserContextId=Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID, + aOpener = null, aTriggeringPrincipal = null) { + 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 { + win = RecentWindow.getMostRecentBrowserWindow({private: aIsPrivate}); + needToFocusWin = true; + } + + if (!win) { + // we couldn't find a suitable window, a new one needs to be opened. + return null; + } + + if (aIsExternal && (!aURI || aURI.spec == "about:blank")) { + win.BrowserOpenTab(); // this also focuses the location bar + win.focus(); + return win.gBrowser.selectedBrowser; + } + + let loadInBackground = gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground"); + + let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank", { + triggeringPrincipal: aTriggeringPrincipal, + referrerURI: aReferrer, + referrerPolicy: aReferrerPolicy, + userContextId: aUserContextId, + fromExternal: aIsExternal, + inBackground: loadInBackground, + forceNotRemote: aForceNotRemote, + opener: aOpener, + }); + let browser = win.gBrowser.getBrowserForTab(tab); + + if (needToFocusWin || (!loadInBackground && aIsExternal)) + win.focus(); + + return browser; + }, + + openURI: function (aURI, aOpener, aWhere, aFlags) { + // This function should only ever be called if we're opening a URI + // from a non-remote browser window (via nsContentTreeOwner). + if (aOpener && Cu.isCrossProcessWrapper(aOpener)) { + Cu.reportError("nsBrowserAccess.openURI was passed a CPOW for aOpener. " + + "openURI should only ever be called from non-remote browsers."); + throw Cr.NS_ERROR_FAILURE; + } + + var newWindow = null; + var isExternal = !!(aFlags & 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; + } + let isPrivate = aOpener + ? PrivateBrowsingUtils.isContentWindowPrivate(aOpener) + : PrivateBrowsingUtils.isWindowPrivate(window); + + 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"; + let features = "all,dialog=no"; + if (isPrivate) { + features += ",private"; + } + // Pass all params to openDialog to ensure that "url" isn't passed through + // loadOneOrMoreURIs, which splits based on "|" + newWindow = openDialog(getBrowserURL(), "_blank", features, url, null, null, null); + break; + case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB : + // If we have an opener, that means that the caller is expecting access + // to the nsIDOMWindow of the opened tab right away. For e10s windows, + // this means forcing the newly opened browser to be non-remote so that + // we can hand back the nsIDOMWindow. The XULBrowserWindow.shouldLoadURI + // will do the job of shuttling off the newly opened browser to run in + // the right process once it starts loading a URI. + let forceNotRemote = !!aOpener; + let userContextId = aOpener && aOpener.document + ? aOpener.document.nodePrincipal.originAttributes.userContextId + : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID; + let openerWindow = (aFlags & Ci.nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener; + let browser = this._openURIInNewTab(aURI, referrer, referrerPolicy, + isPrivate, isExternal, + forceNotRemote, userContextId, + openerWindow, triggeringPrincipal); + if (browser) + newWindow = browser.contentWindow; + 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, { + triggeringPrincipal, + flags: loadflags, + referrerURI: referrer, + referrerPolicy: referrerPolicy, + }); + } + if (!gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground")) + window.focus(); + } + return newWindow; + }, + + openURIInFrame: function browser_openURIInFrame(aURI, aParams, aWhere, aFlags) { + if (aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB) { + dump("Error: openURIInFrame can only open in new tabs"); + return null; + } + + var isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + + var userContextId = aParams.openerOriginAttributes && + ("userContextId" in aParams.openerOriginAttributes) + ? aParams.openerOriginAttributes.userContextId + : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID + + let browser = this._openURIInNewTab(aURI, aParams.referrer, + aParams.referrerPolicy, + aParams.isPrivate, + isExternal, false, + userContextId, null, + aParams.triggeringPrincipal); + if (browser) + return browser.QueryInterface(Ci.nsIFrameLoaderOwner); + + return null; + }, + + isTabContentWindow: function (aWindow) { + return gBrowser.browsers.some(browser => browser.contentWindow == aWindow); + }, + + canClose() { + return CanCloseWindow(); + }, +} + +function getTogglableToolbars() { + let toolbarNodes = Array.slice(gNavToolbox.childNodes); + toolbarNodes = toolbarNodes.concat(gNavToolbox.externalToolbars); + toolbarNodes = toolbarNodes.filter(node => node.getAttribute("toolbarname")); + return toolbarNodes; +} + +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 = getTogglableToolbars(); + + for (let toolbar of toolbarNodes) { + 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", toolbar.getAttribute("toolbarname")); + menuItem.setAttribute("checked", toolbar.getAttribute(hidingAttribute) != "true"); + 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); + } + + + let moveToPanel = popup.querySelector(".customize-context-moveToPanel"); + let removeFromToolbar = popup.querySelector(".customize-context-removeFromToolbar"); + // View -> Toolbars menu doesn't have the moveToPanel or removeFromToolbar items. + if (!moveToPanel || !removeFromToolbar) { + return; + } + + // triggerNode can be a nested child element of a toolbaritem. + let toolbarItem = popup.triggerNode; + + if (toolbarItem && toolbarItem.localName == "toolbarpaletteitem") { + toolbarItem = toolbarItem.firstChild; + } else if (toolbarItem && toolbarItem.localName != "toolbar") { + while (toolbarItem && toolbarItem.parentNode) { + let parent = toolbarItem.parentNode; + if ((parent.classList && parent.classList.contains("customization-target")) || + parent.getAttribute("overflowfortoolbar") || // Needs to work in the overflow list as well. + parent.localName == "toolbarpaletteitem" || + parent.localName == "toolbar") + break; + toolbarItem = parent; + } + } else { + toolbarItem = null; + } + + let showTabStripItems = toolbarItem && toolbarItem.id == "tabbrowser-tabs"; + for (let node of popup.querySelectorAll('menuitem[contexttype="toolbaritem"]')) { + node.hidden = showTabStripItems; + } + + for (let node of popup.querySelectorAll('menuitem[contexttype="tabbar"]')) { + node.hidden = !showTabStripItems; + } + + if (showTabStripItems) { + PlacesCommandHook.updateBookmarkAllTabsCommand(); + + let haveMultipleTabs = gBrowser.visibleTabs.length > 1; + document.getElementById("toolbar-context-reloadAllTabs").disabled = !haveMultipleTabs; + + document.getElementById("toolbar-context-undoCloseTab").disabled = + SessionStore.getClosedTabCount(window) == 0; + return; + } + + // In some cases, we will exit the above loop with toolbarItem being the + // xul:document. That has no parentNode, and we should disable the items in + // this case. + let movable = toolbarItem && toolbarItem.parentNode && + CustomizableUI.isWidgetRemovable(toolbarItem); + if (movable) { + moveToPanel.removeAttribute("disabled"); + removeFromToolbar.removeAttribute("disabled"); + } else { + moveToPanel.setAttribute("disabled", true); + removeFromToolbar.setAttribute("disabled", true); + } +} + +function onViewToolbarCommand(aEvent) { + var toolbarId = aEvent.originalTarget.getAttribute("toolbarId"); + var isVisible = aEvent.originalTarget.getAttribute("checked") == "true"; + CustomizableUI.setToolbarVisibility(toolbarId, isVisible); +} + +function setToolbarVisibility(toolbar, isVisible, persist=true) { + let hidingAttribute; + if (toolbar.getAttribute("type") == "menubar") { + hidingAttribute = "autohide"; + if (AppConstants.platform == "linux") { + Services.prefs.setBoolPref("ui.key.menuAccessKeyFocuses", !isVisible); + } + } else { + hidingAttribute = "collapsed"; + } + + toolbar.setAttribute(hidingAttribute, !isVisible); + if (persist) { + document.persist(toolbar.id, hidingAttribute); + } + + let eventParams = { + detail: { + visible: isVisible + }, + bubbles: true + }; + let event = new CustomEvent("toolbarvisibilitychange", eventParams); + toolbar.dispatchEvent(event); + + PlacesToolbarHelper.init(); + BookmarkingUI.onToolbarVisibilityChange(); + if (isVisible) + ToolbarIconColor.inferFromText(); +} + +var TabletModeUpdater = { + init() { + if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) { + this.update(WindowsUIUtils.inTabletMode); + Services.obs.addObserver(this, "tablet-mode-change", false); + } + }, + + uninit() { + if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) { + Services.obs.removeObserver(this, "tablet-mode-change"); + } + }, + + observe(subject, topic, data) { + this.update(data == "tablet-mode"); + }, + + update(isInTabletMode) { + let wasInTabletMode = document.documentElement.hasAttribute("tabletmode"); + if (isInTabletMode) { + document.documentElement.setAttribute("tabletmode", "true"); + } else { + document.documentElement.removeAttribute("tabletmode"); + } + if (wasInTabletMode != isInTabletMode) { + TabsInTitlebar.updateAppearance(true); + } + }, +}; + +var gTabletModePageCounter = { + enabled: false, + inc() { + this.enabled = AppConstants.isPlatformAndVersionAtLeast("win", "10.0"); + if (!this.enabled) { + this.inc = () => {}; + return; + } + this.inc = this._realInc; + this.inc(); + }, + + _desktopCount: 0, + _tabletCount: 0, + _realInc() { + let inTabletMode = document.documentElement.hasAttribute("tabletmode"); + this[inTabletMode ? "_tabletCount" : "_desktopCount"]++; + }, + + finish() { + if (this.enabled) { + let histogram = Services.telemetry.getKeyedHistogramById("FX_TABLETMODE_PAGE_LOAD"); + histogram.add("tablet", this._tabletCount); + histogram.add("desktop", this._desktopCount); + } + }, +}; + +function displaySecurityInfo() +{ + BrowserPageInfo(null, "securityTab"); +} + + +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 (["about:home", "about:newtab"].includes(homePage.toLowerCase())) + 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; + }, +}; + +const nodeToTooltipMap = { + "bookmarks-menu-button": "bookmarksMenuButton.tooltip", + "new-window-button": "newWindowButton.tooltip", + "new-tab-button": "newTabButton.tooltip", + "tabs-newtab-button": "newTabButton.tooltip", + "fullscreen-button": "fullscreenButton.tooltip", + "downloads-button": "downloads.tooltip", +}; +const nodeToShortcutMap = { + "bookmarks-menu-button": "manBookmarkKb", + "new-window-button": "key_newNavigator", + "new-tab-button": "key_newNavigatorTab", + "tabs-newtab-button": "key_newNavigatorTab", + "fullscreen-button": "key_fullScreen", + "downloads-button": "key_openDownloads" +}; + +if (AppConstants.platform == "macosx") { + nodeToTooltipMap["print-button"] = "printButton.tooltip"; + nodeToShortcutMap["print-button"] = "printKb"; +} + +const gDynamicTooltipCache = new Map(); +function UpdateDynamicShortcutTooltipText(aTooltip) { + let nodeId = aTooltip.triggerNode.id || aTooltip.triggerNode.getAttribute("anonid"); + if (!gDynamicTooltipCache.has(nodeId) && nodeId in nodeToTooltipMap) { + let strId = nodeToTooltipMap[nodeId]; + let args = []; + if (nodeId in nodeToShortcutMap) { + let shortcutId = nodeToShortcutMap[nodeId]; + let shortcut = document.getElementById(shortcutId); + if (shortcut) { + args.push(ShortcutUtils.prettifyShortcut(shortcut)); + } + } + gDynamicTooltipCache.set(nodeId, gNavigatorBundle.getFormattedString(strId, args)); + } + aTooltip.setAttribute("label", gDynamicTooltipCache.get(nodeId)); +} + +function getBrowserSelection(aCharLen) { + Deprecated.warning("getBrowserSelection", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1134769"); + + let focusedElement = document.activeElement; + if (focusedElement && focusedElement.localName == "browser" && + focusedElement.isRemoteBrowser) { + throw "getBrowserSelection doesn't support child process windows"; + } + + return BrowserUtils.getSelectionDetails(window, aCharLen).text; +} + +var gWebPanelURI; +function openWebPanel(title, uri) { + // Ensure that the web panels sidebar is open. + SidebarUI.show("viewWebPanelsSidebar"); + + // Set the title of the panel. + SidebarUI.title = title; + + // Tell the Web Panels sidebar to load the bookmark. + if (SidebarUI.browser.docShell && SidebarUI.browser.contentDocument && + SidebarUI.browser.contentDocument.getElementById("web-panels-browser")) { + SidebarUI.browser.contentWindow.loadWebPanel(uri); + if (gWebPanelURI) { + gWebPanelURI = ""; + SidebarUI.browser.removeEventListener("load", asyncOpenWebPanel, true); + } + } else { + // The panel is still being constructed. Attach an onload handler. + if (!gWebPanelURI) { + SidebarUI.browser.addEventListener("load", asyncOpenWebPanel, true); + } + gWebPanelURI = uri; + } +} + +function asyncOpenWebPanel(event) { + if (gWebPanelURI && SidebarUI.browser.contentDocument && + SidebarUI.browser.contentDocument.getElementById("web-panels-browser")) { + SidebarUI.browser.contentWindow.loadWebPanel(gWebPanelURI); + } + gWebPanelURI = ""; + SidebarUI.browser.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.getAttribute("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; + } + + var referrerURI = doc.documentURIObject; + // if the mixedContentChannel is present and the referring URI passes + // a same origin check with the target URI, we can preserve the users + // decision of disabling MCB on a page for it's child tabs. + var persistAllowMixedContentInChildTab = false; + + if (where == "tab" && gBrowser.docShell.mixedContentChannel) { + const sm = Services.scriptSecurityManager; + try { + var targetURI = makeURI(href); + sm.checkSameOriginURI(referrerURI, targetURI, false); + persistAllowMixedContentInChildTab = true; + } + catch (e) { } + } + + // first get document wide referrer policy, then + // get referrer attribute from clicked link and parse it and + // allow per element referrer to overrule the document wide referrer if enabled + let referrerPolicy = doc.referrerPolicy; + if (Services.prefs.getBoolPref("network.http.enablePerElementReferrer") && + linkNode) { + let referrerAttrValue = Services.netUtils.parseAttributePolicyString(linkNode. + getAttribute("referrerpolicy")); + if (referrerAttrValue != Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) { + referrerPolicy = referrerAttrValue; + } + } + + urlSecurityCheck(href, doc.nodePrincipal); + let params = { + charset: doc.characterSet, + allowMixedContent: persistAllowMixedContentInChildTab, + referrerURI: referrerURI, + referrerPolicy: referrerPolicy, + noReferrer: BrowserUtils.linkHasNoReferrer(linkNode), + originPrincipal: doc.nodePrincipal, + triggeringPrincipal: doc.nodePrincipal, + }; + + // The new tab/window must use the same userContextId + if (doc.nodePrincipal.originAttributes.userContextId) { + params.userContextId = doc.nodePrincipal.originAttributes.userContextId; + } + + openLinkIn(href, where, params); + 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, ""); + + clipboard = stripUnsafeProtocolOnPaste(clipboard); + + // 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 }); + } + }); + + event.stopPropagation(); +} + +function stripUnsafeProtocolOnPaste(pasteData) { + // Don't allow pasting javascript URIs since we don't support + // LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL for those. + return pasteData.replace(/\r?\n/g, "").replace(/^(?:\W*javascript:)+/i, ""); +} + +// 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 userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid"); + + // event is null if links are dropped in content process. + // inBackground should be false, as it's loading into current browser. + let inBackground = false; + if (event) { + inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + if (event.shiftKey) + inBackground = !inBackground; + } + + 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, + userContextId, + }); + } + }); + + // If links are dropped in content process, event.preventDefault() should be + // called in content process. + if (event) { + // Keep the event from being handled by the dragDrop listeners + // built-in to gecko if they happen to be above us. + event.preventDefault(); + } +} + +function BrowserSetForcedCharacterSet(aCharset) +{ + if (aCharset) { + gBrowser.selectedBrowser.characterSet = 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 UpdateCurrentCharset(target) { + let selectedCharset = CharsetMenu.foldCharset(gBrowser.selectedBrowser.characterSet); + for (let menuItem of target.getElementsByTagName("menuitem")) { + let isSelected = menuItem.getAttribute("charset") === selectedCharset; + menuItem.setAttribute("checked", isSelected); + } +} + +var gPageStyleMenu = { + // This maps from a <browser> element (or, more specifically, a + // browser's permanentKey) to an Object that contains the most recent + // information about the browser content's stylesheets. That Object + // is populated via the PageStyle:StyleSheets message from the content + // process. The Object should have the following structure: + // + // filteredStyleSheets (Array): + // An Array of objects with a filtered list representing all stylesheets + // that the current page offers. Each object has the following members: + // + // title (String): + // The title of the stylesheet + // + // disabled (bool): + // Whether or not the stylesheet is currently applied + // + // href (String): + // The URL of the stylesheet. Stylesheets loaded via a data URL will + // have this property set to null. + // + // authorStyleDisabled (bool): + // Whether or not the user currently has "No Style" selected for + // the current page. + // + // preferredStyleSheetSet (bool): + // Whether or not the user currently has the "Default" style selected + // for the current page. + // + _pageStyleSheets: new WeakMap(), + + init: function() { + let mm = window.messageManager; + mm.addMessageListener("PageStyle:StyleSheets", (msg) => { + this._pageStyleSheets.set(msg.target.permanentKey, msg.data); + }); + }, + + /** + * Returns an array of Objects representing stylesheets in a + * browser. Note that the pageshow event needs to fire in content + * before this information will be available. + * + * @param browser (optional) + * The <xul:browser> to search for stylesheets. If omitted, this + * defaults to the currently selected tab's browser. + * @returns Array + * An Array of Objects representing stylesheets in the browser. + * See the documentation for gPageStyleMenu for a description + * of the Object structure. + */ + getBrowserStyleSheets: function (browser) { + if (!browser) { + browser = gBrowser.selectedBrowser; + } + + let data = this._pageStyleSheets.get(browser.permanentKey); + if (!data) { + return []; + } + return data.filteredStyleSheets; + }, + + _getStyleSheetInfo: function (browser) { + let data = this._pageStyleSheets.get(browser.permanentKey); + if (!data) { + return { + filteredStyleSheets: [], + authorStyleDisabled: false, + preferredStyleSheetSet: true + }; + } + + return data; + }, + + fillPopup: function (menuPopup) { + let styleSheetInfo = this._getStyleSheetInfo(gBrowser.selectedBrowser); + var noStyle = menuPopup.firstChild; + var persistentOnly = noStyle.nextSibling; + var sep = persistentOnly.nextSibling; + while (sep.nextSibling) + menuPopup.removeChild(sep.nextSibling); + + let styleSheets = styleSheetInfo.filteredStyleSheets; + var currentStyleSheets = {}; + var styleDisabled = styleSheetInfo.authorStyleDisabled; + var haveAltSheets = false; + var altStyleSelected = false; + + for (let currentStyleSheet of styleSheets) { + 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 = styleSheetInfo.preferredStyleSheetSet ? haveAltSheets : false; + sep.hidden = (noStyle.hidden && persistentOnly.hidden) || !haveAltSheets; + }, + + switchStyleSheet: function (title) { + let mm = gBrowser.selectedBrowser.messageManager; + mm.sendAsyncMessage("PageStyle:Switch", {title: title}); + }, + + disableStyle: function () { + let mm = gBrowser.selectedBrowser.messageManager; + mm.sendAsyncMessage("PageStyle:Disable"); + }, +}; + +/* Legacy global page-style functions */ +var stylesheetFillPopup = gPageStyleMenu.fillPopup.bind(gPageStyleMenu); +function stylesheetSwitchAll(contentWindow, title) { + // We ignore the contentWindow param. Add-ons don't appear to use + // it, and it's difficult to support in e10s (where it will be a + // CPOW). + gPageStyleMenu.switchStyleSheet(title); +} +function setStyleDisabled(disabled) { + if (disabled) + gPageStyleMenu.disableStyle(); +} + + +var LanguageDetectionListener = { + init: function() { + window.messageManager.addMessageListener("Translation:DocumentState", msg => { + Translation.documentStateReceived(msg.target, msg.data); + }); + } +}; + + +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; + + 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 notification is also received because of a loss in connectivity, + // which we ignore by updating the UI to the current value of io.offline + this._updateOfflineUI(Services.io.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 = { + warnUsage(browser, uri) { + if (!browser) + return; + + let mainAction = { + label: gNavigatorBundle.getString("offlineApps.manageUsage"), + accessKey: gNavigatorBundle.getString("offlineApps.manageUsageAccessKey"), + callback: this.manage + }; + + let warnQuotaKB = Services.prefs.getIntPref("offline-apps.quota.warn"); + // This message shows the quota in MB, and so we divide the quota (in kb) by 1024. + let message = gNavigatorBundle.getFormattedString("offlineApps.usage", + [ uri.host, + warnQuotaKB / 1024 ]); + + let anchorID = "indexedDB-notification-icon"; + PopupNotifications.show(browser, "offline-app-usage", message, + anchorID, mainAction); + + // Now that we've warned once, prevent the warning from showing up + // again. + Services.perms.add(uri, "offline-app", + Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN); + }, + + // XXX: duplicated in preferences/advanced.js + _getOfflineAppUsage(host, groups) { + let cacheService = Cc["@mozilla.org/network/application-cache-service;1"]. + getService(Ci.nsIApplicationCacheService); + if (!groups) { + try { + groups = cacheService.getGroups(); + } catch (ex) { + return 0; + } + } + + let usage = 0; + for (let group of groups) { + let uri = Services.io.newURI(group, null, null); + if (uri.asciiHost == host) { + let cache = cacheService.getActiveCache(group); + usage += cache.usage; + } + } + + return usage; + }, + + _usedMoreThanWarnQuota(uri) { + // if the user has already allowed excessive usage, don't bother checking + if (Services.perms.testExactPermission(uri, "offline-app") != + Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN) { + let usageBytes = this._getOfflineAppUsage(uri.asciiHost); + let warnQuotaKB = Services.prefs.getIntPref("offline-apps.quota.warn"); + // The pref is in kb, the usage we get is in bytes, so multiply the quota + // to compare correctly: + if (usageBytes >= warnQuotaKB * 1024) { + return true; + } + } + + return false; + }, + + requestPermission(browser, docId, uri) { + let host = uri.asciiHost; + let notificationID = "offline-app-requested-" + host; + let notification = PopupNotifications.getNotification(notificationID, browser); + + if (notification) { + notification.options.controlledItems.push([ + Cu.getWeakReference(browser), docId, uri + ]); + } else { + let mainAction = { + label: gNavigatorBundle.getString("offlineApps.allow"), + accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"), + callback: function() { + for (let [browser, docId, uri] of notification.options.controlledItems) { + OfflineApps.allowSite(browser, docId, uri); + } + } + }; + let secondaryActions = [{ + label: gNavigatorBundle.getString("offlineApps.never"), + accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"), + callback: function() { + for (let [, , uri] of notification.options.controlledItems) { + OfflineApps.disallowSite(uri); + } + } + }]; + let message = gNavigatorBundle.getFormattedString("offlineApps.available", + [host]); + let anchorID = "indexedDB-notification-icon"; + let options = { + controlledItems : [[Cu.getWeakReference(browser), docId, uri]] + }; + notification = PopupNotifications.show(browser, notificationID, message, + anchorID, mainAction, + secondaryActions, options); + } + }, + + disallowSite(uri) { + Services.perms.add(uri, "offline-app", Services.perms.DENY_ACTION); + }, + + allowSite(browserRef, docId, uri) { + Services.perms.add(uri, "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. + let browser = browserRef.get(); + if (browser && browser.messageManager) { + browser.messageManager.sendAsyncMessage("OfflineApps:StartFetching", { + docId, + }); + } + }, + + manage() { + openAdvancedPreferences("networkTab"); + }, + + receiveMessage(msg) { + switch (msg.name) { + case "OfflineApps:CheckUsage": + let uri = makeURI(msg.data.uri); + if (this._usedMoreThanWarnQuota(uri)) { + this.warnUsage(msg.target, uri); + } + break; + case "OfflineApps:RequestPermission": + this.requestPermission(msg.target, msg.data.docId, makeURI(msg.data.uri)); + break; + } + }, + + init() { + let mm = window.messageManager; + mm.addMessageListener("OfflineApps:CheckUsage", this); + mm.addMessageListener("OfflineApps:RequestPermission", this); + }, +}; + +var IndexedDBPromptHelper = { + _permissionsPrompt: "indexedDB-permissions-prompt", + _permissionsResponse: "indexedDB-permissions-response", + + _notificationIcon: "indexedDB-notification-icon", + + init: + function IndexedDBPromptHelper_init() { + Services.obs.addObserver(this, this._permissionsPrompt, false); + }, + + uninit: + function IndexedDBPromptHelper_uninit() { + Services.obs.removeObserver(this, this._permissionsPrompt); + }, + + observe: + function IndexedDBPromptHelper_observe(subject, topic, data) { + if (topic != this._permissionsPrompt) { + throw new Error("Unexpected topic!"); + } + + var requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor); + + var browser = requestor.getInterface(Ci.nsIDOMNode); + if (browser.ownerGlobal != window) { + // Only listen for notifications for browsers in our chrome window. + return; + } + + var host = browser.currentURI.asciiHost; + + var message; + var responseTopic; + if (topic == this._permissionsPrompt) { + message = gNavigatorBundle.getFormattedString("offlineApps.available", + [ host ]); + responseTopic = this._permissionsResponse; + } + + 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(). + 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); + } + } + }; + + 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 CanCloseWindow() +{ + // Avoid redundant calls to canClose from showing multiple + // PermitUnload dialogs. + if (Services.startup.shuttingDown || window.skipNextCanClose) { + return true; + } + + for (let browser of gBrowser.browsers) { + let {permitUnload, timedOut} = browser.permitUnload(); + if (timedOut) { + return true; + } + if (!permitUnload) { + return false; + } + } + return true; +} + +function WindowIsClosing() +{ + if (!closeWindow(false, warnAboutClosingWindow)) + return false; + + // In theory we should exit here and the Window's internal Close + // method should trigger canClose on nsBrowserAccess. However, by + // that point it's too late to be able to show a prompt for + // PermitUnload. So we do it here, when we still can. + if (CanCloseWindow()) { + // This flag ensures that the later canClose call does nothing. + // It's only needed to make tests pass, since they detect the + // prompt even when it's not actually shown. + window.skipNextCanClose = true; + return true; + } + + return false; +} + +/** + * 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; we also ignore private windows. + let isPBWindow = PrivateBrowsingUtils.isWindowPrivate(window) && + !PrivateBrowsingUtils.permanentPrivateBrowsing; + if (!isPBWindow && !toolbar.visible) + return gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL); + + // Figure out if there's at least one other browser window around. + let otherPBWindowExists = false; + let nonPopupPresent = false; + for (let win of browserWindows()) { + if (!win.closed && 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); + + // 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 AppConstants.platform != "macosx" + || (isPBWindow || gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL)); +} + +var MailIntegration = { + sendLinkForBrowser: function (aBrowser) { + this.sendMessage(aBrowser.currentURI.spec, aBrowser.contentTitle); + }, + + 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) { + return new Promise(resolve => { + 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(); + resolve(emWindow); + return; + } + } + + 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); + resolve(aSubject); + }, "EM-loaded", false); + } else { + resolve(); + } + }); +} + +function AddKeywordForSearchField() { + let mm = gBrowser.selectedBrowser.messageManager; + + let onMessage = (message) => { + mm.removeMessageListener("ContextMenu:SearchFieldBookmarkData:Result", onMessage); + + let bookmarkData = message.data; + let title = gNavigatorBundle.getFormattedString("addKeywordTitleAutoFill", + [bookmarkData.title]); + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: makeURI(bookmarkData.spec) + , title: title + , description: bookmarkData.description + , keyword: "" + , postData: bookmarkData.postData + , charSet: bookmarkData.charset + , hiddenRows: [ "location" + , "description" + , "tags" + , "loadInSidebar" ] + }, window); + } + mm.addMessageListener("ContextMenu:SearchFieldBookmarkData:Result", onMessage); + + mm.sendAsyncMessage("ContextMenu:SearchFieldBookmarkData", {}, { target: gContextMenu.target }); +} + +/** + * Re-open a closed tab. + * @param aIndex + * The index of the tab (via SessionStore.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 && isTabEmpty(gBrowser.selectedTab)) + blankTabToRemove = gBrowser.selectedTab; + + var tab = null; + if (SessionStore.getClosedTabCount(window) > (aIndex || 0)) { + tab = SessionStore.undoCloseTab(window, aIndex || 0); + + if (blankTabToRemove) + gBrowser.removeTab(blankTabToRemove); + } + + return tab; +} + +/** + * Re-open a closed window. + * @param aIndex + * The index of the window (via SessionStore.getClosedWindowData) + * @returns a reference to the reopened window. + */ +function undoCloseWindow(aIndex) { + let window = null; + if (SessionStore.getClosedWindowCount() > (aIndex || 0)) + window = SessionStore.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; + + if (aTab.hasAttribute("customizemode")) + return false; + + let browser = aTab.linkedBrowser; + if (!isBlankPageURL(browser.currentURI.spec)) + return false; + + if (!checkEmptyPageOrigin(browser)) + return false; + + if (browser.canGoForward || browser.canGoBack) + return false; + + return true; +} + +/** + * Check whether a page can be considered as 'empty', that its URI + * reflects its origin, and that if it's loaded in a tab, that tab + * could be considered 'empty' (e.g. like the result of opening + * a 'blank' new tab). + * + * We have to do more than just check the URI, because especially + * for things like about:blank, it is possible that the opener or + * some other page has control over the contents of the page. + * + * @param browser {Browser} + * The browser whose page we're checking (the selected browser + * in this window if omitted). + * @param uri {nsIURI} + * The URI against which we're checking (the browser's currentURI + * if omitted). + * + * @return false if the page was opened by or is controlled by arbitrary web + * content, unless that content corresponds with the URI. + * true if the page is blank and controlled by a principal matching + * that URI (or the system principal if the principal has no URI) + */ +function checkEmptyPageOrigin(browser = gBrowser.selectedBrowser, + uri = browser.currentURI) { + // If another page opened this page with e.g. window.open, this page might + // be controlled by its opener - return false. + if (browser.hasContentOpener) { + return false; + } + let contentPrincipal = browser.contentPrincipal; + // Not all principals have URIs... + if (contentPrincipal.URI) { + // There are two specialcases involving about:blank. One is where + // the user has manually loaded it and it got created with a null + // principal. The other involves the case where we load + // some other empty page in a browser and the current page is the + // initial about:blank page (which has that as its principal, not + // just URI in which case it could be web-based). Especially in + // e10s, we need to tackle that case specifically to avoid race + // conditions when updating the URL bar. + if ((uri.spec == "about:blank" && contentPrincipal.isNullPrincipal) || + contentPrincipal.URI.spec == "about:blank") { + return true; + } + return contentPrincipal.URI.equals(uri); + } + // ... so for those that don't have them, enforce that the page has the + // system principal (this matches e.g. on about:newtab). + let ssm = Services.scriptSecurityManager; + return ssm.isSystemPrincipal(contentPrincipal); +} + +function BrowserOpenSyncTabs() { + gSyncUI.openSyncedTabsPanel(); +} + +/** + * 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 = { + /** + * nsIURI for which the identity UI is displayed. This has been already + * processed by nsIURIFixup.createExposableURI. + */ + _uri: null, + + /** + * We only know the connection type if this._uri has a defined "host" part. + * + * These URIs, like "about:" and "data:" URIs, will usually be treated as a + * non-secure connection, unless they refer to an internally implemented + * browser page or resolve to "file:" URIs. + */ + _uriHasHost: false, + + /** + * Whether this._uri refers to an internally implemented browser page. + * + * Note that this is set for some "about:" pages, but general "chrome:" URIs + * are not included in this category by default. + */ + _isSecureInternalUI: false, + + /** + * nsISSLStatus metadata provided by gBrowser.securityUI the last time the + * identity UI was updated, or null if the connection is not secure. + */ + _sslStatus: null, + + /** + * Bitmask provided by nsIWebProgressListener.onSecurityChange. + */ + _state: 0, + + /** + * This flag gets set if the identity popup was opened by a keypress, + * to be able to focus it on the popupshown event. + */ + _popupTriggeredByKeyboard: false, + + /** + * Whether a permission is just removed from permission list. + */ + _permissionJustRemoved: false, + + get _isBroken() { + return this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN; + }, + + get _isSecure() { + // If a <browser> is included within a chrome document, then this._state + // will refer to the security state for the <browser> and not the top level + // document. In this case, don't upgrade the security state in the UI + // with the secure state of the embedded <browser>. + return !this._isURILoadedFromFile && this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE; + }, + + get _isEV() { + // If a <browser> is included within a chrome document, then this._state + // will refer to the security state for the <browser> and not the top level + // document. In this case, don't upgrade the security state in the UI + // with the EV state of the embedded <browser>. + return !this._isURILoadedFromFile && this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL; + }, + + get _isMixedActiveContentLoaded() { + return this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT; + }, + + get _isMixedActiveContentBlocked() { + return this._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT; + }, + + get _isMixedPassiveContentLoaded() { + return this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT; + }, + + get _isCertUserOverridden() { + return this._state & Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN; + }, + + get _hasInsecureLoginForms() { + // checks if the page has been flagged for an insecure login. Also checks + // if the pref to degrade the UI is set to true + return LoginManagerParent.hasInsecureLoginForms(gBrowser.selectedBrowser) && + Services.prefs.getBoolPref("security.insecure_password.ui.enabled"); + }, + + // smart getters + 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 _identityPopupMultiView () { + delete _identityPopupMultiView; + return document.getElementById("identity-popup-multiView"); + }, + get _identityPopupContentHosts () { + delete this._identityPopupContentHosts; + let selector = ".identity-popup-headline.host"; + return this._identityPopupContentHosts = [ + ...this._identityPopupMultiView._mainView.querySelectorAll(selector), + ...document.querySelectorAll(selector) + ]; + }, + get _identityPopupContentHostless () { + delete this._identityPopupContentHostless; + let selector = ".identity-popup-headline.hostless"; + return this._identityPopupContentHostless = [ + ...this._identityPopupMultiView._mainView.querySelectorAll(selector), + ...document.querySelectorAll(selector) + ]; + }, + 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 _identityPopupMixedContentLearnMore () { + delete this._identityPopupMixedContentLearnMore; + return this._identityPopupMixedContentLearnMore = + document.getElementById("identity-popup-mcb-learn-more"); + }, + get _identityPopupInsecureLoginFormsLearnMore () { + delete this._identityPopupInsecureLoginFormsLearnMore; + return this._identityPopupInsecureLoginFormsLearnMore = + document.getElementById("identity-popup-insecure-login-forms-learn-more"); + }, + get _identityIconLabels () { + delete this._identityIconLabels; + return this._identityIconLabels = document.getElementById("identity-icon-labels"); + }, + get _identityIconLabel () { + delete this._identityIconLabel; + return this._identityIconLabel = document.getElementById("identity-icon-label"); + }, + get _connectionIcon () { + delete this._connectionIcon; + return this._connectionIcon = document.getElementById("connection-icon"); + }, + 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("identity-icon"); + }, + get _permissionList () { + delete this._permissionList; + return this._permissionList = document.getElementById("identity-popup-permission-list"); + }, + get _permissionEmptyHint() { + delete this._permissionEmptyHint; + return this._permissionEmptyHint = document.getElementById("identity-popup-permission-empty-hint"); + }, + get _permissionReloadHint () { + delete this._permissionReloadHint; + return this._permissionReloadHint = document.getElementById("identity-popup-permission-reload-hint"); + }, + get _permissionAnchors () { + delete this._permissionAnchors; + let permissionAnchors = {}; + for (let anchor of document.getElementById("blocked-permissions-container").children) { + permissionAnchors[anchor.getAttribute("data-permission-id")] = anchor; + } + return this._permissionAnchors = permissionAnchors; + }, + + /** + * Handler for mouseclicks on the "More Information" button in the + * "identity-popup" panel. + */ + handleMoreInfoClick : function(event) { + displaySecurityInfo(); + event.stopPropagation(); + this._identityPopup.hidePopup(); + }, + + toggleSubView(name, anchor) { + let view = this._identityPopupMultiView; + if (view.showingSubView) { + view.showMainView(); + } else { + view.showSubView(`identity-popup-${name}View`, anchor); + } + + // If an element is focused that's not the anchor, clear the focus. + // Elements of hidden views have -moz-user-focus:ignore but setting that + // per CSS selector doesn't blur a focused element in those hidden views. + if (Services.focus.focusedElement != anchor) { + Services.focus.clearFocus(window); + } + }, + + disableMixedContentProtection() { + // Use telemetry to measure how often unblocking happens + const kMIXED_CONTENT_UNBLOCK_EVENT = 2; + let histogram = + Services.telemetry.getHistogramById( + "MIXED_CONTENT_UNBLOCK_COUNTER"); + histogram.add(kMIXED_CONTENT_UNBLOCK_EVENT); + // Reload the page with the content unblocked + BrowserReloadWithFlags( + Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT); + this._identityPopup.hidePopup(); + }, + + enableMixedContentProtection() { + gBrowser.selectedBrowser.messageManager.sendAsyncMessage( + "MixedContent:ReenableProtection", {}); + BrowserReload(); + this._identityPopup.hidePopup(); + }, + + removeCertException() { + if (!this._uriHasHost) { + Cu.reportError("Trying to revoke a cert exception on a URI without a host?"); + return; + } + let host = this._uri.host; + let port = this._uri.port > 0 ? this._uri.port : 443; + this._overrideService.clearValidityOverride(host, port); + BrowserReloadSkipCache(); + this._identityPopup.hidePopup(); + }, + + /** + * Helper to parse out the important parts of _sslStatus (of the SSL cert in + * particular) for use in constructing identity UI strings + */ + getIdentityData : function() { + var result = {}; + var cert = this._sslStatus.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; + }, + + /** + * Update the identity user interface for the page currently being displayed. + * + * This examines the SSL certificate metadata, if available, as well as the + * connection type and other security-related state information for the page. + * + * @param state + * Bitmask provided by nsIWebProgressListener.onSecurityChange. + * @param uri + * nsIURI for which the identity UI should be displayed, already + * processed by nsIURIFixup.createExposableURI. + */ + updateIdentity(state, uri) { + let shouldHidePopup = this._uri && (this._uri.spec != uri.spec); + this._state = state; + + // Firstly, populate the state properties required to display the UI. See + // the documentation of the individual properties for details. + this.setURI(uri); + this._sslStatus = gBrowser.securityUI + .QueryInterface(Ci.nsISSLStatusProvider) + .SSLStatus; + if (this._sslStatus) { + this._sslStatus.QueryInterface(Ci.nsISSLStatus); + } + + // Then, update the user interface with the available data. + this.refreshIdentityBlock(); + // Handle a location change while the Control Center is focused + // by closing the popup (bug 1207542) + if (shouldHidePopup) { + this._identityPopup.hidePopup(); + } + this.showWeakCryptoInfoBar(); + + // NOTE: We do NOT update the identity popup (the control center) when + // we receive a new security state on the existing page (i.e. from a + // subframe). If the user opened the popup and looks at the provided + // information we don't want to suddenly change the panel contents. + }, + + /** + * This is called asynchronously when requested by the Logins module, after + * the insecure login forms state for the page has been updated. + */ + refreshForInsecureLoginForms() { + // Check this._uri because we don't want to refresh the user interface if + // this is called before the first page load in the window for any reason. + if (!this._uri) { + Cu.reportError("Unexpected early call to refreshForInsecureLoginForms."); + return; + } + this.refreshIdentityBlock(); + }, + + updateSharingIndicator() { + let tab = gBrowser.selectedTab; + let sharing = tab.getAttribute("sharing"); + if (sharing) + this._identityBox.setAttribute("sharing", sharing); + else + this._identityBox.removeAttribute("sharing"); + + this._sharingState = tab._sharingState; + + if (this._identityPopup.state == "open") { + this._handleHeightChange(() => this.updateSitePermissions()); + } + }, + + /** + * Attempt to provide proper IDN treatment for host names + */ + getEffectiveHost: function() { + if (!this._IDNService) + this._IDNService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + try { + return this._IDNService.convertToDisplayIDN(this._uri.host, {}); + } catch (e) { + // If something goes wrong (e.g. host is an IP address) just fail back + // to the full domain. + return this._uri.host; + } + }, + + /** + * Return the CSS class name to set on the "fullscreen-warning" element to + * display information about connection security in the notification shown + * when a site enters the fullscreen mode. + */ + get pointerlockFsWarningClassName() { + // Note that the fullscreen warning does not handle _isSecureInternalUI. + if (this._uriHasHost && this._isEV) { + return "verifiedIdentity"; + } + if (this._uriHasHost && this._isSecure) { + return "verifiedDomain"; + } + return "unknownIdentity"; + }, + + /** + * Updates the identity block user interface with the data from this object. + */ + refreshIdentityBlock() { + if (!this._identityBox) { + return; + } + + let icon_label = ""; + let tooltip = ""; + let icon_country_label = ""; + let icon_labels_dir = "ltr"; + + if (this._isSecureInternalUI) { + this._identityBox.className = "chromeUI"; + let brandBundle = document.getElementById("bundle_brand"); + icon_label = brandBundle.getString("brandShorterName"); + } else if (this._uriHasHost && this._isEV) { + this._identityBox.className = "verifiedIdentity"; + if (this._isMixedActiveContentBlocked) { + this._identityBox.classList.add("mixedActiveBlocked"); + } + + if (!this._isCertUserOverridden) { + // 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"; + } + + } else if (this._uriHasHost && this._isSecure) { + this._identityBox.className = "verifiedDomain"; + if (this._isMixedActiveContentBlocked) { + this._identityBox.classList.add("mixedActiveBlocked"); + } + if (!this._isCertUserOverridden) { + // It's a normal cert, verifier is the CA Org. + tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier", + [this.getIdentityData().caOrg]); + } + } else { + this._identityBox.className = "unknownIdentity"; + if (this._isBroken) { + if (this._isMixedActiveContentLoaded) { + this._identityBox.classList.add("mixedActiveContent"); + } else if (this._isMixedActiveContentBlocked) { + this._identityBox.classList.add("mixedDisplayContentLoadedActiveBlocked"); + } else if (this._isMixedPassiveContentLoaded) { + this._identityBox.classList.add("mixedDisplayContent"); + } else { + this._identityBox.classList.add("weakCipher"); + } + } + if (this._hasInsecureLoginForms) { + // Insecure login forms can only be present on "unknown identity" + // pages, either already insecure or with mixed active content loaded. + this._identityBox.classList.add("insecureLoginForms"); + } + } + + if (this._isCertUserOverridden) { + this._identityBox.classList.add("certUserOverridden"); + // Cert is trusted because of a security exception, verifier is a special string. + tooltip = gNavigatorBundle.getString("identity.identified.verified_by_you"); + } + + let permissionAnchors = this._permissionAnchors; + + // hide all permission icons + for (let icon of Object.values(permissionAnchors)) { + icon.removeAttribute("showing"); + } + + // keeps track if we should show an indicator that there are active permissions + let hasGrantedPermissions = false; + + // show permission icons + for (let permission of SitePermissions.getAllByURI(this._uri)) { + if (permission.state === SitePermissions.BLOCK) { + + let icon = permissionAnchors[permission.id]; + if (icon) { + icon.setAttribute("showing", "true"); + } + + } else if (permission.state === SitePermissions.ALLOW || + permission.state === SitePermissions.SESSION) { + hasGrantedPermissions = true; + } + } + + if (hasGrantedPermissions) { + this._identityBox.classList.add("grantedPermissions"); + } + + // Push the appropriate strings out to the UI + this._connectionIcon.tooltipText = tooltip; + this._identityIconLabels.tooltipText = tooltip; + this._identityIcon.tooltipText = gNavigatorBundle.getString("identity.icon.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; + }, + + /** + * Show the weak crypto notification bar. + */ + showWeakCryptoInfoBar() { + if (!this._uriHasHost || !this._isBroken || !this._sslStatus.cipherName || + this._sslStatus.cipherName.indexOf("_RC4_") < 0) { + return; + } + + let notificationBox = gBrowser.getNotificationBox(); + let notification = notificationBox.getNotificationWithValue("weak-crypto"); + if (notification) { + return; + } + + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let message = gNavigatorBundle.getFormattedString("weakCryptoOverriding.message", + [brandShortName]); + + let host = this._uri.host; + let port = 443; + try { + if (this._uri.port > 0) { + port = this._uri.port; + } + } catch (e) {} + + let buttons = [{ + label: gNavigatorBundle.getString("revokeOverride.label"), + accessKey: gNavigatorBundle.getString("revokeOverride.accesskey"), + callback: function (aNotification, aButton) { + try { + let weakCryptoOverride = Cc["@mozilla.org/security/weakcryptooverride;1"] + .getService(Ci.nsIWeakCryptoOverride); + weakCryptoOverride.removeWeakCryptoOverride(host, port, + PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)); + BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); + } catch (e) { + Cu.reportError(e); + } + } + }]; + + const priority = notificationBox.PRIORITY_WARNING_MEDIUM; + notificationBox.appendNotification(message, "weak-crypto", null, + priority, buttons); + }, + + /** + * 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 + */ + refreshIdentityPopup() { + // Update "Learn More" for Mixed Content Blocking and Insecure Login Forms. + let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); + this._identityPopupMixedContentLearnMore + .setAttribute("href", baseURL + "mixed-content"); + this._identityPopupInsecureLoginFormsLearnMore + .setAttribute("href", baseURL + "insecure-password"); + + // Determine connection security information. + let connection = "not-secure"; + if (this._isSecureInternalUI) { + connection = "chrome"; + } else if (this._isURILoadedFromFile) { + connection = "file"; + } else if (this._isEV) { + connection = "secure-ev"; + } else if (this._isCertUserOverridden) { + connection = "secure-cert-user-overridden"; + } else if (this._isSecure) { + connection = "secure"; + } + + // Determine if there are insecure login forms. + let loginforms = "secure"; + if (this._hasInsecureLoginForms) { + loginforms = "insecure"; + } + + // Determine the mixed content state. + let mixedcontent = []; + if (this._isMixedPassiveContentLoaded) { + mixedcontent.push("passive-loaded"); + } + if (this._isMixedActiveContentLoaded) { + mixedcontent.push("active-loaded"); + } else if (this._isMixedActiveContentBlocked) { + mixedcontent.push("active-blocked"); + } + mixedcontent = mixedcontent.join(" "); + + // We have no specific flags for weak ciphers (yet). If a connection is + // broken and we can't detect any mixed content loaded then it's a weak + // cipher. + let ciphers = ""; + if (this._isBroken && !this._isMixedActiveContentLoaded && !this._isMixedPassiveContentLoaded) { + ciphers = "weak"; + } + + // Update all elements. + let elementIDs = [ + "identity-popup", + "identity-popup-securityView-body", + ]; + + function updateAttribute(elem, attr, value) { + if (value) { + elem.setAttribute(attr, value); + } else { + elem.removeAttribute(attr); + } + } + + for (let id of elementIDs) { + let element = document.getElementById(id); + updateAttribute(element, "connection", connection); + updateAttribute(element, "loginforms", loginforms); + updateAttribute(element, "ciphers", ciphers); + updateAttribute(element, "mixedcontent", mixedcontent); + updateAttribute(element, "isbroken", this._isBroken); + } + + // Initialize the optional strings to empty values + let supplemental = ""; + let verifier = ""; + let host = ""; + let owner = ""; + let hostless = false; + + try { + host = this.getEffectiveHost(); + } catch (e) { + // Some URIs might have no hosts. + } + + // Fallback for special protocols. + if (!host) { + host = this._uri.specIgnoringRef; + // Special URIs without a host (eg, about:) should crop the end so + // the protocol can be seen. + hostless = true; + } + + // Fill in the CA name if we have a valid TLS certificate. + if (this._isSecure || this._isCertUserOverridden) { + verifier = this._identityIconLabels.tooltipText; + } + + // Fill in organization information if we have a valid EV certificate. + if (this._isEV) { + let iData = this.getIdentityData(); + host = owner = iData.subjectOrg; + verifier = this._identityIconLabels.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; + } + + // Push the appropriate strings out to the UI. + this._identityPopupContentHosts.forEach((el) => { + el.textContent = host; + el.hidden = hostless; + }); + this._identityPopupContentHostless.forEach((el) => { + el.setAttribute("value", host); + el.hidden = !hostless; + }); + this._identityPopupContentOwner.textContent = owner; + this._identityPopupContentSupp.textContent = supplemental; + this._identityPopupContentVerif.textContent = verifier; + + // Update per-site permissions section. + this.updateSitePermissions(); + }, + + setURI(uri) { + this._uri = uri; + + try { + this._uri.host; + this._uriHasHost = true; + } catch (ex) { + this._uriHasHost = false; + } + + let whitelist = /^(?:accounts|addons|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|searchreset|sessionrestore|support|welcomeback)(?:[?#]|$)/i; + this._isSecureInternalUI = uri.schemeIs("about") && whitelist.test(uri.path); + + // Create a channel for the sole purpose of getting the resolved URI + // of the request to determine if it's loaded from the file system. + this._isURILoadedFromFile = false; + let chanOptions = {uri: this._uri, loadUsingSystemPrincipal: true}; + let resolvedURI; + try { + resolvedURI = NetUtil.newChannel(chanOptions).URI; + if (resolvedURI.schemeIs("jar")) { + // Given a URI "jar:<jar-file-uri>!/<jar-entry>" + // create a new URI using <jar-file-uri>!/<jar-entry> + resolvedURI = NetUtil.newURI(resolvedURI.path); + } + // Check the URI again after resolving. + this._isURILoadedFromFile = resolvedURI.schemeIs("file"); + } catch (ex) { + // NetUtil's methods will throw for malformed URIs and the like + } + }, + + /** + * 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 has been modified. + if (gURLBar.getAttribute("pageproxystate") != "valid") { + return; + } + + this._popupTriggeredByKeyboard = event.type == "keypress"; + + // 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.refreshIdentityPopup(); + + // Add the "open" attribute to the identity box for styling + this._identityBox.setAttribute("open", "true"); + + // Now open the popup, anchored off the primary chrome element + this._identityPopup.openPopup(this._identityIcon, "bottomcenter topleft"); + }, + + onPopupShown(event) { + if (event.target == this._identityPopup) { + if (this._popupTriggeredByKeyboard) { + // Move focus to the next available element in the identity popup. + // This is required by role=alertdialog and fixes an issue where + // an already open panel would steal focus from the identity popup. + document.commandDispatcher.advanceFocusIntoSubtree(this._identityPopup); + } + + window.addEventListener("focus", this, true); + } + }, + + onPopupHidden(event) { + if (event.target == this._identityPopup) { + window.removeEventListener("focus", this, true); + this._identityBox.removeAttribute("open"); + } + }, + + handleEvent(event) { + let elem = document.activeElement; + let position = elem.compareDocumentPosition(this._identityPopup); + + if (!(position & (Node.DOCUMENT_POSITION_CONTAINS | + Node.DOCUMENT_POSITION_CONTAINED_BY)) && + !this._identityPopup.hasAttribute("noautohide")) { + // Hide the panel when focusing an element that is + // neither an ancestor nor descendant unless the panel has + // @noautohide (e.g. for a tour). + this._identityPopup.hidePopup(); + } + }, + + observe(subject, topic, data) { + if (topic == "perm-changed") { + this.refreshIdentityBlock(); + } + }, + + onDragStart: function (event) { + if (gURLBar.getAttribute("pageproxystate") != "valid") + return; + + let value = gBrowser.currentURI.spec; + let urlString = value + "\n" + gBrowser.contentTitle; + let htmlString = "<a href=\"" + value + "\">" + value + "</a>"; + + let 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(this._identityIcon, 16, 16); + }, + + onLocationChange: function () { + this._permissionJustRemoved = false; + this.updatePermissionHint(); + }, + + updatePermissionHint: function () { + if (!this._permissionList.hasChildNodes() && !this._permissionJustRemoved) { + this._permissionEmptyHint.removeAttribute("hidden"); + } else { + this._permissionEmptyHint.setAttribute("hidden", "true"); + } + + if (this._permissionJustRemoved) { + this._permissionReloadHint.removeAttribute("hidden"); + } else { + this._permissionReloadHint.setAttribute("hidden", "true"); + } + }, + + updateSitePermissions: function () { + while (this._permissionList.hasChildNodes()) + this._permissionList.removeChild(this._permissionList.lastChild); + + let uri = gBrowser.currentURI; + + let permissions = SitePermissions.getPermissionDetailsByURI(uri); + if (this._sharingState) { + // If WebRTC device or screen permissions are in use, we need to find + // the associated permission item to set the inUse field to true. + for (let id of ["camera", "microphone", "screen"]) { + if (this._sharingState[id]) { + let found = false; + for (let permission of permissions) { + if (permission.id != id) + continue; + found = true; + permission.inUse = true; + break; + } + if (!found) { + // If the permission item we were looking for doesn't exist, + // the user has temporarily allowed sharing and we need to add + // an item in the permissions array to reflect this. + let permission = SitePermissions.getPermissionItem(id); + permission.inUse = true; + permissions.push(permission); + } + } + } + } + for (let permission of permissions) { + let item = this._createPermissionItem(permission); + this._permissionList.appendChild(item); + } + + this.updatePermissionHint(); + }, + + _handleHeightChange: function(aFunction, aWillShowReloadHint) { + let heightBefore = getComputedStyle(this._permissionList).height; + aFunction(); + let heightAfter = getComputedStyle(this._permissionList).height; + // Showing the reload hint increases the height, we need to account for it. + if (aWillShowReloadHint) { + heightAfter = parseInt(heightAfter) + + parseInt(getComputedStyle(this._permissionList.nextSibling).height); + } + let heightChange = parseInt(heightAfter) - parseInt(heightBefore); + if (heightChange) + this._identityPopupMultiView.setHeightToFit(heightChange); + }, + + _createPermissionItem: function (aPermission) { + let container = document.createElement("hbox"); + container.setAttribute("class", "identity-popup-permission-item"); + container.setAttribute("align", "center"); + + let img = document.createElement("image"); + let classes = "identity-popup-permission-icon " + aPermission.id + "-icon"; + if (aPermission.state == SitePermissions.BLOCK) + classes += " blocked-permission-icon"; + if (aPermission.inUse) + classes += " in-use"; + img.setAttribute("class", classes); + + let nameLabel = document.createElement("label"); + nameLabel.setAttribute("flex", "1"); + nameLabel.setAttribute("class", "identity-popup-permission-label"); + nameLabel.textContent = SitePermissions.getPermissionLabel(aPermission.id); + + let stateLabel = document.createElement("label"); + stateLabel.setAttribute("flex", "1"); + stateLabel.setAttribute("class", "identity-popup-permission-state-label"); + stateLabel.textContent = SitePermissions.getStateLabel( + aPermission.id, aPermission.state, aPermission.inUse || false); + + let button = document.createElement("button"); + button.setAttribute("class", "identity-popup-permission-remove-button"); + let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip"); + button.setAttribute("tooltiptext", tooltiptext); + button.addEventListener("command", () => { + this._handleHeightChange(() => + this._permissionList.removeChild(container), !this._permissionJustRemoved); + if (aPermission.inUse && + ["camera", "microphone", "screen"].includes(aPermission.id)) { + let windowId = this._sharingState.windowId; + if (aPermission.id == "screen") { + windowId = "screen:" + windowId; + } else { + // If we set persistent permissions or the sharing has + // started due to existing persistent permissions, we need + // to handle removing these even for frames with different hostnames. + let uris = gBrowser.selectedBrowser._devicePermissionURIs || []; + for (let uri of uris) { + // It's not possible to stop sharing one of camera/microphone + // without the other. + for (let id of ["camera", "microphone"]) { + if (this._sharingState[id] && + SitePermissions.get(uri, id) == SitePermissions.ALLOW) + SitePermissions.remove(uri, id); + } + } + } + let mm = gBrowser.selectedBrowser.messageManager; + mm.sendAsyncMessage("webrtc:StopSharing", windowId); + } + SitePermissions.remove(gBrowser.currentURI, aPermission.id); + this._permissionJustRemoved = true; + this.updatePermissionHint(); + + // Set telemetry values for clearing a permission + let histogram = Services.telemetry.getKeyedHistogramById("WEB_PERMISSION_CLEARED"); + + let permissionType = 0; + if (aPermission.state == SitePermissions.ALLOW) { + // 1 : clear permanently allowed permission + permissionType = 1; + } else if (aPermission.state == SitePermissions.BLOCK) { + // 2 : clear permanently blocked permission + permissionType = 2; + } + // 3 : TODO clear temporary allowed permission + // 4 : TODO clear temporary blocked permission + + histogram.add("(all)", permissionType); + histogram.add(aPermission.id, permissionType); + }); + + container.appendChild(img); + container.appendChild(nameLabel); + container.appendChild(stateLabel); + container.appendChild(button); + + return container; + } +}; + +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() { + return gBrowser; +} +function getNavToolbox() { + return 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()) { + // 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" }, + ].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; + } + }); + } + } + + let urlBarSearchParam = gURLBar.getAttribute("autocompletesearchparam") || ""; + if (!PrivateBrowsingUtils.permanentPrivateBrowsing && + !urlBarSearchParam.includes("disable-private-actions")) { + // Disable switch to tab autocompletion for private windows. + // We leave it enabled for permanent private browsing mode though. + urlBarSearchParam += " disable-private-actions"; + } + if (!urlBarSearchParam.includes("private-window")) { + urlBarSearchParam += " private-window"; + } + gURLBar.setAttribute("autocompletesearchparam", urlBarSearchParam); + } +}; + +var gRemoteTabsUI = { + init: function() { + if (window.location.href != getBrowserURL() && + // Also check hidden window for the Mac no-window case + window.location.href != "chrome://browser/content/hiddenWindow.xul") { + return; + } + + if (AppConstants.platform == "macosx" && + Services.prefs.getBoolPref("layers.acceleration.disabled")) { + // On OS X, "Disable Hardware Acceleration" also disables OMTC and forces + // a fallback to Basic Layers. This is incompatible with e10s. + return; + } + + let newNonRemoteWindow = document.getElementById("menu_newNonRemoteWindow"); + let autostart = Services.appinfo.browserTabsRemoteAutostart; + newNonRemoteWindow.hidden = !autostart; + } +}; + +/** + * Switch to a tab that has a given URI, and focuses 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. + * @param aOpenParams + * If switching to this URI results in us opening a tab, aOpenParams + * will be the parameter object that gets passed to openUILinkIn. Please + * see the documentation for openUILinkIn to see what parameters can be + * passed via this object. + * This object also allows: + * - 'ignoreFragment' property to be set to true to exclude fragment-portion + * matching when comparing URIs. + * If set to "whenComparing", the fragment will be unmodified. + * If set to "whenComparingAndReplace", the fragment will be replaced. + * - 'ignoreQueryString' boolean property to be set to true to exclude query string + * matching when comparing URIs. + * - 'replaceQueryString' boolean property to be set to true to exclude query string + * matching when comparing URIs and overwrite the initial query string with + * the one from the new URI. + * @return True if an existing tab was found, false otherwise + */ +function switchToTabHavingURI(aURI, aOpenNew, aOpenParams={}) { + // Certain URLs can be switched to irrespective of the source or destination + // window being in private browsing mode: + const kPrivateBrowsingWhitelist = new Set([ + "about:addons", + ]); + + let ignoreFragment = aOpenParams.ignoreFragment; + let ignoreQueryString = aOpenParams.ignoreQueryString; + let replaceQueryString = aOpenParams.replaceQueryString; + + // These properties are only used by switchToTabHavingURI and should + // not be used as a parameter for the new load. + delete aOpenParams.ignoreFragment; + delete aOpenParams.ignoreQueryString; + delete aOpenParams.replaceQueryString; + + // This will switch to the tab in aWindow having aURI, if present. + function switchIfURIInWindow(aWindow) { + // Only switch to the tab if neither the source nor the destination window + // are private and they are not in permanent private browsing mode + if (!kPrivateBrowsingWhitelist.has(aURI.spec) && + (PrivateBrowsingUtils.isWindowPrivate(window) || + PrivateBrowsingUtils.isWindowPrivate(aWindow)) && + !PrivateBrowsingUtils.permanentPrivateBrowsing) { + return false; + } + + // Remove the query string, fragment, both, or neither from a given url. + function cleanURL(url, removeQuery, removeFragment) { + let ret = url; + if (removeFragment) { + ret = ret.split("#")[0]; + if (removeQuery) { + // This removes a query, if present before the fragment. + ret = ret.split("?")[0]; + } + } else if (removeQuery) { + // This is needed in case there is a fragment after the query. + let fragment = ret.split("#")[1]; + ret = ret.split("?")[0].concat( + (fragment != undefined) ? "#".concat(fragment) : ""); + } + return ret; + } + + // Need to handle nsSimpleURIs here too (e.g. about:...), which don't + // work correctly with URL objects - so treat them as strings + let ignoreFragmentWhenComparing = typeof ignoreFragment == "string" && + ignoreFragment.startsWith("whenComparing"); + let requestedCompare = cleanURL( + aURI.spec, ignoreQueryString || replaceQueryString, ignoreFragmentWhenComparing); + let browsers = aWindow.gBrowser.browsers; + for (let i = 0; i < browsers.length; i++) { + let browser = browsers[i]; + let browserCompare = cleanURL( + browser.currentURI.spec, ignoreQueryString || replaceQueryString, ignoreFragmentWhenComparing); + if (requestedCompare == browserCompare) { + aWindow.focus(); + if (ignoreFragment == "whenComparingAndReplace" || replaceQueryString) { + browser.loadURI(aURI.spec); + } + 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; + + for (let browserWin of browserWindows()) { + // 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)) + openUILinkIn(aURI.spec, "current", aOpenParams); + else + openUILinkIn(aURI.spec, "tab", aOpenParams); + } + + return false; +} + +var RestoreLastSessionObserver = { + init: function () { + if (SessionStore.canRestoreLastSession && + !PrivateBrowsingUtils.isWindowPrivate(window)) { + Services.obs.addObserver(this, "sessionstore-last-session-cleared", true); + goSetCommandEnabled("Browser:RestoreLastSession", true); + } + }, + + observe: function () { + // The last session can only be restored once so there's + // no way we need to re-enable our menu item. + Services.obs.removeObserver(this, "sessionstore-last-session-cleared"); + goSetCommandEnabled("Browser:RestoreLastSession", false); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]) +}; + +function restoreLastSession() { + SessionStore.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; + + 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 = + SessionStore.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); + + gFxAccounts.updateTabContextMenu(aPopupMenu); + }, + 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; + } + } +}; + +// Prompt user to restart the browser in safe mode +function safeModeRestart() { + if (Services.appinfo.inSafeMode) { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]. + createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + + if (cancelQuit.data) + return; + + Services.startup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit); + return; + } + + Services.obs.notifyObservers(null, "restart-in-safe-mode", ""); +} + +/* 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) { + switch (where) { + case "window": + let otherWin = OpenBrowserWindow(); + let delayedStartupFinished = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && + subject == otherWin) { + Services.obs.removeObserver(delayedStartupFinished, topic); + let otherGBrowser = otherWin.gBrowser; + let otherTab = otherGBrowser.selectedTab; + SessionStore.duplicateTab(otherWin, aTab, delta); + otherGBrowser.removeTab(otherTab, { animate: false }); + } + }; + + Services.obs.addObserver(delayedStartupFinished, + "browser-delayed-startup-finished", + false); + break; + case "tabshifted": + SessionStore.duplicateTab(window, aTab, delta); + // A background tab has been opened, nothing else to do here. + break; + case "tab": + let newTab = SessionStore.duplicateTab(window, aTab, delta); + gBrowser.selectedTab = newTab; + break; + } +} + +var MousePosTracker = { + _listeners: new Set(), + _x: 0, + _y: 0, + get _windowUtils() { + delete this._windowUtils; + return this._windowUtils = window.getInterface(Ci.nsIDOMWindowUtils); + }, + + addListener: function (listener) { + if (this._listeners.has(listener)) + return; + + listener._hover = false; + this._listeners.add(listener); + + this._callListener(listener); + }, + + removeListener: function (listener) { + this._listeners.delete(listener); + }, + + 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 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 = "#navigator-toolbox > toolbar:not([collapsed=true]):not(#addon-bar)"; + if (AppConstants.platform == "macosx") + toolbarSelector += ":not([type=menubar])"; + + // 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"); + } +} + +var PanicButtonNotifier = { + init: function() { + this._initialized = true; + if (window.PanicButtonNotifierShouldNotify) { + delete window.PanicButtonNotifierShouldNotify; + this.notify(); + } + }, + notify: function() { + if (!this._initialized) { + window.PanicButtonNotifierShouldNotify = true; + return; + } + // Display notification panel here... + try { + let popup = document.getElementById("panic-button-success-notification"); + popup.hidden = false; + let widget = CustomizableUI.getWidget("panic-button").forWindow(window); + let anchor = widget.anchor; + anchor = document.getAnonymousElementByAttribute(anchor, "class", "toolbarbutton-icon"); + popup.openPopup(anchor, popup.getAttribute("position")); + } catch (ex) { + Cu.reportError(ex); + } + }, + close: function() { + let popup = document.getElementById("panic-button-success-notification"); + popup.hidePopup(); + }, +}; + +var AboutPrivateBrowsingListener = { + init: function () { + window.messageManager.addMessageListener( + "AboutPrivateBrowsing:OpenPrivateWindow", + msg => { + OpenBrowserWindow({private: true}); + }); + window.messageManager.addMessageListener( + "AboutPrivateBrowsing:ToggleTrackingProtection", + msg => { + const PREF = "privacy.trackingprotection.pbmode.enabled"; + Services.prefs.setBoolPref(PREF, !Services.prefs.getBoolPref(PREF)); + }); + } +}; + +function TabModalPromptBox(browser) { + this._weakBrowserRef = Cu.getWeakReference(browser); +} + +TabModalPromptBox.prototype = { + _promptCloseCallback(onCloseCallback, principalToAllowFocusFor, allowFocusCheckbox, ...args) { + if (principalToAllowFocusFor && allowFocusCheckbox && + allowFocusCheckbox.checked) { + Services.perms.addFromPrincipal(principalToAllowFocusFor, "focus-tab-by-prompt", + Services.perms.ALLOW_ACTION); + } + onCloseCallback.apply(this, args); + }, + + appendPrompt(args, onCloseCallback) { + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let newPrompt = document.createElementNS(XUL_NS, "tabmodalprompt"); + let browser = this.browser; + browser.parentNode.insertBefore(newPrompt, browser.nextSibling); + browser.setAttribute("tabmodalPromptShowing", true); + + newPrompt.clientTop; // style flush to assure binding is attached + + let prompts = this.listPrompts(); + if (prompts.length > 1) { + // Let's hide ourself behind the current prompt. + newPrompt.hidden = true; + } + + let principalToAllowFocusFor = this._allowTabFocusByPromptPrincipal; + delete this._allowTabFocusByPromptPrincipal; + + let allowFocusCheckbox; // Define outside the if block so we can bind it into the callback. + let hostForAllowFocusCheckbox = ""; + try { + hostForAllowFocusCheckbox = principalToAllowFocusFor.URI.host; + } catch (ex) { /* Ignore exceptions for host-less URIs */ } + if (hostForAllowFocusCheckbox) { + let allowFocusRow = document.createElementNS(XUL_NS, "row"); + allowFocusCheckbox = document.createElementNS(XUL_NS, "checkbox"); + let spacer = document.createElementNS(XUL_NS, "spacer"); + allowFocusRow.appendChild(spacer); + let label = gBrowser.mStringBundle.getFormattedString("tabs.allowTabFocusByPromptForSite", + [hostForAllowFocusCheckbox]); + allowFocusCheckbox.setAttribute("label", label); + allowFocusRow.appendChild(allowFocusCheckbox); + newPrompt.appendChild(allowFocusRow); + } + + let tab = gBrowser.getTabForBrowser(browser); + let closeCB = this._promptCloseCallback.bind(null, onCloseCallback, principalToAllowFocusFor, + allowFocusCheckbox); + newPrompt.init(args, tab, closeCB); + return newPrompt; + }, + + removePrompt(aPrompt) { + let browser = this.browser; + browser.parentNode.removeChild(aPrompt); + + let prompts = this.listPrompts(); + if (prompts.length) { + let prompt = prompts[prompts.length - 1]; + prompt.hidden = false; + prompt.Dialog.setDefaultFocus(); + } else { + browser.removeAttribute("tabmodalPromptShowing"); + browser.focus(); + } + }, + + listPrompts(aPrompt) { + // Get the nodelist, then return as an array + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let els = this.browser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt"); + return Array.from(els); + }, + + onNextPromptShowAllowFocusCheckboxFor(principal) { + this._allowTabFocusByPromptPrincipal = principal; + }, + + get browser() { + let browser = this._weakBrowserRef.get(); + if (!browser) { + throw "Stale promptbox! The associated browser is gone."; + } + return browser; + }, +}; |